- technicians' guides: section 31 rewritten around kernels (masks, the end of a list, moving lanes, int8 dot products, choosing instructions at build and at run time) - cheat sheets and engineers' guides: the new operations and their IR; how features are found - Kate and VS Code: @cpu, boolxN and the new built-ins highlighted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
67 KiB
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.
🇬🇧 English version: for-technicians.md
Spis treści
- Czym jest Finch
- Instalacja
- Polecenie finch
- Jak wygląda plik Fincha
- Zmienne
- Typy
- Operatory
- Sterowanie
- Funkcje
- Tablice i mapy
- Tekst (str)
- Struktury i metody
- Pamięć: kto co zwalnia
- Wskaźniki
- Funkcje wbudowane
- Moduły
- Biblioteki C
- Błędy
- Edytory: VS Code, Kate i inne
- Debugowanie
- Rozwiązywanie problemów
- Układ repozytorium i testy
- Enumy i match
- Typy generyczne
- Biblioteka standardowa
- Obecne ograniczenia
- Testy: bloki test i finch test
- Projekt: finch.toml
- Wątki
- C++, Rust i Android
- 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.
Co z tego wynika:
- Szybkość bliska C. Nie ma interpretera, maszyny wirtualnej ani garbage collectora. Sprawdzanie zakresów tablic i inne zabezpieczenia po optymalizacji prawie nic nie kosztują.
- Małe, samodzielne programy, które można skopiować i uruchomić.
- Pamięć zarządza się sama. Listy i teksty są zwalniane automatycznie na końcu bloku, do którego należą, bez udziału garbage collectora.
- Bezpośredni dostęp do bibliotek C.
import "stdio.h"i możesz wołać wszystko, co tam jest, nawet funkcje przyjmujące struktury.
Zasady projektu: na każdą rzecz jeden sposób, składnia jak w C, ale bez jego pułapek, i komunikaty błędów, które mówią, jak naprawić problem.
2. Instalacja
Kompilator Fincha jest napisany w Finchu (zob. przewodnik dla inżynierów). 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).
Sposób A: gotowa wersja
Pobierz archiwum dla swojego systemu z wydań i rozpakuj je.
Katalogi runtime/ i std/ muszą leżeć obok pliku finch (albo zainstaluj go, zob. niżej).
| 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 (archiwum ucrt-x86_64): rozpakuj i dodaj jego katalog bin do PATH |
Na Linuksie:
| Dystrybucja | Pakiety |
|---|---|
| Debian / Ubuntu | clang-21 libclang-21-dev z apt.llvm.org, pkg-config |
| Fedora | clang clang-devel pkgconf |
| Arch | clang pkgconf |
| Nix | nix-shell w repozytorium daje wszystko naraz |
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
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # Linux: clang, libclang, pkg-config (albo zainstaluj je z dystrybucji)
bootstrap/build.sh # build/finch
./build/finch version
bootstrap/build.sh kompiluje plik startowy (bootstrap/<system>.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:
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:
export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll"
export FINCH_LLVM_INCLUDE="C:/llvm/include"
export FINCH_LDFLAGS="-LC:/llvm/bin"
bootstrap/build.sh # build/finch.exe, z libclang.dll skopiowaną obok
Budowanie na inny system
--target buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C tamtego systemu:
--target |
Buduje | Używany kompilator C | finch run uruchamia przez |
|---|---|---|---|
windows |
plik .exe dla Windowsa |
x86_64-w64-mingw32-gcc (MinGW-w64) |
Wine |
arm64 |
program dla Linuksa ARM64 | aarch64-linux-gnu-gcc |
qemu-aarch64 |
linux |
program dla Linuksa x86-64 | clang --target=… |
|
macos |
program dla macOS (na Macu: dla drugiego rodzaju procesora) | clang --target=… |
|
| dowolny triple LLVM | np. aarch64-unknown-linux-gnu |
clang --target=<triple> |
finch build gra.fch --target windows # gra.exe
finch run gra.fch --target arm64 # buduje dla ARM64 i uruchamia przez QEMU
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build narzedzie.fch --target arm64 # inna nazwa kompilatora
Na Niksie te kompilatory to pkgsCross.mingwW64.buildPackages.gcc i
pkgsCross.aarch64-multiplatform.buildPackages.gcc.
Dodanie finch do PATH (opcjonalnie)
sudo bootstrap/install.sh # /usr/local/bin/finch oraz /usr/local/share/finch/{runtime,std}
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.
3. Polecenie finch
finch run <plik.fch> [argumenty...] kompiluje i od razu uruchamia
finch build <plik.fch> [-o nazwa] kompiluje do programu (domyślna nazwa: nazwa pliku)
finch test <plik.fch> 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 <plik.fch> wypisuje LLVM IR (dla ciekawskich)
finch lsp serwer języka, dla edytorów
finch version pokazuje wersję
opcje:
-l <bib> dolinkuj bibliotekę C, to samo co link "bib" w pliku
-I <katalog> szukaj nagłówków C także tam
--target <nazwa> zbuduj na inny system: windows, linux, arm64, macos, android albo triple LLVM
--cpu <nazwa> użyj wszystkich instrukcji procesora: native (tego), x86-64-v3 (AVX2),
x86-64-v4 (AVX-512), apple-m1, ... (rozdział 31)
-g dodaj informacje dla debuggera (gdb i lldb)
-O0 bez optymalizacji
--trace błędy w trakcie działania pokazują ścieżkę wywołań (i tak włączone dla run, test, -g i -O0)
--no-trace nie pokazują (zwykłe finch build ich nie pokazuje: pamiętanie ścieżki spowalnia wywołania)
W katalogu 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ę.
finch fmt zmienia tylko układ: cztery spacje na każdy poziom { }, żadnych spacji na końcach linii, najwyżej jedna
pusta linia z rzędu. Linie kontynuujące wyrażenie (w nawiasach ( ) albo po +) i tekst """ zachowują swój kształt.
finch fmt --check niczego nie zmienia i kończy się błędem, jeśli jakiś plik by się zmienił (przydatne w CI).
Edytory z serwerem języka formatują na żądanie.
Przykłady:
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 run przekazuje kod wyjścia twojego programu. Jeśli program się wysypie, mówi jak
(np. the program crashed: Segmentation fault).
4. Jak wygląda plik Fincha
// komentarz zaczyna się od dwóch ukośników
/* albo zajmuje
kilka linii */
import "math.h" // nagłówki C (opcjonalnie), na początku pliku
import ksztalty // moduły Fincha (opcjonalnie): ksztalty.fch obok tego pliku
link "m" // biblioteki albo pliki C do dolinkowania (opcjonalnie)
struct Punkt { // twoje własne typy
int x
int y
}
fn main() { // tu zaczyna się program
print("Cześć")
}
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 nieif (x > 5) {. - Funkcje i struktury mogą stać w pliku w dowolnej kolejności.
maintofn main(),fn main() -> int(zwraca kod wyjścia) albofn main([]str args)(dostaje argumenty z wiersza poleceń;args[0]to ścieżka samego programu).
5. Zmienne
x := 5 // nowa zmienna, typ wynika z wartości (int)
int y = 10 // nowa zmienna z podanym typem
int z // bez wartości: zero ("" dla str, [] dla tablic, null dla wskaźników)
Punkt p // struktura bez wartości: zera albo wartości domyślne struktury
x = 7 // przypisanie
x += 1 // także -= *= /= %=
- Nazwę można zadeklarować w funkcji tylko raz. Finch nie pozwala użyć tej samej nazwy w bloku wewnętrznym (nie ma „przesłaniania”), bo to częste źródło pomyłek.
- Zmienna istnieje od deklaracji do końca bloku
{ }, w którym stoi. - Nazwy: litery ASCII, cyfry i
_, nie mogą zaczynać się od cyfry. Polskie litery wolno pisać tylko w tekstach. Nazwy typów (int,u8, …), funkcji wbudowanych i słowa kluczowe są zarezerwowane.
Słowa kluczowe: fn return if else while for in break continue true false null import link struct enum match defer or try const export
Stałe
const SZEROKOSC = 640
const WYSOKOSC = SZEROKOSC / 2 // z innych stałych
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.
6. Typy
Typy na co dzień
| 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 |
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] |
| twoje struktury | nazwane grupy wartości | Punkt(1, 2) |
Znaki specjalne w tekstach i znakach: \n nowa linia, \t tabulator, \r, \0, \\, \", \'.
Typy o podanym rozmiarze (do plików, sprzętu, bibliotek C)
| Ze znakiem | Zakres | Bez znaku | Zakres |
|---|---|---|---|
i8 |
−128 … 127 | u8 |
0 … 255 |
i16 |
−32 768 … 32 767 | u16 |
0 … 65 535 |
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).
Zamiana typów
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) |
✅ |
| 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) |
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.
Jawne zamiany zapisuje się nazwą typu, jak wywołanie funkcji:
int(3.99) // 3 (ucina część ułamkową)
float(7) / 2 // 3.5
u8(300) // 44 (zostaje 8 najniższych bitów)
int('A') // 65
char(66) // 'B'
bool(0) // false (każda liczba różna od zera to true)
str(42) // "42" (także liczby zmiennoprzecinkowe, bool i znaki; str([]u8) zamienia bajty na tekst)
int("42") // 42 (zatrzymuje program, jeśli tekst nie jest liczbą)
float("2.5") // 2.5
ptr(p) // wskaźnik z typem jako wskaźnik bez typu
ptr(16) // liczba jako wskaźnik (API C, które przekazują przesunięcia jako wskaźniki, np. OpenGL)
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 * dto 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) + cnie zamienia się wa + (b + c), ax * 0.0nie jest z góry uznawane za 0 (gdyxto NaN, wynik też jest NaN). Jedyny wyjątek tov.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) |
[]u16 wagi = wczytaj_wagi() // liczby f16 z pliku
v := f16_to_f32(u16x8(wagi, 0)) // osiem z nich jako f32, naraz
7. Operatory
Według priorytetu (wyższy wiąże mocniej)
| 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 |
| 3 | == != < <= > >= |
porównania |
| 2 | && |
logiczne AND |
| 1 | || |
logiczne OR |
Jednoargumentowe (przed wartością): -x zmiana znaku, !x logiczne NOT, ~x odwrócenie wszystkich bitów.
Celowo inaczej niż w C: operatory bitowe wiążą mocniej niż porównania, więc
flagi & 4 != 0znaczy(flagi & 4) != 0.
Uwagi:
/na liczbach całkowitych zaokrągla w stronę zera:7 / 2to3.%ma znak lewej strony.- Dzielenie przez zero zatrzymuje program komunikatem
runtime error: division by zeroz 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 znull.strnigdy nie jestnull. - Tablic i struktur nie da się porównać przez
==; porównaj ich części.
8. Sterowanie
if może też dawać wartość; każda gałąź zawiera jedną wartość, a else jest obowiązkowe:
ocena := if wynik > 100 { "wysoki" } else if wynik > 10 { "średni" } else { "niski" }
f32 predkosc = if biegnie { 2.5 } else { 1.0 }
if x > 10 {
...
} else if x > 0 {
...
} else {
...
}
while licznik > 0 {
licznik -= 1
}
for i in 0..10 { // i = 0, 1, …, 9 (koniec nie wchodzi)
if i == 3 { continue }
if i == 8 { break }
}
for imie in imiona { // każdy element tablicy (albo każdy znak tekstu)
print(imie)
}
for i, imie in imiona { // razem z indeksem: 0, 1, 2, ...
print(i, imie)
}
for klucz, wartosc in wiek { // każdy wpis mapy, w kolejności dodawania kluczy
print(klucz, wartosc) // (for klucz in wiek: same klucze)
}
- Warunek musi być typu
bool.if x {z liczbą to błąd; napiszif x != 0 {. - W
for i in a..boba końce są obliczane raz.ijest typuinti nie można go zmieniać w pętli. - W
for x in listateż nie można zmieniaćx. Żeby zmieniać elementy, idź po indeksach:for i in 0..lista.len { lista[i] = ... }. Dodawanie do listy w trakcie pętli jest w porządku. while true { ... }kręci się dobreakalboreturn.- Kod po
return,breakczycontinuew 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:
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
Odroczona instrukcja nie może zawierać return, break ani continue.
9. Funkcje
fn nazwa(typ1 param1, typ2 param2) -> typ_wyniku {
...
return wartosc
}
fn bez_wyniku(str komunikat) { // bez "->": niczego nie zwraca
print(komunikat)
}
- Argumenty zachowują się jak kopie: zmiana parametru nigdy nie zmienia zmiennej wywołującego. (Finch kopiuje naprawdę tylko wtedy, gdy trzeba: funkcja, która nie zmienia tablicy, dostaje ją za darmo.) Żeby zmienić zmienną wywołującego, przekaż wskaźnik: zob. Wskaźniki.
- Funkcja z
-> typmusi zwracać wartość na każdej ścieżce. Finch to sprawdza. - Nie można zdefiniować funkcji wewnątrz innej funkcji.
- Funkcję, która może się nie udać, zapisuje się
-> int!(albo-> !bez wyniku); zob. Błędy jako wartości. - Długie wywołania można rozbić na kilka linii w nawiasach; przecinek po ostatnim argumencie jest dozwolony.
10. Tablice i mapy
liczby := [5, 3, 8] // tablica int
[]str imiona // pusta tablica str
siatka := [[1, 2], [3, 4]] // tablice tablic
[]f32 xs = [1, 2.5] // typ decyduje, czym staną się liczby
| Działanie | Znaczenie |
|---|---|
a.len |
liczba elementów |
a[i] |
element i (licząc od 0); indeks poza zakresem zatrzymuje program jasnym 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.sort(), a.reverse() |
w miejscu (liczby, znaki, str) |
a.slice(p, k) |
nowa tablica z elementami p … k-1 |
a.join(sep) |
dla []str: jeden tekst z sep między częściami |
a.ptr |
adres pierwszego elementu (dla C) |
print(a) |
wypisuje [1, 2, 3] |
Przypisanie kopiuje: po b := a zmiana b nie zmienia a.
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.
wiek := ["anna": 31, "bob": 25] // mapa zapisana wprost: map[str]int
map[str][]str grupy // pusta mapa
map[int]str nazwy = [:] // [:] to pusta mapa tam, gdzie typ jest znany
| Działanie | 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.len |
liczba kluczy |
m.has(k) |
czy k tam jest? |
m.get(k, domyslna) |
wartość albo domyslna (obliczana tylko wtedy, gdy trzeba) |
m.remove(k) |
usuwa k; true, jeśli był |
m.clear() |
usuwa wszystko |
m.keys(), m.values() |
nowe tablice, w kolejności dodawania |
print(m), str(m) |
{"anna": 31, "bob": 25} |
map[str]int ile
for slowo in tekst.split(" ") {
ile[slowo] += 1 // liczenie: nowe słowo zaczyna od 0
}
for slowo, n in ile {
print(slowo, n)
}
Wyszukiwanie 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.
11. Tekst (str)
s := "Cześć"
s += ", świecie" // łączenie
print(s.len, s[0]) // 17 C (.len liczy bajty; ą, ś, ż, … zajmują po 2)
s[0] = 'K' // zmiana znaku
| Metoda | Zwraca |
|---|---|
s.sub(a, b) |
fragment od a do b-1 |
s.find(t) |
gdzie zaczyna się t, albo -1 |
s.contains(t), s.starts_with(t), s.ends_with(t) |
bool |
s.split(sep) |
[]str; split("") daje pojedyncze znaki |
s.trim() |
bez spacji i znaków nowej linii na obu końcach |
s.upper(), s.lower() |
zamienione A–Z / a–z (litery ASCII) |
s.replace(a, b) |
każde a zamienione na b |
s.repeat(n) |
s powtórzone n razy |
s.bytes() |
[]u8 z bajtami |
s.ptr |
ptr[char] dla C (ważny, dopóki s istnieje) |
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)".
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.
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:
shader := """
#version 330
void main() { gl_Position = vec4(0.0); }
"""
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).
12. Struktury i metody
struct Gracz {
str imie
int zycia = 3 // wartość domyślna
[]int wyniki
Punkt poz // struktury w strukturach
}
p := Gracz(imie: "Ola") // przez nazwy: brakujące pola dostają wartość domyślną (albo zero)
q := Punkt(1, 2) // po kolei: wtedy trzeba podać każde pole
p.zycia -= 1
p.wyniki.push(10)
p.poz.x = 5
print(p) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], poz: Punkt(x: 5, y: 0))
- Przypisanie albo przekazanie struktury kopiuje ją, razem z jej tablicami i tekstami.
- Struktura nie może bezpośrednio zawierać samej siebie; do tego służy pole
ptr[Wezel]albo[]Wezel(drzewa, listy).
Metody
Metoda to funkcja należąca do struktury: fn Struktura.nazwa(...). W środku self to struktura, na której ją
wywołano, i to jest sama wartość wywołującego, a nie kopia: zmiany w self zostają.
fn Gracz.ulecz(int ile) {
self.zycia += ile
}
fn Gracz.zyje() -> bool {
return self.zycia > 0
}
p := Gracz(imie: "Ola")
p.ulecz(2) // p.zycia wynosi teraz 5
if p.zyje() { ... }
druzyna[0].ulecz(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 nie może nazywać się tak jak pole.
- Wywołanie metody zmieniającej
selfna zmiennej pętli (for p in druzyna { p.ulecz(1) }) to błąd, bo zmienna pętli jest tylko do odczytu; użyjfor i in 0..druzyna.len { druzyna[i].ulecz(1) }. - Metody, tak jak funkcje, mogą się nie udać (
-> !); zob. 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.
- 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 listaprzenosi 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.
Dla pamięci, którą zarządzasz sam (listy wiązane, drzewa, współdzielenie między strukturami):
wezel := new(Wezel(wartosc: 1)) // wartość na stercie, dostajesz ptr[Wezel]
defer free(wezel) // free(...) oddaje pamięć (i zwalnia tablice i teksty w środku)
Pamięć z C (malloc, obiekty bibliotek) zwalnia się funkcjami C, tak jak w C.
14. Wskaźniki
Wskaźnik przechowuje adres wartości. Finch zamiast * i & z C używa słów:
| C | Finch | Znaczenie |
|---|---|---|
int *p |
ptr[int] p |
p wskazuje na int |
&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 |
void * |
ptr |
wskazuje na coś nieznanego typu |
fn podwoj(ptr[int] p) {
p.value = p.value * 2
}
x := 21
podwoj(addr(x)) // x wynosi 42
Jeśli struktura ma własne pole o nazwie value, to p.value na wskaźniku do niej oznacza to pole.
Użycie .value, .pole albo [i] na wskaźniku null zatrzymuje program jasnym komunikatem.
⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu bloku tej zmiennej. Finch tego nie sprawdza.
15. Funkcje wbudowane
| 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ę. |
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. |
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ęć). |
fma(a, b, c), muladd(a, b, c) |
a * b + c z jednym zaokrągleniem / najszybciej (zob. Liczby zmiennoprzecinkowe). |
f32_bits(x), f32_from_bits(u), f16_to_f32(h), … |
Bity liczby i liczby połówkowej precyzji (zob. Liczby zmiennoprzecinkowe). |
int(x), str(x), u8(x), … |
Zamiany typów (zob. Zamiana typów). str(x) działa na każdym typie i daje ten sam tekst, który pokazuje print. int(tekst) or 0 obsługuje tekst, który nie jest liczbą. |
16. Moduły
Program moż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ę :).
// ksztalty.fch
struct Pudelko {
float w
float h
}
fn pole(Pudelko p) -> float {
return p.w * p.h
}
// main.fch
import ksztalty
fn main() {
p := ksztalty.Pudelko(2, 3)
print(ksztalty.pole(p))
ksztalty.Pudelko 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. - Moduł nie ma
main. Moduły mogą importować inne moduły. - Moduły w katalogach:
import ui/buttonwczytujeui/button.fch, a używa się go jakobutton.draw(). - Pod inną nazwą:
import ui/button as btn→btn.draw(). Dwa importy nie mogą mieć tej samej nazwy.
17. Biblioteki C
Krok 1: zaimportuj nagłówek
import "stdio.h"
import "GLFW/glfw3.h" // ścieżka w systemowych katalogach nagłówków
import "mojabib.h" // nagłówek obok twojego pliku .fch
Finch czyta nagłówek przez libclang (parser C z clanga) i udostępnia:
- funkcje, także te ze zmienną liczbą argumentów, jak
printf, - struktury, przez wartość i przez wskaźnik (
Vector2,SDL_Rect,CXCursor, …), razem z ich polami, - wartości
enumi proste stałe z#define(M_PI,GL_COLOR_BUFFER_BIT), - zmienne globalne, jak
stdoutistderr.
Krok 2: dolinkuj bibliotekę
link "glfw" // używa libglfw.so (najpierw pyta pkg-config, jeśli jest zainstalowany)
link "pomocnicze.c" // kompiluje i linkuje twój plik C (obok pliku .fch)
link "gotowe.a" // albo plik obiektowy / bibliotekę statyczną
Jeśli zapomnisz o linii link, Finch powie dokładnie, czego brakuje:
error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked
...
link "glfw"
Jak typy C wyglądają w Finchu
| C | Finch |
|---|---|
char, signed char, unsigned char |
char, i8, u8 |
short, int, long, long long |
i16, i32, i64, i64 (według faktycznego rozmiaru) |
unsigned … |
u16, u32, u64 |
float, double |
f32, f64 |
_Bool / bool |
bool |
enum |
odpowiednia 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)) |
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 |
void *, wskaźniki na funkcje, niekompletne struktury |
ptr |
Przykład: struktury i funkcje zwrotne
import "raylib.h"
link "raylib"
fn main() {
InitWindow(800, 450, "Finch")
while !WindowShouldClose() {
BeginDrawing()
ClearBackground(Color(r: 30, g: 30, b: 40, a: 255))
DrawCircleV(Vector2(400, 225), 50, Color(255, 136, 0, 255))
EndDrawing()
}
CloseWindow()
}
Żeby dać C funkcję do wywołania (jak funkcję porównującą dla qsort), użyj addr(mojaFunkcja).
Jej parametry muszą być typami C (liczby, wskaźniki, struktury C).
Grafika: OpenGL i Vulkan
Oba działają bezpośrednio; examples/opengl.fch i examples/vulkan.fch to kompletne programy.
-
OpenGL 3+ na Linuksie: nowoczesne funkcje (
glCreateShader,glGenVertexArrays, …) są zadeklarowane tylko wtedy, gdy zdefiniowane jestGL_GLEXT_PROTOTYPES. Finch nie definiuje makr C, więc umieść te#includew malutkim nagłówku obok programu i zaimportuj go:// opengl.h #define GL_GLEXT_PROTOTYPES #include <GL/gl.h> #include <GL/glext.h> #include <GLFW/glfw3.h>Potem
import "opengl.h",link "glfw",link "GL". Parametry wyjściowe przekazuje się przezaddr(...)(glGenBuffers(1, addr(vbo))), dane bufora przez.ptr(wierzcholki.ptr), a przesunięcia w bajtach przezptr(...)(glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, krok, ptr(8))). -
Vulkan:
import "vulkan/vulkan.h"ilink "vulkan". Struktury „create info” buduje się przez nazwy pól (brakujące są zerami), uchwyty toptr, a listy pobiera się zwykłym sposobem z dwoma wywołaniami: raz po liczbę, drugi raz zlista.ptrpolista.resize(liczba). Makra z parametrami, takie jakVK_MAKE_API_VERSION, nie są dostępne; zamiast nich policz liczbę (1 << 22to Vulkan 1.0).
Czego jeszcze nie ma
- 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 (RAYWHITEz raylib,(Color){ 245, 245, 245, 255 }). long doublei liczb 128-bitowych.
18. Błędy
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"):
fn parsuj_port(str tekst) -> int! {
n := int(tekst) or { return error("'" + tekst + "' to nie liczba") }
if n < 1 || n > 65535 {
return error("port " + str(n) + " jest poza zakresem")
}
return n
}
fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, niczego nie zwraca
try write_file(sciezka, tekst)
}
Wywołujący musi obsłużyć niepowodzenie, na jeden z trzech sposobów (zapomnienie to błąd kompilacji):
| Kod | Gdy się nie uda |
|---|---|
port := parsuj_port(s) or 8080 |
użyj tej wartości (jest obliczana tylko przy niepowodzeniu; może to być kolejne wywołanie z or) |
port := parsuj_port(s) or { print(err); return } |
wykonaj blok; err to komunikat (str). Blok musi wyjść (return, break, continue, exit), chyba że wynik nie jest używany. |
port := try parsuj_port(s) |
przerwij bieżącą funkcję z tym samym błędem (ona też musi móc się nie udać) |
fn wczytaj(str sciezka) -> Konfig! {
tekst := try read_file(sciezka)
port := try parsuj_port(tekst.trim())
return Konfig(port: port)
}
fn main() -> ! { // main też może się nie udać: błąd zostanie wypisany, a kod wyjścia to 1
konfig := try wczytaj("app.conf")
...
}
Tak samo działają wbudowane funkcje, które mogą się nie udać: int(tekst), float(tekst), read_file, write_file,
delete_file. Bez or / try zachowują się jak dawniej (int("x") zatrzymuje program, write_file zwraca bool).
Pod spodem taka funkcja zwraca wartość razem z flagą i komunikatem: przy powodzeniu nic nie jest przydzielane, nie ma odwijania stosu, a niepowodzenie kosztuje tyle, co zbudowanie komunikatu.
Błędy kompilacji
gra.fch:12:9: error: 'wynik' must be int, but this is str
12 | wynik = "wysoki"
| ^
Format: plik:linia:kolumna: error: wyjaśnienie, a pod spodem linia z ^ pod miejscem błędu.
Błędy w trakcie działania
Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje program z komunikatem i kodem wyjścia 1:
| Komunikat | Przyczyna |
|---|---|
runtime error: division by zero |
/ albo % przez zmienną równą 0 |
runtime error: division overflows … |
najmniejsza liczba ze znakiem podzielona przez −1 |
runtime error: index 5 is out of range (the length is 3) |
a[5] na krótszej tablicy albo tekście |
runtime error: pop() on an empty array |
|
runtime error: used .value on a null pointer |
także .pole i [i] przez null |
runtime error: can't turn "x" into int |
int(...) / float(...) na tekście, który nie jest liczbą |
runtime error: can't read the file "…" |
read_file na brakującym albo nieczytelnym pliku (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: assert failed: … |
assert(warunek, "…") z fałszywym warunkiem |
Pod komunikatem jest ścieżka wywołań, które do niego doprowadziły, od najgłębszego:
gra.fch:6: runtime error: index 3 is out of range (the length is 2)
in Wrog.nastepny (gra.fch:6)
in krok (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.
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.
Kate (oraz KWrite, KDevelop)
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ę
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.
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.
Instalacja: Extensions → … → Install from VSIX… → wybierz finch-lang-*.vsix. Rozszerzenie uruchamia finch
z PATH; jeśli leży gdzie indziej, ustaw Finch: Path w ustawieniach.
Inne
Neovim, Helix, Zed, Emacs, Sublime i każdy inny edytor obsługujący Language Server Protocol:
uruchamiaj finch lsp dla plików .fch (stdin/stdout, bez opcji).
20. Debugowanie
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) 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.
21. Rozwiązywanie problemów
| 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). |
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). |
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. |
| 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
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
├── 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.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
├── 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
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
23. Enumy i match
Enum to typ z krótką listą nazwanych wartości.
enum Kolor { Czerwony, Zielony, Niebieski }
enum Status { Ok = 200, NotFound = 404 } // wariant może dostać liczbę, jeśli jej potrzebuje
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)
Warianty zapisuje się Kolor.Czerwony. Można je porównywać przez == i != (nie przez <), używać jako kluczy map,
pól struktur i elementów tablic, a enum może mieć metody tak jak struktura: fn Kolor.nastepny() -> Kolor { ... }.
match wybiera, co zrobić, na podstawie wartości, zamiast drabinki if:
match k {
Czerwony -> print("ciepły") // w match na enumie wystarczy sama nazwa
Zielony, Niebieski -> print("zimny") // kilka wartości w jednej gałęzi
}
rozmiar := match n { // jako wyrażenie: daje wartość
1, 2, 3 -> "mały"
4, 5 -> "średni"
else -> "duży"
}
match slowo {
"koniec" -> {
print("pa")
return
}
else -> print("słucham?")
}
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:
enum Ksztalt {
Kolo(float)
Prostokat(float, float)
Punkt
}
fn Ksztalt.pole() -> float {
return match self {
Kolo(r) -> 3.14 * r * r
Prostokat(w, h) -> w * h
Punkt -> 0.0
}
}
_ pomija wartość. match musi obsłużyć każdy wariant (albo kończyć się else). Wypisanie pokazuje Prostokat(2, 3.5).
Taki enum kopiuje się i zwalnia jak struktura; nie da się go porównać przez == (użyj match). Warianty takich enumów
nie mogą mieć liczb.
24. Typy generyczne
Funkcję albo strukturę można napisać raz dla dowolnego typu, podając typ jako nazwę w nawiasach kwadratowych:
fn max[T](T a, T b) -> T {
if a > b {
return a
}
return b
}
struct Stos[T] {
[]T elementy
}
fn Stos[T].push(T x) {
self.elementy.push(x)
}
print(max(3, 9), max("jabłko", "gruszka")) // T wynika z argumentów
print(max[float](1, 2.5)) // albo jest podany, gdy nie da się go ustalić
s := Stos[int]() // struktura generyczna zawsze ma podany typ
s.push(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]).
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ł | 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) |
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).
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
dok.set("name", json.new_text(nazwa + "!"))
write_file(path.join("out", "config.json"), json.pretty(dok))
26. Obecne ograniczenia
Czego jeszcze nie ma:
- Interfejsów.
- Tekstów świadomych Unicode:
.len,s[i]iupper()działają na bajtach / ASCII. - Zabezpieczenia przed zmienianiem tej samej tablicy przez dwa wątki naraz (użyj muteksu).
- Windowsa na ARM64, systemów 32-bitowych, WebAssembly.
Ostrzeżenia o dużych kopiach
Struktura z co najmniej 256 bajtami zwykłych danych (liczb, struktur z liczbami, [N]T z C) jest kopiowana bajt po
bajcie, gdy jest parametrem (fn f(Cialo c)) i w for x in ciala. Finch o tym ostrzega, ale i tak buduje program:
gra.fch:27:11: warning: 'b' is a Body (264 bytes), copied on every call to weight; take ptr[Body] to share it instead (callers can still pass the variable as it is)
gra.fch:45:5: warning: each Body (264 bytes) is copied into 'x'; for i in 0..bodies.len with bodies[i] works on it in place
Teksty, tablice i mapy w strukturze się nie liczą: przekazanie struktury kopiuje tylko ich uchwyty, a nie zawartość.
b := a nie daje ostrzeżenia, bo w kodzie widać, że to kopia. Metoda nigdy nie kopiuje self. Edytory pokazują
ostrzeżenia na żółto.
Zob. plan rozwoju w README.
27. Testy: bloki test i finch test
Testy stoją obok kodu, który sprawdzają:
fn obrazenia(int hp, int cios) -> int {
return max(hp - cios, 0)
}
test "obrażenia nigdy nie schodzą poniżej zera" {
assert(obrazenia(5, 10) == 0)
assert(obrazenia(5, 2) == 3, "wyszło \(obrazenia(5, 2))")
}
finch test gra.fch uruchamia każdy blok test z tego pliku i z importowanych modułów (ale nie z biblioteki
standardowej), każdy osobno: nieudany assert albo dowolny błąd w trakcie działania kończy tylko ten jeden test,
a następny rusza dalej.
ok obrażenia nigdy nie schodzą poniżej zera
FAIL wczytywanie poziomu
poziom.fch:31: runtime error: can't read the file "poziomy/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.
28. Projekt: finch.toml
Plik finch.toml obok kodu zapisuje raz to, co inaczej trzeba by podawać przy każdym poleceniu:
name = "gra" # program, który zbuduje finch build (z pliku entry)
entry = "src/main.fch" # używany, gdy finch run / build / test nie dostaną pliku
paths = ["src", "lib"] # katalogi z modułami Fincha
include = ["vendor/SDL3/include"] # katalogi z nagłówkami C (także dla twoich plików .c / .cpp)
links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a
libdirs = ["vendor/SDL3/lib"] # katalogi z bibliotekami
cflags = ["-DFAST"] # dla twoich plików .c / .cpp
ldflags = ["-Wl,-rpath,$ORIGIN"] # dla linkera
cpu = "native" # to samo co --cpu native (rozdział 31)
[target.windows] # dodawane tylko przy budowaniu na Windowsa
links = ["ws2_32"]
libdirs = ["vendor/SDL3/lib/x64"]
[target.linux-arm64] # system i procesor: windows, linux, macos, android oraz -x86_64 / -arm64
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:
[sekcje], klucz = "tekst", klucz = ["a", "b"] (także w kilku liniach), liczby, true/false i komentarze #.
29. Wątki
import thread
struct Kawalek {
[]float probki
float szczyt
}
fn analizuj(ptr arg) { // funkcja wątku przyjmuje jeden ptr
ptr[Kawalek] k = arg
for p in k.probki {
k.szczyt = max(k.szczyt, abs(p))
}
}
fn main() {
[]Kawalek kawalki = wczytaj_kawalki()
[]thread.Thread dzialajace
for i in 0..kawalki.len {
dzialajace.push(thread.spawn(addr(analizuj), addr(kawalki[i])) or {
print(err)
exit(1)
})
}
for i in 0..dzialajace.len {
dzialajace[i].join() // czekamy na wszystkie
}
}
To, co dostaje wątek, jest współdzielone, a nie kopiowane, więc:
- musi istnieć aż do
join()(tutajkawalkiżyje do końcamain); - 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_....
m := thread.mutex()
defer m.close()
m.lock()
dziennik.push("gotowe") // między lock a unlock pracuje tylko jeden wątek naraz
m.unlock()
atomic_add(addr(gotowe), 1) // gotowe to int wspólny dla wątków
n := atomic_load(addr(gotowe))
atomic_add, atomic_sub i atomic_swap zwracają poprzednią wartość; atomic_cas(p, stara, nowa) wpisuje nowa
tylko wtedy, gdy jest tam stara, i mówi, czy to zrobiło. thread.cpus() mówi, ile wątków komputer wykonuje naraz.
30. C++, Rust i Android
Finch rozmawia z innymi językami przez C, tak jak każdy język.
C++: to, co ma wołać Finch, wystaw przez nagłówek C, z extern "C" po stronie C++:
// glue.h
#ifdef __cplusplus
extern "C" {
#endif
int krok_fizyki(float dt);
#ifdef __cplusplus
}
#endif
i import "glue.h" z link "glue.cpp" (albo links = ["glue.cpp"] w finch.toml). Plik .cpp kompiluje kompilator
C++ (clang++ albo g++, albo FINCH_CXX), który potem też linkuje, więc biblioteka C++ dołącza się sama.
Bibliotekę C++ z API w C (Box2D 3, JoltC dla Jolta, nakładka C na PhysX) importuje się jak każdą bibliotekę C.
Rust: zbuduj staticlib i wystaw funkcje C:
// Cargo.toml: [lib] crate-type = ["staticlib"]
#[no_mangle]
pub extern "C" fn dlugosc(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"].
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.
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.
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).
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 |
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:
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:
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:
@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, poziomyx86-64-v2/v3/v4, a dla ARMneon 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_haszwraca dla nichfalse). - Funkcję z
@cpuwołaj dopiero wtedy, gdycpu_hasodpowie „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ę (alboptr); 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.