diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f173eb8..eba3e44 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, 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. +# 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). on: push: @@ -75,14 +75,7 @@ jobs: fi macos: - 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 }} + runs-on: macos-15 steps: - uses: actions/checkout@v4 - name: Install LLVM 21 (libclang) @@ -96,14 +89,6 @@ jobs: build/finch version - name: Tests run: tests/run.sh - - 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-${{ matrix.arch }}.tar.gz -C dist finch - - uses: actions/upload-artifact@v4 - with: - name: finch-macos-${{ matrix.arch }} - path: finch-macos-${{ matrix.arch }}.tar.gz windows: runs-on: windows-2022 @@ -131,16 +116,6 @@ jobs: export PATH="/c/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH" export FINCH_LDFLAGS="-LC:/llvm/bin" FINCH=../build/finch.exe tests/run.sh - - name: Package - run: | - mkdir -p dist/finch - cp build/finch.exe build/libclang.dll README.md LICENSE dist/finch/ - cp -r runtime std examples dist/finch/ - cd dist && 7z a -tzip finch-windows-x64.zip finch > /dev/null - - uses: actions/upload-artifact@v4 - with: - name: finch-windows-x64 - path: dist/finch-windows-x64.zip vscode: runs-on: ubuntu-24.04 @@ -161,27 +136,3 @@ 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 d53017b..7f62239 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,6 @@ 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) @@ -50,17 +48,13 @@ 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 12k lines. It is built from a - seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical. +- **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. ## Quick start -**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): +**Building it** (Linux x86-64 or ARM64; needs `clang` and libclang, LLVM 21): ```sh git clone https://github.com/Nexoniarz/Finch.git @@ -70,12 +64,13 @@ 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 expected error, -g, a shared library, the language server +tests/run.sh # every program, every refusal, -g, a shared library, the language server ``` -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)). +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)). Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any editor with LSP support (`finch lsp`). @@ -96,15 +91,13 @@ 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]`, vectors `f32x8` `i8x32`… | +| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]` | | Functions | `fn add(int a, int b) -> int { return a + b }` | | Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` | | 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()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(list, i)` | -| Processors | `--cpu native`; `@cpu("avx2") fn …` with `if cpu_has("avx2")`; floats computed as written (`fma`/`muladd` to fuse) | | 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 | @@ -115,7 +108,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`, `--cpu native`, `-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`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors | ## Examples @@ -129,7 +122,6 @@ 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 | @@ -159,17 +151,14 @@ 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` -- [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` -- [x] Masks, shuffles, byte lookup and `dot_add` for kernels; `@cpu` and `cpu_has`; `fma`/`muladd`, float bits, f16 and bf16 +- [x] Threads (`import thread`) and atomics; C++ files; `--target android` (compiles; NDK linking and devices untried) ## 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, thread +std/ the standard library, in Finch: json, path, time, proc, fs 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 68dcc85..52b438d 100644 --- a/README.pl.md +++ b/README.pl.md @@ -5,8 +5,8 @@

-**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. +**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. > 🇬🇧 [English version](README.md) @@ -19,7 +19,7 @@ fn przeciwprostokatna(float a, float b) -> float { fn main() { imie := "Finch" - print("Cześć, tu", imie, "!") + print("Cześć od", imie, "!") for i in 1..4 { print(i, "→", przeciwprostokatna(float(i), 1.0)) @@ -29,58 +29,51 @@ fn main() { ## Dlaczego Finch -- **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ć:** +- **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:** ``` gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) 3 | i32 y = x | ^ ``` -- **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. +- **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`. -## Na start +## Szybki start -**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): +**Budowanie** (Linux x86-64 lub ARM64; potrzebny `clang` i libclang, LLVM 21): ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # albo zainstaluj clang, libclang (LLVM 21) i pkg-config po swojemu -bootstrap/build.sh # gotowy kompilator to build/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 ./build/finch run examples/hello.fch ./build/finch build examples/tour.fch -o tour && ./tour -tests/run.sh # wszystkie programy testowe, oczekiwane błędy, -g, biblioteka współdzielona, serwer języka +tests/run.sh # każdy program, każda odmowa, -g, biblioteka współdzielona, serwer języka ``` -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)). +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)). -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`). +Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo dowolny +edytor z obsługą LSP (`finch lsp`). ## Dokumentacja @@ -88,96 +81,90 @@ Wybierz przewodnik dla siebie: | Przewodnik | Dla kogo | Polski | English | |---|---|---|---| -| **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) | +| **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) | ## Język w pigułce | | | |---|---| -| 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)` | +| 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)` | | 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("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()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(lista, i)` | -| Procesory | `--cpu native`; `@cpu("avx2") fn …` razem z `if cpu_has("avx2")`; liczby zmiennoprzecinkowe liczone tak, jak zapisane (`fma`/`muladd` do łączenia) | -| 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]()` | +| 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]()` | | 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) }` 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 | +| 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 | ## Przykłady -| Plik | Co pokazuje | +| Plik | Pokazuje | |---|---| -| [`examples/hello.fch`](examples/hello.fch) | najkrótszy program | +| [`examples/hello.fch`](examples/hello.fch) | najmniejszy program | | [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle | -| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie liczby: wczytywanie, pętle, `rand` z C | +| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C | | [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki | | [`examples/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 jednokierunkowa z `new`/`free` | -| [`examples/types.fch`](examples/types.fch) | liczby o podanym rozmiarze i wskaźniki | +| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` | +| [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki | | [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` | -| [`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/window.fch`](examples/window.fch) | okno GLFW + 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 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/)) | +| [`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/)) | ## Plan rozwoju - [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2 -- [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] 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] 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 kompiluje sam siebie +- [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie - [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`) -- [x] Serwer języka (`finch lsp`) i rozszerzenie do VS Code +- [x] Serwer języka (`finch lsp`) i rozszerzenie VS Code - [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate -- [x] Metody struktur, mapy, błędy jako wartości (`!`, `or`, `try`) +- [x] Metody w strukturach, 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 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` -- [x] Maski, przestawianie, tabele bajtów i `dot_add` dla jąder obliczeniowych; `@cpu` i `cpu_has`; `fma`/`muladd`, bity liczb, f16 i bf16 +- [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) -## Co gdzie leży +## Struktura ``` -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 +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 examples/ przykładowe programy -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 +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 ``` ## Licencja diff --git a/bootstrap/README.md b/bootstrap/README.md index 3ea5dbe..f4825d8 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -3,16 +3,12 @@ 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 | Built and tested in CI on | +| File | System | Checked | |---|---|---| -| `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. +| `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) | `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. @@ -45,6 +41,5 @@ 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; 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). +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. diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 6e21422..d27262c 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -21,9 +21,8 @@ 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) | +| `-g` | debug info (gdb, lldb, Visual Studio) | | `-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). @@ -40,7 +39,6 @@ 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"] @@ -98,7 +96,6 @@ 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 \\ \" \'`. @@ -243,34 +240,6 @@ 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`, … | a mask `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` | -| `select(m, a, b)` | lane by lane: `a` where `m` is true, else `b` | -| `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()` | every lane (float lanes) | -| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: one rounding always / the fastest way here | -| `v.store(list, i)` | lanes into `list[i]` … | -| `f32x8(list, i, fill)`, `v.store_part(list, i)` | the end of a list: only the lanes inside it (the others are `fill`) | -| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | lanes rearranged (shuffle takes numbers written in the code) | -| `table.lookup(idx)` | `table[idx % table.len]` per lane (`pshufb`, `tbl`) | -| `acc.dot_add(a, b)` | `i32x8` sums gain 4 byte products each (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` where there | -| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | a function that may use these features; check at run time before calling it | - ---- - ## Text `str` | Code | Gives | @@ -331,9 +300,6 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa | `shell(command)` | run a command, its exit code | | `exit(code)` | stop the program now (also ends an `or { }` block) | | `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | for any numbers (your own function of that name wins) | -| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` with one rounding / fused where the processor can (`a * b + c` written out is never fused) | -| `f32_bits(x)`, `f32_from_bits(u)`, `f64_bits`, `f64_from_bits` | a float's bits as `u32`/`u64` and back, unchanged | -| `f16_to_f32(h)`, `f32_to_f16(x)`, `bf16_to_f32(b)`, `f32_to_bf16(x)` | half and bfloat16 numbers (in a `u16`) ↔ `f32`; also on vectors | | `error(message)` | `return error("…")` in a `-> T!` function | | `addr(x)` | pointer to x; `addr(fn)` = C callback | | `assert(cond)`, `assert(cond, "why")` | stop with a runtime error when cond is false (in a `test` block: that test fails) | diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md index 935e0b6..fd3bedc 100644 --- a/docs/en/for-beginners.md +++ b/docs/en/for-beginners.md @@ -53,9 +53,8 @@ The computer reads your program from top to bottom, line by line. ## 2. Getting Finch ready -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.) +Finch runs on **Linux** computers. If a teacher, parent or friend has already set it up +for you, skip to the next section. Otherwise, open the **Terminal** program (a window where you type commands) and type these lines one by one, pressing **Enter** after each: @@ -77,7 +76,7 @@ To check it works, type: ./build/finch version ``` -You should see something like `Finch 2.6.0`. +You should see something like `Finch 2.5.0`. --- diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index 7b055a2..56ce0a9 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -1,11 +1,13 @@ # Finch for Engineers -**The internals of Finch 2.6, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*, +**The internals of Finch 2.5, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*, which `clang` compiles and links with a small C runtime; how C headers are read with libclang from Finch itself and how the C calling conventions are reproduced by hand; the type and ownership rules; how the compiler builds itself from a seed; and the language server. Assumed: you can read Finch (see the [technicians' guide](for-technicians.md)) and have at least seen LLVM IR. +The compiler's first version, in C++ on LLVM's C++ API, is kept on the `before-bootstrap` branch; this guide describes +the Finch one. > 🇵🇱 Polska wersja: [dla-inzynierow.md](../pl/dla-inzynierow.md) @@ -32,7 +34,7 @@ Assumed: you can read Finch (see the [technicians' guide](for-technicians.md)) a ## 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 12k lines) and what lets it be written in Finch itself. +generation and linking. That is what makes it small (about 8k lines) and what lets it be written in Finch itself. ``` main.fch ─► loader ── lex ── parse ──► Program (+ every imported module, recursively) @@ -58,7 +60,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 small C runtime** (`runtime/finch_rt.c`, ~1150 lines) does what is tedious in IR: strings, array growth, maps, input, +- **A tiny C runtime** (`runtime/finch_rt.c`, ~760 lines) does what is tedious in IR: strings, array growth, maps, input, files, panics. It is compiled once and cached in `~/.cache/finch`. --- @@ -68,17 +70,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` | 170 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) | +| `src/ast.fch` | 120 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) | | `src/parser.fch` | 910 | Recursive descent: declarations, statements, expressions, generics (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` | -| `src/gen.fch` | 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/gen.fch` | 6450 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, `-g` debug metadata, the `index` mode | | `src/cimport.fch` | 790 | libclang: functions, structs and unions (fields, offsets, padding), enums, constants, globals, macros (also struct-valued) | -| `src/platform.fch` | 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/platform.fch` | 90 | `Target`: triple, data layout, C compiler, signedness of `char` per system | +| `src/linker.fch` | 430 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice | +| `src/main.fch` | 515 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` | | `src/manifest.fch` | 240 | `finch.toml`: a small TOML reader, per-target lists | | `src/fmt.fch` | 225 | `finch fmt` and LSP formatting | -| `src/lsp.fch` | 950 | The language server (§11) | -| `src/docs.fch` | 140 | One description of the built-ins, shared by hover and completion | +| `src/lsp.fch` | 910 | The language server (§11) | +| `src/docs.fch` | 110 | One description of the built-ins, shared by hover and completion | | `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` | | `runtime/finch_rt.c` | 1150 | The C runtime: text, arrays, maps, files, float printing, fs, threads, the test runner, the call chain | | `bootstrap/` | | `build.sh`, `install.sh`, the seeds (§10) | @@ -114,19 +116,18 @@ dedented by the closing line's indentation in `longString()`. ## 4. Code generation -`gen.fch` is one big `Gen` struct plus functions over it. `generate()` runs: +`gen.fch` is one big `Gen` struct plus functions over it. The passes are: -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`). +1. `declareStructs`: every Finch and C struct gets an LLVM type (C structs with their real padding); recursive-by-value + structs are refused. +2. `declareFns`: signatures, with a *borrow analysis* for parameters (a parameter that is never stored is passed without + a copy). +3. `define`: each body, type checked and emitted. Scopes track what must be dropped when they end (`defer` runs first). +4. `defineMainWrapper`: the C `main(argc, argv)`, and `finch_on_fatal` for embedders. +5. With `-g`, DWARF metadata is attached as the IR is written (use `-O0`). Generics are **monomorphised**: a call `max(1, 2)` infers `T`, then the template's AST is generated again with `T` bound, -under its shown name (`max[int]`, a symbol `max.int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the +under a mangled name (`max$int`). `match` over an enum is exhaustive (a missing variant is an error, `else` covers the rest); on a value it is a chain of comparisons. Enum values are `i32`; `Color(n)` is range checked; printing uses a generated name table. @@ -163,13 +164,10 @@ 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] [--cpu c] [--shared] [--overlay path=file]`. +`finch build|run|ir|index file.fch [-o out] [-O0] [-g] [-l lib] [-I dir] [--target t] [--shared] [--overlay path=file]`. `ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the `export fn`s. -`--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 @@ -212,7 +210,6 @@ 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 @@ -225,10 +222,9 @@ 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, i64 }` (entries, len, used, cap, index, icap, adds; see src/gen.fch) | +| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; see src/gen.fch) | | `ptr`, `ptr[T]`, `null` | `ptr` (opaque) | | `[N]T` | `[N x T]` | -| `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 `-> !`) | @@ -239,16 +235,11 @@ Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `i ### Values -```c -struct Val { - str v // the IR operand: %t12, 42, @.str.3, zeroinitializer - ast.Type ty - bool fresh - bool lit -} +```cpp +struct Value_ { Value *v; FType type; bool literal; bool fresh; }; ``` -- **`lit`**: a number written in the code or an imported C constant. It may become any integer type +- **`literal`**: a number written in the code or an imported C constant. It may become any integer type it fits (`fits()`), or `f32`. It survives unary minus and literal-only arithmetic that folds to a constant; explicit conversions clear it (`u8(250)` is a typed constant, so `u8(250) + 10` wraps to 4). - **`fresh`**: an owning value (str/array/owning struct) that nobody holds yet: the result of a call, @@ -290,64 +281,13 @@ 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`, `v == w` … | `fcmp`/`icmp` → ``, a mask (`boolxN`: `vec` with `bool` lanes) | -| `select(m, a, b)` | `select ` | -| `m.any()`, `.all()`, `.count()` | `llvm.vector.reduce.or`/`.and`; `zext` + `llvm.vector.reduce.add` | -| `f32x8(list, i, fill)`, `v.store_part(list, i)` | `llvm.masked.load`/`llvm.masked.store` with the mask `<0, 1, …> < len - i` | -| `v.shuffle(…)`, `.lo()`, `.hi()`, `concat(a, b)` | `shufflevector` with a constant mask | -| `table.lookup(idx)` | `and` with `len - 1`, then `extractelement`/`insertelement` per lane: the pattern the x86 and ARM backends turn into `pshufb`/`tbl` | -| `acc.dot_add(a, b)` | `llvm.x86.avx512.vpdpbusd.{128,256,512}` or `llvm.aarch64.neon.{usdot,sdot,udot}` when the function's features have them; else `zext`/`sext`, `mul`, four strided `shufflevector`s and `add`s | -| `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)`, `fma(a, b, c)` | `llvm.fma` (one rounding; a libm call per lane without FMA hardware) | -| `v.muladd(b, c)`, `muladd(a, b, c)` | `llvm.fmuladd` (fused where the target has FMA, `fmul` + `fadd` where not) | -| `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. - -Some operations need to know the features at code generation time (`dot_add` picks `vpdpbusd` only where it exists). -`linker.cpuFeatures()` asks clang for its predefined macros with the same target and `--cpu` -(`clang -march=X -dM -E empty.c`: `__AVXVNNI__`, `__AVX512VNNI__`, `__ARM_FEATURE_MATMUL_INT8` …), maps them to Finch's -feature names and caches the answer per clang and flags; they reach `gen` as `Options.features`. `@cpu("…")` on a function -(`Node.cpu`) adds its own: `define … "target-features"="+avx2,+fma"` (with `+evex512` for any AVX-512 feature, which -LLVM 18+ needs for 512-bit registers), and `g.curFeatures` is the union while that function is generated. Features of the -other architecture are dropped (`cpuOf`). `checkCpuCall` refuses a vector wider than 128 bits as a parameter or result -between functions whose `@cpu` differ, because caller and callee would lower it to different registers. -`cpu_has(name)` calls `finch_cpu_has` in the runtime: CPUID leaves 1 and 7 with XGETBV for the AVX/AVX-512 state on x86, -`getauxval(AT_HWCAP/AT_HWCAP2)` on Linux ARM64, `sysctlbyname("hw.optional.arm.FEAT_…")` on macOS. - -Clang is always run as `NIX_ENFORCE_NO_NATIVE=0 clang …` on Unix: Nix's cc-wrapper otherwise drops `-march=native` -inside `nix-shell`. If `--cpu native` still yields no SSE4.2 macro on x86, the driver warns. +--- --- ## 8. Ownership and memory -### 8.1 The model +### 9.1 The model Every value of an **owning type** (`str`, `[]T`, or a Finch struct with an owning field) has exactly one owner: a variable, an array element, a struct field, or a heap cell from `new()`. The owner drops the @@ -371,21 +311,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. -### 8.2 Parameter borrowing: `mutates()` +### 9.2 Parameter borrowing: `mutates()` When declaring a function, every owning parameter is checked with `mutates(name, body)`: an assignment whose root variable (`rootVar()`: through `.field` and `[i]`) is the parameter, a changing method on it (`push pop insert remove clear resize sort reverse`), or `addr(…)` of it. Unchanged -parameters are borrowed (`FnSig.borrow`); changed ones are copied once at entry. The analysis is +parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at entry. The analysis is syntactic and conservative: anything that *might* change the parameter makes a copy, which is always correct. The same analysis decides whether `for x in list` must copy elements. -Methods take part: `declare()` first finds which methods change `self` (`mutates("self", body)`), +Methods take part: `declareFns()` first finds which methods change `self` (`mutates("self", body)`), repeating until nothing changes, because calling a changing method on `self` changes `self` too. Their -names go into `g.mm`, which `mutates()` treats like `push`: `p.move(1)` changes `p`. -Only then is `borrow` computed for every function and method. +names go into `mutatingMethods`, which `mutatesExpr()` treats like `push`: `p.move(1)` changes `p`. +Only then is `borrowParam` computed for every function and method. -### 8.3 Copy and drop +### 9.3 Copy and drop `dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses: @@ -395,10 +335,10 @@ Only then is `borrow` 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 (`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. +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. -### 8.4 What it looks like +### 9.4 What it looks like ```c fn greet(str name) -> str { @@ -441,22 +381,22 @@ The `-O0` IR of `main` (abridged): and `greet` returns `msg` without copying (`ret … %5` after loading the slot: the variable was moved out). -### 8.5 defer +### 9.5 defer -`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, +`defer stmt` pushes a `Cleanup{isDefer, body, order = current varOrder}` into the current scope; it +emits nothing there. Whenever the scope's cleanups are emitted (falling off the end of the block, `return`, `break`/`continue` leaving it) the body is generated **at that exit**, with `deferLimit = order` so that `lookup()` hides variables declared after the defer: those are already dropped by then (cleanups run in reverse). `return`/`break`/`continue` inside a deferred statement are errors (`inDefer`). Defer bodies are therefore duplicated per exit path, which is what makes them free on the normal path. -### 8.6 Manual memory +### 9.6 Manual memory `new(v)` allocates `sizeof(T)` with `finch_alloc`, stores `own(v)` and returns `ptr[T]`. `free(p)` drops the pointee if `T` is owning (skipping null) and calls libc `free`. Pointers never own anything: `addr(x)` can dangle after `x`'s block ends; this is not checked. -### 8.7 Verifying it +### 9.7 Verifying it `MEMCHECK=1 tests/run.sh` builds every test program and runs it under valgrind with `--leak-check=full --errors-for-leak-kinds=all`: zero leaks and zero invalid accesses across all of them, @@ -465,6 +405,8 @@ 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 @@ -486,9 +428,8 @@ 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). 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. +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. --- @@ -504,17 +445,16 @@ 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. +test (22 checks). --- ## 12. Semantics vs. C -| Situation | C | Finch | +| Situation | C | Finch 2.0 | |---|---|---| | Signed integer overflow | UB | defined wraparound (no `nsw`) | | Integer division by zero, `INT_MIN / -1` | UB | compile error if constant, otherwise runtime error | @@ -527,22 +467,13 @@ test. | 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 | poison, as in LLVM (not checked) | -| `a * b + c` on floats | may be fused into an FMA (`-ffp-contract`) | never fused: two roundings | - -### Floating point - -Float operations are emitted as plain `fadd`/`fsub`/`fmul`/`fdiv`/`fneg` with **no fast-math flags**, so LLVM may not -reassociate, contract or assume no NaNs; `linker.fch` also compiles the IR with `-ffp-contract=on`, which fuses only the -`llvm.fmuladd` calls Finch asks for. The only flagged instruction is `v.sum()`'s `llvm.vector.reduce.fadd` (`reassoc`). -`fma` → `llvm.fma`, `muladd` → `llvm.fmuladd`, on scalars and vectors (`fmaCall`, `fusedOp`). - -`f32_bits`/`f32_from_bits`/`f64_…` are `bitcast` (also between vectors). `f16_to_f32` is `bitcast i16 → half` + `fpext`, -`f32_to_f16` is `fptrunc` + `bitcast`: one instruction with F16C or on ARM64, `__extendhfsf2`/`__truncsfhf2` from the C -compiler's runtime library elsewhere. `bf16_to_f32` is `zext` + `shl 16` + `bitcast`; `f32_to_bf16` rounds to nearest even -in integer ops (`u + 0x7fff + ((u >> 16) & 1)`, then `>> 16`) and keeps NaNs quiet (`| 0x40`), so it needs no library. +| Shift ≥ width, out-of-range float→int | UB | still poison (not checked yet) | | Dangling pointer from `addr()` / `new` + `free` | UB | still UB | +--- + +--- + ### The chain of calls on runtime errors With `Options.trace` (default for `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; never with `--shared`) every Finch @@ -587,10 +518,13 @@ 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). -- Vectors have no integer division, and `lookup` on baseline x86-64 (no SSSE3) is done lane by lane. +- `--target android` is checked to the object file only; linking needs the NDK and has not been run on a device. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index c3ab1b7..9e41283 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -41,7 +41,6 @@ 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) --- @@ -68,24 +67,19 @@ 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), **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 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. -### Option A: a ready build +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. -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). +### Option A: a ready build (Linux) -| 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: +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: | Distribution | Packages | |---|---| @@ -94,15 +88,12 @@ On Linux: | 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 # Linux: clang, libclang, pkg-config (or install them with your distribution) +nix-shell # clang, libclang, pkg-config (or install them with your distribution) bootstrap/build.sh # build/finch ./build/finch version ``` @@ -111,26 +102,6 @@ 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: @@ -180,9 +151,7 @@ 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 - --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) + -g add debug info (for gdb / lldb / Visual Studio) -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) @@ -345,40 +314,6 @@ str(p) // a C char pointer as text **Fixed-size numbers wrap around**, like real hardware: a `u8` holding 255 plus 1 becomes 0. Calculations happen in the type of the values involved, so `u8(250) + 10` is `4`, not `260`. -### Decimal numbers: what is guaranteed - -`f32` and `f64` are IEEE 754 numbers, and Finch computes them **exactly as written**, the same on every processor and -with every `--cpu`: - -- Each operation rounds once, in the order written. `a * b - c * d` is two multiplies and a subtract, never folded into a - fused multiply-add (C compilers often do that by default, which changes the last digits). -- Nothing is reordered or simplified: `(a + b) + c` is not turned into `a + (b + c)`, and `x * 0.0` is not assumed to be 0 - (it is NaN when `x` is). The one exception is `v.sum()` on a vector, which adds the lanes in the fastest order. - -When speed matters more than the last bit, ask for it by name: - -| | | -|---|---| -| `fma(a, b, c)` | `a * b + c` with a single rounding, always. One instruction where the processor has FMA (with `--cpu`, on ARM64); a slower library call elsewhere. | -| `muladd(a, b, c)` | `a * b + c` the fastest way on this processor: fused where it can be, a multiply and an add where not. | - -Both work on numbers and on vectors (`v.fma(b, c)`, `v.muladd(b, c)`). - -**Numbers as bits.** File formats and model weights store numbers as raw bits. These look at the bits without -converting the value (all of them also work lane by lane on vectors): - -| | | -|---|---| -| `f32_bits(x)` / `f32_from_bits(u)` | `f32` ↔ the `u32` with the same bits (`f32_bits(1.0)` is `0x3f800000`) | -| `f64_bits(x)` / `f64_from_bits(u)` | `f64` ↔ `u64` | -| `f16_to_f32(h)` / `f32_to_f16(x)` | half precision, kept in a `u16`, ↔ `f32` (rounded to the nearest) | -| `bf16_to_f32(b)` / `f32_to_bf16(x)` | bfloat16, kept in a `u16`, ↔ `f32` (rounded to the nearest, ties to even) | - -```c -[]u16 weights = read_weights() // f16 numbers from a file -v := f16_to_f32(u16x8(weights, 0)) // 8 of them as f32, at once -``` - --- ## 7. Operators @@ -731,8 +666,6 @@ Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with | `exit(code)` | Ends the program right away. | | `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). | | `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). | -| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` with one rounding / the fastest way (see [Decimal numbers](#decimal-numbers-what-is-guaranteed)). | -| `f32_bits(x)`, `f32_from_bits(u)`, `f16_to_f32(h)`, … | A number's bits, and half-precision numbers (see [Decimal numbers](#decimal-numbers-what-is-guaranteed)). | | `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). `str(x)` works on every type and gives the same text `print` shows. `int(text) or 0` handles text that isn't a number. | --- @@ -1013,7 +946,8 @@ 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 the debug info is DWARF as well: use the `lldb` that comes with llvm-mingw, or gdb. +visible; without it the optimizer may remove some. On Windows, `-g` writes CodeView, which the +Visual Studio debugger and WinDbg read. --- @@ -1025,11 +959,10 @@ visible; without it the optimizer may remove some. On Windows the debug info is | `… 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: `no C compiler found to link with ('clang')` | Put llvm-mingw's `bin` folder on your PATH (section 2). | +| Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. | | Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. | | macOS: `finch` doesn't start (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. | | macOS: `can't find the C header 'stdio.h'` | Install the command line tools: `xcode-select --install`. | @@ -1047,7 +980,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, wordcount, c_import, simd, window, opengl, vulkan, raylib, llvm +├── examples/ hello, tour, types, structs, todo, guess, c_import, 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 @@ -1061,12 +994,35 @@ Finch/ ``` ```sh -tests/run.sh # programs, expected errors, -g builds, a shared library, the language server +tests/run.sh # programs, refusals, -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 @@ -1113,29 +1069,6 @@ 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: @@ -1201,7 +1134,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. +- 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. ### Warnings about big copies @@ -1260,7 +1193,6 @@ 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"] @@ -1360,195 +1292,6 @@ 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`). 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[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` with one rounding, always (the same on every processor) | -| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can (the last bit may differ between processors) | -| `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) | - -**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).muladd(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. - -### Masks: comparing, choosing, the end of a list - -Comparing vectors compares every lane, so the answer is a **mask**: one `true`/`false` a lane, of type `boolx8` (for 8 lanes). - -```c -v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0) -m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true) -relu := select(m, v, 0.0) // v where the mask is true, 0 elsewhere -print(m.count(), m.any(), m.all()) // 4 true false -if (v == w).all() { ... } // a condition needs one bool: all() or any() -``` - -| | | -|---|---| -| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | a mask (a plain number on one side counts for every lane) | -| `select(m, a, b)` | lane by lane: `a` where `m` is true, `b` where false (either may be a plain number) | -| `m.any()`, `m.all()`, `m.count()` | is any lane true, are all, how many | -| `m & n`, `m \| n`, `m ^ n`, `!m` | masks combined | -| `m[i]` | one lane, as a `bool` | -| `i32x8(m)`, `boolx8(v)` | true is 1; a lane that isn't 0 is true | - -**The end of a list.** A list is rarely a whole number of vectors long. Two forms touch only the lanes that are inside it: - -| | | -|---|---| -| `f32x8(list, i, fill)` | `list[i]` … as far as the list goes; the lanes past its end are `fill` | -| `v.store_part(list, i)` | writes only the lanes that fit in the list | - -```c -fn add_one([]f32 xs) { - i := 0 - while i < xs.len { // no separate loop for the rest - (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i) - i += 8 - } -} -``` - -### Moving lanes - -| | | -|---|---| -| `v.shuffle(3, 2, 1, 0)` | the lanes with these numbers (written in the code), in this order; as many lanes as numbers | -| `v.lo()`, `v.hi()` | the lower and the upper half | -| `concat(a, b)` | `a`'s lanes, then `b`'s | -| `table.lookup(idx)` | for every lane of `idx`: `table[idx % table.len]`. A byte table (`u8x16`, `i8x16`) is one `pshufb` on x86, `tbl` on ARM | - -Unpacking 4-bit numbers (two to a byte, as quantized models store them) is shuffle, mask and lookup: - -```c -fn unpack4(u8x8 packed, i8x16 meaning) -> i8x16 { - pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // each byte twice - odd := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1 - nibbles := select(odd, pairs >> 4, pairs & 15) // low nibble, then high - return meaning.lookup(nibbles) // 0..15 -> what it stands for -} -``` - -### Int8 dot products: dot_add - -`acc.dot_add(a, b)` is the step of a quantized model's matrix multiply. `acc` is an `i32x8` of sums; `a` and `b` have four -times as many bytes (`u8x32` and `i8x32`; `i8` × `i8` and `u8` × `u8` work too). Each lane of `acc` gains the sum of four -products, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, exactly (no saturation), and the sums stay in the vector: - -```c -acc := i32x8(0) -i := 0 -while i + 32 <= weights.len { - acc = acc.dot_add(u8x32(activations, i), i8x32(weights, i)) - i += 32 -} -total := acc.sum() // once, at the end -``` - -It is one instruction where the processor has one: `vpdpbusd` with AVX-VNNI (Intel since Alder Lake, AMD Zen 5) or -AVX-512 VNNI, `usdot`/`sdot`/`udot` on ARM with the dot-product features. Elsewhere it is a few instructions -(`pmaddwd` and adds), with the same result. Which one is used depends on `--cpu` or the function's `@cpu` (below). - -### Choosing the instructions: --cpu, @cpu and cpu_has - -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). Three ways to use more: - -**`--cpu`, for the whole program.** `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the -processor that builds it has: AVX2 and FMA, AVX-512, VNNI, or NEON's newer parts on ARM. The program then needs a processor -like that one. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512), -`--cpu alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`. - -**`@cpu`, for one function, and `cpu_has` to choose at run time.** One program can carry an AVX-512 version, an AVX2 -version and a plain one, and pick the best the computer it runs on has: - -```c -@cpu("avx512f,avx512bw,avx512vl,avx512vnni") -fn matmul_avx512([]u8 a, []i8 b, []i32 out) { ... } // may use AVX-512 inside - -@cpu("x86-64-v3") -fn matmul_avx2([]u8 a, []i8 b, []i32 out) { ... } // a level works too - -fn matmul([]u8 a, []i8 b, []i32 out) { - if cpu_has("avx512vnni") && cpu_has("avx512vl") { - matmul_avx512(a, b, out) - } else if cpu_has("avx2") { - matmul_avx2(a, b, out) - } else { - matmul_plain(a, b, out) - } -} -``` - -- The features are x86's `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl - avx512vnni avx512bf16 avx512fp16 avxvnni`, the levels `x86-64-v2`/`v3`/`v4`, and ARM's `neon fp16 dotprod i8mm bf16 sve sve2`. -- Names of the other architecture are left out when building, so the x86 and ARM versions sit side by side in one file - (and `cpu_has` says `false` for them). -- Call a `@cpu` function only after `cpu_has` said yes: on a processor without the feature it stops with *Illegal instruction*. -- A vector wider than 16 bytes (`f32x8`, `i32x16` …) can't be passed between functions with different `@cpu`: the two - sides would expect it in different registers. Pass the list (or a `ptr`) instead; Finch says so if you try. - -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. - -On NixOS, Nix's clang wrapper removes `-march=native` inside `nix-shell`. Finch tells the wrapper to keep it, so -`--cpu native` works there too; if it ever has no effect, Finch warns. +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. diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index eefbeb0..c948e86 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -1,11 +1,12 @@ # Finch dla inżynierów -**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. +**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. -Zakładam, że umiesz czytać kod w Finchu (zob. [przewodnik dla techników](dla-technikow.md)) i że LLVM IR nie jest ci obcy. +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. > 🇬🇧 English version: [for-engineers.md](../en/for-engineers.md) @@ -14,210 +15,194 @@ Zakładam, że umiesz czytać kod w Finchu (zob. [przewodnik dla techników](dla ## Spis treści 1. [Architektura](#1-architektura) -2. [Pliki źródłowe](#2-pliki-źródłowe) -3. [Część frontowa: lekser, parser, moduły](#3-część-frontowa-lekser-parser-moduły) +2. [Drzewo źródeł](#2-drzewo-źródeł) +3. [Front end: lekser, parser, moduły](#3-front-end-lekser-parser-moduły) 4. [Generowanie kodu](#4-generowanie-kodu) -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) +5. [Import C i ABI C](#5-import-c-i-abi-c) +6. [Sterownik, linkowanie i cele](#6-sterownik-linkowanie-i-cele) 7. [System typów](#7-system-typów) 8. [Własność i pamięć](#8-własność-i-pamięć) -9. [Typy generyczne, enumy i match](#9-typy-generyczne-enumy-i-match) +9. [Generyki, enumy i match](#9-generyki-enumy-i-match) 10. [Bootstrap](#10-bootstrap) 11. [Serwer języka](#11-serwer-języka) -12. [Semantyka w porównaniu z C](#12-semantyka-w-porównaniu-z-c) +12. [Semantyka a C](#12-semantyka-a-c) 13. [Znane ograniczenia](#13-znane-ograniczenia) --- ## 1. Architektura -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. +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. ``` main.fch ─► loader ── lex ── parse ──► Program (+ każdy importowany moduł, rekurencyjnie) │ - `import "x.h"` ─► cimport.fch (libclang wołany z Fincha) ─► funkcje, struktury i stałe C + `import "x.h"` ─► cimport.fch (libclang wołany z Fincha) ─► funkcje, struktury, stałe 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 + gen.fch ─► generate(): struktury, sygnatury, ciała funkcji + kontrola typów i IR w jednym przejściu; klasyfikacja ABI 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, od których zależy cała reszta: +Decyzje projektowe: -- **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`. +- **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`. --- -## 2. Pliki źródłowe +## 2. Drzewo źródeł -| Plik | Linie | Za co odpowiada | +| Plik | Linie | Zadanie | |---|---|---| -| `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 | +| `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 | | `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` | -| `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) | +| `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) | --- -## 3. Część frontowa: lekser, parser, moduły +## 3. Front end: lekser, parser, moduły -**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. +**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. -**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ą. +**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ą. -**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ł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ł 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`). +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`). -**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. +**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. -**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. +**Dodatki leksera.** `1e9` / `2.5e-3` to tokeny float (tekst dostaje `.` dla LLVM); teksty `"""` tracą wcięcie +linii zamykającej w `longString()`. --- ## 4. Generowanie kodu -`gen.fch` to jedna duża struktura `Gen` i funkcje, które na niej pracują. `generate()` wykonuje kolejno: +`gen.fch` to jedna duża struktura `Gen` i funkcje na niej. Przejścia: -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`. +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`). -Przy `-g` metadane DWARF są dopisywane na bieżąco, w trakcie zapisywania IR (najlepiej razem z `-O0`). +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. -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. +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. --- -## 5. Import z C i ABI języka C +## 5. Import C i ABI C -`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. +`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. -LLVM IR nie realizuje sam konwencji wywołań C, więc `gen.fch` odtwarza ją ręcznie: +LLVM IR nie realizuje za ciebie konwencji wywołań C, więc `gen.fch` ją odtwarza: -- `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). +- `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). 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 systemy docelowe +## 6. Sterownik, linkowanie i cele -`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`. +`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`. -`--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. +`--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. -`--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. +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`). -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`). +Kompilator znajduje `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`). -Kompilator szuka `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`). +**Pamięć podręczna budowania** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`): -**Pamięć podręczna kompilacji** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`): +- `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. -- `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. +**`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++. -**`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++. +**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++. **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`, a nie dodaje `-lpthread` (bionic ma wątki w libc). Wariantem modułów jest `linux`. +`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`. -**`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 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 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 @@ -230,61 +215,55 @@ zaczęła się instrukcja, więc zachowują kształt (i usuwanie wcięcia z `""" | `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, i64 }` (wpisy, len, used, cap, indeks, icap, adds; zob. src/gen.fch) | +| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; zob. src/gen.fch) | | `ptr`, `ptr[T]`, `null` | `ptr` (nieprzezroczysty) | | `[N]T` | `[N x T]` | -| `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) | +| 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) | -Znak liczby istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, `sext/zext`, -`sitofp/uitofp`, `fptosi/fptoui`. +Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, +`sext/zext`, `sitofp/uitofp`, `fptosi/fptoui`. ### Wartości -```c -struct Val { - str v // operand IR: %t12, 42, @.str.3, zeroinitializer - ast.Type ty - bool fresh - bool lit -} +```cpp +struct Value_ { Value *v; FType type; bool literal; bool fresh; }; ``` -- **`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). +- **`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). ### Konwersje niejawne: `coerce(v, want)` -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. +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. ### Argumenty operatorów: `unify(l, r)` -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`. +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`. ### Konwersje jawne: `convert()` | Z → Na | IR | |---|---| -| int/char ↔ int/char | `trunc`/`sext`/`zext` według znaku źródła | +| int/char ↔ int/char | `trunc`/`sext`/`zext` wg znakowości źródła | | bool → int | `zext` | -| zmiennoprzecinkowa ↔ całkowita | `fptosi`/`fptoui`, `sitofp`/`uitofp` | -| zmiennoprzecinkowa ↔ zmiennoprzecinkowa | `fpext`/`fptrunc` | +| float ↔ int | `fptosi`/`fptoui`, `sitofp`/`uitofp` | +| float ↔ float | `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` (przy złym tekście błąd w trakcie działania) | +| str → int/float | `finch_str_to_int/float` (panic przy złym tekście) | | `[]u8`/`[]char` → str | `finch_str_from_bytes` | | ptr → str | `finch_str_from_c` (pożyczony, `cap = -1`) | | ptr ↔ ptr | nic | @@ -292,154 +271,102 @@ zostaje `u8`. Dwa literały całkowite składają się w literał, więc `FLAG_A ### Porównania -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. +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. -### 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`, `v == w` … | `fcmp`/`icmp` → ``, maska (`boolxN`: `vec` z elementami `bool`) | -| `select(m, a, b)` | `select ` | -| `m.any()`, `.all()`, `.count()` | `llvm.vector.reduce.or`/`.and`; `zext` + `llvm.vector.reduce.add` | -| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | `llvm.masked.load`/`llvm.masked.store` z maską `<0, 1, …> < len - i` | -| `v.shuffle(…)`, `.lo()`, `.hi()`, `concat(a, b)` | `shufflevector` ze stałą maską | -| `tabela.lookup(idx)` | `and` z `len - 1`, potem `extractelement`/`insertelement` dla każdego elementu: wzorzec, który zaplecza x86 i ARM zamieniają na `pshufb`/`tbl` | -| `acc.dot_add(a, b)` | `llvm.x86.avx512.vpdpbusd.{128,256,512}` albo `llvm.aarch64.neon.{usdot,sdot,udot}`, gdy funkcja ma te rozszerzenia; w przeciwnym razie `zext`/`sext`, `mul`, cztery `shufflevector` co czwarty element i `add` | -| `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)`, `fma(a, b, c)` | `llvm.fma` (jedno zaokrąglenie; bez sprzętowego FMA wywołanie libm dla każdego elementu) | -| `v.muladd(b, c)`, `muladd(a, b, c)` | `llvm.fmuladd` (jedna instrukcja tam, gdzie procesor ma FMA, a gdzie nie, `fmul` + `fadd`) | -| `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. - -Niektóre działania muszą znać rozszerzenia procesora już przy generowaniu kodu (`dot_add` wybiera `vpdpbusd` tylko tam, -gdzie ono istnieje). `linker.cpuFeatures()` pyta clanga o makra, które sam definiuje dla tego samego systemu i `--cpu` -(`clang -march=X -dM -E pusty.c`: `__AVXVNNI__`, `__AVX512VNNI__`, `__ARM_FEATURE_MATMUL_INT8` …), zamienia je na nazwy -rozszerzeń Fincha i zapamiętuje odpowiedź dla danego clanga i flag; do `gen` trafiają jako `Options.features`. `@cpu("…")` -przy funkcji (`Node.cpu`) dodaje własne: `define … "target-features"="+avx2,+fma"` (z `+evex512` przy każdym rozszerzeniu -AVX-512, którego LLVM 18+ wymaga dla rejestrów 512-bitowych), a `g.curFeatures` to suma obu na czas generowania tej -funkcji. Rozszerzenia drugiej architektury są pomijane (`cpuOf`). `checkCpuCall` nie pozwala przekazać wektora szerszego -niż 128 bitów jako parametru albo wyniku między funkcjami o różnym `@cpu`, bo wywołujący i wywoływany umieściliby go -w innych rejestrach. `cpu_has(nazwa)` woła `finch_cpu_has` z runtime'u: na x86 CPUID (liście 1 i 7) z XGETBV dla stanu -AVX/AVX-512, na Linuksie ARM64 `getauxval(AT_HWCAP/AT_HWCAP2)`, na macOS `sysctlbyname("hw.optional.arm.FEAT_…")`. - -Na Uniksach clang jest zawsze uruchamiany jako `NIX_ENFORCE_NO_NATIVE=0 clang …`: inaczej nakładka cc-wrapper z Niksa -usuwa `-march=native` w `nix-shell`. Jeśli `--cpu native` mimo to nie daje na x86 makra SSE4.2, sterownik ostrzega. +--- --- ## 8. Własność i pamięć -### 8.1 Model +### 9.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; wszystkie decyzje zapadają przy kompilacji. +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. | Sytuacja | Co się dzieje | |---|---| -| `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ść | +| `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ść | -Literał tekstowy to świeża wartość z `cap = 0`: przeniesienie go nic nie kosztuje, zwalnianie jest pomijane +Literał tekstowy to wartość fresh z `cap = 0`: przeniesienie nic nie kosztuje, zwalnianie jest pomijane (`release()` ignoruje stałe), a zapis do niego (`s[i] = c`) najpierw robi kopię na stercie (`finch_str_own`). -Teksty z C są pożyczone z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane. +Teksty z C są pożyczane z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane. -### 8.2 Pożyczanie parametrów: `mutates()` +### 9.2 Pożyczanie parametrów: `mutates()` -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. +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. -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`. +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. -### 8.3 Kopiowanie i zwalnianie +### 9.3 Kopiowanie i zwalnianie -`dropAt(adres, T)` / `copyAt(cel, zrodlo, T)` działają na adresach: +`dropAt(addr, T)` / `copyAt(dst, src, 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 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ęć. +- `[]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. -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ń. +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. -### 8.4 Jak to wygląda +### 9.4 Jak to wygląda ```c -fn przywitaj(str imie) -> str { - wiad := "Cześć, " + imie - return wiad +fn greet(str name) -> str { + msg := "Hi, " + name + return msg } fn main() { a := ["x"] b := a - b.push(przywitaj("Ola")) + b.push(greet("Ola")) print(b[1]) } ``` -IR funkcji `main` przy `-O0` (w skrócie): +IR funkcji `main` z `-O0` (skrócony): ```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 (świeże) + store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (fresh) %a1 = load { ptr, i64, i64 }, ptr %a store { ptr, i64, i64 } %a1, ptr %1 call void @finch.copy.arr_str(ptr %2, ptr %1) ; b := a kopiuje %8 = load { ptr, i64, i64 }, ptr %2 store { ptr, i64, i64 } %8, ptr %b - %9 = call { ptr, i64, i64 } @finch.przywitaj({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 }) + %9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 }) ... - call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: powiększenie, jeśli trzeba - store { ptr, i64, i64 } %9, ptr %15 ; wynik przywitaj przeniesiony, bez kopii + 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 ... - %20 = icmp uge i64 1, %19 ; b[1]: sprawdzenie zakresu + %20 = icmp uge i64 1, %19 ; b[1]: kontrola zakresu br i1 %20, label %out.of.range, label %in.range ... call void @finch.drop.arr_str(ptr %b) ; koniec main: od najnowszej @@ -447,116 +374,103 @@ IR funkcji `main` przy `-O0` (w skrócie): ret void ``` -a `przywitaj` zwraca `wiad` bez kopiowania (`ret … %5` po wczytaniu zmiennej: została przeniesiona). +a `greet` zwraca `msg` bez kopiowania (`ret … %5` po wczytaniu slotu: zmienna została przeniesiona). -### 8.5 defer +### 9.5 defer -`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ą. +`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ą. -### 8.6 Pamięć zarządzana ręcznie +### 9.6 Pamięć ręczna -`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. +`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. -### 8.7 Sprawdzanie +### 9.7 Weryfikacja `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 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. +`--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. --- -## 9. Typy generyczne, enumy i match +--- -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/`. +## 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/`. --- ## 10. Bootstrap -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: +Kompilator budowany jest z **ziarna**: własnego IR zapisanego jako `bootstrap/.ll` (zobacz +[bootstrap/README.md](../../bootstrap/README.md)). `bootstrap/build.sh`: ``` -plik .ll ──clang──► finch0 ──buduje src/──► finch1 ──buduje src/──► build/finch +ziarno .ll ──clang──► finch0 ──buduje src/──► finch1 ──buduje src/──► build/finch ``` -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. +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. -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. +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. --- ## 11. Serwer języka -`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: +`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: | Rekord | Znaczenie | |---|---| -| `@R plik linia kol dlug plikDef liniaDef kolDef dymek` | odwołanie: co pokazać w dymku i gdzie jest definicja | +| `@R plik linia kol dł plikDef liniaDef kolDef podpowiedź` | odwołanie: co pokazuje hover i gdzie jest definicja | | `@V nazwa typ plik linia kol plikFn liniaFn koniecFn` | zmienna i funkcja, w której żyje | -| `@F modul nazwa odbiorca plik linia kol koniec sygnatura parametry` | funkcja albo metoda | +| `@F moduł nazwa odb plik linia kol koniec sygnatura parametry` | funkcja lub metoda | | `@S` / `@SF`, `@E`, `@I` | struktury (i pola), enumy, importy | -| `@W` | ostrzeżenie | -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`. +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). --- -## 12. Semantyka w porównaniu z C +## 12. Semantyka a C -| Sytuacja | C | Finch | +| Sytuacja | C | Finch 2.0 | |---|---|---| -| 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]` | +| 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]` | | Użycie po zwolnieniu / podwójne zwolnienie tablic, tekstów, struktur | częste błędy | niemożliwe: tych nie zwalnia się ręcznie | -| Wyciek pamięci tablic, tekstów, struktur | częsty błąd | niemożliwy bez `new` | +| Wyciek 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 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) | -| `a * b + c` na liczbach zmiennoprzecinkowych | może zostać złożone w FMA (`-ffp-contract`) | nigdy nie jest łączone: dwa zaokrąglenia | - -### Liczby zmiennoprzecinkowe - -Działania na liczbach zmiennoprzecinkowych to zwykłe `fadd`/`fsub`/`fmul`/`fdiv`/`fneg` **bez flag fast-math**, więc LLVM -nie może zmieniać kolejności działań, łączyć ich ani zakładać braku NaN; `linker.fch` kompiluje też IR z -`-ffp-contract=on`, które łączy tylko wywołania `llvm.fmuladd`, o które prosi Finch. Jedyna instrukcja z flagą to -`llvm.vector.reduce.fadd` w `v.sum()` (`reassoc`). `fma` → `llvm.fma`, `muladd` → `llvm.fmuladd`, na liczbach i wektorach -(`fmaCall`, `fusedOp`). - -`f32_bits`/`f32_from_bits`/`f64_…` to `bitcast` (także między wektorami). `f16_to_f32` to `bitcast i16 → half` + `fpext`, -`f32_to_f16` to `fptrunc` + `bitcast`: jedna instrukcja z F16C albo na ARM64, a gdzie indziej `__extendhfsf2`/`__truncsfhf2` -z biblioteki uruchomieniowej kompilatora C. `bf16_to_f32` to `zext` + `shl 16` + `bitcast`; `f32_to_bf16` zaokrągla do -najbliższej parzystej na liczbach całkowitych (`u + 0x7fff + ((u >> 16) & 1)`, potem `>> 16`) i zostawia NaN cichym -(`| 0x40`), więc nie potrzebuje żadnej biblioteki. +| 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) | | Wiszący wskaźnik z `addr()` / `new` + `free` | UB | nadal UB | -### Ścieżka wywołań przy błędach w trakcie działania +--- + +--- + +### Łańcuch wywołań przy błędach w czasie działania Z `Options.trace` (domyślnie dla `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; nigdy z `--shared`) każda funkcja -Fincha ma na stosie ramkę: +Fincha ma ramkę na swoim stosie: ```llvm %frame = alloca { ptr, ptr, ptr, i64 } ; ramka wywołującego, nazwa, plik, linia @@ -565,44 +479,45 @@ store ptr @"nazwa", ...pole 1 ; ptr @"plik", pole 2 ; i64 , pole 3 store ptr %frame, ptr @finch_trace ``` -`@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. +`@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. ### Wątki i operacje atomowe -`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. +`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. ### Ostrzeżenia -`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. +`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. -### Struktury przekazywane przez adres +### Struktura jako argument przez adres -`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ą. - ---- +`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. ## 13. Znane ograniczenia -- 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ą dzielenia całkowitego, a `lookup` na podstawowym x86-64 (bez SSSE3) działa element po elemencie. +- 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. + diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md index 4f79846..5b48cf2 100644 --- a/docs/pl/dla-poczatkujacych.md +++ b/docs/pl/dla-poczatkujacych.md @@ -1,8 +1,8 @@ # Finch dla początkujących -**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. +**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. > 🇬🇧 English version: [for-beginners.md](../en/for-beginners.md) @@ -13,7 +13,7 @@ Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak 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 ekranie](#4-wypisywanie-na-ekranie) +4. [Wypisywanie na ekran](#4-wypisywanie-na-ekran) 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,35 +28,36 @@ Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak 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łowa, które będziesz słyszeć](#19-słowa-które-będziesz-słyszeć) +19. [Słowniczek](#19-słowniczek) --- ## 1. Co to jest program? -Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu każesz, krok po kroku. +Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu się każe, +krok po kroku. -**Program** to spisana lista takich kroków. Trochę jak przepis: +**Program** to spisana lista takich kroków. Trochę jak przepis kuchenny: > 1. Weź dwa jajka. > 2. Wbij je do miski. > 3. Wymieszaj. -**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. +**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. -Komputer czyta program od góry do dołu, linia po linii. +Komputer czyta program od góry do dołu, linijka po linijce. --- ## 2. Przygotowanie Fincha -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.) +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. -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**: +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: ```sh git clone https://github.com/Nexoniarz/Finch.git @@ -65,24 +66,24 @@ nix-shell bootstrap/build.sh ``` -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.) +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.) -Żeby sprawdzić, czy wszystko działa, wpisz: +Żeby sprawdzić, czy działa, wpisz: ```sh ./build/finch version ``` -Powinien pojawić się napis w rodzaju `Finch 2.6.0`. +Powinieneś zobaczyć coś w stylu `finch 2.5.0`. --- ## 3. Twój pierwszy program -1. Otwórz dowolny edytor tekstu (np. *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*). -2. Przepisz dokładnie to: +1. Otwórz dowolny edytor tekstu (na przykład *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*). +2. Wpisz dokładnie to: ```c fn main() { @@ -90,11 +91,11 @@ fn main() { } ``` -3. Zapisz plik w katalogu `Finch` pod nazwą **`hello.fch`**. Końcówka `.fch` mówi, że to program w Finchu. +3. Zapisz plik w folderze `Finch` pod nazwą **`czesc.fch`**. Końcówka `.fch` mówi, że to program w Finchu. 4. W Terminalu wpisz: ```sh -./build/finch run hello.fch +./build/finch run czesc.fch ``` Zobaczysz: @@ -103,7 +104,7 @@ Zobaczysz: Cześć! ``` -🎉 **Właśnie udało ci się napisać i uruchomić program.** +🎉 **Właśnie napisałeś i uruchomiłeś program.** ### Co właściwie napisaliśmy? @@ -113,26 +114,26 @@ fn main() { ← „tu zaczyna się główna część mojego programu } ← „tu główna część się kończy” ``` -- `fn main()` to miejsce, w którym **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`. +- `fn main()` to miejsce, od którego **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 stoi w cudzysłowie: `"Cześć!"`. +- Tekst zawsze piszemy w cudzysłowie: `"Cześć!"`. -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. +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. --- -## 4. Wypisywanie na ekranie +## 4. Wypisywanie na ekran -`print` potrafi pokazać tekst i liczby. Oddziel je przecinkami, a Finch sam wstawi między nimi spacje: +`print` umie pokazać tekst i liczby. Między kolejne rzeczy wstaw przecinek, a Finch sam doda spację: ```c fn main() { print("Mam", 10, "lat") print("2 + 2 =", 2 + 2) print() - print("Linia wyżej jest pusta") + print("Linijka wyżej jest pusta") } ``` @@ -142,10 +143,10 @@ Pokaże: Mam 10 lat 2 + 2 = 4 -Linia wyżej jest pusta +Linijka wyżej jest pusta ``` -Każde `print` zaczyna nową linię. `print()` bez niczego w środku robi pustą linię. +Każdy `print` zaczyna nową linijkę. `print()` bez niczego w środku robi pustą linijkę. ### Notatki dla siebie: komentarze @@ -162,8 +163,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 później 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 potem sprawdzić, +co jest w środku, czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**. ```c fn main() { @@ -172,9 +173,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, pisząc samo `=`: +Później możesz zmienić zawartość pudełka, używając samego `=`: ```c fn main() { @@ -195,39 +196,42 @@ Punkty: 5 Punkty: 6 ``` -`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`. +`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`. **Zapamiętaj:** -- `:=` robi **nowe** pudełko. Dla każdej nazwy używasz tego tylko raz. +- `:=` robi **nowe** pudełko. Używasz tego tylko raz dla danej nazwy. - `=` wkłada nową wartość do pudełka, **które już istnieje**. ### Nazywanie pudełek -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. +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. + Wybieraj nazwy, które mówią, co jest w środku: `cena` jest lepsza niż `c`. --- ## 6. Rodzaje wartości -Finch pilnuje, jaki *rodzaj* rzeczy leży w każdym pudełku. Taki rodzaj to **typ**. +Finch pilnuje, *jakiego rodzaju* rzecz jest w każdym pudełku. Ten rodzaj nazywa się **typem**. 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`, `false` | -| `char` | jedną literę albo 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` (prawda), `false` (fałsz) | +| `char` | jeden znak | `'A'`, `'z'`, `'?'` | -Uwaga: w programach część ułamkową oddziela się **kropką**, nie przecinkiem: `3.14`, a nie `3,14`. +⚠️ W liczbach zamiast przecinka piszemy **kropkę**: `3.14`, a nie `3,14`. Tak jest w prawie wszystkich językach programowania. -Przy `:=` Finch sam rozpoznaje typ: +Kiedy używasz `:=`, Finch sam rozpoznaje typ: ```c imie := "Ola" // str @@ -236,7 +240,7 @@ wzrost := 1.42 // float lubi_koty := true // bool ``` -Typ można też napisać samemu, przed nazwą: +Typ możesz też napisać sam, przed nazwą: ```c int wiek = 10 @@ -245,23 +249,24 @@ str imie = "Ola" Oba sposoby robią to samo. Wybierz ten, który ci bardziej pasuje. -**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. +**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. --- ## 7. Liczenie -| 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` | +| 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` | -**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`. +**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`. Mnożenie i dzielenie wykonują się przed dodawaniem i odejmowaniem, tak jak w szkole. Nawiasy zmieniają kolejność: @@ -271,28 +276,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 linii to się stało. +**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linijce to się stało. --- ## 8. Podejmowanie decyzji: if -Program może wybierać, co zrobić. `if` znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*. +Program może wybierać, co zrobić. `if` (czyt. „if”, po polsku „jeżeli”) znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*. ```c fn main() { temperatura := 25 if temperatura > 20 { - print("Jest ciepło, włóż koszulkę") + print("Jest ciepło, załóż koszulkę") } } ``` -Dopisz `else`, żeby powiedzieć *„a jeśli nie, zrób to”*: +Dodaj `else` („w przeciwnym razie”): ```c if temperatura > 20 { @@ -302,11 +307,12 @@ if temperatura > 20 { } ``` -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ą: +A `else if` dodaje kolejne możliwości. Finch sprawdza je od góry i wykonuje tylko +**pierwszą** prawdziwą: ```c if temperatura > 25 { - print("Upał!") + print("Gorąco!") } else if temperatura > 15 { print("Przyjemnie") } else { @@ -322,18 +328,18 @@ if temperatura > 25 { | `!=` | nie jest równe | | `<` | jest mniejsze niż | | `>` | jest większe niż | -| `<=` | jest mniejsze albo równe | -| `>=` | jest większe albo równe | +| `<=` | jest mniejsze lub równe | +| `>=` | jest większe lub równe | ⚠️ **Jedno `=` wkłada wartość do pudełka. Dwa `==` pytają „czy to jest równe?”.** -Porównywać można też teksty: `if imie == "Ola" { ... }`. +Tekst też można porównywać: `if imie == "Ola" { ... }`. ### Łączenie pytań - `&&` znaczy **i**: oba warunki muszą być prawdziwe. `if wiek > 6 && wiek < 12 { ... }` -- `||` znaczy **lub**: przynajmniej jeden musi być prawdziwy. `if dzien == "sob" || dzien == "niedz" { ... }` -- `!` znaczy **nie**: zamienia prawdę w fałsz i odwrotnie. `if !pada { ... }` +- `||` znaczy **lub**: wystarczy, że jeden jest prawdziwy. `if dzien == "sob" || dzien == "nd" { ... }` +- `!` znaczy **nie**: odwraca prawdę i fałsz. `if !pada { ... }` --- @@ -359,7 +365,7 @@ Liczba 4 Liczba 5 ``` -`for i in 1..6` znaczy: *„licz w `i` od 1 do 6, ale **bez** 6”*. +`for i in 1..6` znaczy: *„licz za pomocą `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` @@ -382,8 +388,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ę bez końca. -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ę +w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać. ### Wcześniejsze wyjście i pomijanie @@ -393,9 +399,9 @@ Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać ```c for i in 1..100 { if i == 5 { - break // zatrzymaj się, gdy dojdziemy do 5 + break // stop, gdy dojdziemy do 5 } - print(i) // wypisze 1 2 3 4 + print(i) // pokaże 1 2 3 4 } ``` @@ -403,7 +409,7 @@ for i in 1..100 { ## 10. Własne polecenia: funkcje -Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany blok to **funkcja**. +Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany kawałek kodu to **funkcja**. Jedną już znasz: `main`. ```c @@ -422,12 +428,12 @@ Cześć, Ola Cześć, Tomek ``` -`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. +`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. -### Funkcje, które dają odpowiedź +### Funkcje, które oddają odpowiedź -Napisz `->` i typ odpowiedzi, a odpowiedź oddaj przez `return`: +Napisz `->` i typ odpowiedzi, a odpowiedź oddaj słowem `return`: ```c fn dodaj(int a, int b) -> int { @@ -440,14 +446,14 @@ fn main() { } ``` -Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad `main` czy pod nim. +Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad czy pod `main`. --- ## 11. Listy: wiele wartości w jednym pudełku -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: +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: ```c fn main() { @@ -464,34 +470,34 @@ Mam 3 owoce Pierwszy to jabłko ``` -- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddziela się przecinkami. +- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddzielamy przecinkami. - `.len` mówi, ile rzeczy jest na liście. -- `owoce[0]` to **pierwsza** rzecz. Liczy się od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia. +- `owoce[0]` to **pierwsza** rzecz. Liczymy 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)` (indeks 10 jest poza listą, która ma długość 3). +`index 10 is out of range (the length is 3)`, czyli „indeks 10 jest poza zakresem (długość to 3)”. ### Dodawanie i zabieranie ```c fn main() { []int wyniki // pusta lista liczb całkowitych - wyniki.push(10) // dodaj 10 na końcu + wyniki.push(10) // dołóż 10 na koniec wyniki.push(25) wyniki.push(7) print(wyniki) // [10, 25, 7] - ostatni := wyniki.pop() // zdejmij ostatni + ostatni := wyniki.pop() // zabierz ostatni print(ostatni, wyniki) // 7 [10, 25] - wyniki.sort() + wyniki.sort() // posortuj print(wyniki) } ``` ### Przechodzenie po liście -`for ... in` odwiedza po kolei każdą wartość: +`for ... in` odwiedza każdą wartość po kolei: ```c fn main() { @@ -503,8 +509,8 @@ fn main() { } ``` -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ść: +Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, daj `for` dwie nazwy: pierwsza liczy +0, 1, 2…, druga to wartość: ```c fn main() { @@ -520,20 +526,21 @@ fn main() { 3 wiśnia ``` -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). +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). ### Szukanie po nazwie: mapy -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ść**: +Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem chcesz znaleźć coś po **nazwie**, tak jak +słowo w słowniku albo numer w książce telefonicznej. Do tego służy **mapa** (`map`): każdy +**klucz** (nazwa) ma swoją **wartość**: ```c fn main() { telefon := ["Ola": "555-1234", "Jan": "555-9876"] print(telefon["Ola"]) // 555-1234 - telefon["Ewa"] = "555-0000" // dodaj nowe imię + telefon["Ewa"] = "555-0000" // nowa osoba print(telefon.len) // 3 if telefon.has("Tomek") { @@ -542,26 +549,26 @@ fn main() { print("nie ma Tomka") } - for imie, numer in telefon { // każde imię razem z numerem + for imie, numer in telefon { // każde imię z jego numerem print(imie, numer) } } ``` -- `["klucz": wartość, ...]` tworzy mapę. Kluczami są często teksty, ale mogą być też liczby. +- `["klucz": wartość, ...]` tworzy mapę. Klucze to zwykle tekst, ale mogą być też liczby. - `telefon["Ola"]` daje wartość dla klucza. Pytanie o klucz, którego nie ma, zatrzymuje program, - więc najpierw sprawdź `telefon.has("Tomek")` 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. + więc najpierw sprawdź `telefon.has("Tomek")` (czy ma) albo użyj `telefon.get("Tomek", "nie wiem")`. +- `for imie, numer in telefon` odwiedza każdy klucz z wartością, w kolejności dodawania. Mapy świetnie nadają się do **liczenia**. Nowy klucz zaczyna od 0, więc `+= 1` po prostu działa: ```c fn main() { - map[str]int glosy // pusta mapa: klucze to teksty, wartości to liczby całkowite + map[str]int glosy // pusta mapa: klucze to tekst, wartości to liczby for g in ["kot", "pies", "kot", "kot", "rybka"] { glosy[g] += 1 } - print(glosy) // {"kot": 3, "pies": 1, "rybka": 1} + print(glosy) // {"kot": 3, "pies": 1, "rybka": 1} } ``` @@ -569,7 +576,7 @@ fn main() { ## 12. Zabawa tekstem -Teksty można sklejać przez `+`: +Teksty sklejasz znakiem `+`: ```c fn main() { @@ -593,16 +600,16 @@ fn main() { slowo := "Finch" print(slowo.len) // 5 liter print(slowo[0]) // F (pierwsza litera) - print(slowo.upper()) // FINCH - print(slowo.lower()) // finch - print(slowo.contains("in")) // true + print(slowo.upper()) // FINCH (wielkimi literami) + print(slowo.lower()) // finch (małymi literami) + print(slowo.contains("in")) // true (czy zawiera "in") for litera in slowo { - print(litera) // F, i, n, c, h, każda w osobnej linii + print(litera) // F, i, n, c, h, każda w osobnej linijce } } ``` -(`.len` liczy bajty, a polskie litery zajmują po dwa, więc `"żółw".len` to 7, a nie 4.) +⚠️ `.len` liczy bajty, a polskie litery (ą, ę, ż…) zajmują po 2 bajty. `"żółw".len` to 7, a nie 4. --- @@ -627,15 +634,15 @@ fn main() { } ``` -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ę). +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”). --- ## 14. Własne rodzaje wartości: struktury -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: +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: ```c struct Ksiazka { @@ -657,11 +664,11 @@ Hobbit napisał Tolkien Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1938) ``` -- `struct Ksiazka { ... }` opisuje, co ma każda książka. Umieść to **poza** `main`. +- `struct Ksiazka { ... }` opisuje, co ma każda książka. Pisz to **poza** `main`. - `Ksiazka(tytul: ..., autor: ..., rok: ...)` tworzy jedną książkę. -- `k.tytul` odczytuje jedną część. `k.rok = 1938` ją zmienia. +- `k.tytul` odczytuje jedną część, a `k.rok = 1938` ją zmienia. -Częściom można nadać wartość początkową. Wtedy nie trzeba ich wypełniać: +Częściom możesz nadać wartość początkową. Wtedy nie trzeba ich podawać: ```c struct Gracz { @@ -675,12 +682,12 @@ fn main() { } ``` -Wiele takich wartości można trzymać na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. +Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. -### Polecenia dla struktury: metody +### Polecenia twojej struktury: metody -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: +Struktura może mieć własne polecenia (**metody**). Napisz nazwę struktury, kropkę i nazwę polecenia. +W środku `self` (ang. „sam”) to struktura, na której użyto polecenia: ```c struct Gracz { @@ -690,7 +697,7 @@ struct Gracz { fn Gracz.trafiony() { self.zycia -= 1 - print(self.imie, "- pozostałe życia:", self.zycia) + print(self.imie, "ma jeszcze", self.zycia, "życia") } fn Gracz.zyje() -> bool { @@ -699,8 +706,8 @@ fn Gracz.zyje() -> bool { fn main() { g := Gracz(imie: "Ola") - g.trafiony() // Ola - pozostałe życia: 2 - g.trafiony() // Ola - pozostałe życia: 1 + g.trafiony() // Ola ma jeszcze 2 życia + g.trafiony() // Ola ma jeszcze 1 życia print(g.zyje()) // true } ``` @@ -724,14 +731,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 katalogu, w którym uruchomiono program. +Plik pojawi się w folderze, w którym uruchomiłeś program. --- ## 16. Kiedy coś pójdzie nie tak -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: +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: ```c fn main() { @@ -741,67 +748,68 @@ fn main() { ``` ``` -hello.fch:3:11: error: there is no variable named 'wiekk' +czesc.fch:3:11: error: there is no variable named 'wiekk' 3 | print(wiekk) | ^ ``` Jak to czytać: -- `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. +- `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. -Komunikaty są po angielsku; tutaj znaczy to „nie ma zmiennej o nazwie 'wiekk'”. Po prostu literówka w `wiek`. +Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o nazwie 'wiekk'”. -### Częste pomyłki +### Częste błędy | Finch mówi | Co to znaczy | Jak naprawić | |---|---|---| -| `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 `=` | +| `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 `=` | | `'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 linii | Rozdziel je Enterem | +| `put each statement on its own line` | Dwa polecenia w jednej linijce | Wciśnij Enter między nimi | | `this '{' is never closed` | Brakuje `}` | Dopisz `}` | -| `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) | +| `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) | ### Rzeczy, które mogą się nie udać: `or` i `try` -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ć: +Niektóre rzeczy mogą pójść źle *w trakcie działania programu*: ktoś wpisze `abc` zamiast liczby +albo pliku nie ma. Zamiast zatrzymywać program, możesz powiedzieć, co wtedy zrobić, słowem **`or`** („albo”): ```c fn main() { - wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0 + 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 nie ma notatki)" + notatka := read_file("notatka.txt") or "(jeszcze brak notatki)" print(notatka) } ``` -Po `or` może też stać blok. W jego środku `err` to komunikat, który mówi, co poszło nie tak: +Po `or` może też być blok. W środku `err` to komunikat, co poszło nie tak: ```c fn main() { while true { - n := int(input("Wybierz liczbę: ")) or { + n := int(input("Podaj liczbę: ")) or { print("To nie jest liczba, spróbuj jeszcze raz") continue } - print("Wybrana liczba:", n) + print("Wybrałeś", n) break } } ``` -Twoje własne funkcje też mogą się nie udać. Postaw `!` po typie wyniku i użyj `return error("...")`: +Twoje własne funkcje też mogą się nie udać. Dopisz `!` po typie wyniku i użyj `return error("...")`: ```c fn bezpieczne_dzielenie(int a, int b) -> int! { @@ -817,15 +825,15 @@ fn main() { } ``` -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 +Finch nie pozwoli ci zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który +przypomina, żeby to obsłużyć. (Można też przekazać problem dalej przez `try`; wyjaśnia to [przewodnik dla techników](dla-technikow.md#błędy-jako-wartości).) --- ## 17. Małe projekty do zrobienia -Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero wtedy, gdy utkniesz. +Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero, gdy utkniesz. ### Projekt 1: Tabliczka mnożenia @@ -844,7 +852,7 @@ fn main() { ### Projekt 2: Parzysta czy nieparzysta -Dla każdej liczby od 1 do 10 pokaż, czy jest parzysta, czy nieparzysta. +Dla każdej liczby od 1 do 10 napisz, czy jest parzysta, czy nieparzysta.
Rozwiązanie @@ -863,7 +871,7 @@ fn main() { ### Projekt 3: Stopnie Celsjusza na Fahrenheita -Pokaż tabelę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`. +Pokaż tabelkę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`.
Rozwiązanie @@ -885,8 +893,8 @@ fn main() { ### Projekt 4: Fizz Buzz -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ę. +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ę.
Rozwiązanie @@ -909,7 +917,7 @@ fn main() { ### Projekt 5: Największa z trzech -Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która zwraca największą liczbę. +Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która oddaje największą z trzech liczb.
Rozwiązanie @@ -932,8 +940,7 @@ fn main() { ### Projekt 6: Zgadnij liczbę -Komputer wybiera tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem -mówi „więcej” albo „mniej”. +Komputer ma tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem mówi „więcej” albo „mniej”.
Rozwiązanie @@ -949,7 +956,7 @@ fn main() { } else if strzal > tajna { print("Mniej!") } else { - print("Tak! Liczba prób:", proby) + print("Tak! Potrzebowałeś", proby, "prób") return } } @@ -961,7 +968,7 @@ fn main() { ### Projekt 7: Średnia ocen -Zapytaj o 3 oceny, zapisz je na liście i pokaż listę oraz średnią. +Zapytaj o 3 oceny, zapisz je na liście, a potem pokaż listę i średnią.
Rozwiązanie @@ -991,9 +998,9 @@ fn main() { fn main() { // tu zaczyna się program print("tekst", 42) // pokaż coś - x := 5 // nowe pudełko (typ rozpoznany sam) + x := 5 // nowe pudełko (typ zgadnięty) int y = 10 // nowe pudełko (typ podany) - x = 7 // zmiana zawartości pudełka + x = 7 // zmień zawartość x += 1 // dodaj 1 do x if x > 5 { // decyzja @@ -1013,49 +1020,72 @@ fn main() { // tu zaczyna się program } } -fn dodaj(int a, int b) -> int { // twoje własne polecenie +fn dodaj(int a, int b) -> int { // własne polecenie return a + b } -struct Zwierzak { // twój własny rodzaj wartości +struct Zwierzak { // 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, w 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, lata in wiek { } +// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x") // pytanie: imie := input("Imię? ") -// pliki: write_file("p.txt", tekst) read_file("p.txt") -// metody: fn Zwierzak.urosnij() { self.wiek += 1 } z.urosnij() +// 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łowa, które będziesz słyszeć +## 19. Słowniczek | Słowo | Znaczenie | |---|---| | **program** | Lista kroków dla komputera | | **kod** | Tekst programu | | **uruchomić** | Kazać komputerowi wykonać kroki | -| **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ść | +| **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ść | | **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 typu tak/nie, np. `x > 5` | +| **warunek** | Pytanie tak/nie, np. `x > 5` | | **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` | -| **indeks** | Miejsce wartości na liście, liczone od 0 | -| **struktura** | Twój własny rodzaj wartości z nazwanych części | +| **indeks** | Pozycja wartości na liście, liczona od 0 | +| **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części | | **metoda** | Polecenie należące do struktury, np. `g.trafiony()` | -| **mapa** | 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 | +| **mapa** (map) | Wartości znajdowane po kluczu (nazwie), jak w słowniku | +| **wejście** (input) | To, co użytkownik wpisuje do programu | +| **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie | | **bug** | Pomyłka w programie | +### 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 będzie już łatwe, przejdź do +**Co dalej?** Kiedy to wszystko wyda ci się ł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 1285f4a..4ebceba 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ą 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). +**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). > 🇬🇧 English version: [for-technicians.md](../en/for-technicians.md) @@ -18,7 +18,7 @@ i rozwiązywanie typowych problemów. Nie opisuje, jak kompilator działa od śr 5. [Zmienne](#5-zmienne) 6. [Typy](#6-typy) 7. [Operatory](#7-operatory) -8. [Sterowanie](#8-sterowanie) +8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem) 9. [Funkcje](#9-funkcje) 10. [Tablice i mapy](#10-tablice-i-mapy) 11. [Tekst (str)](#11-tekst-str) @@ -27,12 +27,12 @@ i rozwiązywanie typowych problemów. Nie opisuje, jak kompilator działa od śr 14. [Wskaźniki](#14-wskaźniki) 15. [Funkcje wbudowane](#15-funkcje-wbudowane) 16. [Moduły](#16-moduły) -17. [Biblioteki C](#17-biblioteki-c) +17. [Biblioteki z C](#17-biblioteki-z-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. [Układ repozytorium i testy](#22-układ-repozytorium-i-testy) +22. [Struktura projektu i testy](#22-struktura-projektu-i-testy) 23. [Enumy i match](#23-enumy-i-match) 24. [Typy generyczne](#24-typy-generyczne) 25. [Biblioteka standardowa](#25-biblioteka-standardowa) @@ -41,111 +41,82 @@ i rozwiązywanie typowych problemów. Nie opisuje, jak kompilator działa od śr 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 **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. +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. -Co z tego wynika: +Co to daje: -- **Szybkość bliska C.** Nie ma interpretera, maszyny wirtualnej ani garbage collectora. Sprawdzanie zakresów tablic - i inne zabezpieczenia po optymalizacji prawie nic nie kosztują. +- **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ą. - **Małe, samodzielne programy**, które można skopiować i uruchomić. -- **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. +- **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. -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. +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ć. --- ## 2. Instalacja -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 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. -### Sposób A: gotowa wersja +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`. -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). +### Opcja A: gotowa wersja (Linux) -| 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: +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: | 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 naraz | +| Nix | `nix-shell` w repozytorium daje wszystko | -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 +### Opcja B: zbuduj sam ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # Linux: clang, libclang, pkg-config (albo zainstaluj je z dystrybucji) +nix-shell # clang, libclang, pkg-config (albo zainstaluj z dystrybucji) bootstrap/build.sh # build/finch ./build/finch version ``` -`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 -``` +`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`. ### Budowanie na inny system -`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C tamtego systemu: +`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C dla tego systemu: -| `--target` | Buduje | Używany kompilator C | `finch run` uruchamia przez | +| `--target` | Buduje | Kompilator C | `finch run` używa | |---|---|---|---| -| `windows` | plik `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine | +| `windows` | `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine | | `arm64` | program dla Linuksa ARM64 | `aarch64-linux-gnu-gcc` | `qemu-aarch64` | | `linux` | program dla Linuksa x86-64 | `clang --target=…` | | -| `macos` | program dla macOS (na Macu: dla drugiego rodzaju procesora) | `clang --target=…` | | +| `macos` | program dla macOS (na Macu: dla drugiego procesora) | `clang --target=…` | | | dowolny triple LLVM | np. `aarch64-unknown-linux-gnu` | `clang --target=` | | ```sh finch build gra.fch --target windows # gra.exe -finch run gra.fch --target arm64 # buduje dla ARM64 i uruchamia przez QEMU +finch run gra.fch --target arm64 # buduje na ARM64 i uruchamia w QEMU FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build narzedzie.fch --target arm64 # inna nazwa kompilatora ``` @@ -160,44 +131,41 @@ bootstrap/install.sh ~/.local # albo do własnego katalogu finch version ``` -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. +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. --- ## 3. Polecenie finch ``` -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ę +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ę opcje: - -l dolinkuj bibliotekę C, to samo co link "bib" w pliku - -I szukaj nagłówków C także tam + -l dolinkuj bibliotekę C, to samo co link "lib" w pliku + -I szukaj nagłówków C także tam --target zbuduj na inny system: windows, linux, arm64, macos, android albo triple LLVM - --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) + -g dodaj informacje dla debuggera (gdb / lldb / Visual Studio) -O0 bez optymalizacji - --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) + --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) ``` -W katalogu z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku. +W folderze z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku. -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ę. +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ę. -`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. +`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. Przykłady: @@ -205,10 +173,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 do debuggera +finch build gra.fch -g -O0 # wersja dla debuggera ``` -`finch run` przekazuje kod wyjścia twojego programu. Jeśli program się wysypie, mówi jak +`finch run` przekazuje dalej kod wyjścia programu. Jeśli program się wysypie, powie jak (np. `the program crashed: Segmentation fault`). --- @@ -218,13 +186,13 @@ finch build gra.fch -g -O0 # wersja do debuggera ```c // komentarz zaczyna się od dwóch ukośników /* albo zajmuje - kilka linii */ + kilka linijek */ -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) +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) -struct Punkt { // twoje własne typy +struct Punkt { // własne typy int x int y } @@ -236,32 +204,32 @@ fn main() { // tu zaczyna się program Zasady, które warto znać: -- **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. +- **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. - `main` to `fn main()`, `fn main() -> int` (zwraca kod wyjścia) albo `fn main([]str args)` - (dostaje argumenty z wiersza poleceń; `args[0]` to ścieżka samego programu). + (dostaje argumenty z linii poleceń; `args[0]` to ścieżka samego programu). --- ## 5. Zmienne ```c -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) +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) Punkt p // struktura bez wartości: zera albo wartości domyślne struktury x = 7 // przypisanie x += 1 // także -= *= /= %= ``` -- 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. +- 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. 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` @@ -275,8 +243,8 @@ const f32 GRAWITACJA = 9.8 // z typem const TYTUL = "Moja gra" ``` -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`. +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`. --- @@ -286,18 +254,18 @@ Stałą zapisuje się na najwyższym poziomie pliku, obok funkcji, i nie da się | Typ | Przechowuje | Przykład | |---------|-------------|----------| -| `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` | +| `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` | | `bool` | `true` albo `false` | | | `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` | | `str` | tekst | `"cześć\tświecie"` | -| `[]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]` | +| `[]T` | lista (tablica) elementów `T` | `[1, 2, 3]` | +| `map[K]V` | wartości typu `V` znajdowane po kluczu typu `K` | `["a": 1, "b": 2]` | | twoje struktury | nazwane grupy wartości | `Punkt(1, 2)` | -Znaki specjalne w tekstach i znakach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`. +Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`. -### Typy o podanym rozmiarze (do plików, sprzętu, bibliotek C) +### Typy z rozmiarem (do plików, sprzętu, bibliotek C) | Ze znakiem | Zakres | Bez znaku | Zakres | |---|---|---|---| @@ -306,9 +274,7 @@ Znaki specjalne w tekstach i znakach: `\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 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). +`f32` to 32-bitowa liczba z przecinkiem (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16). ### Zamiana typów @@ -316,27 +282,27 @@ Finch zamienia typy sam **tylko wtedy, gdy nic nie może zginąć**: | Z | Na | Automatycznie? | |---|---|---| -| mniejsza liczba całkowita | większa o tym samym znaku (`i8`→`i32`, `u8`→`u64`) | ✅ | -| bez znaku | większa ze znakiem (`u8`→`i16`, `u32`→`i64`) | ✅ | +| 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`) | ✅ | | dowolna liczba całkowita | `float` / `f32` | ✅ | | `f32` | `f64` | ✅ | | `null` | dowolny wskaźnik | ✅ | -| dowolny wskaźnik | `ptr` i z `ptr` na dowolny wskaźnik | ✅ | -| wszystko inne | | ❌ trzeba napisać: `typ(wartosc)` | +| dowolny wskaźnik | `ptr`, a `ptr` na dowolny wskaźnik | ✅ | +| wszystko inne | | ❌ napisz to jawnie: `typ(wartość)` | -**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. +**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. -**Jawne zamiany** zapisuje się nazwą typu, jak wywołanie funkcji: +**Jawna zamiana** wygląda jak wywołanie funkcji o nazwie typu: ```c -int(3.99) // 3 (ucina część ułamkową) +int(3.99) // 3 (obcina część ułamkową) float(7) / 2 // 3.5 -u8(300) // 44 (zostaje 8 najniższych bitów) +u8(300) // 44 (zostawia najniższe 8 bitów) int('A') // 65 char(66) // 'B' bool(0) // false (każda liczba różna od zera to true) -str(42) // "42" (także liczby zmiennoprzecinkowe, bool i znaki; str([]u8) zamienia bajty na tekst) +str(42) // "42" (też float, bool, char; 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 @@ -345,43 +311,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. -Obliczenia odbywają się w typie użytych wartości, więc `u8(250) + 10` to `4`, a nie `260`. - -### Liczby zmiennoprzecinkowe: co jest gwarantowane - -`f32` i `f64` to liczby w standardzie IEEE 754, a Finch liczy je **dokładnie tak, jak są zapisane**, tak samo na każdym -procesorze i z każdym `--cpu`: - -- Każde działanie zaokrągla raz, w zapisanej kolejności. `a * b - c * d` to dwa mnożenia i odejmowanie, nigdy nie - złożone w jedno „mnożenie z dodawaniem” (FMA); kompilatory C często robią to domyślnie, co zmienia ostatnie cyfry. -- Nic nie jest przestawiane ani upraszczane: `(a + b) + c` nie zamienia się w `a + (b + c)`, a `x * 0.0` nie jest - z góry uznawane za 0 (gdy `x` to NaN, wynik też jest NaN). Jedyny wyjątek to `v.sum()` na wektorze, które dodaje - elementy w najszybszej kolejności. - -Gdy szybkość jest ważniejsza niż ostatni bit, poproś o nią wprost: - -| | | -|---|---| -| `fma(a, b, c)` | `a * b + c` z jednym zaokrągleniem, zawsze. Jedna instrukcja tam, gdzie procesor ma FMA (z `--cpu`, na ARM64); gdzie indziej wolniejsza funkcja biblioteczna. | -| `muladd(a, b, c)` | `a * b + c` najszybciej, jak się da na tym procesorze: jedną instrukcją, gdzie można, a gdzie nie, mnożeniem i dodawaniem. | - -Obie działają na liczbach i na wektorach (`v.fma(b, c)`, `v.muladd(b, c)`). - -**Liczby jako bity.** Formaty plików i wagi modeli zapisują liczby jako surowe bity. Te funkcje patrzą na bity bez -przeliczania wartości (wszystkie działają też na wektorach, element po elemencie): - -| | | -|---|---| -| `f32_bits(x)` / `f32_from_bits(u)` | `f32` ↔ `u32` z tymi samymi bitami (`f32_bits(1.0)` to `0x3f800000`) | -| `f64_bits(x)` / `f64_from_bits(u)` | `f64` ↔ `u64` | -| `f16_to_f32(h)` / `f32_to_f16(x)` | liczba połówkowej precyzji, trzymana w `u16`, ↔ `f32` (zaokrąglenie do najbliższej) | -| `bf16_to_f32(b)` / `f32_to_bf16(x)` | bfloat16, trzymany w `u16`, ↔ `f32` (zaokrąglenie do najbliższej, remisy do parzystej) | - -```c -[]u16 wagi = wczytaj_wagi() // liczby f16 z pliku -v := f16_to_f32(u16x8(wagi, 0)) // osiem z nich jako f32, naraz -``` +**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`. --- @@ -391,13 +322,13 @@ v := f16_to_f32(u16x8(wagi, 0)) // osiem z nich jako f32, naraz | Priorytet | Operatory | Znaczenie | |---|---|---| -| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcie w lewo/prawo, bitowe AND | -| 4 | `+` `-` `\|` `^` | dodawanie (i łączenie tekstów), odejmowanie, bitowe OR, bitowe XOR | +| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcia bitów, bitowe AND | +| 4 | `+` `-` `\|` `^` | dodawanie (i sklejanie tekstu), odejmowanie, bitowe OR, bitowe XOR | | 3 | `==` `!=` `<` `<=` `>` `>=` | porównania | -| 2 | `&&` | logiczne AND | -| 1 | `\|\|` | logiczne OR | +| 2 | `&&` | logiczne I | +| 1 | `\|\|` | logiczne LUB | -Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NOT, `~x` odwrócenie wszystkich bitów. +Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NIE, `~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`. @@ -405,20 +336,20 @@ Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NOT, `~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 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. +- 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. --- -## 8. Sterowanie +## 8. Sterowanie przebiegiem -`if` może też dawać wartość; każda gałąź zawiera jedną wartość, a `else` jest obowiązkowe: +`if` może też dawać wartość; każda gałąź to jedna wartość, a `else` jest wymagane: ```c -ocena := if wynik > 100 { "wysoki" } else if wynik > 10 { "średni" } else { "niski" } +opis := if wynik > 100 { "dużo" } else if wynik > 10 { "średnio" } else { "mało" } f32 predkosc = if biegnie { 2.5 } else { 1.0 } ``` @@ -435,12 +366,12 @@ while licznik > 0 { licznik -= 1 } -for i in 0..10 { // i = 0, 1, …, 9 (koniec nie wchodzi) +for i in 0..10 { // i = 0, 1, …, 9 (koniec nie jest wliczany) if i == 3 { continue } if i == 8 { break } } -for imie in imiona { // każdy element tablicy (albo każdy znak tekstu) +for imie in imiona { // każdy element tablicy (albo każdy znak str) print(imie) } @@ -448,30 +379,31 @@ 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ą 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. +- 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. - `while true { ... }` kręci się do `break` albo `return`. -- Kod po `return`, `break` czy `continue` w tym samym bloku to błąd, bo nigdy się nie wykona. +- Kod po `return`, `break` lub `continue` w tym samym bloku to błąd, bo nigdy się nie wykona. ### defer -`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: +`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: ```c f := fopen("dane.txt", "r") defer fclose(f) -// ... używaj f; zostanie zamknięty na końcu bloku, którąkolwiek drogą program z niego wyjdzie +// ... używaj f; zostanie zamknięty na końcu bloku, na każdej ścieżce ``` -Odroczona instrukcja nie może zawierać `return`, `break` ani `continue`. +Instrukcja w `defer` nie może zawierać `return`, `break` ani `continue`. --- @@ -483,19 +415,19 @@ fn nazwa(typ1 param1, typ2 param2) -> typ_wyniku { return wartosc } -fn bez_wyniku(str komunikat) { // bez "->": niczego nie zwraca - print(komunikat) +fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca + print(wiadomosc) } ``` -- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej wywołującego. +- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej u 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: 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. + Żeby zmienić zmienną wywołującego, przekaż wskaźnik, zobacz [Wskaźniki](#14-wskaźniki). +- Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza. +- Funkcji nie można definiować wewnątrz innej funkcji. +- Funkcję, która może się nie udać, zapisuje się `-> int!` (albo `-> !` bez wyniku); zobacz [Błędy jako wartości](#błędy-jako-wartości). -- Długie wywołania można rozbić na kilka linii w nawiasach; przecinek po ostatnim argumencie jest dozwolony. +- Długie wywołania możesz rozbić na kilka linijek wewnątrz nawiasów; przecinek po ostatnim argumencie jest dozwolony. --- @@ -508,20 +440,20 @@ siatka := [[1, 2], [3, 4]] // tablice tablic []f32 xs = [1, 2.5] // typ decyduje, czym staną się liczby ``` -| Działanie | Znaczenie | +| Operacja | Znaczenie | |---|---| | `a.len` | liczba elementów | -| `a[i]` | element `i` (licząc od 0); indeks poza zakresem zatrzymuje program jasnym komunikatem | +| `a[i]` | element `i` (od 0); poza zakresem program zatrzymuje się z czytelnym komunikatem | | `a[i] = x` | zmiana elementu | -| `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.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.sort()`, `a.reverse()` | w miejscu (liczby, znaki, str) | -| `a.slice(p, k)` | nowa tablica z elementami `p` … `k-1` | +| `a.slice(s, e)` | nowa tablica z elementami `s` … `e-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]` | @@ -530,23 +462,24 @@ siatka := [[1, 2], [3, 4]] // tablice tablic ### Mapy -`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. +`map[K]V` znajduje wartości po kluczu. Kluczem może być liczba całkowita, `char`, `bool` albo `str`; +wartością cokolwiek (tablice, struktury, inne mapy). Przechodzenie pętlą idzie w kolejności, w jakiej +klucze zostały dodane po raz pierwszy. ```c -wiek := ["anna": 31, "bob": 25] // mapa zapisana wprost: map[str]int +wiek := ["anna": 31, "bob": 25] // literał mapy: map[str]int map[str][]str grupy // pusta mapa map[int]str nazwy = [:] // [:] to pusta mapa tam, gdzie typ jest znany ``` -| Działanie | Znaczenie | +| Operacja | Znaczenie | |---|---| | `m[k]` | wartość dla `k`; brak klucza zatrzymuje program komunikatem `the key "k" is not in the map` | -| `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[k] = v` | dodaj albo zastąp | +| `m[k] += 1`, `m[k].push(x)`, `m[k].pole = …` | zmiana brakującego klucza najpierw go dodaje, z wartością domyślną typu (0, `""`, `[]`, …) | | `m.len` | liczba kluczy | -| `m.has(k)` | czy `k` tam jest? | -| `m.get(k, domyslna)` | wartość albo `domyslna` (obliczana tylko wtedy, gdy trzeba) | +| `m.has(k)` | czy `k` jest w mapie? | +| `m.get(k, domyslna)` | wartość albo `domyslna` (liczona tylko w razie potrzeby) | | `m.remove(k)` | usuwa `k`; `true`, jeśli był | | `m.clear()` | usuwa wszystko | | `m.keys()`, `m.values()` | nowe tablice, w kolejności dodawania | @@ -562,9 +495,9 @@ for slowo, n in ile { } ``` -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. +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. --- @@ -572,37 +505,37 @@ i wartości, kopiują się przy przypisaniu i zwalniają na końcu bloku. Nie do ```c s := "Cześć" -s += ", świecie" // łączenie -print(s.len, s[0]) // 17 C (.len liczy bajty; ą, ś, ż, … zajmują po 2) -s[0] = 'K' // zmiana znaku +s += ", świecie" // sklejanie +print(s.len, s[0]) // .len liczy bajty; ą, ś, ż… zajmują po 2 +s[0] = 'D' // zmiana znaku ``` -| Metoda | Zwraca | +| Metoda | Oddaje | |---|---| | `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 znaków nowej linii na obu końcach | -| `s.upper()`, `s.lower()` | zamienione A–Z / a–z (litery ASCII) | +| `s.trim()` | bez spacji i nowych linii na obu końcach | +| `s.upper()`, `s.lower()` | zmienione litery A–Z / a–z (tylko ASCII) | | `s.replace(a, b)` | każde `a` zamienione na `b` | -| `s.repeat(n)` | `s` powtórzone n razy | +| `s.repeat(n)` | `s` n razy | | `s.bytes()` | `[]u8` z bajtami | -| `s.ptr` | `ptr[char]` dla C (ważny, dopóki `s` istnieje) | +| `s.ptr` | `ptr[char]` dla C (ważny, dopóki żyje `s`) | -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)"`. +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)"`. ```c print("hp \(hp) z \(max_hp), prędkość \(v, 2), imię \(p.imie.upper())") ``` -`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. +`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. -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: +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: ```c shader := """ @@ -611,8 +544,8 @@ shader := """ """ ``` -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`). +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`). --- @@ -623,27 +556,27 @@ struct Gracz { str imie int zycia = 3 // wartość domyślna []int wyniki - Punkt poz // struktury w strukturach + Punkt pozycja // struktury w strukturach } -p := Gracz(imie: "Ola") // przez nazwy: brakujące pola dostają wartość domyślną (albo zero) +g := Gracz(imie: "Ola") // po nazwie: brakujące pola dostają domyślne (albo zero) q := Punkt(1, 2) // po kolei: wtedy trzeba podać każde pole -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)) +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)) ``` -- 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). +- 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). ### 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, a 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, nie kopia: zmiany w `self` zostają. ```c -fn Gracz.ulecz(int ile) { +fn Gracz.wylecz(int ile) { self.zycia += ile } @@ -651,41 +584,41 @@ fn Gracz.zyje() -> bool { return self.zycia > 0 } -p := Gracz(imie: "Ola") -p.ulecz(2) // p.zycia wynosi teraz 5 -if p.zyje() { ... } +g := Gracz(imie: "Ola") +g.wylecz(2) // g.zycia wynosi teraz 5 +if g.zyje() { ... } -druzyna[0].ulecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ... +druzyna[0].wylecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ... ``` -- 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 jest w tym samym pliku (module) co jej struktura i jest dostępna wszędzie, gdzie ta struktura. +- Metody można też dopisywać do **struktur z nagłówków C**: `fn Vector2.length() -> f32 { ... }`. - Metoda nie może nazywać się tak jak pole. -- Wywołanie metody zmieniającej `self` na zmiennej pętli (`for 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). +- 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). --- ## 13. Pamięć: kto co zwalnia -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`. +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`. -- 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. +- 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. -Dla pamięci, którą zarządzasz sam (listy wiązane, drzewa, współdzielenie między strukturami): +Do pamięci zarządzanej samodzielnie (listy wiązane, drzewa, dzielenie między strukturami): ```c -wezel := new(Wezel(wartosc: 1)) // wartość na stercie, dostajesz ptr[Wezel] -defer free(wezel) // free(...) oddaje pamięć (i zwalnia tablice i teksty w środku) +wezel := new(Wezel(wartosc: 1)) // wstaw wartość na stertę, dostajesz ptr[Wezel] +defer free(wezel) // free(...) ją oddaje (razem z jej tablicami i tekstami) ``` -Pamięć z C (`malloc`, obiekty bibliotek) zwalnia się funkcjami C, tak jak w C. +Pamięć z C (`malloc`, obiekty bibliotek) zwalniasz funkcjami z C, jak w C. --- @@ -699,8 +632,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 sprawdzania zakresu) | -| `NULL` | `null` | nigdzie nie wskazuje | +| `p[i]` | `p[i]` | indeksowanie jak w C (bez kontroli zakresu) | +| `NULL` | `null` | wskazuje donikąd | | `void *` | `ptr` | wskazuje na coś nieznanego typu | ```c @@ -709,14 +642,14 @@ fn podwoj(ptr[int] p) { } x := 21 -podwoj(addr(x)) // x wynosi 42 +podwoj(addr(x)) // x to 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** jasnym komunikatem. +Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program** z czytelnym komunikatem. -⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu bloku tej zmiennej. Finch tego nie sprawdza. +⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu jej bloku. Finch tego nie sprawdza. --- @@ -724,36 +657,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, 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ę. | +| `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ę. | | `file_exists(sciezka)` | `true` / `false` | | `delete_file(sciezka)` | Usuwa plik; `true`, jeśli się udało (albo przyczyna, z `or` / `try`). | -| `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. | +| `error(komunikat)` | Porażka, którą zwraca funkcja mogąca się nie udać: `return error("...")`. | +| `shell(polecenie)` | Uruchamia polecenie powłoki i oddaje jego kod wyjścia. | | `exit(kod)` | Natychmiast kończy program. | -| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (funkcję zwrotną). | -| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zob. [Pamięć](#13-pamięć-kto-co-zwalnia)). | -| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` z jednym zaokrągleniem / najszybciej (zob. [Liczby zmiennoprzecinkowe](#liczby-zmiennoprzecinkowe-co-jest-gwarantowane)). | -| `f32_bits(x)`, `f32_from_bits(u)`, `f16_to_f32(h)`, … | Bity liczby i liczby połówkowej precyzji (zob. [Liczby zmiennoprzecinkowe](#liczby-zmiennoprzecinkowe-co-jest-gwarantowane)). | -| `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ą. | +| `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ą. | --- ## 16. Moduły -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ę `:`). +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 `:`). ```c // ksztalty.fch -struct Pudelko { +struct Pudlo { float w float h } -fn pole(Pudelko p) -> float { +fn pole(Pudlo p) -> float { return p.w * p.h } ``` @@ -763,45 +694,45 @@ fn pole(Pudelko p) -> float { import ksztalty fn main() { - p := ksztalty.Pudelko(2, 3) + p := ksztalty.Pudlo(2, 3) print(ksztalty.pole(p)) - ksztalty.Pudelko inne // typ z modułu w deklaracji + ksztalty.Pudlo inne // typ z modułu w deklaracji } ``` -- Wszystkiego z modułu używa się z nazwą modułu na początku: `ksztalty.pole`, `ksztalty.Pudelko`. +- Wszystkiego z modułu używasz z jego nazwą z przodu: `ksztalty.pole`, `ksztalty.Pudlo`. - Moduł nie ma `main`. Moduły mogą importować inne moduły. -- 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. +- 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. --- -## 17. Biblioteki C +## 17. Biblioteki z C ### Krok 1: zaimportuj nagłówek ```c import "stdio.h" import "GLFW/glfw3.h" // ścieżka w systemowych katalogach nagłówków -import "mojabib.h" // nagłówek obok twojego pliku .fch +import "mojalib.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 ich polami, -- **wartości `enum`** i **proste stałe z `#define`** (`M_PI`, `GL_COLOR_BUFFER_BIT`), -- **zmienne globalne**, jak `stdout` i `stderr`. +- **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`. ### Krok 2: dolinkuj bibliotekę ```c -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 "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 "gotowe.a" // albo plik obiektowy / bibliotekę statyczną ``` -Jeśli zapomnisz o linii `link`, Finch powie dokładnie, czego brakuje: +Jeśli zapomnisz linijki `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 @@ -818,15 +749,15 @@ error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from " | `unsigned …` | `u16`, `u32`, `u64` | | `float`, `double` | `f32`, `f64` | | `_Bool` / `bool` | `bool` | -| `enum` | odpowiednia liczba całkowita ze znakiem | +| `enum` | odpowiadająca liczba całkowita ze znakiem | | `char *` jako **parametr albo wynik** | `str` | -| `char *` w strukturze albo za wskaźnikiem | `ptr[char]` (czyta się go przez `str(p)`) | +| `char *` w strukturze albo za wskaźnikiem | `ptr[char]` (czytasz przez `str(p)`) | | `struct X` przez wartość | `X`, struktura z tymi samymi polami | | `struct X *`, `int *`, … | `ptr[X]`, `ptr[i32]`, … | -| `int tab[16]` w strukturze | `[16]i32`: można go indeksować i użyć `.len` | +| `int arr[16]` w strukturze | `[16]i32`: indeksujesz i używasz `.len` | | `void *`, wskaźniki na funkcje, niekompletne struktury | `ptr` | -### Przykład: struktury i funkcje zwrotne +### Przykład: struktury i raylib ```c import "raylib.h" @@ -844,15 +775,15 @@ fn main() { } ``` -Żeby dać C funkcję do wywołania (jak funkcję porównującą dla `qsort`), użyj `addr(mojaFunkcja)`. +Żeby dać C funkcję do wywołania zwrotnego (np. porównywarkę 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ą zadeklarowane tylko - wtedy, gdy zdefiniowane jest `GL_GLEXT_PROTOTYPES`. Finch nie definiuje makr C, więc umieść te `#include` w malutkim +- **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 nagłówku obok programu i zaimportuj go: ```c @@ -863,21 +794,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 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). + 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). -### Czego jeszcze nie ma +### Co jeszcze nie działa -- **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. +- **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. --- @@ -885,12 +816,12 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k ### Błędy jako wartości -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")`: +Niektóre porażki są normalne: użytkownik wpisze `abc`, pliku nie ma. Finch obsługuje je bez wyjątków +i bez ukrytych skoków. Funkcja, która może się nie udać, mówi to znakiem `!` po typie wyniku i kończy się +porażką przez `return error("komunikat")`: ```c -fn parsuj_port(str tekst) -> int! { +fn port_z_tekstu(str tekst) -> int! { n := int(tekst) or { return error("'" + tekst + "' to nie liczba") } if n < 1 || n > 65535 { return error("port " + str(n) + " jest poza zakresem") @@ -898,23 +829,23 @@ fn parsuj_port(str tekst) -> int! { return n } -fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, niczego nie zwraca +fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, nic nie oddaje try write_file(sciezka, tekst) } ``` -Wywołujący **musi** obsłużyć niepowodzenie, na jeden z trzech sposobów (zapomnienie to błąd kompilacji): +Wywołujący **musi** obsłużyć porażkę, na jeden z trzech sposobów (zapomnienie to błąd kompilacji): -| Kod | Gdy się nie uda | +| Kod | W razie porażki | |---|---| -| `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ć) | +| `port := port_z_tekstu(s) or 8080` | użyj tej wartości (liczonej tylko przy porażce; może to być kolejne wywołanie z `or`) | +| `port := port_z_tekstu(s) or { print(err); return }` | wykonaj blok; `err` to komunikat (`str`). Blok musi wyjść (`return`, `break`, `continue`, `exit`), chyba że wynik nie jest używany. | +| `port := try port_z_tekstu(s)` | zakończ bieżącą funkcję tym samym błędem (musi też móc się nie udać) | ```c fn wczytaj(str sciezka) -> Konfig! { tekst := try read_file(sciezka) - port := try parsuj_port(tekst.trim()) + port := try port_z_tekstu(tekst.trim()) return Konfig(port: port) } @@ -924,11 +855,12 @@ fn main() -> ! { // main też może się nie udać: błąd zostan } ``` -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`). +Funkcje wbudowane, które mogą się nie udać, działają tak samo: `int(tekst)`, `float(tekst)`, `read_file`, +`write_file`, `delete_file`. Bez `or` / `try` zachowują się jak wcześniej (`int("x")` zatrzymuje program, +`write_file` oddaje `bool`). -Pod spodem 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. +Pod spodem funkcja, która może się nie udać, zwraca wartość z flagą i komunikatem: bez alokacji przy +sukcesie, bez zwijania stosu, a porażka kosztuje tyle, co zbudowanie komunikatu. ### Błędy kompilacji @@ -938,11 +870,12 @@ gra.fch:12:9: error: 'wynik' must be int, but this is str | ^ ``` -Format: `plik:linia:kolumna: error: wyjaśnienie`, a pod spodem linia z `^` pod miejscem błędu. +Format: `plik:linijka:kolumna: error: wyjaśnienie`, a pod spodem linijka ze znakiem `^`. +Komunikaty są po angielsku. ### Błędy w trakcie działania -Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje program z komunikatem i kodem wyjścia 1: +Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikatem i kodem wyjścia 1: | Komunikat | Przyczyna | |---|---| @@ -954,29 +887,29 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje program z komuni | `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 `ptr[T]` równy `null` | +| `runtime error: called .f() on a null pointer` | metoda wywołana przez `null` w `ptr[T]` | | `runtime error: assert failed: …` | `assert(warunek, "…")` z fałszywym warunkiem | -Pod komunikatem jest ścieżka wywołań, które do niego doprowadziły, od najgłębszego: +Pod komunikatem jest łańcuch 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 Wrog.nastepny (gra.fch:6) - in krok (gra.fch:10) + in Enemy.next (gra.fch:6) + in step (gra.fch:10) in main (gra.fch:15) ``` -`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. +`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. --- ## 19. Edytory: VS Code, Kate i inne -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. +Wszystkie funkcje edytorów pochodzą z jednego serwera języka, `finch lsp`, który używa prawdziwego +kompilatora: błędy na bieżąco, typy i opisy po najechaniu myszą, idź do definicji, podpowiedzi (pola, +metody, metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), konspekt pliku i podpowiedzi +parametrów. ### Kate (oraz KWrite, KDevelop) @@ -984,20 +917,20 @@ metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), spis editors/kate/install.sh ``` -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ę +Instaluje podświetlanie (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) i dopisuje `finch` +do ustawień klienta LSP w Kate, zachowując twoje pozostałe serwery. Uruchom Kate ponownie i włącz wtyczkę **LSP Client**. Żeby uruchamiać programy, dodaj `finch run %f` jako cel we wtyczce **Build & Run**; komunikaty -`plik:linia:kolumna: error:` będą wtedy klikalne. Szczegóły: `editors/kate/README.md`. +Fincha `plik:linia:kolumna: error:` są wtedy klikalne. Szczegóły: `editors/kate/README.md`. ### VS Code Rozszerzenie **Finch** (`editors/vscode`, dołączane też do każdego wydania jako plik `.vsix`) daje: -podświetlanie, 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. +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. -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. +Instalacja: Rozszerzenia → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia +`finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach. ### Inne @@ -1011,14 +944,14 @@ finch build gra.fch -g -O0 -o gra gdb ./gra (gdb) break gra.fch:12 (gdb) run -(gdb) bt # gdzie jesteśmy i przez które funkcje +(gdb) bt # gdzie jesteśmy, przez które funkcje (gdb) info locals # wszystkie zmienne (gdb) print gracz.zycia ``` -`-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. +`-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. --- @@ -1027,51 +960,73 @@ albo gdb. | 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` | 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`). | +| `… 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. | | `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 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). | +| `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++*. | | 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 znajduje `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi stać przed `llvmPackages.libclang`. | +| `clang` w `nix-shell` nie widzi `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi być przed `llvmPackages.libclang`. | | Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. | --- -## 22. Układ repozytorium i testy +## 22. Struktura projektu i testy ``` Finch/ -├── 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 +├── 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 ├── tests/ -│ ├── 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/ 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.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem) -│ ├── 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 +│ ├── 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 ├── editors/ rozszerzenie VS Code (vscode/), podświetlanie i konfiguracja LSP dla Kate (kate/) -├── docs/ ta dokumentacja (en, pl), razem ze ściągą -└── shell.nix środowisko programistyczne dla Niksa +├── docs/ ta dokumentacja (en, pl), łącznie ze ściągą +└── shell.nix środowisko deweloperskie Nix ``` ```sh -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 +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 ``` +### 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 @@ -1080,22 +1035,22 @@ tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomi ``` enum Kolor { Czerwony, Zielony, Niebieski } -enum Status { Ok = 200, NotFound = 404 } // wariant może dostać liczbę, jeśli jej potrzebuje +enum Status { Ok = 200, Brak = 404 } // wariantowi można nadać numer k := Kolor.Zielony print(k) // Zielony -print(int(Status.NotFound)) // 404 -print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzane: Kolor(7) to błąd) +print(int(Status.Brak)) // 404 +print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzana: Kolor(7) to błąd) ``` -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 { ... }`. +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 { ... }`. -**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 match na enumie wystarczy sama nazwa + Czerwony -> print("ciepły") // w matchu na enumie wystarczy sama nazwa Zielony, Niebieski -> print("zimny") // kilka wartości w jednej gałęzi } @@ -1110,41 +1065,17 @@ match slowo { print("pa") return } - else -> print("słucham?") + else -> print("co?") } ``` -`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. +`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. ## 24. Typy generyczne -Funkcję albo strukturę można napisać raz dla dowolnego typu, podając typ jako nazwę w nawiasach kwadratowych: +Napisz funkcję lub strukturę raz dla dowolnego typu, z typem jako nazwą w nawiasach kwadratowych: ``` fn max[T](T a, T b) -> T { @@ -1158,72 +1089,73 @@ struct Stos[T] { []T elementy } -fn Stos[T].push(T x) { +fn Stos[T].dodaj(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 jest podany, gdy nie da się go ustalić -s := Stos[int]() // struktura generyczna zawsze ma podany typ -s.push(7) +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) ``` -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])`. +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])`. ## 25. Biblioteka standardowa -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ł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ł | Co robi | |---|---| -| `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) | +| `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) | | `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, lo, hi)` działają na dowolnych liczbach (i wektorach) -bez importu (twoja funkcja o tej samej nazwie ma pierwszeństwo). +`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). ``` 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 wywołań się nie wysypie +nazwa := dok.get("name").str_or("bez nazwy") // brakujący klucz to null, więc łańcuch nigdy się nie psuje dok.set("name", json.new_text(nazwa + "!")) write_file(path.join("out", "config.json"), json.pretty(dok)) ``` ## 26. Obecne ograniczenia -Czego jeszcze nie ma: +Jeszcze nie ma: - Interfejsów. -- 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. +- 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. ### Ostrzeżenia o dużych kopiach -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: +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: ``` -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 +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 ``` -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ą +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ą ostrzeżenia na żółto. -Zob. [plan rozwoju w README](../../README.pl.md#plan-rozwoju). +Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju). ## 27. Testy: bloki test i finch test @@ -1240,35 +1172,33 @@ test "obrażenia nigdy nie schodzą poniżej zera" { } ``` -`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. +`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. ``` ok obrażenia nigdy nie schodzą poniżej zera -FAIL wczytywanie poziomu - poziom.fch:31: runtime error: can't read the file "poziomy/1.txt" +FAIL wczytanie poziomu + poziom.fch:31: runtime error: can't read the file "levels/1.txt" 1 passed, 1 failed ``` -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. +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. ## 28. Projekt: finch.toml -Plik `finch.toml` obok kodu zapisuje raz to, co inaczej trzeba by podawać przy każdym poleceniu: +`finch.toml` obok kodu mówi raz to, co inaczej trzeba by podawać w każdym poleceniu: ```toml -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 +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 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"] @@ -1278,8 +1208,8 @@ libdirs = ["vendor/SDL3/lib/x64"] libdirs = ["vendor/SDL3/lib/arm64"] ``` -Ś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: +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: `[sekcje]`, `klucz = "tekst"`, `klucz = ["a", "b"]` (także w kilku liniach), liczby, `true`/`false` i komentarze `#`. ## 29. Wątki @@ -1292,10 +1222,10 @@ struct Kawalek { float szczyt } -fn analizuj(ptr arg) { // funkcja wątku przyjmuje jeden ptr +fn analizuj(ptr arg) { // funkcja wątku bierze jeden ptr ptr[Kawalek] k = arg - for p in k.probki { - k.szczyt = max(k.szczyt, abs(p)) + for s in k.probki { + k.szczyt = max(k.szczyt, abs(s)) } } @@ -1309,258 +1239,65 @@ fn main() { }) } for i in 0..dzialajace.len { - dzialajace[i].join() // czekamy na wszystkie + dzialajace[i].join() // poczekaj na wszystkie } } ``` -To, co dostaje wątek, jest współdzielone, a nie kopiowane, więc: +To, co dostaje wątek, jest współdzielone, nie kopiowane, więc: -- 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_...`. +- 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_...`. ```c m := thread.mutex() defer m.close() m.lock() -dziennik.push("gotowe") // między lock a unlock pracuje tylko jeden wątek naraz +dziennik.push("gotowe") // jeden wątek naraz między lock a unlock 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` 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. +`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. ## 30. C++, Rust i Android -Finch rozmawia z innymi językami przez C, tak jak każdy język. +Finch rozmawia z innymi językami przez C, jak każdy język. -**C++**: to, co ma wołać Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++: +**C++**: to, co woła Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++: ```cpp // glue.h #ifdef __cplusplus extern "C" { #endif -int krok_fizyki(float dt); +int physics_step(float dt); #ifdef __cplusplus } #endif ``` -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. +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. -**Rust**: zbuduj `staticlib` i wystaw funkcje C: +**Rust**: zrób `staticlib` i wystaw funkcje C: ```rust // Cargo.toml: [lib] crate-type = ["staticlib"] #[no_mangle] -pub extern "C" fn dlugosc(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() } +pub extern "C" fn path_len(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() } ``` -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"]`. +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"]`. -**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[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` z jednym zaokrągleniem, zawsze (tak samo na każdym procesorze) | -| `v.muladd(b, c)` | `v * b + c` najszybciej, jak się da: jedną instrukcją, gdzie procesor to potrafi (ostatni bit może się różnić między procesorami) | -| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … (albo przez `ptr`) | - -**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).muladd(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łą. - -### Maski: porównywanie, wybieranie, koniec listy - -Porównanie wektorów porównuje każdy element, więc odpowiedzią jest **maska**: jedno `true`/`false` na element, -typu `boolx8` (dla 8 elementów). - -```c -v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0) -m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true) -relu := select(m, v, 0.0) // v tam, gdzie maska jest prawdą, a gdzie indziej 0 -print(m.count(), m.any(), m.all()) // 4 true false -if (v == w).all() { ... } // warunek potrzebuje jednego bool: all() albo any() -``` - -| | | -|---|---| -| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | maska (zwykła liczba po jednej stronie działa na każdy element) | -| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, `b` tam, gdzie fałszem (każde z nich może być zwykłą liczbą) | -| `m.any()`, `m.all()`, `m.count()` | czy któryś element jest prawdą, czy wszystkie, ile ich jest | -| `m & n`, `m \| n`, `m ^ n`, `!m` | łączenie masek | -| `m[i]` | jeden element jako `bool` | -| `i32x8(m)`, `boolx8(v)` | prawda to 1; element różny od 0 to prawda | - -**Koniec listy.** Lista rzadko ma długość będącą wielokrotnością wektora. Dwie formy dotykają tylko elementów, -które są w liście: - -| | | -|---|---| -| `f32x8(lista, i, wypelnienie)` | `lista[i]` … tak daleko, jak sięga lista; elementy za jej końcem dostają `wypelnienie` | -| `v.store_part(lista, i)` | zapisuje tylko elementy, które mieszczą się w liście | - -```c -fn dodaj_jeden([]f32 xs) { - i := 0 - while i < xs.len { // bez osobnej pętli na resztę - (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i) - i += 8 - } -} -``` - -### Przestawianie elementów - -| | | -|---|---| -| `v.shuffle(3, 2, 1, 0)` | elementy o tych numerach (zapisanych w kodzie), w tej kolejności; tyle elementów, ile numerów | -| `v.lo()`, `v.hi()` | dolna i górna połowa | -| `concat(a, b)` | elementy `a`, a po nich elementy `b` | -| `tabela.lookup(idx)` | dla każdego elementu `idx`: `tabela[idx % tabela.len]`. Tabela bajtów (`u8x16`, `i8x16`) to jedno `pshufb` na x86 i `tbl` na ARM | - -Rozpakowanie liczb 4-bitowych (po dwie w bajcie, tak jak trzymają je skwantowane modele) to przestawienie, maska i tabela: - -```c -fn rozpakuj4(u8x8 spakowane, i8x16 znaczenie) -> i8x16 { - pary := spakowane.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // każdy bajt dwa razy - nieparzyste := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1 - polowki := select(nieparzyste, pary >> 4, pary & 15) // najpierw dolne 4 bity, potem górne - return znaczenie.lookup(polowki) // 0..15 -> to, co oznacza -} -``` - -### Iloczyn skalarny na bajtach: dot_add - -`acc.dot_add(a, b)` to krok mnożenia macierzy w skwantowanym modelu. `acc` to `i32x8` z sumami; `a` i `b` mają cztery -razy więcej bajtów (`u8x32` i `i8x32`; działają też `i8` × `i8` i `u8` × `u8`). Każdy element `acc` dostaje sumę czterech -iloczynów, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, dokładnie (bez nasycania), a sumy zostają w wektorze: - -```c -acc := i32x8(0) -i := 0 -while i + 32 <= wagi.len { - acc = acc.dot_add(u8x32(aktywacje, i), i8x32(wagi, i)) - i += 32 -} -suma := acc.sum() // raz, na końcu -``` - -To jedna instrukcja tam, gdzie procesor ją ma: `vpdpbusd` z AVX-VNNI (Intel od Alder Lake, AMD Zen 5) albo -AVX-512 VNNI, `usdot`/`sdot`/`udot` na ARM z rozszerzeniami do iloczynu skalarnego. Gdzie indziej to kilka instrukcji -(`pmaddwd` i dodawania), z tym samym wynikiem. Wybór zależy od `--cpu` albo `@cpu` funkcji (niżej). - -### Wybór instrukcji: --cpu, @cpu i cpu_has - -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). Są trzy sposoby, żeby użyć więcej: - -**`--cpu`, dla całego programu.** `--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, VNNI albo nowszych części NEON na ARM. Program potrzebuje wtedy -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 alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`. - -**`@cpu` dla jednej funkcji i `cpu_has` do wyboru w trakcie działania.** Jeden program może mieć wersję z AVX-512, -wersję z AVX2 i zwykłą, i wybrać najlepszą, jaką ma komputer, na którym działa: - -```c -@cpu("avx512f,avx512bw,avx512vl,avx512vnni") -fn mnoz_avx512([]u8 a, []i8 b, []i32 wynik) { ... } // może w środku używać AVX-512 - -@cpu("x86-64-v3") -fn mnoz_avx2([]u8 a, []i8 b, []i32 wynik) { ... } // poziom też działa - -fn mnoz([]u8 a, []i8 b, []i32 wynik) { - if cpu_has("avx512vnni") && cpu_has("avx512vl") { - mnoz_avx512(a, b, wynik) - } else if cpu_has("avx2") { - mnoz_avx2(a, b, wynik) - } else { - mnoz_zwykle(a, b, wynik) - } -} -``` - -- Rozszerzenia to dla x86 `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl - avx512vnni avx512bf16 avx512fp16 avxvnni`, poziomy `x86-64-v2`/`v3`/`v4`, a dla ARM `neon fp16 dotprod i8mm bf16 sve sve2`. -- Nazwy z drugiej architektury są przy budowaniu pomijane, więc wersje dla x86 i ARM mogą stać obok siebie w jednym pliku - (a `cpu_has` zwraca dla nich `false`). -- Funkcję z `@cpu` wołaj dopiero wtedy, gdy `cpu_has` odpowie „tak”: na procesorze bez tego rozszerzenia program - zatrzyma się z *Illegal instruction*. -- Wektora szerszego niż 16 bajtów (`f32x8`, `i32x16` …) nie da się przekazać między funkcjami o różnym `@cpu`: obie strony - oczekiwałyby go w innych rejestrach. Przekaż zamiast tego listę (albo `ptr`); Finch o tym powie, jeśli spróbujesz. - -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. - -Na NixOS nakładka Niksa na clanga usuwa `-march=native` w `nix-shell`. Finch mówi jej, żeby to zostawiła, więc -`--cpu native` działa i tam; gdyby kiedyś nie miało żadnego skutku, Finch ostrzeże. +**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. diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index 1d88ed0..cb4e7e7 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -1,6 +1,6 @@ -# Ściąga z Fincha +# Ściąga Fincha -Cały Finch na jednej stronie. Wyjaśnienia znajdziesz w [przewodnikach](../../README.pl.md#dokumentacja). +Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.pl.md#dokumentacja). > 🇬🇧 English version: [cheatsheet.md](../en/cheatsheet.md) @@ -8,41 +8,39 @@ Cały Finch na jednej stronie. Wyjaśnienia znajdziesz w [przewodnikach](../../R ## Polecenie `finch` -| Polecenie | Co robi | +| Polecenie | Robi | |---|---| -| `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 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 lsp` | serwer języka (korzystają z niego edytory) | | `finch version` | wersja | -| `-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) | +| `-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) | | `-O0` | bez optymalizacji | -| `--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) | +| `--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ść) | -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. +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ą. -`finch.toml` (obok kodu; wtedy `finch run` nie potrzebuje ani nazwy pliku, ani opcji): +`finch.toml` (obok kodu; `finch run` w tym folderze nie potrzebuje pliku ani flag): ```toml -name = "gra" # co zbuduje finch build +name = "gra" # co robi finch build entry = "src/main.fch" -paths = ["src"] # katalogi z modułami +paths = ["src"] # foldery z modułami include = ["vendor/SDL3/include"] # nagłówki C -links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a +links = ["SDL3", "native/glue.cpp"] # biblioteki i własne .c / .cpp / .o / .a libdirs = ["vendor/SDL3/lib"] -cflags = ["-DFAST"] # dla twoich plików .c / .cpp +cflags = ["-DFAST"] # dla własnych plików .c / .cpp ldflags = ["-Wl,-rpath,."] -cpu = "native" # to samo co --cpu native -[target.windows] # tylko przy budowaniu na ten system (są też linux, macos, android, linux-arm64) +[target.windows] # tylko przy budowaniu dla tego systemu (też linux, macos, android, linux-arm64) links = ["ws2_32"] ``` @@ -65,8 +63,8 @@ fn main() { } ``` -`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. +`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. --- @@ -74,12 +72,12 @@ Jedna instrukcja w linii, bez średników, `{` w tej samej linii, warunki bez na | Kod | Znaczenie | |---|---| -| `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 := 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 = 7` | przypisanie | | `x += 1` `-=` `*=` `/=` `%=` | zmiana | -| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; z modułu: `ksztalty.W`) | +| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; `ksztalty.W` z modułu) | --- @@ -89,16 +87,15 @@ Jedna instrukcja w linii, bez średników, `{` w tej samej linii, warunki bez na |---|---| | `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` | | `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` | -| `bool` | `true`, `false` | +| `bool` | `true` (prawda), `false` (fałsz) | | `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ą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 | +| `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 | | `[]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 \\ \" \'`. @@ -110,11 +107,11 @@ Znaki specjalne: `\n \t \r \0 \\ \" \'`. | Priorytet | Operatory | |---|---| | 5 | `*` `/` `%` `<<` `>>` `&` | -| 4 | `+` `-` `\|` `^` (`+` łączy też teksty) | +| 4 | `+` `-` `\|` `^` (`+` skleja też tekst) | | 3 | `==` `!=` `<` `<=` `>` `>=` | -| 2 | `&&` | -| 1 | `\|\|` | -| jednoargumentowe | `-x` `!x` `~x` | +| 2 | `&&` (i) | +| 1 | `\|\|` (lub) | +| jednoargumentowe | `-x` `!x` (nie) `~x` | `x & 1 == 0` znaczy `(x & 1) == 0`. @@ -124,14 +121,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ść: po jednej wartości w każdej gałęzi, else obowiązkowe +x := if a > b { a } else { b } // jako wartość: jedna wartość na gałąź, else wymagane while x > 0 { … } for i in 0..10 { … } // 0 … 9 -for el in lista { … } // każdy element (albo znak tekstu) +for el in lista { … } // każdy element (albo znak str) for i, el in lista { … } // razem z indeksem -for klucz, wartosc in m { … } // każdy wpis mapy, w kolejności dodania +for klucz, wart in m { … } // każdy wpis mapy, w kolejności dodawania break continue return wartosc -defer sprzatnij() // wykona się, gdy blok się skończy +defer sprzatanie() // uruchomi się na końcu bloku ``` --- @@ -147,48 +144,48 @@ fn przywitaj(str imie) { // bez wyniku } ``` -Parametry zachowują się jak kopie. Kolejność funkcji w pliku nie ma znaczenia. Po ostatnim argumencie może stać przecinek. +Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przecinek po ostatnim argumencie jest dozwolony. --- ## Metody ```c -fn Punkt.przesun(int dx, int dy) { // self to punkt, na którym ją wywołano (nie kopia) +fn Punkt.przesun(int dx, int dy) { // self to Punkt, na którym wywołano (nie kopia) self.x += dx self.y += dy } fn Punkt.odleglosc() -> float { … } -p.przesun(1, 2) lista[0].przesun(1, 1) wskaznik_na_punkt.przesun(0, 1) +p.przesun(1, 2) lista[0].przesun(1, 1) wsk_na_punkt.przesun(0, 1) ``` -Działają też na strukturach z C: `fn Vector2.length() -> f32 { … }`. +Także na strukturach z C: `fn Vector2.length() -> f32 { … }`. --- ## Błędy jako wartości ```c -fn parsuj(str s) -> int! { // ! = może się nie udać +fn parsuj(str s) -> int! { // ! = może się nie udać if s.len == 0 { - return error("pusty tekst") + return error("pusty") } return int(s) or { return error("to nie liczba: " + s) } } -fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku +fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku try write_file(p, t) } ``` -| Kod | Gdy się nie uda | +| Kod | Przy porażce | |---|---| -| `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 | +| `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 | -Wbudowane funkcje, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`. +Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`. --- @@ -196,9 +193,9 @@ Wbudowane funkcje, które mogą się nie udać: `int(s)`, `float(s)`, `read_file | Kod | Znaczenie | |---|---| -| `struct P { … }` | definicja (jedno pole w linii, `= wartość` opcjonalnie) | +| `struct P { … }` | definicja (jedno pole na linijkę, `= domyślna` opcjonalnie) | | `P(1, 2)` | utworzenie, wszystkie pola po kolei | -| `P(x: 1)` | utworzenie przez nazwy pól, reszta domyślna albo zero | +| `P(x: 1)` | utworzenie po nazwie, reszta domyślna/zero | | `p.x`, `p.x = 5` | odczyt, zmiana pola | | `b := a` | pełna kopia | @@ -206,90 +203,62 @@ Wbudowane funkcje, które mogą się nie udać: `int(s)`, `float(s)`, `read_file ## Tablice `[]T` -| Kod | Wynik | +| Kod | Daje | |---|---| | `a.len` | liczba elementów | -| `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[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.find(x)` | indeks albo -1 | | `a.contains(x)` | bool | -| `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.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.ptr` | adres pierwszego elementu (dla C) | --- ## Mapy `map[K]V` -| Kod | Wynik | +| Kod | Daje | |---|---| | `m := ["a": 1, "b": 2]` | mapa (klucze: liczby, char, bool, str) | -| `m[k]` | wartość (brak klucza zatrzymuje program) | -| `m[k] = v` | dodaje albo podmienia | +| `m[k]` | wartość (brak klucza: program się zatrzymuje) | +| `m[k] = v` | dodaj / zastąp | | `m[k] += 1`, `m[k].push(x)` | brakujący klucz zaczyna od wartości domyślnej (0, `[]`, …) | | `m.len` | liczba kluczy | -| `m.has(k)` | 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`, … | maska `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` | -| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, a gdzie nie, `b` | -| `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()` | każdy element (liczby zmiennoprzecinkowe) | -| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: zawsze z jednym zaokrągleniem / najszybciej na tym procesorze | -| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … | -| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | koniec listy: tylko elementy, które są w niej (pozostałe to `wypelnienie`) | -| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | przestawione elementy (shuffle przyjmuje numery zapisane w kodzie) | -| `tabela.lookup(idx)` | `tabela[idx % tabela.len]` dla każdego elementu (`pshufb`, `tbl`) | -| `acc.dot_add(a, b)` | sumy `i32x8` dostają po 4 iloczyny bajtów (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` tam, gdzie są | -| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | funkcja, która może używać tych rozszerzeń; przed jej wywołaniem sprawdź procesor | +| `m.has(k)` | czy jest | +| `m.get(k, domyslna)` | wartość albo domyślna | +| `m.remove(k)` | bool: czy był | +| `m.clear()` | wyczyść | +| `m.keys()`, `m.values()` | tablice, w kolejności dodawania | --- ## Tekst `str` -| Kod | Wynik | +| Kod | Daje | |---|---| -| `s.len` | liczba bajtów | +| `s.len` | liczba bajtów (ą, ż… to 2) | | `s[i]`, `s[i] = 'x'` | znak | -| `a + b` | połączony tekst | -| `"x = \(x)"` | wartość dowolnego kodu wstawiona w tekst (`\(x, 2)`: liczba z dwiema cyframi po przecinku) | +| `a + b` | sklejony tekst | +| `"x = \(x)"` | wartość dowolnego kodu w tekście (`\(x, 2)`: liczba z 2 cyframi po przecinku) | | `s.sub(a, b)` | fragment a … b-1 | | `s.find(t)` | pozycja albo -1 | -| `s.contains(t)` | bool | -| `s.starts_with(t)`, `s.ends_with(t)` | bool | +| `s.contains(t)` | czy zawiera | +| `s.starts_with(t)`, `s.ends_with(t)` | czy zaczyna / kończy się | | `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 zamienione na b | -| `s.repeat(n)` | tekst n razy | +| `s.replace(a, b)` | każde a → b | +| `s.repeat(n)` | n razy | | `s.bytes()` | `[]u8` | -| `s.ptr` | `ptr[char]` (dla C; parametr `const char *` przyjmie też samo `str`) | +| `s.ptr` | `ptr[char]` (dla C; `str` trafia też do parametru `const char *` bez zmian) | | `==` `!=` `<` `>` | porównanie | --- @@ -302,44 +271,41 @@ Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i6 | `int("42")` | `42` (zatrzymuje program, jeśli to nie liczba) | | `int('A')` | `65` | | `float(7)`, `float("2.5")` | `7`, `2.5` | -| `u8(300)` | `44` (zostają najniższe bity) | +| `u8(300)` | `44` (zostawia najniższe bity) | | `char(66)` | `'B'` | | `bool(0)` | `false` | -| `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(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([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 z liczby | +| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo liczby | | `int(p)` | wskaźnik → adres | -Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, liczba całkowita → zmiennoprzecinkowa, `f32` → `f64`. +Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowolna całkowita → float, `f32` → `f64`. --- ## Funkcje wbudowane -| Funkcja | Co robi | +| Funkcja | Robi | |---|---| -| `print(a, b, …)` | wypisuje wartości oddzielone spacjami i kończy linię | -| `input()`, `input("Pytanie? ")` | wczytuje linię | +| `print(a, b, …)` | wypisz, spacje między, nowa linia na końcu | +| `input()`, `input("Pytanie? ")` | wczytaj linijkę | | `read_file(sciezka)` | plik → str | | `write_file(sciezka, tekst)` | str → plik, `true`, jeśli się udało | -| `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) | -| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` z jednym zaokrągleniem / jedną instrukcją, gdzie procesor potrafi (zapisane `a * b + c` nigdy nie jest łączone) | -| `f32_bits(x)`, `f32_from_bits(u)`, `f64_bits`, `f64_from_bits` | bity liczby zmiennoprzecinkowej jako `u32`/`u64` i z powrotem, bez zmian | -| `f16_to_f32(h)`, `f32_to_f16(x)`, `bf16_to_f32(b)`, `f32_to_bf16(x)` | liczby połówkowej precyzji i bfloat16 (w `u16`) ↔ `f32`; także na wektorach | +| `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) | | `error(komunikat)` | `return error("…")` w funkcji `-> T!` | -| `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)`) | +| `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)`) | | `new(wartosc)` | wartość na stercie → `ptr[T]` | -| `free(p)` | oddaje tę pamięć | +| `free(p)` | oddaj ją | --- @@ -348,7 +314,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, licz | Finch | C | |---|---| | `ptr[int] p` | `int *p` | -| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie oczekiwany jest `ptr[TaStruktura]`: `push(cialo)` = `push(addr(cialo))`) | +| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie idzie `ptr[TaStruktura]`: `pchnij(cialo)` = `pchnij(addr(cialo))`) | | `p.value` | `*p` | | `p.pole` | `p->pole` | | `p[i]` | `p[i]` | @@ -359,23 +325,23 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, licz ## Pamięć -- 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`, …). +- 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`, …). --- ## Moduły ```c -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() +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() ``` --- @@ -384,14 +350,14 @@ import ui/button as btn // pod inną nazwą: btn.draw() | Kod | Znaczenie | |---|---| -| `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ą | +| `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 | -| Typ w C | W Finchu | +| Typ C | Finch | |---|---| -| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (według rozmiaru) | +| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (wg rozmiaru) | | `float` / `double` | `f32` / `f64` | | `char *` (parametr, wynik) | `str` | | `char *` (pole, za wskaźnikiem) | `ptr[char]` | @@ -399,46 +365,46 @@ import ui/button as btn // pod inną nazwą: btn.draw() | `T *` | `ptr[T]` | | `void *`, uchwyty | `ptr` | | `T x[N]` (w strukturze) | `[N]T` | -| `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` | +| `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` | -**`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). +**`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). -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). +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). --- -## Błędy w trakcie działania (program kończy się z plikiem i linią) +## Błędy w czasie działania (program zatrzymuje się z plikiem i linijką) | 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() on an empty array` | `pop()` na pustej tablicy | | `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` | | `can't turn "x" into int` | `int("x")` | | `can't read the file "…"` | `read_file` (bez `or`) | | `the key "x" is not in the map` | `m["x"]` | | `assert failed: dlaczego` | `assert(warunek, "dlaczego")` | -Pod komunikatem pojawiają się wywołania, które do niego doprowadziły (`finch run`, `finch test`, `-g`, `-O0`, `--trace`): +Pod komunikatem 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 Wrog.nastepny (gra.fch:6) - in krok (gra.fch:10) + in Enemy.next (gra.fch:6) + in step (gra.fch:10) in main (gra.fch:15) ``` ## Testy ```c -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)") +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)") } ``` @@ -447,55 +413,74 @@ test "wysokość skoku" { // obok kodu; uruchamia je finch test plik.fch ## Edytory Kate: `editors/kate/install.sh`, potem włącz wtyczkę **LSP Client**; uruchamianie przez **Build & Run** → `finch run %f`. -Dowolny edytor z LSP: `finch lsp`. +Każdy edytor z LSP: `finch lsp`. ### VS Code (rozszerzenie „Finch”) -| Klawisz / akcja | Co robi | +| Klawisz / akcja | Robi | |---|---| -| ▶ / `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+F5` | uruchom plik | +| `Finch: Build This File` | zbuduj; błędy trafiają do panelu Problems | +| `F12` | idź do definicji | +| najechanie myszą | typ / sygnatura / opis | | `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) | | `Ctrl+Shift+O` | funkcje i struktury w pliku | -| `main`, `fn`, `fnr`, `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 | +| `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 | --- ## Enumy i `match` ``` -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 +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 fn Kolor.nastepny() -> Kolor { ... } // metody działają -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 +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 ``` ## Typy generyczne ``` -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ą: +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: s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // nigdy samo Stos() ``` -## Biblioteka standardowa (`import nazwa`; twój plik o tej samej nazwie ma pierwszeństwo) +## Biblioteka standardowa (`import nazwa`; własny plik o tej 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, wzor)` `days_in_month(r, m)` | -| `proc` | `run(polecenie)` `output(polecenie)` (przy błędzie zwraca jego wyjście) `env(nazwa, domyslna)` `pid()` `cwd()` | +| `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()` | | `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(katalog)` `remove_dir(p)` `data_dir(aplikacja)` | +| `fs` | `read_bytes(p)` `write_bytes(p, dane)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(folder)` `remove_dir(p)` `data_dir(app)` | | `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 -`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) +| 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 | diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml index b8a9229..883a6e0 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. --> - @@ -82,19 +82,6 @@ atomic_store atomic_swap atomic_cas - select - concat - fma - muladd - cpu_has - f32_bits - f32_from_bits - f64_bits - f64_from_bits - f16_to_f32 - f32_to_f16 - bf16_to_f32 - f32_to_bf16 @@ -116,9 +103,6 @@ - - - diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index fc8f613..195374b 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,10 +1,5 @@ # Changelog -## 2.6.0 - -- Vector types (`f32x8`, `i8x32`, `f64x4`, …) and masks (`boolx8`) highlighted as types; completion of their methods (`sum`, `dot_add`, `shuffle`, `lookup`, `store`, …). -- `@cpu(...)` and the new built-ins (`select`, `concat`, `fma`, `muladd`, `cpu_has`, `f32_bits`, `f16_to_f32`, …) highlighted. - ## 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 5a0404d..f98d8ad 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "finch-lang", - "version": "2.6.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "finch-lang", - "version": "2.6.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 9c17bd1..40c7715 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.6.0", + "version": "2.5.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 8ce8dd8..9154a4e 100644 --- a/editors/vscode/syntaxes/finch.tmLanguage.json +++ b/editors/vscode/syntaxes/finch.tmLanguage.json @@ -146,16 +146,12 @@ "name": "variable.language.self.finch", "match": "\\bself\\b" }, - { - "name": "storage.modifier.attribute.finch", - "match": "@cpu\\b" - }, { "name": "storage.type.primitive.finch", - "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|(?:[iuf](?:8|16|32|64)|bool)x(?:2|4|8|16|32|64))\\b" + "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b" }, { - "match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas|select|concat|fma|muladd|cpu_has|f32_bits|f32_from_bits|f64_bits|f64_from_bits|f16_to_f32|f32_to_f16|bf16_to_f32|f32_to_bf16)\\s*(?=\\()", + "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*(?=\\()", "captures": { "1": { "name": "support.function.builtin.finch" diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js index ce951a9..bf2aae7 100644 --- a/editors/vscode/test/grammar.js +++ b/editors/vscode/test/grammar.js @@ -29,8 +29,6 @@ const code = [ 'test "lives \\(hp + f(1)) left" {', ' assert(clamp(x, 0, 9) == 3)', '}', - 'fn dot(f32x8 a, i8x32 b) {', - '@cpu("avx2") fn k(boolx8 m) { select(m, a, b) }', ]; const expect = [ // [line, token text, scope that must be present] [0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'], @@ -51,8 +49,6 @@ 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'], - [21, '@cpu', 'storage.modifier.attribute'], [21, 'boolx8', 'storage.type.primitive'], [21, 'select', 'support.function.builtin'], ]; registry.loadGrammar('source.finch').then((g) => { let state = vsctm.INITIAL, toks = []; diff --git a/examples/simd.fch b/examples/simd.fch deleted file mode 100644 index a2b2af1..0000000 --- a/examples/simd.fch +++ /dev/null @@ -1,69 +0,0 @@ -// 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]; muladd multiplies and adds, in one step where it can -fn dot_simd([]f32 a, []f32 b) -> f32 { - acc := f32x8(0.0) - i := 0 - while i + 8 <= a.len { - acc = f32x8(a, i).muladd(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 dd9afee..ab806a3 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/gen.fch): +// Layout shared with the compiler (src/codegen.cpp): // 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 @@ -1147,96 +1147,3 @@ __attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : ""); finch_panic(file, line, msg); } - -// ---------- cpu_has("avx2"): what the processor running the program can do ---------- -// -// x86: CPUID, and for the AVX and AVX-512 families also XGETBV (the system must save those registers, or the -// instructions fault). ARM64: the kernel's hwcaps on Linux and Android, sysctl on macOS. A name of the other -// architecture is false. - -#if defined(__x86_64__) || defined(__i386__) -#include -static int x86_leaf(unsigned leaf, unsigned sub, unsigned r[4]) { - unsigned max = __get_cpuid_max(0, 0); - if (leaf > max) return 0; - __cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]); - return 1; -} -static unsigned long long x86_xcr0(void) { - unsigned lo, hi; - __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0)); - return ((unsigned long long)hi << 32) | lo; -} -static int finch_cpu_has_c(const char *n) { - unsigned l1[4] = {0}, l7[4] = {0}, l71[4] = {0}; - x86_leaf(1, 0, l1); - x86_leaf(7, 0, l7); - x86_leaf(7, 1, l71); - int osx = (l1[2] >> 27) & 1; // OSXSAVE - unsigned long long xcr0 = osx ? x86_xcr0() : 0; - int ymm = osx && (xcr0 & 6) == 6; // the system saves xmm and ymm - int zmm = ymm && (xcr0 & 0xe0) == 0xe0; // and the AVX-512 registers - if (!strcmp(n, "sse3")) return l1[2] & 1; - if (!strcmp(n, "ssse3")) return (l1[2] >> 9) & 1; - if (!strcmp(n, "sse4.1")) return (l1[2] >> 19) & 1; - if (!strcmp(n, "sse4.2")) return (l1[2] >> 20) & 1; - if (!strcmp(n, "popcnt")) return (l1[2] >> 23) & 1; - if (!strcmp(n, "avx")) return ymm && ((l1[2] >> 28) & 1); - if (!strcmp(n, "fma")) return ymm && ((l1[2] >> 12) & 1); - if (!strcmp(n, "f16c")) return ymm && ((l1[2] >> 29) & 1); - if (!strcmp(n, "avx2")) return ymm && ((l7[1] >> 5) & 1); - if (!strcmp(n, "bmi1")) return (l7[1] >> 3) & 1; - if (!strcmp(n, "bmi2")) return (l7[1] >> 8) & 1; - if (!strcmp(n, "avxvnni")) return ymm && ((l71[0] >> 4) & 1); - if (!strcmp(n, "avx512f")) return zmm && ((l7[1] >> 16) & 1); - if (!strcmp(n, "avx512dq")) return zmm && ((l7[1] >> 17) & 1); - if (!strcmp(n, "avx512bw")) return zmm && ((l7[1] >> 30) & 1); - if (!strcmp(n, "avx512vl")) return zmm && ((l7[1] >> 31) & 1); - if (!strcmp(n, "avx512vnni")) return zmm && ((l7[2] >> 11) & 1); - if (!strcmp(n, "avx512fp16")) return zmm && ((l7[3] >> 23) & 1); - if (!strcmp(n, "avx512bf16")) return zmm && ((l71[0] >> 5) & 1); - return 0; -} -#elif defined(__aarch64__) && defined(__APPLE__) -#include -static int sysctl_flag(const char *name) { - int v = 0; - size_t len = sizeof v; - if (sysctlbyname(name, &v, &len, NULL, 0) != 0) return 0; - return v != 0; -} -static int finch_cpu_has_c(const char *n) { - if (!strcmp(n, "neon")) return 1; - if (!strcmp(n, "fp16")) return sysctl_flag("hw.optional.arm.FEAT_FP16"); - if (!strcmp(n, "dotprod")) return sysctl_flag("hw.optional.arm.FEAT_DotProd"); - if (!strcmp(n, "i8mm")) return sysctl_flag("hw.optional.arm.FEAT_I8MM"); - if (!strcmp(n, "bf16")) return sysctl_flag("hw.optional.arm.FEAT_BF16"); - return 0; // no SVE on Apple chips -} -#elif defined(__aarch64__) && defined(__linux__) -#include -static int finch_cpu_has_c(const char *n) { - unsigned long h = getauxval(AT_HWCAP), h2 = getauxval(AT_HWCAP2); - if (!strcmp(n, "neon")) return 1; - if (!strcmp(n, "fp16")) return (h >> 10) & 1; // HWCAP_ASIMDHP - if (!strcmp(n, "dotprod")) return (h >> 20) & 1; // HWCAP_ASIMDDP - if (!strcmp(n, "sve")) return (h >> 22) & 1; // HWCAP_SVE - if (!strcmp(n, "sve2")) return (h2 >> 1) & 1; // HWCAP2_SVE2 - if (!strcmp(n, "i8mm")) return (h2 >> 13) & 1; // HWCAP2_I8MM - if (!strcmp(n, "bf16")) return (h2 >> 14) & 1; // HWCAP2_BF16 - return 0; -} -#else -static int finch_cpu_has_c(const char *n) { - (void)n; - return 0; -} -#endif - -int finch_cpu_has(const FStr *name) { - char n[32]; - int64_t len = name->len < 31 ? name->len : 31; - memcpy(n, name->ptr ? name->ptr : "", (size_t)len); - n[len] = 0; - return finch_cpu_has_c(n) ? 1 : 0; -} diff --git a/shell.nix b/shell.nix index 90201e2..1e3655a 100644 --- a/shell.nix +++ b/shell.nix @@ -15,7 +15,4 @@ pkgs.mkShell { vulkan-headers vulkan-loader ]; - - # Nix's clang wrapper drops -march=native unless this is 0 (finch --cpu native also tells it so itself) - NIX_ENFORCE_NO_NATIVE = 0; } diff --git a/src/ast.fch b/src/ast.fch index abbc87b..0870037 100644 --- a/src/ast.fch +++ b/src/ast.fch @@ -36,41 +36,6 @@ 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", "bool"].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 1 // bool: a mask, one bit a lane - 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 @@ -78,10 +43,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.kind == "vec") && a.count != b.count { + if a.kind == "fixed" && a.count != b.count { return false } - if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" || a.kind == "vec" { + if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" { if a.elem.len != b.elem.len { return false } @@ -102,15 +67,6 @@ 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]) } @@ -147,7 +103,6 @@ struct Node { bool flag // decl: has a type; or: the right side is a { block } bool fallible // fn: -> T! or -> ! bool exported // export fn: a C function of a library (finch build --shared) - str cpu // @cpu("avx2,fma") fn: the processor features the function may use []Node kids []str names // named arguments: Point(x: 1) int line diff --git a/src/cimport.fch b/src/cimport.fch index 5d8fa14..8d1701f 100644 --- a/src/cimport.fch +++ b/src/cimport.fch @@ -1,9 +1,10 @@ // 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. 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. +// 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. // // 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 44f4615..977f0de 100644 --- a/src/docs.fch +++ b/src/docs.fch @@ -26,19 +26,6 @@ fn builtins() -> []Doc { doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."), doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."), doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."), - doc("cpu_has", "cpu_has(str feature) -> bool", "Does the processor running the program have this feature (\"avx2\", \"avx512f\", \"avxvnni\", \"dotprod\", \"i8mm\" ...)? Check before calling a @cpu(...) function."), - doc("concat", "concat(V a, V b) -> V2", "One vector with a's lanes, then b's (twice as many)."), - doc("select", "select(mask, a, b) -> V", "Lane by lane: a where the mask is true, b where it is false. The mask comes from comparing vectors (v > 0.0)."), - doc("fma", "fma(a, b, c) -> float", "a * b + c with one rounding, always (numbers or vectors). a * b + c written out is never fused: two roundings."), - doc("muladd", "muladd(a, b, c) -> float", "a * b + c the fastest way here: one fused instruction where the processor has one, else a multiply and an add."), - doc("f32_bits", "f32_bits(f32 x) -> u32", "The bits of x, unchanged (also f32xN -> u32xN). f64_bits gives a u64."), - doc("f32_from_bits", "f32_from_bits(u32 bits) -> f32", "The f32 with these bits (also u32xN -> f32xN). f64_from_bits takes a u64."), - doc("f64_bits", "f64_bits(float x) -> u64", "The bits of x, unchanged (also f64xN -> u64xN)."), - doc("f64_from_bits", "f64_from_bits(u64 bits) -> float", "The float with these bits (also u64xN -> f64xN)."), - doc("f16_to_f32", "f16_to_f32(u16 half) -> f32", "A half-precision number, kept in a u16, as an f32 (also u16xN -> f32xN)."), - doc("f32_to_f16", "f32_to_f16(f32 x) -> u16", "x as a half-precision number in a u16, rounded to the nearest (also f32xN -> u16xN)."), - doc("bf16_to_f32", "bf16_to_f32(u16 b) -> f32", "A bfloat16, kept in a u16, as an f32 (also u16xN -> f32xN)."), - doc("f32_to_bf16", "f32_to_bf16(f32 x) -> u16", "x as a bfloat16 in a u16, rounded to the nearest, ties to even (also f32xN -> u16xN)."), doc("atomic_add", "atomic_add(ptr[int] p, n) -> int", "Adds n to the number at p in one step other threads can't split; gives back what was there before."), doc("atomic_sub", "atomic_sub(ptr[int] p, n) -> int", "Takes n from the number at p in one step; gives back what was there before."), doc("atomic_load", "atomic_load(ptr[int] p) -> int", "Reads the number at p, as another thread last wrote it."), @@ -53,8 +40,7 @@ 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("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.") + doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C).") ] } @@ -108,42 +94,13 @@ 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 with one rounding, always (f32/f64 lanes). One instruction with FMA hardware, slower without."), - doc("muladd", "muladd(V b, V c) -> V", "v * b + c the fastest way here: fused into one instruction where the processor can (f32/f64 lanes)."), - doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T])."), - doc("store_part", "store_part([]T list, int i)", "Writes the lanes that fit in the list (for its last, shorter piece)."), - doc("shuffle", "shuffle(int a, int b, ...) -> V", "A vector of the lanes with these numbers (written in the code): v.shuffle(3, 2, 1, 0) reverses an f32x4."), - doc("lo", "lo() -> V", "The lower half of the lanes."), - doc("hi", "hi() -> V", "The upper half of the lanes."), - doc("lookup", "lookup(I indices) -> V", "For every lane of indices: this vector's lane (index % len). A byte-table lookup (pshufb, tbl)."), - doc("dot_add", "dot_add(a, b) -> i32xN", "On an i32xN of sums: each lane gains the sum of 4 byte products of a and b (u8/i8, 4N lanes). One instruction with AVX-VNNI, AVX-512 VNNI or ARM's dot product."), - doc("any", "any() -> bool", "A mask: is any lane true?"), - doc("all", "all() -> bool", "A mask: are all lanes true?"), - doc("count", "count() -> int", "A mask: how many lanes are true.") - ] -} - 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", - "f32x4", "f32x8", "f32x16", "f64x2", "f64x4", "f64x8", "i8x16", "i8x32", "i8x64", "u8x16", "u8x32", "u8x64", - "i16x8", "i16x16", "i16x32", "i32x4", "i32x8", "i32x16", "u32x4", "u32x8", "u32x16", "i64x2", "i64x4", "i64x8"] + return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"] } // the entry called `name`, or an empty Doc diff --git a/src/embedded.fch b/src/embedded.fch index 465820d..cfef97d 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/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\n// ---------- cpu_has(\"avx2\"): what the processor running the program can do ----------\n//\n// x86: CPUID, and for the AVX and AVX-512 families also XGETBV (the system must save those registers, or the\n// instructions fault). ARM64: the kernel's hwcaps on Linux and Android, sysctl on macOS. A name of the other\n// architecture is false.\n\n#if defined(__x86_64__) || defined(__i386__)\n#include \nstatic int x86_leaf(unsigned leaf, unsigned sub, unsigned r[4]) {\n unsigned max = __get_cpuid_max(0, 0);\n if (leaf > max) return 0;\n __cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]);\n return 1;\n}\nstatic unsigned long long x86_xcr0(void) {\n unsigned lo, hi;\n __asm__ volatile(\"xgetbv\" : \"=a\"(lo), \"=d\"(hi) : \"c\"(0));\n return ((unsigned long long)hi << 32) | lo;\n}\nstatic int finch_cpu_has_c(const char *n) {\n unsigned l1[4] = {0}, l7[4] = {0}, l71[4] = {0};\n x86_leaf(1, 0, l1);\n x86_leaf(7, 0, l7);\n x86_leaf(7, 1, l71);\n int osx = (l1[2] >> 27) & 1; // OSXSAVE\n unsigned long long xcr0 = osx ? x86_xcr0() : 0;\n int ymm = osx && (xcr0 & 6) == 6; // the system saves xmm and ymm\n int zmm = ymm && (xcr0 & 0xe0) == 0xe0; // and the AVX-512 registers\n if (!strcmp(n, \"sse3\")) return l1[2] & 1;\n if (!strcmp(n, \"ssse3\")) return (l1[2] >> 9) & 1;\n if (!strcmp(n, \"sse4.1\")) return (l1[2] >> 19) & 1;\n if (!strcmp(n, \"sse4.2\")) return (l1[2] >> 20) & 1;\n if (!strcmp(n, \"popcnt\")) return (l1[2] >> 23) & 1;\n if (!strcmp(n, \"avx\")) return ymm && ((l1[2] >> 28) & 1);\n if (!strcmp(n, \"fma\")) return ymm && ((l1[2] >> 12) & 1);\n if (!strcmp(n, \"f16c\")) return ymm && ((l1[2] >> 29) & 1);\n if (!strcmp(n, \"avx2\")) return ymm && ((l7[1] >> 5) & 1);\n if (!strcmp(n, \"bmi1\")) return (l7[1] >> 3) & 1;\n if (!strcmp(n, \"bmi2\")) return (l7[1] >> 8) & 1;\n if (!strcmp(n, \"avxvnni\")) return ymm && ((l71[0] >> 4) & 1);\n if (!strcmp(n, \"avx512f\")) return zmm && ((l7[1] >> 16) & 1);\n if (!strcmp(n, \"avx512dq\")) return zmm && ((l7[1] >> 17) & 1);\n if (!strcmp(n, \"avx512bw\")) return zmm && ((l7[1] >> 30) & 1);\n if (!strcmp(n, \"avx512vl\")) return zmm && ((l7[1] >> 31) & 1);\n if (!strcmp(n, \"avx512vnni\")) return zmm && ((l7[2] >> 11) & 1);\n if (!strcmp(n, \"avx512fp16\")) return zmm && ((l7[3] >> 23) & 1);\n if (!strcmp(n, \"avx512bf16\")) return zmm && ((l71[0] >> 5) & 1);\n return 0;\n}\n#elif defined(__aarch64__) && defined(__APPLE__)\n#include \nstatic int sysctl_flag(const char *name) {\n int v = 0;\n size_t len = sizeof v;\n if (sysctlbyname(name, &v, &len, NULL, 0) != 0) return 0;\n return v != 0;\n}\nstatic int finch_cpu_has_c(const char *n) {\n if (!strcmp(n, \"neon\")) return 1;\n if (!strcmp(n, \"fp16\")) return sysctl_flag(\"hw.optional.arm.FEAT_FP16\");\n if (!strcmp(n, \"dotprod\")) return sysctl_flag(\"hw.optional.arm.FEAT_DotProd\");\n if (!strcmp(n, \"i8mm\")) return sysctl_flag(\"hw.optional.arm.FEAT_I8MM\");\n if (!strcmp(n, \"bf16\")) return sysctl_flag(\"hw.optional.arm.FEAT_BF16\");\n return 0; // no SVE on Apple chips\n}\n#elif defined(__aarch64__) && defined(__linux__)\n#include \nstatic int finch_cpu_has_c(const char *n) {\n unsigned long h = getauxval(AT_HWCAP), h2 = getauxval(AT_HWCAP2);\n if (!strcmp(n, \"neon\")) return 1;\n if (!strcmp(n, \"fp16\")) return (h >> 10) & 1; // HWCAP_ASIMDHP\n if (!strcmp(n, \"dotprod\")) return (h >> 20) & 1; // HWCAP_ASIMDDP\n if (!strcmp(n, \"sve\")) return (h >> 22) & 1; // HWCAP_SVE\n if (!strcmp(n, \"sve2\")) return (h2 >> 1) & 1; // HWCAP2_SVE2\n if (!strcmp(n, \"i8mm\")) return (h2 >> 13) & 1; // HWCAP2_I8MM\n if (!strcmp(n, \"bf16\")) return (h2 >> 14) & 1; // HWCAP2_BF16\n return 0;\n}\n#else\nstatic int finch_cpu_has_c(const char *n) {\n (void)n;\n return 0;\n}\n#endif\n\nint finch_cpu_has(const FStr *name) {\n char n[32];\n int64_t len = name->len < 31 ? name->len : 31;\n memcpy(n, name->ptr ? name->ptr : \"\", (size_t)len);\n n[len] = 0;\n return finch_cpu_has_c(n) ? 1 : 0;\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/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("// 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 ad6ebce..4768440 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -1,7 +1,8 @@ // 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 Finch's C runtime -// (runtime/finch_rt.c). Values are copied when assigned and freed when their block ends. +// 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. import ast import docs import cimport @@ -33,7 +34,6 @@ struct Options { bool trace // runtime errors show the chain of calls (off for --shared and --no-trace) bool test // finch test: main runs the test "..." blocks instead []str testModules // the modules whose tests run ("" is the main file); not the standard library's - []str features // what the processor it is built for has (from --cpu, asked of clang): avx2, i8mm ... } // A generic struct or function, kept as written until something asks for a version of it. @@ -206,7 +206,6 @@ 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 @@ -219,8 +218,6 @@ struct Gen { bool deferOn // running a defer: variables declared after it are out of sight int deferLimit int inDefer - []str features // the processor features of the whole program (--cpu) - []str curFeatures // and of the function being built: those and its @cpu(...) } // ---------- basics ---------- @@ -370,7 +367,7 @@ fn byteSize(ptr[Gen] g, ast.Type t) -> int { } return n } - if (k == "fixed" || k == "vec") && t.elem.len > 0 { + if k == "fixed" && t.elem.len > 0 { return t.count * byteSize(g, t.elem[0]) } if k == "str" || k == "array" || k == "map" { @@ -439,22 +436,12 @@ 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 + alignFor(g, ty)) + emit(g, t + " = load " + ty + ", ptr " + at) return t } fn store(ptr[Gen] g, str ty, str v, str 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 "" + emit(g, "store " + ty + " " + v + ", ptr " + at) } fn spill(ptr[Gen] g, Val v) -> str { @@ -479,7 +466,7 @@ fn T(str kind) -> ast.Type { return ast.simple(kind) } -// `int` is i64 and `float` is f64: the everyday names +// `int` is i64 and `float` is f64, as in the C++ compiler: 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 @@ -716,13 +703,6 @@ 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" } @@ -794,9 +774,6 @@ 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) } @@ -1244,19 +1221,7 @@ fn formatFn(ptr[Gen] g, ast.Type t) -> str { } g.done.push(name) s := beginHelper(g) - 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" { + 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")) @@ -1463,15 +1428,6 @@ 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 == "bool" && v.ty.kind == "vec" && v.ty.elem[0].kind == "bool" { - msg = msg + " (a comparison of vectors has an answer for every lane: use .all() or .any())" - } - 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 } @@ -2098,13 +2054,7 @@ fn resolveStructBody(ptr[Gen] g, int i) { g.structs[i].owning = owns g.module = saved g.structs[i].state = 2 - 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(", ") + " }") + g.head.push("%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) + " = type { " + fields.join(", ") + " }") } // ---------- debug info (-g) ---------- @@ -2158,10 +2108,6 @@ 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 { @@ -2250,10 +2196,6 @@ 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) + ")") @@ -2363,13 +2305,6 @@ fn define(ptr[Gen] g, int si) { g.instNote = " (while building " + g.cur.shown + ")" } f := g.progs[g.cur.prog].fns[g.cur.index] - ownCpu := cpuOf(g, f) - g.curFeatures = g.features - for x in ownCpu { - if !g.curFeatures.contains(x) { - g.curFeatures.push(x) - } - } []str empty g.body = empty g.allocas = empty @@ -2454,20 +2389,8 @@ fn define(ptr[Gen] g, int si) { } } attach := "" - if ownCpu.len > 0 { - []str plus - wide := false - for x in ownCpu { - plus.push("+" + llvmFeature(x)) - wide = wide || x.starts_with("avx512") - } - if wide { - plus.push("+evex512") // LLVM 18 and later: the 512-bit registers themselves (clang adds it for C too) - } - attach = " \"target-features\"=\"" + plus.join(",") + "\"" - } if dbgsp >= 0 { - attach = attach + " !dbg !" + str(dbgsp) + attach = " !dbg !" + str(dbgsp) } g.dbgScope = -1 g.out.push("define internal " + retTy(g, g.cur) + " " + g.cur.ll + "(" + params.join(", ") + ")" + attach + " {\nentry:\n" + @@ -3705,9 +3628,6 @@ 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 != "!" || v.ty.elem[0].kind == "bool") { - 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")) @@ -3731,13 +3651,6 @@ 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)) @@ -3833,9 +3746,6 @@ 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) } @@ -3937,9 +3847,6 @@ 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) } @@ -4374,15 +4281,6 @@ 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") @@ -4478,27 +4376,6 @@ 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" && t.elem[0].kind == "bool" { - // a mask keeps a lane in a bit, so a lane is read from the whole value - if write { - fail(g, e, "a lane of a mask can't be set by itself; make a new mask (with a comparison or select)") - } - boundsCheck(g, i, str(t.count), e) - mv := obj.val.v - if obj.isPlace { - mv = load(g, llty(g, t), obj.pl.addr) - } - return Ref(isPlace: false, val: Val(v: op(g, "extractelement " + llty(g, t) + " " + mv + ", i64 " + i), ty: T("bool"))) - } - 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 { @@ -4678,12 +4555,11 @@ fn convert(ptr[Gen] g, ast.Node c, ast.Type to) -> Val { return v } -// ---------- the C calling convention for structs passed by value ---------- +// ---------- the C calling convention for structs passed by value (System V x86-64) ---------- // -// System V x86-64: a struct of up to 16 bytes travels in registers, one INTEGER or SSE class per eightbyte; +// 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). -// ARM64 (AAPCS64, and Apple's variant) and Windows x64 have their own rules: classifyArm64, and the -// windows branch of classify. +// Other targets (Windows, ARM64) have their own rules and are not done here yet. struct Abi { str kind // "direct", "expand" (in registers: `parts`) or "memory" @@ -5431,7 +5307,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" || k == "vec" { + } else if k == "bool" || k == "char" || k == "array" || k == "map" || k == "struct" || k == "enum" { store(g, "{ ptr, i64, i64 }", "zeroinitializer", out) emitFormat(g, out, v.v, v.ty, false) release(g, v) @@ -5444,10 +5320,6 @@ 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") @@ -5609,21 +5481,6 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { if ei >= 0 { return enumFrom(g, c, ei) } - if n == "select" && lookup(g, n) < 0 && findFn(g, g.module, n) < 0 && !g.ci.fns.has(n) { - return selectCall(g, c) - } - if n == "cpu_has" && lookup(g, n) < 0 && findFn(g, g.module, n) < 0 { - return cpuHasCall(g, c) - } - if n == "concat" && lookup(g, n) < 0 && findFn(g, g.module, n) < 0 && !g.ci.fns.has(n) { - return concatCall(g, c) - } - if ["fma", "muladd"].contains(n) { - return fmaCall(g, c) // Finch's, also for f32 and vectors (C's fma is the same for one double) - } - if ["f32_bits", "f32_from_bits", "f64_bits", "f64_from_bits", "f16_to_f32", "f32_to_f16", "bf16_to_f32", "f32_to_bf16"].contains(n) && !g.ci.fns.has(n) { - return bitsCall(g, c) - } if g.ci.fns.has(n) && !["min", "max", "abs", "clamp"].contains(n) { return callC(g, c, g.ci.fns[n]) // (C's abs only takes an int; Finch's takes any number) } @@ -5713,7 +5570,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) && v.ty.kind != "vec" { + if !isNumber(v.ty) { fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty)) } vals.push(v) @@ -5721,9 +5578,6 @@ 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 } @@ -5805,9 +5659,6 @@ 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)) { @@ -5862,7 +5713,6 @@ fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, st // The call itself, once the arguments are values ("i64 5"); the receiver's address, if any, is already first. fn emitCall(ptr[Gen] g, FnSig sig, ast.Node c, []str args, str shown) -> Val { - checkCpuCall(g, sig, c, shown) if sig.fallible && g.ftarget != nodeKey(c) { fail(g, c, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or , " + shown + "(...) or { ... }, or try " + shown + "(...)") } @@ -6052,15 +5902,6 @@ 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 { @@ -6551,7 +6392,6 @@ fn runtimeDecls() -> str { "declare i32 @finch_test_summary()", "declare void @finch_input(ptr, ptr)", "declare i32 @finch_file_exists(ptr)", - "declare i32 @finch_cpu_has(ptr)", "declare i32 @finch_str_try_int(ptr, ptr, ptr)", "declare i32 @finch_str_try_float(ptr, ptr, ptr)", "declare i32 @finch_try_read_file(ptr, ptr, ptr)", @@ -6600,7 +6440,7 @@ fn assemble(ptr[Gen] g) -> str { fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) -> str { shared := opt.shared - gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1, features: opt.features) + gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1) g := addr(gen) g.trace = opt.trace && !opt.shared if g.debug { @@ -6690,1052 +6530,3 @@ 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 (or f32, i64 for a plain number): how LLVM's intrinsics are named for this type -fn vecSuffix(ast.Type vt) -> str { - if vt.kind != "vec" { - if isFloat(vt) { - return "f" + str(bitsOf(vt)) - } - return "i" + str(bitsOf(vt)) - } - 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 lane.kind == "bool" { - fail(g, c, "a mask isn't loaded from a list; compare instead, like f32x8(list, i) > 0.0") - } - if c.kids.len != 2 && c.kids.len != 3 { - fail(g, c, shown + "(list, i): which index to start at? (" + shown + "(list, 0) for the first " + str(n) + ")") - } - if c.kids.len == 3 && ft.kind == "ptr" { - fail(g, c, shown + "(list, i, fill) needs a list: a ptr doesn't know where its numbers end") - } - 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 - fill := "" - if c.kids.len == 3 { - fill = coerce(g, expr(g, c.kids[2]), lane, c.kids[2], "the number for the lanes past the end").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) - } - len := op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1") - data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") - if fill.len > 0 { - // the end of a list: the lanes that are in it are loaded, the others get `fill` - m := tailMask(g, i, n, len, c) - ep := op(g, "getelementptr " + llty(g, lane) + ", ptr " + data + ", i64 " + i) - ml := llty(g, shaped(vt, "bool")) - fname := "@llvm.masked.load." + vecSuffix(vt) + ".p0" - declareOnce(g, "declare " + lt + " " + fname + "(ptr, i32, " + ml + ", " + lt + ")") - out := op(g, "call " + lt + " " + fname + "(ptr " + ep + ", i32 " + laneBytes(vt) + ", " + ml + " " + m + ", " + lt + " " + splat(g, fill, vt) + ")") - if !first.isPlace { - release(g, first.val) - } - return Val(v: out, ty: vt) - } - vecBounds(g, i, n, len, c) - } - 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] - if tl.kind == "bool" { - // a lane that isn't 0 is true - if isFloat(fl) { - return Val(v: op(g, "fcmp une " + llty(g, from) + " " + v.v + ", zeroinitializer"), ty: to) - } - return Val(v: op(g, "icmp ne " + llty(g, from) + " " + v.v + ", zeroinitializer"), ty: to) - } - if fl.kind == "bool" { - // true is 1 - if isFloat(tl) { - return Val(v: op(g, "uitofp " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) - } - return Val(v: op(g, "zext " + llty(g, from) + " " + v.v + " to " + llty(g, to)), ty: to) - } - 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 lane.kind == "bool" { - if e.name != "!" && e.name != "~" { - fail(g, e, "can't put '" + e.name + "' in front of a mask (" + ast.show(v.ty) + "); '!' flips it") - } - return Val(v: op(g, "xor " + lt + " " + v.v + ", " + splat(g, "true", v.ty)), ty: 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] - if lane.kind == "bool" && o != "&" && o != "|" && o != "^" { - fail(g, at, "can't use '" + o + "' on a mask (" + ast.show(vt) + "); masks take & | ^ and !") - } - 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, v == w ...: a mask (boolxN), the answer for every lane. m.all(), m.any(), select(m, a, b) use it. -fn vecCompare(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] - mask := shaped(vt, "bool") - if lane.kind == "bool" && o != "==" && o != "!=" { - fail(g, at, "masks can only be compared with == and !=") - } - if isFloat(lane) { - ops := ["==", "!=", "<", "<=", ">", ">="] - preds := ["oeq", "une", "olt", "ole", "ogt", "oge"] - return Val(v: op(g, "fcmp " + preds[ops.find(o)] + " " + llty(g, vt) + " " + a.v + ", " + b.v), ty: mask) - } - return Val(v: op(g, "icmp " + intPred(o, isSigned(lane)) + " " + llty(g, vt) + " " + a.v + ", " + b.v), ty: mask) -} - -// 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 lane.kind == "bool" && n != "shuffle" && n != "lo" && n != "hi" { - return maskMethod(g, m, v) - } - 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" || n == "muladd" { - if !isFloat(lane) { - fail(g, m, n + "() is for f32 and f64 lanes, but this is " + ast.show(vt)) - } - if n == "fma" || n == "muladd" { - need(g, m, 2) - b := asVec(g, expr(g, m.kids[1]), vt, n, m.kids[1]) - c := asVec(g, expr(g, m.kids[2]), vt, n, m.kids[2]) - return Val(v: fusedOp(g, n, vt, 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" || n == "store_part" { - 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], n + "(list, i) needs a list in a variable (this one would be thrown away)") - } - if n == "store_part" && dt.kind != "array" { - fail(g, m.kids[1], "store_part(list, i) needs a list: a ptr doesn't know where its numbers end") - } - 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) - len := op(g, "extractvalue { ptr, i64, i64 } " + av + ", 1") - data = op(g, "extractvalue { ptr, i64, i64 } " + av + ", 0") - if n == "store_part" { - // only the lanes that are in the list are written - mk := tailMask(g, i, vt.count, len, m) - ep := op(g, "getelementptr " + llty(g, lane) + ", ptr " + data + ", i64 " + i) - ml := llty(g, shaped(vt, "bool")) - fname := "@llvm.masked.store." + vecSuffix(vt) + ".p0" - declareOnce(g, "declare void " + fname + "(" + lt + ", ptr, i32, " + ml + ")") - emit(g, "call void " + fname + "(" + lt + " " + v.v + ", ptr " + ep + ", i32 " + laneBytes(vt) + ", " + ml + " " + mk + ")") - return Val(ty: T("void")) - } - vecBounds(g, i, vt.count, len, m) - } - 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")) - } - if n == "shuffle" || n == "lo" || n == "hi" || n == "lookup" { - return shuffleMethod(g, m, v) - } - if n == "dot_add" { - return dotAdd(g, m, v) - } - fail(g, m, "a " + ast.show(vt) + " has no method '" + n + "' (it has: sum, min, max, dot, abs, sqrt, floor, ceil, round, fma, muladd, store, store_part, shuffle, lo, hi, lookup, dot_add)") - return v -} - -// ---------- fused multiply-add, and numbers as bits ---------- -// -// Finch never fuses on its own: a * b + c is a multiply and an add, two roundings, on every processor and with every -// --cpu (the IR has no fast-math flags, and clang compiles it with -ffp-contract=on, which fuses only what asks to be). -// Fusing is asked for by name: -// fma(a, b, c) one rounding, always (one instruction with FMA hardware, a library call without it) -// muladd(a, b, c) whichever is faster here: fused where the processor can, a multiply and an add where it can't - -// the llvm.fma / llvm.fmuladd call for a number or a vector type -fn fusedOp(ptr[Gen] g, str name, ast.Type t, str a, str b, str c) -> str { - lt := llty(g, t) - which := "fma" - if name == "muladd" { - which = "fmuladd" - } - return vecIntrinsic(g, lt, which, t, [lt, lt, lt], [a, b, c], "") -} - -fn fmaCall(ptr[Gen] g, ast.Node c) -> Val { - n := c.name - if g.index { - noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) - } - checkArgs(g, c, 0, 3, n) - []Val vals - for a in c.kids { - v := expr(g, a) - 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) - } - // the type all three take: a vector if one is, else the float type they fit - t := T("void") - for v in vals { - if v.ty.kind == "vec" { - t = v.ty - } - } - if t.kind == "vec" { - if !isFloat(t.elem[0]) { - fail(g, c, n + "(...) is for f32 and f64 lanes, but this is " + ast.show(t)) - } - []str vs - for i in 0..3 { - vs.push(asVec(g, vals[i], t, n, c.kids[i]).v) - } - return Val(v: fusedOp(g, n, t, vs[0], vs[1], vs[2]), ty: t) - } - // f32 when an f32 takes part and nothing wider does (numbers written in the code adapt), else float - narrow := false - wide := false - for v in vals { - if v.ty.kind == "f32" { - narrow = true - } else if !v.lit { - wide = true - } - } - t = T("float") - if narrow && !wide { - t = T("f32") - } - []str vs - for i in 0..3 { - vs.push(coerce(g, vals[i], t, c.kids[i], "each number of " + n + "(...)").v) - } - return Val(v: fusedOp(g, n, t, vs[0], vs[1], vs[2]), ty: t) -} - -// the same shape as t (a number, or a vector of as many lanes) with lanes of another kind -fn shaped(ast.Type t, str lane) -> ast.Type { - if t.kind != "vec" { - return T(lane) - } - out := ast.Type(kind: "vec", count: t.count) - out.elem.push(T(lane)) - return out -} - -// a constant in every lane of t (or just the number) -fn konst(ptr[Gen] g, ast.Type t, str n) -> str { - if t.kind != "vec" { - return n - } - return splat(g, n, t) -} - -// f32_bits(x) -> u32 and back, f64 the same; f16 and bf16 (as the u16 that holds them) to f32 and back. -// Each works on a number or on a vector, lane by lane: f16_to_f32(u16x8) is an f32x8. -fn bitsCall(ptr[Gen] g, ast.Node c) -> Val { - n := c.name - if g.index { - noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) - } - checkArgs(g, c, 0, 1, n) - v := expr(g, c.kids[0]) - from := "" // the lane type it takes - to := "" - if n == "f32_bits" || n == "f32_to_f16" || n == "f32_to_bf16" { - from = "f32" - to = "u32" - if n != "f32_bits" { - to = "u16" - } - } else if n == "f64_bits" { - from = "float" - to = "u64" - } else if n == "f32_from_bits" { - from = "u32" - to = "f32" - } else if n == "f64_from_bits" { - from = "u64" - to = "float" - } else { - from = "u16" - to = "f32" - } - lane := v.ty - if v.ty.kind == "vec" { - lane = v.ty.elem[0] - } - // the bits may come as a signed number of the same size (i32 for u32), and a number written in the code fits - if isInt(T(from)) && isInt(lane) && bitsOf(lane) == bitsOf(T(from)) { - v = Val(v: v.v, ty: shaped(v.ty, from)) - } else if v.ty.kind != "vec" && v.lit { - v = coerce(g, v, T(from), c.kids[0], "the value of " + n + "(...)") - } else if lane.kind != from { - fail(g, c.kids[0], n + "(...) takes " + ast.show(T(from)) + " (or a vector of them), but this is " + ast.show(v.ty)) - } - src := shaped(v.ty, from) - out := shaped(v.ty, to) - il := llty(g, src) - ol := llty(g, out) - if n == "f32_bits" || n == "f64_bits" || n == "f32_from_bits" || n == "f64_from_bits" { - return Val(v: op(g, "bitcast " + il + " " + v.v + " to " + ol), ty: out) - } - if n == "bf16_to_f32" { - // bf16 is the top half of an f32 - wide := shaped(v.ty, "u32") - wl := llty(g, wide) - z := op(g, "zext " + il + " " + v.v + " to " + wl) - sh := op(g, "shl " + wl + " " + z + ", " + konst(g, wide, "16")) - return Val(v: op(g, "bitcast " + wl + " " + sh + " to " + ol), ty: out) - } - if n == "f32_to_bf16" { - // the top half, rounded to the nearest (ties to even); a NaN stays a NaN - wide := shaped(v.ty, "u32") - wl := llty(g, wide) - u := op(g, "bitcast " + il + " " + v.v + " to " + wl) - hi := op(g, "lshr " + wl + " " + u + ", " + konst(g, wide, "16")) - odd := op(g, "and " + wl + " " + hi + ", " + konst(g, wide, "1")) - bias := op(g, "add " + wl + " " + odd + ", " + konst(g, wide, "32767")) - rounded := op(g, "lshr " + wl + " " + op(g, "add " + wl + " " + u + ", " + bias) + ", " + konst(g, wide, "16")) - quiet := op(g, "or " + wl + " " + hi + ", " + konst(g, wide, "64")) - isNan := op(g, "fcmp uno " + il + " " + v.v + ", " + v.v) - pickd := op(g, "select " + llty(g, shaped(v.ty, "bool")) + " " + isNan + ", " + wl + " " + quiet + ", " + wl + " " + rounded) - return Val(v: op(g, "trunc " + wl + " " + pickd + " to " + ol), ty: out) - } - // f16: LLVM's half type (one instruction with F16C or on ARM64, a small library function elsewhere) - hl := "half" - if v.ty.kind == "vec" { - hl = "<" + str(v.ty.count) + " x half>" - } - if n == "f16_to_f32" { - h := op(g, "bitcast " + il + " " + v.v + " to " + hl) - return Val(v: op(g, "fpext " + hl + " " + h + " to " + ol), ty: out) - } - h := op(g, "fptrunc " + il + " " + v.v + " to " + hl) - return Val(v: op(g, "bitcast " + hl + " " + h + " to " + ol), ty: out) -} - -// ---------- masks: boolxN ---------- -// -// A comparison of vectors gives a mask, one true/false a lane (). It picks lanes with select, says -// whether any or all lanes are true, and loads and stores the end of a list (f32x8(list, i, fill), store_part). - -// lanes 0, 1, ... n-1 that are inside a list of `len` from index i (i may be past the end: then none) -fn tailMask(ptr[Gen] g, str i, int n, str len, ast.Node at) -> str { - panicIf(g, op(g, "icmp slt i64 " + i + ", 0"), "a vector's index is below 0", at) - rest := op(g, "sub i64 " + len + ", " + i) - []str lanes - for k in 0..n { - lanes.push("i64 " + str(k)) - } - wide := ast.Type(kind: "vec", count: n) - wide.elem.push(T("int")) - return op(g, "icmp slt <" + str(n) + " x i64> <" + lanes.join(", ") + ">, " + splat(g, rest, wide)) -} - -// select(mask, a, b): a's lane where the mask is true, b's where it is false -fn selectCall(ptr[Gen] g, ast.Node c) -> Val { - if g.index { - noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "select")), "", 0, 0) - } - checkArgs(g, c, 0, 3, "select") - m := expr(g, c.kids[0]) - if m.ty.kind != "vec" || m.ty.elem[0].kind != "bool" { - fail(g, c.kids[0], "select(mask, a, b) takes a mask first, from a comparison of vectors like v > 0.0; this is " + ast.show(m.ty)) - } - a := expr(g, c.kids[1]) - b := expr(g, c.kids[2]) - vt := a.ty - if vt.kind != "vec" { - vt = b.ty - } - if vt.kind != "vec" { - fail(g, c, "select(mask, a, b): a or b has to be a vector (the other may be a number for every lane)") - } - if vt.count != m.ty.count { - fail(g, c, "the mask has " + str(m.ty.count) + " lanes, but " + ast.show(vt) + " has " + str(vt.count)) - } - av := asVec(g, a, vt, "select", c.kids[1]) - bv := asVec(g, b, vt, "select", c.kids[2]) - lt := llty(g, vt) - return Val(v: op(g, "select " + llty(g, m.ty) + " " + m.v + ", " + lt + " " + av.v + ", " + lt + " " + bv.v), ty: vt) -} - -fn maskMethod(ptr[Gen] g, ast.Node m, Val v) -> Val { - n := m.name - ml := llty(g, v.ty) - if n == "any" || n == "all" { - need(g, m, 0) - how := "or" - if n == "all" { - how = "and" - } - return Val(v: vecIntrinsic(g, "i1", "vector.reduce." + how, v.ty, [ml], [v.v], ""), ty: T("bool")) - } - if n == "count" { - need(g, m, 0) - wide := shaped(v.ty, "i32") - wl := llty(g, wide) - z := op(g, "zext " + ml + " " + v.v + " to " + wl) - sum := vecIntrinsic(g, "i32", "vector.reduce.add", wide, [wl], [z], "") - return Val(v: op(g, "zext i32 " + sum + " to i64"), ty: T("int")) - } - fail(g, m, "a mask (" + ast.show(v.ty) + ") has .len, [i] and the methods any, all, count; pick lanes with select(mask, a, b)") - return v -} - -// ---------- moving lanes around ---------- - -fn indexList([]int idx) -> str { - []str parts - for k in idx { - parts.push("i32 " + str(k)) - } - return "<" + parts.join(", ") + ">" -} - -fn shuffleOf(ptr[Gen] g, Val v, []int idx, str v2) -> Val { - vt := v.ty - out := ast.Type(kind: "vec", count: idx.len) - out.elem.push(vt.elem[0]) - second := v2 - if second.len == 0 { - second = "poison" - } - return Val(v: op(g, "shufflevector " + llty(g, vt) + " " + v.v + ", " + llty(g, vt) + " " + second + ", <" + str(idx.len) + " x i32> " + indexList(idx)), ty: out) -} - -fn fitsVector(ptr[Gen] g, ast.Node at, ast.Type lane, int n) { - if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 { - fail(g, at, "a vector has 2, 4, 8, 16, 32 or 64 lanes, not " + str(n)) - } - bits := bitsOf(lane) - if n * bits > 512 { - fail(g, at, "that would be " + str(n) + " lanes of " + ast.show(lane) + ", more than 512 bits") - } -} - -// v.shuffle(3, 2, 1, 0): lanes picked by number (numbers written in the code); v.lo(), v.hi(): the halves; -// table.lookup(idx): table[idx % table.len] for every lane of idx (pshufb on x86, tbl on ARM) -fn shuffleMethod(ptr[Gen] g, ast.Node m, Val v) -> Val { - vt := v.ty - n := m.name - if n == "lo" || n == "hi" { - need(g, m, 0) - if vt.count < 4 { - fail(g, m, "a " + ast.show(vt) + " has only " + str(vt.count) + " lanes; take them with v[0] and v[1]") - } - []int idx - start := 0 - if n == "hi" { - start = vt.count / 2 - } - for k in 0..vt.count / 2 { - idx.push(start + k) - } - return shuffleOf(g, v, idx, "") - } - if n == "shuffle" { - []int idx - for q in 1..m.kids.len { - x := expr(g, m.kids[q]) - if !isInt(x.ty) || !x.lit { - fail(g, m.kids[q], "shuffle(...) takes lane numbers written in the code (or constants), like v.shuffle(3, 2, 1, 0)") - } - k := int(x.v) or -1 - if k < 0 || k >= vt.count { - fail(g, m.kids[q], "lane " + x.v + " isn't in a " + ast.show(vt) + " (its lanes are 0 to " + str(vt.count - 1) + ")") - } - idx.push(k) - } - fitsVector(g, m, vt.elem[0], idx.len) - return shuffleOf(g, v, idx, "") - } - // lookup - need(g, m, 1) - ix := expr(g, m.kids[1]) - if ix.ty.kind != "vec" || !isInt(ix.ty.elem[0]) { - fail(g, m.kids[1], "lookup(indices) takes a vector of whole numbers (like u8x16), but this is " + ast.show(ix.ty)) - } - if vt.elem[0].kind == "bool" { - fail(g, m, "a mask can't be a lookup table") - } - fitsVector(g, m.kids[1], vt.elem[0], ix.ty.count) - il := llty(g, ix.ty) - masked := op(g, "and " + il + " " + ix.v + ", " + splat(g, str(vt.count - 1), ix.ty)) - out := shaped(ix.ty, vt.elem[0].kind) - ol := llty(g, out) - el := llty(g, vt.elem[0]) - xl := llty(g, ix.ty.elem[0]) - cur := "poison" - for k in 0..ix.ty.count { - at := op(g, "extractelement " + il + " " + masked + ", i64 " + str(k)) - lane := op(g, "extractelement " + llty(g, vt) + " " + v.v + ", " + xl + " " + at) - cur = op(g, "insertelement " + ol + " " + cur + ", " + el + " " + lane + ", i64 " + str(k)) - } - return Val(v: cur, ty: out) -} - -// concat(a, b): a's lanes, then b's -fn concatCall(ptr[Gen] g, ast.Node c) -> Val { - if g.index { - noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "concat")), "", 0, 0) - } - checkArgs(g, c, 0, 2, "concat") - a := expr(g, c.kids[0]) - b := expr(g, c.kids[1]) - if a.ty.kind != "vec" || !ast.same(a.ty, b.ty) { - fail(g, c, "concat(a, b) joins two vectors of the same type, but these are " + ast.show(a.ty) + " and " + ast.show(b.ty)) - } - fitsVector(g, c, a.ty.elem[0], a.ty.count * 2) - []int idx - for k in 0..a.ty.count * 2 { - idx.push(k) - } - return shuffleOf(g, a, idx, b.v) -} - -// ---------- processor features: @cpu(...), cpu_has(...) ---------- -// -// --cpu gives the whole program a processor (main.fch asks clang which features that means); @cpu("avx2,fma") gives one -// function more, as LLVM's "target-features". Such a function is only called after cpu_has("avx2") said yes. Features of -// the other architecture are dropped, so one source holds the x86 and the ARM versions side by side. - -fn x86Features() -> []str { - return ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt", "avx", "avx2", "fma", "f16c", "bmi1", "bmi2", "avx512f", "avx512bw", - "avx512dq", "avx512vl", "avx512vnni", "avx512bf16", "avx512fp16", "avxvnni"] -} - -fn armFeatures() -> []str { - return ["neon", "fp16", "dotprod", "i8mm", "bf16", "sve", "sve2"] -} - -// the name LLVM uses for a feature -fn llvmFeature(str x) -> str { - if x == "bmi1" { - return "bmi" - } - if x == "fp16" { - return "fullfp16" - } - return x -} - -// the features of a level: x86-64-v3 is the AVX2 generation, v4 the AVX-512 one -fn levelFeatures(str name) -> []str { - []str out - if name == "x86-64-v2" || name == "x86-64-v3" || name == "x86-64-v4" { - for x in ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt"] { - out.push(x) - } - } - if name == "x86-64-v3" || name == "x86-64-v4" { - for x in ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma"] { - out.push(x) - } - } - if name == "x86-64-v4" { - for x in ["avx512f", "avx512bw", "avx512dq", "avx512vl"] { - out.push(x) - } - } - return out -} - -// a function's own @cpu features, for the architecture it is built for -fn cpuOf(ptr[Gen] g, ast.Node f) -> []str { - []str out - if f.cpu.len == 0 { - return out - } - for part in f.cpu.split(",") { - x := part.trim() - names := levelFeatures(x) - if names.len == 0 { - if !x86Features().contains(x) && !armFeatures().contains(x) { - fail(g, f, "@cpu: '" + x + "' isn't a feature Finch knows (x86: " + x86Features().join(", ") + ", x86-64-v2/v3/v4; ARM: " + armFeatures().join(", ") + ")") - } - names.push(x) - } - for y in names { - if armFeatures().contains(y) == g.tg.arm64 && !out.contains(y) { - out.push(y) - } - } - } - return out -} - -// a vector wider than 16 bytes travels in different registers depending on the features (two xmm, or one ymm), so it -// can't cross from a function with one set of features to a function with another -fn checkCpuCall(ptr[Gen] g, FnSig sig, ast.Node c, str shown) { - if sig.prog < 0 || sig.prog >= g.progs.len || sig.index >= g.progs[sig.prog].fns.len { - return - } - callee := g.progs[sig.prog].fns[sig.index].cpu - mine := g.progs[g.cur.prog].fns[g.cur.index].cpu - if callee == mine { - return - } - []ast.Type ts = sig.params - ts.push(sig.ret) - for t in ts { - if t.kind == "vec" && t.count * bitsOf(t.elem[0]) > 128 { - fail(g, c, "'" + shown + "' has other @cpu features than this function, so a " + ast.show(t) + " can't be passed between them (its registers differ); pass a list or a ptr, or give both the same @cpu") - } - } -} - -// cpu_has("avx2"): does the processor running the program have it? (false for the other architecture's names) -fn cpuHasCall(ptr[Gen] g, ast.Node c) -> Val { - if g.index { - noteRef(g, c, c.col, c.name.len, docs.hover(docs.find(docs.builtins(), "cpu_has")), "", 0, 0) - } - checkArgs(g, c, 0, 1, "cpu_has") - if c.kids[0].kind == "str" { - x := c.kids[0].text - if !x86Features().contains(x) && !armFeatures().contains(x) { - fail(g, c.kids[0], "cpu_has: '" + x + "' isn't a feature Finch knows (x86: " + x86Features().join(", ") + "; ARM: " + armFeatures().join(", ") + ")") - } - } - s := strArg(g, c.kids[0], "the feature's name") - r := op(g, "call i32 @finch_cpu_has(ptr " + spill(g, s) + ")") - release(g, s) - return Val(v: op(g, "icmp ne i32 " + r + ", 0"), ty: T("bool")) -} - -// ---------- acc.dot_add(a, b): the step of an int8 dot product ---------- -// -// acc is i32xN; a and b have 4N bytes (a u8 or i8, b i8 or u8). Lane k of acc gains a[4k]*b[4k] + ... + a[4k+3]*b[4k+3], -// exactly (no saturation). That is vpdpbusd (AVX-VNNI, AVX-512 VNNI) and usdot / sdot / udot (ARM), used when the -// function's features have them; elsewhere the same sum is written out, which LLVM makes into pmaddwd and adds. -fn dotAdd(ptr[Gen] g, ast.Node m, Val acc) -> Val { - need(g, m, 2) - at := acc.ty - if at.elem[0].kind != "i32" && at.elem[0].kind != "u32" { - fail(g, m, "dot_add is called on an i32 vector (the sums), like acc.dot_add(a, b) with acc an i32x8; this is " + ast.show(at)) - } - a := expr(g, m.kids[1]) - b := expr(g, m.kids[2]) - for x in [a, b] { - if x.ty.kind != "vec" || (x.ty.elem[0].kind != "u8" && x.ty.elem[0].kind != "i8") || x.ty.count != at.count * 4 { - fail(g, m, "dot_add(a, b) on an " + ast.show(at) + " takes two vectors of " + str(at.count * 4) + " bytes (u8 or i8), but these are " + ast.show(a.ty) + " and " + ast.show(b.ty)) - } - } - au := a.ty.elem[0].kind == "u8" - bu := b.ty.elem[0].kind == "u8" - if !au && bu { - // u8 x i8 is the same sum as i8 x u8: put the unsigned one first - t := a - a = b - b = t - au = true - bu = false - } - al := llty(g, at) - bits := at.count * 32 - f := g.curFeatures - if !g.tg.arm64 && au && !bu { - vnni := (bits == 512 && f.contains("avx512vnni")) || (bits <= 256 && (f.contains("avxvnni") || (f.contains("avx512vnni") && f.contains("avx512vl")))) - if vnni { - a32 := op(g, "bitcast " + llty(g, a.ty) + " " + a.v + " to " + al) - b32 := op(g, "bitcast " + llty(g, b.ty) + " " + b.v + " to " + al) - name := "@llvm.x86.avx512.vpdpbusd." + str(bits) - declareOnce(g, "declare " + al + " " + name + "(" + al + ", " + al + ", " + al + ")") - return Val(v: op(g, "call " + al + " " + name + "(" + al + " " + acc.v + ", " + al + " " + a32 + ", " + al + " " + b32 + ")"), ty: at) - } - } - if g.tg.arm64 && bits % 128 == 0 { - which := "" - if au && !bu && f.contains("i8mm") { - which = "usdot" - } else if au && bu && f.contains("dotprod") { - which = "udot" - } else if !au && !bu && f.contains("dotprod") { - which = "sdot" - } - if which.len > 0 { - // one instruction per 16 bytes - name := "@llvm.aarch64.neon." + which + ".v4i32.v16i8" - declareOnce(g, "declare <4 x i32> " + name + "(<4 x i32>, <16 x i8>, <16 x i8>)") - parts := at.count / 4 - []str outs - for q in 0..parts { - []int ai - for k in 0..4 { - ai.push(q * 4 + k) - } - []int bi - for k in 0..16 { - bi.push(q * 16 + k) - } - ac := shuffleOf(g, acc, ai, "").v - ap := shuffleOf(g, a, bi, "").v - bp := shuffleOf(g, b, bi, "").v - outs.push(op(g, "call <4 x i32> " + name + "(<4 x i32> " + ac + ", <16 x i8> " + ap + ", <16 x i8> " + bp + ")")) - } - return Val(v: joinParts(g, outs, 4), ty: at) - } - } - // written out: widen, multiply, add each group of four - w := shaped(a.ty, "i32") - wl := llty(g, w) - ext := "sext" - if au { - ext = "zext" - } - aw := op(g, ext + " " + llty(g, a.ty) + " " + a.v + " to " + wl) - ext = "sext" - if bu { - ext = "zext" - } - bw := op(g, ext + " " + llty(g, b.ty) + " " + b.v + " to " + wl) - prod := Val(v: op(g, "mul " + wl + " " + aw + ", " + bw), ty: w) - sum := acc.v - for k in 0..4 { - []int idx - for j in 0..at.count { - idx.push(4 * j + k) - } - sum = op(g, "add " + al + " " + sum + ", " + shuffleOf(g, prod, idx, "").v) - } - return Val(v: sum, ty: at) -} - -// <4 x i32> pieces back into one vector of n * 4 lanes -fn joinParts(ptr[Gen] g, []str parts, int each) -> str { - if parts.len == 1 { - return parts[0] - } - []str cur = parts - size := each - while cur.len > 1 { - []str next - t := ast.Type(kind: "vec", count: size) - t.elem.push(T("i32")) - k := 0 - while k < cur.len { - []int idx - for j in 0..size * 2 { - idx.push(j) - } - next.push(shuffleOf(g, Val(v: cur[k], ty: t), idx, cur[k + 1]).v) - k += 2 - } - cur = next - size = size * 2 - } - return cur[0] -} diff --git a/src/lexer.fch b/src/lexer.fch index 19accef..ed4337b 100644 --- a/src/lexer.fch +++ b/src/lexer.fch @@ -70,7 +70,7 @@ fn operator(ptr[Lexer] lx) -> str { return two } } - ones := "(){}[],.:+-*/%&|^~<>=!@" + ones := "(){}[],.:+-*/%&|^~<>=!" c := lx.src.sub(lx.i, lx.i + 1) if ones.contains(c) { return c diff --git a/src/linker.fch b/src/linker.fch index 9cda7a2..e55addc 100644 --- a/src/linker.fch +++ b/src/linker.fch @@ -371,7 +371,10 @@ struct Build { // IR -> program. 0 if it worked. fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { - clang := clangCmd() + clang := getenv("FINCH_CLANG") + if clang.len == 0 { + clang = "clang" + } // the C compiler of the runtime and the linker: the target's own when building for another system cc := clang if tg.cross { @@ -388,8 +391,7 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { target = " --target=" + tg.triple } log := b.out + ".log" - // -ffp-contract=on: only what asks to be fused is (muladd); a * b + c stays two roundings, whatever clang's default - obj := irObject(clang, target + " " + b.opt + " -ffp-contract=on" + b.extra, b.ir, b.name + "\n" + tg.triple, log) or { + obj := irObject(clang, target + " " + b.opt + b.extra, b.ir, b.name + "\n" + tg.triple, log) or { print(err) host.remove(log) return 1 @@ -475,74 +477,3 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { } return 0 } - -// clang, as the program is compiled with it. Nix's clang wrapper removes -march=native when NIX_ENFORCE_NO_NATIVE -// is 1, as it is in nix-shell; --cpu native asks for it on purpose, so the wrapper is told to keep it. -fn clangCmd() -> str { - clang := getenv("FINCH_CLANG") - if clang.len == 0 { - clang = "clang" - } - if host.pathSeparator() != ";" { - return "NIX_ENFORCE_NO_NATIVE=0 " + clang - } - return clang -} - -// -march=x86-64-v3 or -mcpu=apple-m1: how clang is told the processor -fn cpuFlag(platform.Target tg, str cpu) -> str { - if cpu.len == 0 { - return "" - } - if tg.arm64 { - return " -mcpu=" + quote(cpu) - } - return " -march=" + quote(cpu) -} - -// What the processor the program is for can do, as Finch's feature names (avx2, avxvnni, i8mm ...): clang says it in -// the macros it predefines for that target and --cpu. Kept in the cache, one file per clang, target and cpu. -fn cpuFeatures(platform.Target tg, str cpu) -> []str { - clang := clangCmd() - flags := cpuFlag(tg, cpu) - if tg.cross || tg.windows { - flags = " --target=" + tg.triple + flags - } - dir := host.cacheDir() + "/cpu" - host.makeDirs(dir) - slot := dir + "/" + fingerprint2(clang + flags) - text := read_file(slot + ".txt") or "" - if text.len == 0 { - empty := slot + ".part" + str(host.pid()) + ".c" - out := slot + ".part" + str(host.pid()) + ".txt" - write_file(empty, "") - host.run(clang + flags + " -dM -E " + quote(empty) + " > " + quote(out) + " 2> " + host.nullDevice()) - host.remove(empty) - text = read_file(out) or "" - if text.len == 0 { - host.remove(out) - []str none - return none - } - host.move(out, slot + ".txt") - } - macros := ["__SSE3__", "__SSSE3__", "__SSE4_1__", "__SSE4_2__", "__POPCNT__", "__AVX__", "__AVX2__", "__FMA__", "__F16C__", - "__BMI__", "__BMI2__", "__AVX512F__", "__AVX512BW__", "__AVX512DQ__", "__AVX512VL__", "__AVX512VNNI__", - "__AVX512BF16__", "__AVX512FP16__", "__AVXVNNI__", "__ARM_NEON", "__ARM_FEATURE_FP16_VECTOR_ARITHMETIC", - "__ARM_FEATURE_DOTPROD", "__ARM_FEATURE_MATMUL_INT8", "__ARM_FEATURE_BF16_VECTOR_ARITHMETIC", "__ARM_FEATURE_SVE", - "__ARM_FEATURE_SVE2"] - names := ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt", "avx", "avx2", "fma", "f16c", "bmi1", "bmi2", "avx512f", "avx512bw", - "avx512dq", "avx512vl", "avx512vnni", "avx512bf16", "avx512fp16", "avxvnni", "neon", "fp16", "dotprod", "i8mm", "bf16", - "sve", "sve2"] - []str have - for line in text.split("\n") { - parts := line.split(" ") - if parts.len >= 2 && parts[0] == "#define" { - k := macros.find(parts[1]) - if k >= 0 && !have.contains(names[k]) { - have.push(names[k]) - } - } - } - return have -} diff --git a/src/lsp.fch b/src/lsp.fch index fc8299d..0bc0fcc 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -7,7 +7,6 @@ import json import host import docs -import ast import fmt import "stdio.h" import "stdlib.h" @@ -622,8 +621,6 @@ 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 6cf7f61..f4c4f00 100644 --- a/src/main.fch +++ b/src/main.fch @@ -1,4 +1,7 @@ -// finch: the Finch compiler, written in Finch (`finch` with no arguments lists the commands). +// finch-boot: the Finch compiler written in Finch. +// +// finch-boot build [-o name] compile a program +// finch-boot ir print the LLVM IR // // 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. @@ -171,9 +174,6 @@ 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.6.0 (the compiler is written in Finch)") + print("Finch 2.5.0 (the compiler is written in Finch)") return 0 } if args.len >= 2 && args[1] == "fmt" { @@ -293,7 +293,6 @@ 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 @@ -336,9 +335,6 @@ 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 { @@ -422,9 +418,6 @@ 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 { @@ -471,15 +464,7 @@ fn main([]str args) -> int { testModules.push(prog.module) // your own files' tests, not the standard library's } } - // what the processor can do (dot_add and the like pick their instructions by it); not needed for the editor's index - []str features - if cmd != "index" { - features = linker.cpuFeatures(tg, cpu) - if cpu == "native" && !tg.arm64 && !features.contains("sse4.2") { - print("warning: --cpu native made no difference to clang (a clang wrapper may remove -march=native; try --cpu with a name, like x86-64-v3)") - } - } - ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg, test: cmd == "test", testModules: testModules, trace: trace, features: features)) + ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg, test: cmd == "test", testModules: testModules, trace: trace)) if cmd == "ir" || cmd == "index" { print(ir) return 0 @@ -509,7 +494,6 @@ fn main([]str args) -> int { if debug { extra = extra + " -g" // or clang drops the debug info that is in the IR } - extra = extra + linker.cpuFlag(tg, cpu) // x86-64 names a processor with -march, ARM with -mcpu []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 7fef3c4..ce3aff3 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) || ast.vecType(n).kind.len > 0 + return names.contains(n) } // int, str, ptr[T], []T, Name, module.Name @@ -117,10 +117,6 @@ 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) @@ -289,22 +285,6 @@ fn parseProgram(ptr[Parser] p) -> ast.Program { if e.flag { prog.structs.push(sumStruct(e)) // an enum with values inside is stored as a struct (see sumStruct) } - } else if at(p, "@") { - // @cpu("avx2,fma") fn ...: a function that may use these processor features (check with cpu_has first) - a := next(p) - name := expect(p, "ident", "an attribute name, like @cpu(\"avx2\")").text - if name != "cpu" { - fail(p, a, "the attribute here is @cpu(\"...\"): the processor features a function may use") - } - expect(p, "(", "'('") - feats := expect(p, "string", "the features in quotes, like @cpu(\"avx2,fma\")").text - expect(p, ")", "')'") - if !at(p, "fn") && !at(p, "export") { - fail(p, cur(p), "@cpu(...) goes right before a function") - } - f := parseFn(p) - f.cpu = feats - prog.fns.push(f) } else { prog.fns.push(parseFn(p)) } diff --git a/src/platform.fch b/src/platform.fch index 2cbb0c2..44a678f 100644 --- a/src/platform.fch +++ b/src/platform.fch @@ -1,5 +1,6 @@ // 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/cpu_attr_name.fch b/tests/fail/cpu_attr_name.fch deleted file mode 100644 index f7702a9..0000000 --- a/tests/fail/cpu_attr_name.fch +++ /dev/null @@ -1,7 +0,0 @@ -// expect: @cpu: 'avx3' isn't a feature Finch knows -@cpu("avx2,avx3") -fn f() { -} -fn main() { - f() -} diff --git a/tests/fail/cpu_has_name.fch b/tests/fail/cpu_has_name.fch deleted file mode 100644 index c1ba4be..0000000 --- a/tests/fail/cpu_has_name.fch +++ /dev/null @@ -1,4 +0,0 @@ -// expect: cpu_has: 'avx-512' isn't a feature Finch knows -fn main() { - print(cpu_has("avx-512")) -} diff --git a/tests/fail/cpu_wide_vector.fch b/tests/fail/cpu_wide_vector.fch deleted file mode 100644 index a384b7d..0000000 --- a/tests/fail/cpu_wide_vector.fch +++ /dev/null @@ -1,8 +0,0 @@ -// expect: can't be passed between them (its registers differ) -@cpu("avx2") -fn double(f32x8 v) -> f32x8 { - return v * 2.0 -} -fn main() { - print(double(f32x8(1.0))) -} diff --git a/tests/fail/vec_intdiv.fch b/tests/fail/vec_intdiv.fch deleted file mode 100644 index 146aa49..0000000 --- a/tests/fail/vec_intdiv.fch +++ /dev/null @@ -1,5 +0,0 @@ -// 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 deleted file mode 100644 index 8d71340..0000000 --- a/tests/fail/vec_lane_index.fch +++ /dev/null @@ -1,6 +0,0 @@ -// 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 deleted file mode 100644 index 3e4d0f2..0000000 --- a/tests/fail/vec_lanes.fch +++ /dev/null @@ -1,4 +0,0 @@ -// 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 deleted file mode 100644 index 36c975f..0000000 --- a/tests/fail/vec_less.fch +++ /dev/null @@ -1,7 +0,0 @@ -// expect: use .all() or .any() -fn main() { - a := f32x4(1.0) - if a < a { - print("no") - } -} diff --git a/tests/fail/vec_load_end.fch b/tests/fail/vec_load_end.fch deleted file mode 100644 index aa5cb71..0000000 --- a/tests/fail/vec_load_end.fch +++ /dev/null @@ -1,5 +0,0 @@ -// 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 deleted file mode 100644 index 4b05a3c..0000000 --- a/tests/fail/vec_mix.fch +++ /dev/null @@ -1,6 +0,0 @@ -// 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_shuffle_range.fch b/tests/fail/vec_shuffle_range.fch deleted file mode 100644 index 516bac5..0000000 --- a/tests/fail/vec_shuffle_range.fch +++ /dev/null @@ -1,5 +0,0 @@ -// expect: lane 4 isn't in a f32x4 (its lanes are 0 to 3) -fn main() { - v := f32x4(1.0) - print(v.shuffle(0, 1, 2, 4)) -} diff --git a/tests/fail/vec_shuffle_var.fch b/tests/fail/vec_shuffle_var.fch deleted file mode 100644 index 94c98a2..0000000 --- a/tests/fail/vec_shuffle_var.fch +++ /dev/null @@ -1,6 +0,0 @@ -// expect: shuffle(...) takes lane numbers written in the code -fn main() { - v := f32x4(1.0) - k := 2 - print(v.shuffle(k, 0, 1, 3)) -} diff --git a/tests/fail/vec_type.fch b/tests/fail/vec_type.fch deleted file mode 100644 index 0f44299..0000000 --- a/tests/fail/vec_type.fch +++ /dev/null @@ -1,5 +0,0 @@ -// expect: f32x8(x) puts x in every lane -fn main() { - f32x8 v = 1.0 - print(v) -} diff --git a/tests/run/dot_int8.fch b/tests/run/dot_int8.fch deleted file mode 100644 index 3615628..0000000 --- a/tests/run/dot_int8.fch +++ /dev/null @@ -1,69 +0,0 @@ -// The int8 dot product of quantized models: plain, with dot_add, and in @cpu functions picked by cpu_has - -fn plain([]u8 a, []i8 b) -> int { - total := 0 - for i in 0..a.len { - total += int(a[i]) * int(b[i]) - } - return total -} - -// 32 bytes a step, 8 sums kept in a vector until the end -fn with_vectors([]u8 a, []i8 b) -> int { - acc := i32x8(0) - i := 0 - while i + 32 <= a.len { - acc = acc.dot_add(u8x32(a, i), i8x32(b, i)) - i += 32 - } - return int(acc.sum()) -} - -@cpu("avxvnni") -fn with_vnni([]u8 a, []i8 b) -> int { - acc := i32x8(0) - i := 0 - while i + 32 <= a.len { - acc = acc.dot_add(u8x32(a, i), i8x32(b, i)) - i += 32 - } - return int(acc.sum()) -} - -@cpu("avx512f,avx512bw,avx512vl,avx512vnni,i8mm") -fn with_avx512([]u8 a, []i8 b) -> int { - acc := i32x16(0) - i := 0 - while i + 64 <= a.len { - acc = acc.dot_add(u8x64(a, i), i8x64(b, i)) - i += 64 - } - return int(acc.sum()) -} - -fn best([]u8 a, []i8 b) -> int { - if cpu_has("avx512vnni") && cpu_has("avx512vl") && cpu_has("avx512bw") { - return with_avx512(a, b) - } - if cpu_has("avxvnni") { - return with_vnni(a, b) - } - return with_vectors(a, b) -} - -fn main() { - []u8 a - []i8 b - for k in 0..1024 { - a.push(u8((k * 37 + 11) % 256)) - b.push(i8((k * 53 + 7) % 256 - 128)) - } - want := plain(a, b) - print(want, with_vectors(a, b) == want, best(a, b) == want) - - // the extremes: 255 * -128 four times in a lane, no saturation - acc := i32x4(1, 2, 3, 4).dot_add(u8x16(255), i8x16(-128)) - print(acc, i32x4(0).dot_add(i8x16(-128), i8x16(-128)), i32x4(0).dot_add(u8x16(255), u8x16(255))) - print(i32x4(0).dot_add(i8x16(-1), u8x16(200))) - print(cpu_has("avx2") || cpu_has("neon") || true) -} diff --git a/tests/run/dot_int8.out b/tests/run/dot_int8.out deleted file mode 100644 index 28479e1..0000000 --- a/tests/run/dot_int8.out +++ /dev/null @@ -1,4 +0,0 @@ --68096 true true -i32x4(-130559, -130558, -130557, -130556) i32x4(65536, 65536, 65536, 65536) i32x4(260100, 260100, 260100, 260100) -i32x4(-800, -800, -800, -800) -true diff --git a/tests/run/floatbits.fch b/tests/run/floatbits.fch deleted file mode 100644 index fe63f3a..0000000 --- a/tests/run/floatbits.fch +++ /dev/null @@ -1,31 +0,0 @@ -// a * b + c is never fused; fma rounds once; numbers as their bits; f16 and bf16 - -fn plain(float a, float b, float c) -> float { - return a * b + c -} - -fn main() { - // 0.1 * 10 is 1.0000000000000000555..., which rounds to 1 before the - 1 (two roundings), but not inside fma - print(plain(0.1, 10.0, -1.0), fma(0.1, 10.0, -1.0)) - print(fma(f32(0.1), 10.0, -1.0), fma(2, 3, 4)) - m := muladd(1.5, 2.0, 0.25) // fused or not, exact here - print(m, f32x4(1.5).muladd(2.0, 0.25), fma(f32x4(0.1), 10.0, f32x4(-1.0))[0]) - det := f32x4(0.1) * f32x4(10.0) - f32x4(1.0) - print(det[0]) - - // bits - print(f32_bits(1.0), f64_bits(1.0), f32_bits(-0.0)) - print(f32_from_bits(0x40490fdb), f64_from_bits(f64_bits(2.5)), f32_from_bits(i32(-1082130432))) - u32 inf = 0x7f800000 - print(f32_from_bits(inf), f32_bits(f32x4(1.0, 2.0, -2.0, 0.5))) - - // f16 - print(f16_to_f32(0x3c00), f16_to_f32(0xc000), f16_to_f32(0x7bff), f16_to_f32(0x0001)) - print(f32_to_f16(0.1), f32_to_f16(65504.0), f32_to_f16(1.0e6), f16_to_f32(f32_to_f16(0.333))) - print(f16_to_f32(u16x8(0x3c00, 0x4000, 0x4200, 0x4400, 0, 0x8000, 0x3800, 0x3400))) - - // bf16 - print(bf16_to_f32(0x3f80), bf16_to_f32(0x4049), f32_to_bf16(3.14159)) - print(f32_to_bf16(f32_from_bits(0x3f808000)), f32_to_bf16(f32_from_bits(0x3f818000)), f32_to_bf16(f32_from_bits(0x7fc00001))) - print(f32_to_bf16(f32x4(1.0, -2.0, 0.5, 3.14159)), bf16_to_f32(u16x4(0x3f80, 0xc000, 0x3f00, 0x4049))) -} diff --git a/tests/run/floatbits.out b/tests/run/floatbits.out deleted file mode 100644 index 10ab32f..0000000 --- a/tests/run/floatbits.out +++ /dev/null @@ -1,13 +0,0 @@ -0 5.551115123125783e-17 -1.4901161e-08 10 -3.25 f32x4(3.25, 3.25, 3.25, 3.25) 1.4901161e-08 -0 -1065353216 4607182418800017408 2147483648 -3.1415927 2.5 -1 -inf u32x4(1065353216, 1073741824, 3221225472, 1056964608) -1 -2 65504 5.9604645e-08 -11878 31743 31744 0.3330078 -f32x8(1, 2, 3, 4, 0, -0, 0.5, 0.25) -1 3.140625 16457 -16256 16258 32704 -u16x4(16256, 49152, 16128, 16457) f32x4(1, -2, 0.5, 3.140625) diff --git a/tests/run/masks.fch b/tests/run/masks.fch deleted file mode 100644 index 38cefed..0000000 --- a/tests/run/masks.fch +++ /dev/null @@ -1,50 +0,0 @@ -// Masks: comparisons of vectors, select, any/all/count, and the end of a list - -fn relu(f32x8 v) -> f32x8 { - return select(v > 0.0, v, 0.0) -} - -// the sum of a list of any length: whole vectors, then the last piece with its missing lanes as 0 -fn total([]f32 xs) -> f32 { - acc := f32x8(0.0) - i := 0 - while i < xs.len { - acc = acc + f32x8(xs, i, 0.0) - i += 8 - } - return acc.sum() -} - -// add 1 to every number of a list, in place, the last piece too -fn bump([]f32 xs) { - i := 0 - while i < xs.len { - (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i) - i += 8 - } -} - -fn main() { - v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0) - m := v > 0.0 - print(m, m.count(), m.any(), m.all(), m[3]) - print(relu(v), select(!m, v, -1.0)) - print((v >= -1.0) & (v < 3.0), (v == 0.0) | (v == 5.0), m ^ (v > 1.0)) - - a := i32x4(1, 5, 3, 7) - b := i32x4(4, 4, 4, 4) - print(select(a > b, a, b), (a == a).all(), (a != b).all()) - u := u8x16(200) - print((u > u8x16(100)).all(), i32x4(a > b), f32x4(a < b)) - k := i32x4(0, 2, 0, -1) - print(boolx4(k), boolx4(true, false, true, false)) - - []f32 xs - for n in 0..13 { - xs.push(f32(n)) - } - print(total(xs)) - bump(xs) - print(xs) - print(f32x8(xs, 8, -1.0), f32x8(xs, 13, 9.0), f32x8(xs, 100, 7.0)) -} diff --git a/tests/run/masks.out b/tests/run/masks.out deleted file mode 100644 index a034c35..0000000 --- a/tests/run/masks.out +++ /dev/null @@ -1,9 +0,0 @@ -boolx8(false, false, false, true, true, true, false, true) 4 true false true -f32x8(0, 0, 0, 1, 2, 3, 0, 5) f32x8(-2, -1, 0, -1, -1, -1, -4, -1) -boolx8(false, true, true, true, true, false, false, false) boolx8(false, false, true, false, false, false, false, true) boolx8(false, false, false, true, false, false, false, false) -i32x4(4, 5, 4, 7) true true -true i32x4(0, 1, 0, 1) f32x4(1, 0, 1, 0) -boolx4(false, true, false, true) boolx4(true, false, true, false) -78 -[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] -f32x8(9, 10, 11, 12, 13, -1, -1, -1) f32x8(9, 9, 9, 9, 9, 9, 9, 9) f32x8(7, 7, 7, 7, 7, 7, 7, 7) diff --git a/tests/run/shuffles.fch b/tests/run/shuffles.fch deleted file mode 100644 index c7b06bf..0000000 --- a/tests/run/shuffles.fch +++ /dev/null @@ -1,27 +0,0 @@ -// Moving lanes: shuffle, lo/hi, concat, and a byte table lookup (how 4-bit weights are unpacked) - -// sixteen 4-bit numbers, two to a byte, made into sixteen bytes through a table of what each nibble means -fn unpack4(u8x8 packed, i8x16 table) -> i8x16 { - pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // u8x16: each byte twice - odd := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1 - nibbles := select(odd, pairs >> 4, pairs & 15) // low nibble first, then the high one - return table.lookup(nibbles) -} - -fn main() { - v := f32x4(1.0, 2.0, 3.0, 4.0) - print(v.shuffle(3, 2, 1, 0), v.shuffle(0, 0, 1, 1, 2, 2, 3, 3), v.shuffle(1, 2)) - w := i32x8(0, 1, 2, 3, 4, 5, 6, 7) - print(w.lo(), w.hi(), concat(w.hi(), w.lo())) - print(concat(w, w).len, concat(v, v * 10.0).shuffle(0, 4, 1, 5, 2, 6, 3, 7)) - - table := u8x16(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25) - print(table.lookup(u8x16(15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14))) - print(table.lookup(u8x16(16, 17, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).lo()) - print(table.lookup(u8x32(3)).hi().lo().lo()) - - q4 := i8x16(-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7) // what each 4-bit number means - print(unpack4(u8x8(0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe), q4)) - m := f32x4(1.0, -1.0, 2.0, -2.0) > 0.0 - print(m.shuffle(1, 0, 3, 2), m.lo()) -} diff --git a/tests/run/shuffles.out b/tests/run/shuffles.out deleted file mode 100644 index 5d724e2..0000000 --- a/tests/run/shuffles.out +++ /dev/null @@ -1,8 +0,0 @@ -f32x4(4, 3, 2, 1) f32x8(1, 1, 2, 2, 3, 3, 4, 4) f32x2(2, 3) -i32x4(0, 1, 2, 3) i32x4(4, 5, 6, 7) i32x8(4, 5, 6, 7, 0, 1, 2, 3) -16 f32x8(1, 10, 2, 20, 3, 30, 4, 40) -u8x16(25, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24) -u8x8(10, 11, 25, 10, 10, 10, 10, 10) -u8x4(13, 13, 13, 13) -i8x16(-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7) -boolx4(false, true, false, true) boolx2(true, false) diff --git a/tests/run/vectors.fch b/tests/run/vectors.fch deleted file mode 100644 index 5255421..0000000 --- a/tests/run/vectors.fch +++ /dev/null @@ -1,69 +0,0 @@ -// 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).muladd(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)).all(), (a != a).any()) - - // 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 deleted file mode 100644 index 8e130e6..0000000 --- a/tests/run/vectors.out +++ /dev/null @@ -1,15 +0,0 @@ -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)