diff --git a/README.md b/README.md index ffa7660..0abd7dc 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,24 @@ fn main() { ## Why Finch - **Simple.** No semicolons, no headers, no preprocessor. Each thing has one way to do it. -- **Fast.** It compiles through LLVM's `-O2` pipeline to native code, with no runtime or garbage collector. - Recursive `fib(40)` runs in about 0.19 s (`clang -O2` on the same C: about 0.23 s). -- **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Division by zero - and null `.value` stop with a clear runtime error instead of undefined behavior. Operator precedence - that does not bite (`x & 1 == 0` means what it says). +- **Fast.** It compiles through LLVM's `-O2` pipeline to native code, with no garbage collector. + Recursive `fib(40)` runs in about 0.19 s (`clang -O2` on the same C: about 0.23 s); bounds-checked + array code matches C. +- **Memory without the pain.** Lists, text and structs are values: assigning copies, and the end of a + block frees them. There is no `free` to forget, no use-after-free, and every test is clean under valgrind. +- **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Out-of-range + indexes, division by zero and null pointers stop with a clear runtime error instead of undefined behavior. + Operator precedence that does not bite (`x & 1 == 0` means what it says). - **Errors that talk like a person:** ``` game.fn:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) 3 | i32 y = x | ^ ``` -- **Any C library, directly.** `import "GLFW/glfw3.h"` + `link "glfw"`, and you can call it. - Headers are read by libclang, so there is nothing to declare by hand. +- **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, too.** `boot/` is a Finch compiler written in Finch that compiles itself to a + byte-identical result. ## Quick start @@ -52,7 +57,8 @@ ninja -C build ./build/finch run examples/hello.fn ./build/finch build examples/tour.fn -o tour && ./tour -tests/run.sh # 34 passed, 0 failed +tests/run.sh # 58 passed, 0 failed +tests/boot.sh # the compiler written in Finch builds itself ``` ## Documentation @@ -70,14 +76,17 @@ Pick the guide that fits you: | | | |---|---| | Variables | `x := 5` (inferred) or `int x = 5` | -| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, pointers `ptr[T]` / `ptr` | +| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, pointers `ptr[T]` | | Functions | `fn add(int a, int b) -> int { return a + b }` | -| Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue` | -| Pointers | `p := addr(x)`, `p.value = 5`, `null` | -| Conversions | `int(3.9)`, `u8(x)`, `float(n)` (implicit only when nothing is lost) | -| Operators | `+ - * / %`, `& \| ^ << >> ~`, `== != < <= > >=`, `&& \|\| !` | -| C interop | `import "stdio.h"`, `link "glfw"` | -| Output | `print(a, b, c)` prints anything, separated by spaces | +| Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` | +| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for n in nums { }` | +| Text | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | +| Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` | +| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures | +| Modules | `import shapes` → `shapes.area(b)` | +| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks | +| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | +| Tools | `finch run/build/ir`, `-g` for gdb, `-O0` | ## Examples @@ -85,32 +94,42 @@ Pick the guide that fits you: |---|---| | [`examples/hello.fn`](examples/hello.fn) | the smallest program | | [`examples/tour.fn`](examples/tour.fn) | variables, functions, conditions, loops | +| [`examples/guess.fn`](examples/guess.fn) | a guessing game: input, loops, C's `rand` | +| [`examples/todo.fn`](examples/todo.fn) | structs, arrays, strings and files | +| [`examples/structs.fn`](examples/structs.fn) | structs, arrays of structs, a linked list with `new`/`free` | | [`examples/types.fn`](examples/types.fn) | sized numbers and pointers | | [`examples/c_import.fn`](examples/c_import.fn) | `printf`, `math.h`, `malloc`/`free`, `stderr` | | [`examples/window.fn`](examples/window.fn) | a GLFW + OpenGL window | +| [`examples/raylib.fn`](examples/raylib.fn) | raylib with C structs by value | | [`examples/llvm.fn`](examples/llvm.fn) | Finch building LLVM IR through the LLVM-C API | +| [`boot/`](boot/) | the Finch compiler, written in Finch | ## Roadmap - [x] Lexer, parser, LLVM codegen, O2 optimization - [x] Types incl. sized integers, pointers, conversions; functions; `if`/`while`/`for` -- [x] Readable compile errors; runtime checks (division, null) +- [x] Readable compile errors; runtime checks (division, null, bounds) - [x] Bitwise operators with sane precedence - [x] C headers through libclang; `link` with pkg-config; link-error advice -- [ ] `struct` (and C structs by value) -- [ ] Arrays/slices with `.len` and bounds checks -- [ ] Memory: automatic free at the end of the owning block, `defer`, manual `free` when wanted -- [ ] Strings: `+`, `len`, `str(x)`; `input()` -- [ ] Finch modules (`import math`) -- [ ] Debug info (DWARF) -- [ ] Bootstrap: the Finch compiler written in Finch +- [x] `struct`, including C structs by value (System V ABI) and callbacks +- [x] Arrays with `.len`, bounds checks and methods; `for x in list` +- [x] Memory: values freed at the end of their block, `defer`, `new`/`free` +- [x] Strings: `+`, `.len`, indexing, methods, `str(x)`, `int(s)`; `input()`; files +- [x] Finch modules (`import name`) +- [x] Debug info (`-g`) +- [x] Bootstrap: a Finch compiler written in Finch that builds itself +- [ ] Methods on structs, maps, `match` +- [ ] Error values instead of stopping the program +- [ ] More platforms (ARM64, macOS) ## Layout ``` -src/ compiler: lexer, parser, AST, codegen, C import, driver (C++17, ~2.4k lines) +src/ compiler: lexer, parser, AST, codegen, C import + ABI, driver (C++17, ~4.5k lines) +runtime/ the small C runtime linked into every program +boot/ the Finch compiler written in Finch (~3k lines) examples/ example programs -tests/ run/ (golden output), fail/ (expected errors), run.sh +tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh docs/ en/ and pl/ guides for three audiences ``` diff --git a/README.pl.md b/README.pl.md index d373542..9991354 100644 --- a/README.pl.md +++ b/README.pl.md @@ -25,10 +25,13 @@ fn main() { ## Dlaczego Finch - **Prosty.** Bez średników, bez plików nagłówkowych, bez preprocesora. Każdą rzecz robi się na jeden sposób. -- **Szybki.** Kompiluje się przez potok `-O2` z LLVM do kodu natywnego, bez środowiska uruchomieniowego i garbage collectora. - Rekurencyjne `fib(40)` trwa ok. 0,19 s (`clang -O2` na tym samym kodzie C: ok. 0,23 s). +- **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. Dzielenie przez zero i `.value` na null zatrzymują program czytelnym błędem, zamiast + i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać). - **Błędy mówią po ludzku:** ``` @@ -36,8 +39,10 @@ fn main() { 3 | i32 y = x | ^ ``` -- **Każda biblioteka C, bezpośrednio.** Piszesz `import "GLFW/glfw3.h"` i `link "glfw"`, i możesz z niej korzystać. - Nagłówki czyta libclang, więc nie trzeba niczego deklarować ręcznie. +- **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 także w sobie.** `boot/` to kompilator Fincha napisany w Finchu, który kompiluje sam siebie + do identycznego co do bajtu wyniku. ## Szybki start @@ -52,7 +57,8 @@ ninja -C build ./build/finch run examples/hello.fn ./build/finch build examples/tour.fn -o tour && ./tour -tests/run.sh # 34 passed, 0 failed +tests/run.sh # 58 passed, 0 failed +tests/boot.sh # kompilator napisany w Finchu buduje sam siebie ``` ## Dokumentacja @@ -70,14 +76,17 @@ Wybierz przewodnik dla siebie: | | | |---|---| | Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` | -| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, wskaźniki `ptr[T]` / `ptr` | +| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, wskaźniki `ptr[T]` | | Funkcje | `fn add(int a, int b) -> int { return a + b }` | -| Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue` | -| Wskaźniki | `p := addr(x)`, `p.value = 5`, `null` | -| Konwersje | `int(3.9)`, `u8(x)`, `float(n)` (niejawne tylko wtedy, gdy nic nie ginie) | -| Operatory | `+ - * / %`, `& \| ^ << >> ~`, `== != < <= > >=`, `&& \|\| !` | -| Współpraca z C | `import "stdio.h"`, `link "glfw"` | -| Wypisywanie | `print(a, b, c)` wypisuje wszystko, oddzielając spacjami | +| Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` | +| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for n in liczby { }` | +| Tekst | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | +| Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` | +| Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie | +| Moduły | `import ksztalty` → `ksztalty.pole(p)` | +| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków | +| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` | +| Narzędzia | `finch run/build/ir`, `-g` dla gdb, `-O0` | ## Przykłady @@ -85,32 +94,42 @@ Wybierz przewodnik dla siebie: |---|---| | [`examples/hello.fn`](examples/hello.fn) | najmniejszy program | | [`examples/tour.fn`](examples/tour.fn) | zmienne, funkcje, warunki, pętle | +| [`examples/guess.fn`](examples/guess.fn) | gra w zgadywanie: wejście, pętle, `rand` z C | +| [`examples/todo.fn`](examples/todo.fn) | struktury, tablice, teksty i pliki | +| [`examples/structs.fn`](examples/structs.fn) | struktury, tablice struktur, lista wiązana z `new`/`free` | | [`examples/types.fn`](examples/types.fn) | liczby z rozmiarem i wskaźniki | | [`examples/c_import.fn`](examples/c_import.fn) | `printf`, `math.h`, `malloc`/`free`, `stderr` | | [`examples/window.fn`](examples/window.fn) | okno GLFW + OpenGL | +| [`examples/raylib.fn`](examples/raylib.fn) | raylib ze strukturami C przez wartość | | [`examples/llvm.fn`](examples/llvm.fn) | Finch budujący LLVM IR przez API LLVM-C | +| [`boot/`](boot/) | kompilator Fincha napisany w Finchu | ## Plan rozwoju - [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2 - [x] Typy, w tym liczby z rozmiarem, wskaźniki, konwersje; funkcje; `if`/`while`/`for` -- [x] Czytelne błędy kompilacji; kontrole w czasie działania (dzielenie, null) +- [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 -- [ ] `struct` (i struktury C przez wartość) -- [ ] Tablice/wycinki z `.len` i kontrolą zakresu -- [ ] Pamięć: automatyczne zwalnianie na końcu bloku, `defer`, ręczne `free` dla chętnych -- [ ] Teksty: `+`, `len`, `str(x)`; `input()` -- [ ] Moduły Fincha (`import math`) -- [ ] Informacje dla debuggera (DWARF) -- [ ] Bootstrap: kompilator Fincha napisany w Finchu +- [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 buduje sam siebie +- [ ] Metody w strukturach, mapy, `match` +- [ ] Błędy jako wartości zamiast zatrzymywania programu +- [ ] Więcej platform (ARM64, macOS) ## Struktura ``` -src/ kompilator: lekser, parser, AST, codegen, import C, sterownik (C++17, ok. 2,4 tys. linii) +src/ kompilator: lekser, parser, AST, codegen, import C + ABI, sterownik (C++17, ok. 4,5 tys. linii) +runtime/ mały runtime w C dołączany do każdego programu +boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii) examples/ przykładowe programy -tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh +tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh docs/ przewodniki en/ i pl/ dla trzech grup odbiorców ``` diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md index aa17c75..6f17b69 100644 --- a/docs/en/for-beginners.md +++ b/docs/en/for-beginners.md @@ -20,10 +20,15 @@ Take it slowly, one section at a time, and type every example yourself: that is 8. [Making decisions: if](#8-making-decisions-if) 9. [Repeating things: loops](#9-repeating-things-loops) 10. [Your own commands: functions](#10-your-own-commands-functions) -11. [When something goes wrong](#11-when-something-goes-wrong) -12. [Small projects to try](#12-small-projects-to-try) -13. [Cheat sheet](#13-cheat-sheet) -14. [Words you will hear](#14-words-you-will-hear) +11. [Lists: many values in one box](#11-lists-many-values-in-one-box) +12. [Playing with text](#12-playing-with-text) +13. [Asking the user](#13-asking-the-user) +14. [Your own kinds of values: structs](#14-your-own-kinds-of-values-structs) +15. [Saving to a file](#15-saving-to-a-file) +16. [When something goes wrong](#16-when-something-goes-wrong) +17. [Small projects to try](#17-small-projects-to-try) +18. [Cheat sheet](#18-cheat-sheet) +19. [Words you will hear](#19-words-you-will-hear) --- @@ -72,7 +77,7 @@ To check it works, type: ./build/finch version ``` -You should see something like `finch 1.0.0`. +You should see something like `finch 2.0.0`. --- @@ -441,7 +446,200 @@ It does not matter where in the file you put your functions, above or below `mai --- -## 11. When something goes wrong +## 11. Lists: many values in one box + +Sometimes one box is not enough. A shopping list has many things on it. +A **list** (programmers also say **array**) keeps many values in order: + +```c +fn main() { + fruits := ["apple", "banana", "cherry"] + print(fruits) + print("I have", fruits.len, "fruits") + print("The first one is", fruits[0]) +} +``` + +``` +["apple", "banana", "cherry"] +I have 3 fruits +The first one is apple +``` + +- Square brackets `[ ]` make a list. Commas separate the values. +- `.len` tells you how many things are in the list. +- `fruits[0]` is the **first** thing. Counting starts at **0**, so `fruits[1]` is the second, and `fruits[2]` the third. + +If you ask for a place that doesn't exist, like `fruits[10]`, the program stops and tells you: +`index 10 is out of range (the length is 3)`. + +### Adding and taking away + +```c +fn main() { + []int scores // an empty list of whole numbers + scores.push(10) // put 10 at the end + scores.push(25) + scores.push(7) + print(scores) // [10, 25, 7] + + last := scores.pop() // take the last one out + print(last, scores) // 7 [10, 25] + + scores.sort() + print(scores) +} +``` + +### Going through a list + +`for ... in` visits every value, one by one: + +```c +fn main() { + total := 0 + for n in [3, 5, 2] { + total += n + } + print("Total:", total) // Total: 10 +} +``` + +Lists can do more: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`. +The [guide for technicians](for-technicians.md#arrays) lists them all. + +--- + +## 12. Playing with text + +Text can be glued together with `+`: + +```c +fn main() { + name := "Ola" + greeting := "Hello, " + name + "!" + print(greeting) +} +``` + +To glue a number to text, turn the number into text first with `str(...)`: + +```c +age := 10 +print("Age: " + str(age)) +``` + +Text has handy tools too: + +```c +fn main() { + word := "Finch" + print(word.len) // 5 letters + print(word[0]) // F (the first letter) + print(word.upper()) // FINCH + print(word.lower()) // finch + print(word.contains("in")) // true + for letter in word { + print(letter) // F, i, n, c, h, one per line + } +} +``` + +--- + +## 13. Asking the user + +`input` shows a question and waits until the person types an answer and presses Enter: + +```c +fn main() { + name := input("What's your name? ") + print("Nice to meet you,", name) +} +``` + +The answer is always **text**. If you need a number, turn it into one with `int(...)`: + +```c +fn main() { + answer := input("How old are you? ") + age := int(answer) + print("Next year you will be", age + 1) +} +``` + +If the person types something that isn't a number, the program stops with a message +like `can't turn "abc" into int`. + +--- + +## 14. Your own kinds of values: structs + +Imagine a card in a library: each card has a title, an author and a year. +A **struct** lets you make your own kind of value with named parts: + +```c +struct Book { + str title + str author + int year +} + +fn main() { + b := Book(title: "The Hobbit", author: "Tolkien", year: 1937) + print(b.title, "by", b.author) + b.year = 1938 + print(b) +} +``` + +``` +The Hobbit by Tolkien +Book(title: "The Hobbit", author: "Tolkien", year: 1938) +``` + +- `struct Book { ... }` describes what every book has. Put it **outside** `main`. +- `Book(title: ..., author: ..., year: ...)` makes one book. +- `b.title` reads one part. `b.year = 1938` changes it. + +You can give parts a starting value. Then you don't have to fill them in: + +```c +struct Player { + str name + int lives = 3 +} + +fn main() { + p := Player(name: "Ola") + print(p.lives) // 3 +} +``` + +And you can keep many of them in a list: `[]Book shelf`, then `shelf.push(b)`. + +--- + +## 15. Saving to a file + +Programs forget everything when they end. To remember things, save them to a file: + +```c +fn main() { + write_file("note.txt", "Buy milk") + print(read_file("note.txt")) +} +``` + +- `write_file(name, text)` saves the text in a file (and replaces what was there). +- `read_file(name)` gives you the file's text back. +- `file_exists(name)` tells you if the file is there (`true` or `false`). + +The file appears in the folder where you ran the program. + +--- + +## 16. When something goes wrong Everyone makes mistakes when programming, even experts, every day. Finch tries hard to explain what went wrong in plain words. Example: @@ -479,10 +677,13 @@ Here we simply misspelled `age`. | `this '{' is never closed` | A `}` is missing | Add the `}` | | `a condition must be bool` | `if` needs a yes/no question | Compare something: `if x > 0` | | `runtime error: division by zero` | You divided by 0 while the program ran | Check the number before dividing | +| `index 5 is out of range (the length is 3)` | You asked a list for a place it doesn't have | Remember counting starts at 0; check `.len` | +| `can't turn "abc" into int` | `int(...)` got text that isn't a number | Check what the person typed | +| `can't use '+' on str and int` | Text plus a number | Use `str(...)` on the number | --- -## 12. Small projects to try +## 17. Small projects to try Try writing each one yourself first. Look at the solution only if you get stuck. @@ -589,9 +790,60 @@ fn main() { ``` +### Project 6: Guess the number + +The computer picks a secret number. The player guesses until they find it, +and the computer says "higher" or "lower" each time. + +
Solution + +```c +fn main() { + secret := 42 + tries := 0 + while true { + guess := int(input("Your guess: ")) + tries += 1 + if guess < secret { + print("Higher!") + } else if guess > secret { + print("Lower!") + } else { + print("Yes! You needed", tries, "tries") + return + } + } +} +``` + +`return` inside `main` ends the program. (A version with a random number is in `examples/guess.fn`.) +
+ +### Project 7: Class average + +Ask for 3 grades, keep them in a list, and show the list and the average. + +
Solution + +```c +fn main() { + []int grades + for i in 0..3 { + grades.push(int(input("Grade: "))) + } + sum := 0 + for g in grades { + sum += g + } + print("Grades:", grades) + print("Average:", float(sum) / grades.len) +} +``` +
+ --- -## 13. Cheat sheet +## 18. Cheat sheet ```c // a comment @@ -624,11 +876,21 @@ fn main() { // the program starts here fn add(int a, int b) -> int { // your own command return a + b } + +struct Pet { // your own kind of value + str name + int age = 1 +} + +// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } +// text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x") +// ask: name := input("Name? ") +// files: write_file("f.txt", text) read_file("f.txt") ``` --- -## 14. Words you will hear +## 19. Words you will hear | Word | Meaning | |---|---| @@ -641,6 +903,10 @@ fn add(int a, int b) -> int { // your own command | **function** | A named group of steps you can use again | | **loop** | Steps that repeat | | **condition** | A yes/no question, like `x > 5` | +| **list / array** | Many values kept in order, like `[1, 2, 3]` | +| **index** | The position of a value in a list, starting at 0 | +| **struct** | Your own kind of value, made of named parts | +| **input** | What the person types into the program | | **error** | A message saying something is wrong, and where | | **bug** | A mistake in a program | diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index 9a2a7cf..2e60de8 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -1,10 +1,11 @@ # Finch for Engineers -**The complete internals of the Finch 1.0 compiler.** It covers how source becomes tokens, -tokens become an AST, and the AST becomes LLVM IR, which is then optimized, emitted and -linked. It describes the exact type rules, how every construct lowers to IR (with real -compiler output), how C headers are imported through libclang, the ABI details, and what -each function in the compiler is for and how it differs from its neighbours. +**The complete internals of Finch 2.0.** How source becomes tokens, tokens an AST, and the AST +LLVM IR, which is then optimized, emitted and linked against a small C runtime. The exact type +rules; the ownership model and how it lowers to copies and drops; how every construct becomes IR +(with real compiler output); how C headers, C structs and the System V ABI are handled; modules, +debug info, and the self-hosted compiler in `boot/`. Every function in the compiler is listed with +what it does and how it differs from its neighbours. Assumed: you know C/C++ and have at least seen LLVM IR. For the language from a user's point of view, see the [technicians' guide](for-technicians.md). @@ -20,60 +21,65 @@ For the language from a user's point of view, see the [technicians' guide](for-t 3. [Lexer](#3-lexer) 4. [Grammar and parser](#4-grammar-and-parser) 5. [AST](#5-ast) -6. [Semantic model](#6-semantic-model) -7. [Type system](#7-type-system) -8. [Code generation](#8-code-generation) -9. [Runtime checks](#9-runtime-checks) -10. [C interop via libclang](#10-c-interop-via-libclang) -11. [Optimization, emission, linking](#11-optimization-emission-linking) -12. [Diagnostics](#12-diagnostics) -13. [Build system](#13-build-system) -14. [Testing](#14-testing) -15. [Semantics vs. C: defined and undefined behavior](#15-semantics-vs-c-defined-and-undefined-behavior) -16. [Function reference: who does what](#16-function-reference-who-does-what) -17. [Extending the compiler](#17-extending-the-compiler) -18. [Known limitations and roadmap](#18-known-limitations-and-roadmap) +6. [Loading: files and modules](#6-loading-files-and-modules) +7. [Semantic model](#7-semantic-model) +8. [Type system](#8-type-system) +9. [Ownership and memory](#9-ownership-and-memory) +10. [Code generation](#10-code-generation) +11. [Runtime checks](#11-runtime-checks) +12. [The C runtime](#12-the-c-runtime) +13. [C interop: headers, structs, ABI](#13-c-interop-headers-structs-abi) +14. [Optimization, emission, linking](#14-optimization-emission-linking) +15. [Debug info](#15-debug-info) +16. [Diagnostics](#16-diagnostics) +17. [The self-hosted compiler (boot/)](#17-the-self-hosted-compiler-boot) +18. [Build system](#18-build-system) +19. [Testing](#19-testing) +20. [Semantics vs. C](#20-semantics-vs-c) +21. [Function reference](#21-function-reference) +22. [Extending the compiler](#22-extending-the-compiler) +23. [Known limitations](#23-known-limitations) --- ## 1. Architecture -Finch is a classic single-pass front end in front of LLVM. Type checking and IR generation -are one walk over the AST. There is no separate semantic pass and no intermediate IR of its own. +Finch is a single-pass front end in front of LLVM: type checking and IR generation are one walk +over the AST, with no intermediate representation of its own. ``` - file.fn - │ readFile() src/main.cpp - ▼ - lex() ──────────► std::vector src/lexer.cpp - ▼ - parse() ─────────► Program (AST) src/parser.cpp, src/ast.h - │ ├─ imports ──► importHeaders() ──► CImports src/cimport.cpp (libclang) - │ └─ links - ▼ - hostMachine() ───► llvm::TargetMachine (triple, data layout) - ▼ - generate() ──────► llvm::Module (type check + IRBuilder, then verifyModule) src/codegen.cpp - ▼ - optimize() ──────► PassBuilder default pipeline (skipped with -O0) - ▼ - emitObject() ────► file.o (legacy::PassManager + addPassesToEmitFile) - ▼ - link() ──────────► cc file.o -o file -lm [-l… | pkg-config --libs …] - ▼ - executable (finch run: executed from a temp file, then deleted) + main.fn ──► Loader::load() ── lex() ── parse() ──► Program (+ each imported module, recursively) + │ + all `import "x.h"` ──► importHeaders() (libclang) ──► CImports + │ + hostMachine() ──► TargetMachine (triple + data layout)│ + ▼ + generate(progs, cimports, tm, debug) ──► Codegen::run() + declareStructs() all Finch + C structs, LLVM struct bodies, cycle check + declareFns() every function's signature (+ "borrow" analysis of parameters) + define() each body: type check + IRBuilder, scopes, drops, runtime checks + defineMainWrapper the C `main(argc, argv)` + DIBuilder::finalize (with -g) ; verifyModule + ▼ + optimize() PassBuilder default (skipped with -O0) + emitObject() legacy PM + addPassesToEmitFile ──► prog.o + link() cc prog.o ~/.cache/finch/rt-.o [your .c/.o/.a] -lm [pkg-config/-l…] + ▼ + executable (finch run: executed from a temp file with the remaining arguments, then deleted) ``` -Design decisions that shape everything else: +Design decisions that shape the rest: -- **Fail fast.** The first error prints a diagnostic and calls `exit(1)` (`fail()` in `src/error.h`). - There is no error recovery, so the AST and codegen never deal with half-valid input. -- **One walk.** `Codegen` carries the scope, loop and function state. Every expression returns - its IR value and its Finch type together (`Value_`). -- **Let LLVM do the work.** Locals are `alloca`s, and `mem2reg`/SROA promote them to SSA. - Finch never builds SSA or φ-nodes itself, except for `&&`/`||`. -- **The system C toolchain is the runtime.** `printf`, `strcmp`, `dprintf`, `exit` come from libc. - There is no Finch runtime library. +- **Fail fast.** The first error prints a diagnostic and exits (`failAt()` in `src/error.h`). No error + recovery, so later phases never see half-valid input. +- **One walk.** `Codegen` carries scopes, loops, the current function and module. Each expression + returns its IR value, its Finch type and two flags (`Value_`, §8). +- **Let LLVM do the work.** Locals are `alloca`s promoted by `mem2reg`/SROA. Finch builds φ-nodes by + hand only for `&&`/`||`. +- **Values, not references.** Arrays, strings and structs have value semantics: assignment copies, the + end of a block drops. Ownership is decided at compile time with no reference counting (§9). +- **A tiny C runtime** (`runtime/finch_rt.c`, ~430 lines) does what would be tedious in IR: string + operations, array growth, input, files, panics. It is embedded in the compiler and cached. --- @@ -81,1051 +87,798 @@ Design decisions that shape everything else: | File | Lines | Responsibility | |---|---|---| -| `src/error.h` | ~25 | `fail(line, col, msg)`: prints `file:line:col: error:` and a caret excerpt, then exits. Holds `g_file`, `g_source`. | -| `src/lexer.h/.cpp` | ~240 | `Tok` enum, `Token`, `lex()`, `tokName()` | -| `src/ast.h` | ~235 | `Type`, all `Expr`/`Stmt` nodes, `FnDecl`, `Import`, `Link`, `Program` | -| `src/parser.h/.cpp` | ~430 | Recursive-descent `Parser`, `typeFromName()` | -| `src/cimport.h/.cpp` | ~280 | libclang header import: `importHeaders()` → `CImports` (functions, constants, globals) | -| `src/codegen.h/.cpp` | ~900 | `Codegen`: type checking + LLVM IR generation, `generate()` | -| `src/main.cpp` | ~260 | Driver: CLI, target machine, optimization, object emission, linking, link-error analysis, `run` | -| `tests/run.sh` | | Golden-output and expected-failure tests | +| `src/error.h` | 40 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)` | +| `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(fileIndex)`, `tokName()` | +| `src/ast.h` | 310 | `Type` (incl. Array, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` | +| `src/parser.h/.cpp` | 540 | Recursive-descent `Parser`, `typeFromName()` | +| `src/cimport.h/.cpp` | 420 | libclang import: functions, C structs (fields, offsets), constants, globals | +| `src/codegen.h` | 15 | `generate()` | +| `src/codegen_impl.h` | 265 | The `Codegen` class and its helper structs, shared by the four files below | +| `src/codegen.cpp` | 1090 | Program, declarations, scopes and cleanups, statements, expressions, places (`ref`), operators, mutation analysis | +| `src/codegen_types.cpp` | 500 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), printing, DWARF types | +| `src/codegen_builtins.cpp` | 710 | Calls, constructors, built-ins, conversions, array and str methods, runtime declarations, panics | +| `src/abi.cpp` | 310 | System V x86-64 classification, C calls with structs by value, C-callable thunks | +| `src/main.cpp` | 390 | Driver: CLI, loader, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` | +| `runtime/finch_rt.c` | 440 | The runtime library (§12) | +| `boot/*.fn` | 3080 | The self-hosted compiler (§17) | --- ## 3. Lexer -`lex(src, file)` makes one linear pass over the source and returns a `std::vector` -terminated by `Tok::End`. +`lex(file)` makes one pass over `g_files[file].text` and returns `std::vector` ending in `Tok::End`. ```cpp -struct Token { - Tok kind; - std::string text; // identifier / literal contents (escapes already decoded) - int line, col; // 1-based - bool newlineBefore; // a newline occurred between the previous token and this one -}; +struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; }; ``` -### Newlines as statement terminators - -Finch has no semicolons. Instead of emitting `NEWLINE` tokens, as Python and Go do internally, -the lexer **marks** each token with `newlineBefore`. The parser decides what that means -(section 4). This keeps the grammar free of newline tokens, which would otherwise have to be -skipped everywhere: in argument lists, between `}` and `else`, and so on. - -### Tokens - -- **Positions:** `line` and `col` are 1-based, and `col` counts **characters**, not bytes: - UTF-8 continuation bytes do not advance it, so carets line up under text such as `"Błąd"`. - An unexpected non-ASCII character is reported whole (`'ż'`), with a hint that names are ASCII-only. -- **Identifiers / keywords:** `[A-Za-z_][A-Za-z0-9_]*`. Keywords: `fn return if else while for in break continue true false null import link`. - **Type names are not keywords.** `int`, `u8`, `ptr`… lex as `Ident`, and the parser recognises them through `typeFromName()`. - This keeps the lexer independent of the type list, and lets `u8(x)` parse as an ordinary call. -- **Integers:** decimal or `0x` hex, with `_` separators (removed from `text`). Leading zeros are decimal. - Finch has no octal, so `010` is 10. Value parsing happens in the parser (`strtoull`, base 10 or 16). -- **Floats:** `digits.digits`. A dot is only part of a number when **followed by a digit**, - so `0..10` lexes as `Int DotDot Int`, not `Float(0.) Dot …`. -- **Strings / chars:** `"…"` and `'…'`. Escapes `\n \t \r \0 \\ \" \'` are decoded into `text`. - An unknown escape, an unterminated literal or a newline inside a literal is an error. A char literal must decode to exactly one byte. -- **Operators:** maximal munch: `->` before `-`, `<<` and `<=` before `<`, `&&` before `&`, `..` before `.`, `:=` (a lone `:` is an error that suggests `:=`). -- **Comments:** `//` to end of line, `/* … */` (not nested; an unterminated one is an error). +- **Newlines are not tokens.** Each token records `newlineBefore`; the parser decides what it means (§4). + This keeps newline handling out of every list rule in the grammar. +- **Positions:** 1-based; `col` counts characters (UTF-8 continuation bytes don't advance it), so + carets line up under `"Błąd"`. A stray non-ASCII character is reported whole, with a hint that names are ASCII-only. +- **Identifiers / keywords:** `[A-Za-z_][A-Za-z0-9_]*`. Keywords: + `fn return if else while for in break continue true false null import link struct defer`. + **Type names are not keywords**: `int`, `u8`, `ptr`… are `Ident`s recognised by the parser through + `typeFromName()`, so `u8(x)` is an ordinary call and the lexer doesn't depend on the type list. +- **Numbers:** decimal or `0x` hex with `_` separators. No octal (`010` is 10). A `.` belongs to a number + only when a digit follows, so `0..10` is `Int DotDot Int`. +- **Strings / chars:** escapes `\n \t \r \0 \\ \" \'`. No newline inside a literal. +- **Operators:** maximal munch (`->` before `-`, `<<`/`<=` before `<`, `&&` before `&`, `..` before `.`, + `:=` before `:`). `:` alone is a token (named arguments). +- **Comments:** `//` and non-nesting `/* */`. --- ## 4. Grammar and parser -`Parser` (`src/parser.cpp`) is a hand-written recursive-descent parser with one token of -lookahead (`cur()`), plus a second (`peekTok()`) in two places. - -### Grammar (EBNF) +`Parser` (`src/parser.cpp`) is hand-written recursive descent with one token of lookahead, plus up to +three in `atDeclaration()`. ```ebnf -program = { import | link | function } ; -import = "import" STRING ; -link = "link" STRING ; +program = { import | link | struct | function } ; +import = "import" ( STRING | IDENT ) ; (* "x.h" = C header, name = Finch module *) +link = "link" STRING ; (* "glfw" | "file.c" | "file.o" | "lib.a" *) +struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NEWLINE } "}" ; function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ; param = type IDENT ; -type = TYPENAME | "ptr" "[" type "]" ; (* TYPENAME: int float bool char str ptr i8..u64 f32 f64 *) +type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ; block = "{" { statement TERMINATOR } "}" ; -statement = if | while | for | return | "break" | "continue" | block - | type IDENT [ "=" expr ] (* declaration *) - | IDENT ":=" expr (* inferred declaration *) - | target assignop expr (* target: IDENT or postfix ending in .value *) - | call ; -if = "if" expr block [ "else" ( if | block ) ] ; -while = "while" expr block ; -for = "for" IDENT "in" expr ".." expr block ; -return = "return" [ expr ] ; (* expr only if on the same line *) +statement = if | while | for | return | "break" | "continue" | "defer" statement | block + | type IDENT [ "=" expr ] (* declaration, see atDeclaration *) + | IDENT ":=" expr + | target assignop expr (* target: var, field, element, p.value *) + | call | method ; +for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* range | for-each *) assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; -expr = or ; -or = and { "||" and } ; -and = cmp { "&&" cmp } ; +expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ; cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ; add = mul { ( "+" | "-" | "|" | "^" ) mul } ; mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; unary = ( "-" | "!" | "~" ) unary | postfix ; -postfix = primary { "." IDENT } ; +postfix = primary { "." IDENT [ args ] | "[" expr "]" } ; primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" - | IDENT [ "(" [ expr { "," expr } ] ")" ] - | "(" expr ")" ; + | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ; +args = "(" [ arg { "," arg } ] ")" ; arg = [ IDENT ":" ] expr ; ``` -### The statement terminator rule +**Statement termination.** After a statement, `endOfStatement()` requires `}`, end of file, or a token +with `newlineBefore`. Inside expressions `sameLine()` (`parenDepth > 0 || !newlineBefore`) decides whether +an operator, `.`, `[` or a call's `(` continues the expression. `parenDepth` grows inside `( )` and `[ ]` +and is saved/reset to 0 when entering `{ }`. -`TERMINATOR` is not a token. After every statement, `endOfStatement()` requires that the next -token is `}`, end-of-file, or has `newlineBefore == true`. Otherwise the error is -`put each statement on its own line`. +**Precedence is Go's**, not C's: `&` binds like `*`, and `|`/`^` like `+`, all above comparisons, so +`x & MASK == 0` means `(x & MASK) == 0`. -Inside expressions, `sameLine()` decides whether a binary operator, `.`, or a call's `(` -**continues** the current expression: +**Recognising statements.** `atDeclaration()` is true for `[` `]` (an array type), `TYPE IDENT`, +`ptr [`, and `IDENT . IDENT IDENT` (a module's type). Otherwise the parser **parses a whole expression, +then looks at the next token**: an assignment operator makes it a target (it must be a `Var`, `Member` or +`Index`), otherwise it must be a call or method call, or it's `this value is computed but never used`. -```cpp -bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; } -``` - -So: - -```c -a := 1 + // '+' is on this line → the expression continues to the next line - 2 -b := (1 // inside parentheses (parenDepth > 0) newlines are ignored - + 2) -c := 1 --5 // '-' starts a new line → the statement ended at '1'; "-5" is then an error -``` - -`parenDepth` is incremented around `( … )` groups, argument lists and parameter lists. It is -**saved and reset to 0** when entering a `{ }` block, so a block inside a parenthesised -context, which does not occur yet, would still use line rules. - -A call's `(` must be on the same line as the callee. Otherwise `f⏎(x)` would silently parse as a call. - -### Precedence: Go's, not C's - -| Level | Operators | -|---|---| -| 5 | `*` `/` `%` `<<` `>>` `&` | -| 4 | `+` `-` `\|` `^` | -| 3 | `==` `!=` `<` `<=` `>` `>=` | -| 2 | `&&` | -| 1 | `\|\|` | - -All binary operators are left-associative. Comparisons are also parsed left-associatively -(`a < b < c` parses), but the type checker then rejects comparing `bool < int`. -C puts `&`, `^`, `|` **below** `==`, which makes `x & MASK == 0` mean `x & (MASK == 0)`. -Finch adopts Go's table, which removes that whole class of bugs. - -### How statements are recognised - -`statement()` dispatches on the first token: - -1. Keywords (`if`, `while`, `for`, `return`, `break`, `continue`, `{`) dispatch directly. - `fn`, `import` and `link` inside a block produce targeted errors. -2. **Typed declaration:** `atDeclaration()` returns true when the current token is a type name - and the next is an identifier (`int x`), or when it is `ptr` followed by `[` (`ptr[int] p`). - This is the only place that needs two-token lookahead. -3. **Inferred declaration:** `IDENT` followed by `:=`. -4. Otherwise the parser **parses a full expression first**, then looks at the next token. If it is - an assignment operator on the same line, the expression becomes the assignment target. It must - be a `VarExpr` or `MemberExpr`, otherwise: `only a variable or p.value can be assigned to`. - This "expression, then decide" approach lets arbitrary l-value forms (`pp.value.value = 1`) - work without a separate l-value grammar. -5. A bare expression statement must be a `CallExpr`. Anything else is - `this value is computed but never used`. - -### Top level - -`program()` loops over `import`, `link` and `fn`. A top-level item that starts with a type name -gets the hint `functions start with 'fn', like: fn add(int a, int b) -> int { ... }`, which catches -C-style function definitions. `link` arguments are validated: `"libfoo.so"` or a path is -rejected with a suggestion for the bare name. +**Struct literals are calls**: `Point(1, 2)` or `Point(x: 1, y: 2)`. Brace-based literals (`Point{…}`) +would be ambiguous with blocks (`if done {`); a call needs no special grammar, and named arguments +(`IDENT ":"` in `args()`) are only accepted by constructors at code generation time. --- ## 5. AST -All nodes live in `src/ast.h`. Expressions and statements are small class hierarchies with -an explicit `kind` tag. Consumers `switch` on `kind` and `static_cast`, which avoids RTTI and -`dynamic_cast` and keeps dispatch obvious. - -```cpp -struct Expr { ExprKind kind; Pos pos; virtual ~Expr(); }; -using ExprPtr = std::unique_ptr; -``` +All nodes are in `src/ast.h`: small class hierarchies with an explicit `kind`, dispatched with +`switch` + `static_cast`. | ExprKind | Node | Fields | |---|---|---| -| `Int` | `IntExpr` | `long long value` | -| `Float` | `FloatExpr` | `double value` | -| `Bool` / `Char` / `Str` | `BoolExpr` / `CharExpr` / `StrExpr` | `value` | +| `Int` `Float` `Bool` `Char` `Str` | literals | `value` | | `Null` | `NullExpr` | | | `Var` | `VarExpr` | `name` | -| `Unary` | `UnaryExpr` | `char op` (`-` `!` `~`), `operand` | +| `Unary` | `UnaryExpr` | `op` (`- ! ~`), `operand` | | `Binary` | `BinaryExpr` | `BinOp op`, `lhs`, `rhs` | -| `Call` | `CallExpr` | `std::string callee`, `args` | -| `Member` | `MemberExpr` | `obj`, `field` (only `value` is meaningful in 1.0) | +| `Call` | `CallExpr` | `callee` (a name), `args`, `argNames` | +| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) | +| `Index` | `IndexExpr` | `obj`, `index` | +| `ArrayLit` | `ArrayLitExpr` | `elems` | +| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fn(...)` | -| StmtKind | Node | Fields | -|---|---|---| -| `Block` | `BlockStmt` | `std::vector body` | -| `VarDecl` | `VarDeclStmt` | `hasType`, `type`, `name`, `init` (nullable if `hasType`) | -| `Assign` | `AssignStmt` | `target` (Var/Member), `char op` (`= + - * / %`), `value` | -| `Expr` | `ExprStmt` | `expr` (always a call) | -| `If` | `IfStmt` | `cond`, `then`, `otherwise` (null, `BlockStmt`, or nested `IfStmt` for `else if`) | -| `While` | `WhileStmt` | `cond`, `body` | -| `For` | `ForStmt` | `var`, `from`, `to`, `body` | -| `Return` | `ReturnStmt` | `value` (nullable) | -| `Break` / `Continue` | | | +| StmtKind | Node | +|---|---| +| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `list`, `body`; `DeferStmt` holds one statement | -Calls store the callee **by name**, not as an expression. Finch has no first-class functions, -and resolving names at codegen time lets one `CallExpr` mean a built-in (`print`, `addr`), -a conversion (`u8`), a Finch function or a C function (section 8.10). +`math.sqrt(x)` parses as a `MethodExpr` on `VarExpr("math")`; whether `math` is a module or a variable +is decided during code generation. Calls name their callee instead of holding an expression: Finch has +no first-class functions, and resolving the name late lets one `CallExpr` be a built-in, a conversion +(`u8(x)`), a struct constructor, a Finch function or a C function. -### Example - -```c -x += add(x, 2) -``` - -parses to: - -``` -AssignStmt (op '+') -├─ target: VarExpr "x" -└─ value: CallExpr "add" - ├─ VarExpr "x" - └─ IntExpr 2 -``` - -There is **no "load" node in the AST**. Whether a `VarExpr` means "the address of x" or "the -value in x" depends on where it appears. Codegen decides that (`place()` vs `expr()`, section 8.2). +There is no "load" node: whether `x` means its address or its value depends on context (`ref()` vs +`expr()`, §10.2). ### `Type` ```cpp struct Type { - enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null }; + enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, + Str, Ptr, Null, Array, Fixed, Struct, Named }; Kind kind; - std::shared_ptr elem; // Ptr only: pointee; null = untyped `ptr` + std::shared_ptr elem; // Ptr (null = untyped `ptr`), Array, Fixed + long long count; // Fixed + std::string name, module; // Named (as written) / Struct (resolved) + StructInfo *info; // Struct }; ``` -- `int` and `i64` are **the same type** (`I64`), and `name()` prints it as `int`. Likewise `float` is `F64`. -- `Null` is the type of the `null` literal only. No variable can have it. -- `Void` is the "type" of calls that return nothing, and of functions without `->`. -- Equality is structural (`ptr[ptr[int]] == ptr[ptr[int]]`). -- Kinds are ordered so that `isInt()` is `I8..U64`, `isSigned()` is `I8..I64`, `isUnsigned()` is `U8..U64`. +The parser produces `Named` for struct names; `Codegen::resolve()` turns them into `Struct` with an +`info` pointer. `Fixed` (`[N]T`) only comes from C struct fields. `int` is `I64` and `float` is `F64`. +Equality is structural; two `Struct`s are equal iff they share `info`. --- -## 6. Semantic model +## 6. Loading: files and modules -All semantic checking happens inside `Codegen` while it emits IR. +`Loader::load(path, module, from)` in `main.cpp` reads a file, appends it to `g_files` (positions carry +the file index in `Pos::file`), lexes and parses it, then loads each `import name` that wasn't seen yet: +`name.fn` next to the importing file, or in a `FINCH_PATH` folder. The result is a `std::vector`, +main file first. Cycles are fine: a module is marked loaded before its own imports are followed. -### Functions +All files become **one LLVM module**. Each Finch module has a `ModuleScope` (functions, structs, the set +of modules it imports). Names are looked up in the current module; another module's names are reachable +only qualified (`geometry.length`, `geometry.Vec`) and only if imported there. Functions are emitted as +`finch..` (`finch.` in the main file), struct types as `finch..`. -`Codegen::run()` runs two passes over `Program::fns`: +--- -1. `declare()` creates every `llvm::Function` (signature only), so **definition order does not matter** - and mutual recursion works. It also checks: duplicate names, built-in names, and `main`'s - signature (no parameters; returns nothing, `int` or `i32`). -2. `define()` generates each body. +## 7. Semantic model -Without a `main`, compilation fails with `there is no main function`. +### Declaration order -### Scopes +`Codegen::run()` first declares **all structs** of all modules plus all imported C structs, resolves +every field type and sets the LLVM bodies (`declareStructs` → `resolveStruct`), then declares **all +function signatures** (`declare`), then defines bodies. So order inside a file and across modules never +matters, and recursion (also mutual, also across modules) works. -`scopes` is a `std::vector>`. A new map is pushed for -the function's parameters, for every `{ }` block, and for every `for` loop (which holds the -loop variable). +`resolveStruct` detects by-value cycles with a three-state flag (`struct A { A inner }` is an error with +the advice to use `ptr[A]` or `[]A`). Pointers and arrays of a struct don't need its layout, so +`struct Node { []Node kids; ptr[Node] parent }` is fine: they resolve the name "shallowly" (`resolveT(…, deep=false)`). + +### Scopes and variables ```cpp -struct Var { AllocaInst *slot; FType type; bool readonly; }; +struct Var { Value *slot; FType type; bool readonly; std::string readonlyWhy; + bool owned; int order; bool moved; }; +struct Cleanup { bool isDefer; std::string var; const Stmt *body; int order; }; +struct Scope { unordered_map vars; vector cleanups; }; ``` -**No shadowing:** `addVar()` rejects a name that `lookup()` finds in *any* enclosing scope, -not just the current one. It also rejects names of functions and built-ins. This is a -deliberate simplicity rule: a name means one thing in a whole function body. +A scope is pushed for the function's parameters, each `{ }`, each `for` (holds `i`), each `for … in` +(the hidden list temporary) and each iteration of it (holds `x`). -**Read-only:** `for` variables are `readonly`. Assigning to them, or taking `addr()` of them, -is an error, so the loop counter cannot be changed behind the loop's back. +- **No shadowing:** `addVar()` rejects a name found in *any* enclosing scope, a function of the module, + an imported module name or a built-in. +- **Read-only:** range and for-each variables (`readonlyWhy` gives the specific message). +- `order` numbers declarations; `defer` uses it to hide variables declared after it (§9.5). -### Name resolution for identifiers +### Name resolution -For a `VarExpr name` in value position, the order is: - -1. a local variable (`lookup`), -2. a C constant (`cimports.consts`: enum values, `#define` numbers), -3. a C global (`cimports.globals`: `stdout`, …), -4. error `there is no variable named 'name'`. - -For calls, the order is: `print` → `addr` → type name (conversion) → Finch function → C function → error. -Since Finch functions are checked before C functions, **a Finch function shadows a C function -of the same name**. The Finch one is emitted as `finch.`, so both symbols coexist at link time. +Value position `x`: local variable → C constant → C global → (module name: error with a hint) → error. +Call `n(...)`: built-ins (`print addr input new free exit shell read_file write_file file_exists`) → +type names (conversion) → function of the current module → struct of the current module / C struct +(constructor) → C function → error (with a hint if the function exists in another module, or a +`headerHint` for well-known libc names). --- -## 7. Type system +## 8. Type system ### LLVM representation -| Finch | LLVM | Notes | -|---|---|---| -| `bool` | `i1` | | -| `char`, `i8`, `u8` | `i8` | signedness lives in the Finch type, not in LLVM | -| `i16`, `u16` | `i16` | | -| `i32`, `u32` | `i32` | | -| `int`/`i64`, `u64` | `i64` | | -| `f32` | `float` | | -| `float`/`f64` | `double` | | -| `str` | `ptr` | NUL-terminated C string | -| `ptr`, `ptr[T]` | `ptr` | LLVM 21 has opaque pointers; the pointee type exists only in the Finch type | -| `null` | `ptr` | `ConstantPointerNull` | -| nothing | `void` | | +| Finch | LLVM | +|---|---| +| `bool` | `i1` | +| `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) | +| `ptr`, `ptr[T]`, `null` | `ptr` (opaque) | +| `[N]T` | `[N x T]` | +| Finch struct | named struct type, natural layout | +| C struct | named **packed** struct with explicit padding bytes (§13.2) | -LLVM integers are signless. Every operation where signedness matters (`sdiv`/`udiv`, -`srem`/`urem`, `ashr`/`lshr`, `icmp slt`/`ult`, `sext`/`zext`, `sitofp`/`uitofp`, -`fptosi`/`fptoui`) chooses the instruction from the **Finch** type. +Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, +`sext/zext`, `sitofp/uitofp`, `fptosi/fptoui`. -### Values in codegen +### Values ```cpp -struct Value_ { - Value *v; // the IR value - FType type; // its Finch type - bool literal; // written as a number in the source (or an imported C constant) -}; +struct Value_ { Value *v; FType type; bool literal; bool fresh; }; ``` -### Literals ("untyped constants") +- **`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, + a literal, a concatenation, a constructor. It must be stored (moved) or released (§9). -`literal` is set on `IntExpr`, `FloatExpr` and imported C constants. It is preserved through -unary minus and through arithmetic **between two literals** that folds to a constant -(`arith()` sets `literal = l.literal && r.literal && isa(result)`). It is **cleared by -explicit conversions**: `u8(250)` is a typed `u8` constant, not a literal. +### Implicit conversions: `coerce(v, want)` -A literal integer may become **any integer type it fits in** (`fits()` checks the range, -accounting for the source literal's signedness). This is what makes `u8 b = 200`, -`malloc(4 * 10)` (to `u64`) and `glClear(GL_COLOR_BUFFER_BIT)` (an `i64` macro to a `u32` parameter) -work without casts, while `u8 b = 300` is rejected with `the number 300 doesn't fit in u8`. -A literal `f64` may become `f32` (`f32 h = 0.5`). - -Why not simply check `isa`? `IRBuilder` constant-folds, so `u8(250)` is also a -`ConstantInt`. If "is a constant" were the rule, `u8(250) + 10` would quietly widen to -`int` and give 260, while the same code with a `u8` variable would wrap to 4. The explicit -flag makes the behavior depend on what you *wrote*, not on what the optimizer could see. - -### Implicit conversions: `coerce(value, want, pos, what)` - -Used wherever a value flows into a slot of known type: initializers, assignments, arguments, -return values, conditions (`want = bool`), range bounds (`want = int`). Allowed, in order: - -1. identical types, -2. `null` → any `ptr…` or `str`, -3. any `ptr…` or `str` → untyped `ptr`, and untyped `ptr` → any `ptr[T]` (the `void*` rules; this is how `ptr[i32] n = malloc(4)` works), -4. int → int when the value is a fitting literal, **or** `widens(from, to)`: - - same signedness, strictly more bits, **or** - - unsigned → signed with strictly more bits, - - (signed → unsigned never widens), -5. any int → any float (`sitofp`/`uitofp`), -6. `f32` → `f64` (`fpext`), -7. literal `f64` → `f32` (`fptrunc`, constant-folded). - -Anything else is an error. The message suggests the explicit conversion -(`use i32(...) to convert`) and warns when it would truncate (`it cuts off the fraction`). +Identical types; `null` → any pointer; typed pointer ↔ untyped `ptr`; integer → integer if the value is a +fitting literal or the type `widens()` (same signedness and more bits, or unsigned → strictly bigger +signed); integer → float; `f32` → `f64`; literal `f64` → `f32`. Nothing else: the error suggests the +explicit conversion. Owning types convert only to themselves. Array literals take their element type +from the slot they flow into (`exprWant()`), so `[]f32 xs = [1, 2.5]` works. ### Binary operands: `unify(l, r)` -Before arithmetic or comparison, both sides are brought to one type: +A literal side adapts to the other; otherwise the side that widens grows; `i32` + `u32` is an error; +int + float → float; f32 + f64 → f64. There is no C integer promotion: `u8 + u8` stays `u8`. -1. same type → done, -2. both ints: a literal side adapts to the other side if it fits; otherwise the side that - `widens()` into the other is extended; otherwise **error** (`i32` + `u32`: neither widens - into the other, and Finch refuses to guess), -3. int and float → the int converts to that float type, -4. `f32` and `f64` → both `f64`. +### Explicit conversions: `convert()` -The result type of arithmetic is the unified type. **There is no C-style integer promotion:** -`u8 + u8` is computed in `u8` and wraps. This is deliberate. It is what the sized type asks for, -and it matches Rust and Zig. - -### Explicit conversions: `convert(call, to)` - -`T(x)` is parsed as a call named after a type and lowered here: - -| From → To | Instruction | +| From → To | IR | |---|---| -| int/char → int/char | `CreateIntCast(signed = from.isSigned())` → `trunc` / `sext` / `zext` | -| bool → int/char | `zext` | -| float → int | `fptosi` / `fptoui` (by target signedness) | -| int → float | `sitofp` / `uitofp` (by source signedness) | -| float → float | `fpext` / `fptrunc` (`CreateFPCast`) | -| int → bool | `icmp ne x, 0` | -| ptr/str/null → ptr…, ptr → str | no instruction (all are `ptr`) | +| int/char ↔ int/char | `trunc`/`sext`/`zext` by source signedness | +| bool → int | `zext` | +| float ↔ int | `fptosi`/`fptoui`, `sitofp`/`uitofp` | +| float ↔ float | `fpext`/`fptrunc` | +| int → bool | `icmp ne 0` | +| number/bool/char → str | `finch_str_from_int/uint/float/char/bool` (fresh) | +| str → int/float | `finch_str_to_int/float` (panics on bad text) | +| `[]u8`/`[]char` → str | `finch_str_from_bytes` | +| ptr → str | `finch_str_from_c` (borrowed, `cap = -1`) | +| ptr ↔ ptr | nothing | -`fptosi` of an out-of-range value is poison in LLVM (UB in C). Finch 1.0 does not check this. See section 15. +### Comparisons -### Comparisons: `compare()` - -- **Pointers:** if either side is a `ptr…` or `null`, only `==`/`!=` are allowed and they compile to - `icmp eq/ne ptr`. A `str` compared with `null` also compares addresses. -- **`str == str`:** a call to `strcmp`, then `icmp eq/ne i32 %r, 0` (content equality). `<`/`>` on `str` are rejected. -- **Floats:** `oeq olt ole ogt oge` (ordered: false if NaN) and `une` for `!=` (unordered: `NaN != NaN` is true). This matches C. -- **Integers / char:** `eq ne`, and `slt sle sgt sge` or `ult ule ugt uge` by signedness. `char` compares unsigned. -- **bool:** only `==`/`!=`. +Pointers/null: `icmp eq/ne ptr` only. `str`: `finch_str_eq` (length + `memcmp`) for `==`/`!=`, +`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. --- -## 8. Code generation +## 9. Ownership and memory -`Codegen` owns an `llvm::IRBuilder<> b`, the `Module`, the function table `fns` -(`name → {Function*, const FnDecl*}`), `scopes`, the `loops` stack and `curFn`. +### 9.1 The model -### 8.1 Variables are stack slots +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 +value when it goes away. There is no reference counting and no garbage collector; every decision is static. -Every variable, **including parameters**, gets an `alloca` created by `slot()`. -`slot()` always inserts at the **top of the entry block**, using a second `IRBuilder` positioned at -`entry.begin()`, no matter where the declaration appears. Allocas in the entry block are what -`mem2reg`/`SROA` can promote to SSA registers, so the O2 output contains no stack traffic for -scalars. Because each new slot goes to the very beginning, slots appear in **reverse declaration -order** in `-O0` IR (`%b2` before `%a1` below). +| Situation | What happens | +|---|---| +| `x := expr` / `T x = expr` / `x = expr` / field, element, `push` | **`own(v)`**: a fresh value is moved in; a borrowed one is deep-copied | +| `x = …` (owning) | the new value is computed first, the old one dropped after the store | +| end of a block, `return`, `break`, `continue` | owned variables of every scope being left are dropped (newest first), defers run | +| `return local` | the variable is **moved out** (marked `moved` while the cleanups for that return are emitted) | +| passing an argument | **lent**: the callee gets a shallow copy and never drops it; fresh arguments are released by the caller after the call | +| a parameter the function changes | copied on entry and owned by the callee (decided by `mutates()`, below) | +| a temporary nobody keeps (`print(a + b)`, `f(g())`, an expression statement) | **`release(v)`**: dropped right after use | +| `a.pop()`, `a.remove(i)` | the element is moved out (fresh) | +| `for x in list` | `x` borrows each element; if the body may change `list`, each element is copied instead | -Parameters are copied into their slots at function entry (`store %a, %a1`), which makes -parameters ordinary mutable locals. +A string literal is a fresh value with `cap = 0`: moving it costs nothing, dropping it is skipped +(`release()` ignores constants), and writing into it (`s[i] = c`) first makes a heap copy +(`finch_str_own`). Strings from C are borrowed with `cap = -1` and always copied before being kept. -### 8.2 Loads and stores: `expr()` vs `place()` +### 9.2 Parameter borrowing: `mutates()` -This is the heart of how memory access is generated: +When declaring a function, every owning parameter is checked with `mutates(name, body)`: an +assignment whose root variable (`rootVar()`: through `.field` and `[i]`) is the parameter, a changing +method on it (`push pop insert remove clear resize sort reverse`), or `addr(…)` of it. Unchanged +parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at entry. The analysis is +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. -- `place(e)` returns a **`Place {Value *addr; FType type}`**, the *address* of an l-value. - - `VarExpr` → the variable's `alloca` (or the C global's `GlobalVariable`). - - `MemberExpr` `p.value` → `expr(p)` (which **loads the pointer** from p's slot), plus a null check - (section 9). The address is the loaded pointer itself, and the type is `*p.type.elem`. -- `expr(e)` returns a **`Value_`**, an r-value. For `Var` and `Member` it calls `place()` and then - emits **one `load`** of `ty(place.type)` from `place.addr`. +### 9.3 Copy and drop -So the `load` for an identifier is created in `Codegen::expr()`, case `ExprKind::Var`, as -`b.CreateLoad(ty(pl.type), pl.addr, v.name)`. Naming it after the variable is why `-O0` IR shows -`%x1`, `%x2` and so on (LLVM uniquifies repeated names). `p.value` costs **two** loads: one for the -pointer, one for the pointee. `pp.value.value` costs three. +`dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses: -Stores are generated in exactly three places: `addVar()` (initialization), `assign()`, and the `for` step. +- `str`: `finch_str_drop` (frees if `cap > 0`) / `finch_str_copy` (shares literals, duplicates the rest). +- `[]T` and owning structs: generated helpers `finch.drop.` and `finch.copy.`, created once per + type (`dropFn`, `copyFn`, keyed by `typeKey()`, e.g. `arr_S_main_Player`). Arrays drop each owning + 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. -### 8.3 Assignment and evaluation order +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. + +### 9.4 What it looks like ```c -x += add(x, 2) -``` +fn greet(str name) -> str { + msg := "Hi, " + name + return msg +} -`assign()` computes `place(target)` **first**. For compound operators it then **loads the current -value**, then evaluates the right-hand side, then calls `arith()`, then `coerce()`s back to the -target type, then stores. Real `-O0` output: - -```llvm -define i32 @main() { -entry: - %x = alloca i64, align 8 - store i64 5, ptr %x, align 8 ; x := 5 - %0 = load i64, ptr %x, align 8 ; current x (for +=) - %x1 = load i64, ptr %x, align 8 ; argument x - %1 = call i64 @finch.add(i64 %x1, i64 2) - %2 = add i64 %0, %1 - store i64 %2, ptr %x, align 8 - %x2 = load i64, ptr %x, align 8 - %3 = call i32 (ptr, ...) @printf(ptr @fmt, i64 %x2) - ret i32 0 +fn main() { + a := ["x"] + b := a + b.push(greet("Ola")) + print(b[1]) } ``` -Order of evaluation in Finch is **always left to right**: the target, then the operands of a -binary operator left to right, then arguments left to right. C leaves most of this unspecified. -Note that for `x += f()` the old value of `x` is read **before** `f()` runs. If `f` modified `x` -through a pointer, the modification would be overwritten. That is the same as `x = x + f()` -read left to right. - -After O2 the whole program above is constant-folded into `printf(@fmt, i64 12)`. - -### 8.4 Functions, linkage and `main` +The `-O0` IR of `main` (abridged): ```llvm -define internal i64 @finch.add(i64 %a, i64 %b) { -entry: - %b2 = alloca i64, align 8 - %a1 = alloca i64, align 8 - store i64 %a, ptr %a1, align 8 - store i64 %b, ptr %b2, align 8 - %a3 = load i64, ptr %a1, align 8 - %b4 = load i64, ptr %b2, align 8 - %0 = add i64 %a3, %b4 - ret i64 %0 -} + %3 = call ptr @finch_alloc(i64 24) ; ["x"]: one element on the heap + store { ptr, i64, i64 } { ptr @str.1, i64 1, i64 0 }, ptr %4 ; the literal, cap 0 + ... + store { ptr, i64, i64 } %7, ptr %a ; moved into 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 copies + %8 = load { ptr, i64, i64 }, ptr %2 + store { ptr, i64, i64 } %8, ptr %b + %9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 }) + ... + call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: grow if needed + store { ptr, i64, i64 } %9, ptr %15 ; greet's result moved in, no copy + ... + %20 = icmp uge i64 1, %19 ; b[1]: bounds check + br i1 %20, label %out.of.range, label %in.range + ... + call void @finch.drop.arr_str(ptr %b) ; end of main: newest first + call void @finch.drop.arr_str(ptr %a) + ret void ``` -- User functions are named **`finch.`** with **internal linkage**. The dot cannot appear in a C - identifier, so a Finch function can never collide with a libc or library symbol, and internal - linkage lets LLVM inline, specialise or delete them freely. In C, a non-`static` function is - external and must be kept as written. This likely contributes to recursive `fib(40)` running - slightly faster than the same C code at `clang -O2` (about 0.19 s vs 0.23 s on the author's machine). -- `main` is emitted as **`i32 @main()` with external linkage**. A `fn main()` returns `i32 0` on every - exit path, including an explicit `return`. A `fn main() -> int` truncates its `i64` result with - `CreateIntCast(…, i32, signed)`. -- Missing return: after the body, if the current block has no terminator, `void` functions get - `ret void` (or `ret i32 0` for `main`). Non-void functions are a compile error: - `function 'f' can reach its end without returning int`. This is checked on the CFG that was - actually built (section 8.5), so `if/else` that returns on both branches is accepted without any - extra analysis. -- Arithmetic is emitted **without `nsw`/`nuw` flags**, so integer overflow is defined two's-complement - wraparound (section 15). +and `greet` returns `msg` without copying (`ret … %5` after loading the slot: the variable was moved out). -### 8.5 Control flow and the "terminated" state +### 9.5 defer -The builder's insert block is the single source of truth for reachability: +`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. + +### 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. + +### 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, +including returns from inside loops, `continue`/`break` with owned locals, defers, mutation during +iteration, nested arrays of structs, and C struct passing. + +--- + +## 10. Code generation + +### 10.1 Variables + +Every variable, including parameters, gets an `alloca` at the top of the entry block (`slot()`; +C structs get their C alignment). `addVar()` stores the initial value, registers the variable, adds a +drop cleanup if it owns its value, and (with `-g`) declares it to the debugger. + +### 10.2 Places: `ref()`, `place()`, `expr()` ```cpp -bool terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; } +struct LRef { bool isPlace; Place pl; Value_ val; }; // an address, or (if none) a value ``` -After `return`, `break` or `continue`, the current block has a terminator. -`blockBody()` checks `terminated()` **before each statement**. If it is set, the statement is -unreachable and that is a compile error (`this code can never run`). No dead blocks are ever created -for code after a jump. +`ref(e, forWrite)` walks a `Var`/`Member`/`Index` chain and returns an **address** when one exists: -`continueAt(bb, deadEnd)` is used after `if` and `while`. If the merge block `bb` has **no -predecessors** (all paths returned or broke), it is erased and the builder is left in `deadEnd`, -which is terminated. So the "terminated" state propagates outward through nested constructs, and -the missing-return check sees it. +- `Var` → its `alloca` (or a C global). +- `.field` on a struct place → `getelementptr` to the field (`llvmIndex`; C structs skip padding elements). +- through a pointer (`p.value`, `p.field`, `p[i]`) → load the pointer, check it isn't null, address the target. + `p.value` on a pointer to a struct that has its own `value` field means that field. +- `a[i]` on an array/str place → load `{ptr,len,cap}`, `boundsCheck(i, len)`, `getelementptr`. For + `forWrite` on a `str`, `finch_str_own` first (copy-on-write for literals). `[N]T` checks against `N`. +- `.len` / `.ptr` → values. +- a temporary (call result…) has no address: its field/element is extracted, copied if owning, and the + temporary released. -**`if / else if / else`:** blocks `then`, `else` (if present), and `endif`. `else if` is an -`IfStmt` in `otherwise`, generated recursively inside the `else` block. +`expr()` on these kinds calls `ref()` and loads from the address: this is where every `load` of a +variable, field or element comes from. `place()` demands an address (assignment targets, `addr()`). -**`while`:** blocks `while.cond`, `while.body`, `while.end`. If the condition folds to the -constant `true`, an unconditional `br` is emitted, so `while.end` has predecessors only through -`break`. A `while true` without `break` therefore leaves the function "terminated", and a function -whose last statement is such a loop does not need a trailing `return`. +### 10.3 Assignment -**`for i in a..b`:** `a` and `b` are evaluated **once**, before the loop, and coerced to `int`. -Blocks: `for.cond` (`icmp slt i, end`), `for.body`, `for.step` (`i + 1`), `for.end`. `continue` -jumps to `for.step`, not `for.cond`, so the increment is never skipped. The loop variable lives in -its own scope around the loop. +`x = v`: evaluate `v` **first** (it may reallocate memory the target lives in: `a[0] = f()` where `f` +pushes to `a`), then `place(target, forWrite)`, `coerce`, `own`, load the old value, store, drop the old +one. Compound `x += v`: evaluate `v`, load the target, `arith()`, store, drop the old value (for `str +=`). -```llvm - store i64 0, ptr %i, align 8 - br label %for.cond -for.cond: - %3 = load i64, ptr %i, align 8 - %4 = icmp slt i64 %3, 2 - br i1 %4, label %for.body, label %for.end -for.body: - ... - br label %for.step -for.step: - %6 = load i64, ptr %i, align 8 - %7 = add i64 %6, 1 - store i64 %7, ptr %i, align 8 - br label %for.cond -for.end: -``` +### 10.4 Functions and `main` -**`break` / `continue`:** `loops` is a stack of `{continueTo, breakTo}`. `while` pushes -`{while.cond, while.end}` and `for` pushes `{for.step, for.end}`. Outside a loop it is an error. +Functions are `internal` with their natural LLVM signature (aggregates by value). Owning parameters are +copied at entry only if `borrowParam` is false. `fn main` is the internal `finch.main`; a separate +external `i32 main(i32, ptr)` (`defineMainWrapper`) builds `[]str args` with `finch_args` if requested, +calls it, drops the arguments and returns the exit code. A function that falls off its end gets the +scope cleanups and `ret void`, or the error `can reach its end without returning T`. -### 8.6 Short-circuit `&&` / `||`: the only hand-built φ +### 10.5 Control flow + +The builder's current block is the source of truth for reachability (`terminated()`): statements after +a terminator are errors (`this code can never run`). `continueAt()` deletes a merge block nobody jumps +to. `if`/`while`/`for` are the classic block layouts; `while true` without `break` leaves the function +terminated (no `return` needed after it). `for i in a..b` evaluates both ends once; `continue` goes to +`for.step`. `for x in list` reads the list's length from its address **every round** (pushing inside the +loop is safe) and keeps the index in a stack slot. `break`/`continue` emit the cleanups of the scopes +opened inside the loop (`Loop::scopeDepth`) before jumping. + +### 10.6 Short-circuit `&&` / `||` + +The only hand-built φ: the left side ends in block `from`, the right side in `rhsEnd`; +`phi i1 [!isAnd, from], [rhs, rhsEnd]`. + +### 10.7 Calls + +`callFinch()` coerces arguments to parameter types (array literals via `exprWant`), passes owning ones +shallowly, calls, then releases fresh arguments. Results of owning type are fresh. +`construct()` builds a struct from `zeroinitializer` with `insertvalue`: positional (all fields) or named +(missing ones take the field's default expression, evaluated in the struct's module with the caller's +scopes swapped out, or zero). C calls are in §13. + +### 10.8 Printing + +`print` emits `printf` per argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`, `%.*s` for +`str` with its length, `%p`), separated by spaces, then `\n`. Arrays, fixed arrays and structs call +generated helpers `finch.print.` that print `[1, 2]` / `Point(x: 1, y: 2)`, quoting strings and chars inside. + +### 10.9 Array and str methods + +Arrays: `push` (`finch_arr_reserve` + store + len++), `pop`/`remove` (move out, `remove_gap`), +`insert` (`insert_gap`), `clear`/`resize` (drop the elements that go away, `finch_arr_resize`), +`find`/`contains` (a loop with `compare(Eq)`), `slice` (new array, `copyAt` per element), `reverse` +(swap loop), `sort` (libc `qsort` with a generated comparator `finch.cmp.`), `join` (runtime). +Methods that change the array require an address (a variable/field/element). +Strings: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, all in the runtime. + +--- + +## 11. Runtime checks + +All runtime errors go through `finch_panic(file, line, msg)` / `finch_panic_index(file, line, i, len)` +(declared `noreturn cold`), so the failing branch is laid out cold and the check costs a compare and a +well-predicted branch. LLVM removes checks it can prove (constant indexes in range, loop-hoisted bounds). + +| Check | Where | +|---|---| +| division/remainder by zero (constant zero: compile error) | `checkDivisor()` | +| `MIN / -1` for signed types | `checkDivisor()` | +| array/str/fixed-array index out of range (`icmp uge`, so negative indexes too) | `boundsCheck()` | +| `.value`, `.field`, `[i]` through null | `member()`, `index()` | +| `pop()` on empty, `slice()`/`sub()` out of range | methods, runtime | +| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` | runtime | + +A 20-million-element sieve with a bounds check on every access runs as fast as the same C (≈0.09 s): +the checks are hoisted or folded. + +--- + +## 12. The C runtime + +`runtime/finch_rt.c` is plain C with no global state. CMake embeds it into the compiler as a raw string +(`rt_source.inc`); on first use `runtimeObject()` compiles it with `$CC -O2 -fPIC -c` into +`~/.cache/finch/rt-.o`, where the hash covers the source, the Finch version and the C compiler. +It is linked statically into every program. + +The shared layout: ```c -fn check(int a, int b) -> bool { - return a > 0 && b > 0 -} +typedef struct { char *ptr; int64_t len; int64_t cap; } FStr; // ptr always NUL-terminated +typedef struct { void *ptr; int64_t len; int64_t cap; } FArr; +// str cap: > 0 heap (owned), 0 literal (static), -1 borrowed from C (copy before keeping) ``` -```llvm - %a3 = load i64, ptr %a1, align 8 - %0 = icmp sgt i64 %a3, 0 - br i1 %0, label %and.rhs, label %and.end -and.rhs: - %b4 = load i64, ptr %b2, align 8 - %1 = icmp sgt i64 %b4, 0 - br label %and.end -and.end: - %2 = phi i1 [ false, %entry ], [ %1, %and.rhs ] -``` - -`logic()` records the block where the left side **ended** (`from`) and the block where the right -side **ended** (`rhsEnd`). These are not necessarily the blocks where evaluation started, because a -nested `&&` creates its own blocks. The φ uses `!isAnd` as the short-circuit constant (`false` for -`&&`, `true` for `||`). - -### 8.7 `print` - -`print(a, b, …)` compiles to **one `printf` call**. The format string is built at compile time from -the argument types, joined with spaces and ending in `\n`: - -| Finch type | Format | Argument conversion | -|---|---|---| -| signed ints | `%lld` | `sext` → `i64` | -| unsigned ints | `%llu` | `zext` → `i64` | -| `f32`, `f64` | `%g` | `fpext` → `double` | -| `char` | `%c` | `zext` → `i32` | -| `str` | `%s` | as is | -| `bool` | `%s` | `select i1, "true", "false"` | -| `ptr…` | `%p` | as is (glibc prints `(nil)` for null) | -| `null` | literal text `null` | none | - -Since the format is generated, user strings are always passed as `%s` arguments, never as the -format, so a `%` in user text is safe. O2 often rewrites `printf("%s\n", s)` into `puts(s)`. - -### 8.8 Strings - -A string literal is a private `unnamed_addr` constant global (`CreateGlobalString`), and its value is -the `ptr` to it. Identical literals may be merged by the optimizer. A `str` variable declared -without an initializer points to a shared `""` constant. Strings are **immutable and static** in -1.0. There is no concatenation or allocation (section 18). - -### 8.9 `addr()` and pointers - -`addrOf()` requires a `Var` or `Member` argument and returns `place(arg).addr` with type -`ptr[T]`. No load happens: `addr(x)` *is* x's alloca. The `alloca` is therefore not promotable -by mem2reg, which is the expected cost of taking an address. `addr(p.value)` yields the pointer -already stored in `p`, after a null check. `addr` of a `for` variable is rejected (it is read-only). - -### 8.10 Calls: `callFinch()` vs `callC()` - -Both check the argument count and `coerce()` each argument to the parameter type. The differences: - -| | `callFinch()` | `callC()` | -|---|---|---| -| Callee | `finch.`, created in `declare()` | `getOrInsertFunction(name, …)`: declared lazily on first use, reused afterwards | -| Variadic | never | supported; extra args get C default promotions | -| Parameter attributes | none needed | `signext`/`zeroext` on params and return values narrower than 32 bits, on both the declaration and the call site | -| Unsupported signatures | impossible | rejected with the reason saved by cimport (`passes a C struct by value`, …) | -| Calling `main` | rejected | n/a | - -Default argument promotions for the variadic part: `f32` → `double`; `bool`, `char` and integers -narrower than 32 bits → `i32` (sign- or zero-extended). Pointers, `str` and 32/64-bit integers -pass unchanged. This mirrors C's rules, which is what `printf`'s `va_arg` expects. - -Why `signext`/`zeroext`: on x86-64 System V, clang passes `char`/`short`/`_Bool` already extended to -32 bits and marks them so. Some callees rely on it. Without the attribute LLVM would leave the upper -bits undefined, which would be an ABI mismatch with C code compiled by clang. +Functions take and return these **by pointer**, so their C ABI is trivial (no struct classification): +string building (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`, `replace`, +`split`, `join`, `repeat`, `from_bytes`), comparison (`eq`, `cmp`, `find`, `starts`, `ends`), ownership +(`copy`, `own`, `drop`, `from_c`), arrays (`reserve`, `make`, `resize`, `clone_raw`, `free`, +`insert_gap`, `remove_gap`), `args`, `input`, files, `shell`, and the panics. Allocation failure prints +`out of memory` and exits. --- -## 9. Runtime checks +## 13. C interop: headers, structs, ABI -Finch removes the two most common sources of C undefined behavior that can be checked cheaply. -All checks share one helper function per module: +### 13.1 Importing headers -```llvm -; Function Attrs: cold noinline noreturn -define internal void @finch.panic(ptr %0, i64 %1) #0 { -entry: - %2 = call i32 (i32, ptr, ...) @dprintf(i32 2, ptr @panic.fmt, ptr @panic.file, i64 %1, ptr %0) - call void @exit(i32 1) - unreachable -} -``` +`importHeaders(imports, dirs)` parses each `import "x.h"` as its own translation unit from an in-memory +`#include "x.h"`, with `-x c -std=gnu11`, `-I` for every `.fn` file's folder, and `-isystem` for each +directory the system C compiler searches (read from `$CC -E -v -x c /dev/null`, which is what makes it work +on NixOS). `gnu11` because glibc hides `M_PI` and friends in strict ISO mode. -`panicFn()` creates it on first use. `panicIf(cond, msg, pos)` emits -`br cond, %panic, %ok`, a call to `finch.panic(msg, line)` and `unreachable` in `%panic`, and -continues in `%ok`. It writes to fd 2 with `dprintf`, so it does not depend on libc's `stderr` -symbol. The function is `cold` and `noinline`, so branch layout favours the non-failing path and the -check costs one compare and a predicted branch. +The `Importer` visits top-level cursors: -### Integer division and remainder: `checkDivisor()` - -```llvm -define internal i64 @finch.div(i64 %a, i64 %b) { - ... - %0 = icmp eq i64 %b4, 0 - br i1 %0, label %panic, label %ok -panic: - call void @finch.panic(ptr @panic.msg, i64 2) - unreachable -ok: - %1 = icmp eq i64 %b4, -1 - %2 = icmp eq i64 %a3, -9223372036854775808 - %3 = and i1 %2, %1 - br i1 %3, label %panic5, label %ok6 -panic5: - call void @finch.panic(ptr @panic.msg.1, i64 2) - unreachable -ok6: - %4 = sdiv i64 %a3, %b4 -``` - -- A divisor that is a **constant zero** is a **compile-time** error. -- A constant non-zero divisor (other than −1 for signed types) emits no check at all. -- For signed division, `MIN / -1` overflows (it traps with SIGFPE on x86). That is checked too. -- When everything is known, O2 removes the checks entirely: `div(10, 2)` above becomes `5` in `main`. - -### Null `.value`: in `place()` - -Every `p.value` whose pointer is not trivially non-null (not directly an `alloca` or a global) -gets `panicIf(icmp eq p, null, "used .value on a null pointer")`. Without this, LLVM treats a load -from null as UB and may **delete it**. Before this check existed, `print(p.value)` on a null `p` -printed a garbage value at O2 instead of crashing. - ---- - -## 10. C interop via libclang - -`importHeaders(imports, dir)` (`src/cimport.cpp`) runs **before** codegen and produces: - -```cpp -struct CImports { - std::unordered_map fns; // name, header, ret, params, variadic, unsupported reason - std::unordered_map consts; // enum values + numeric #defines - std::unordered_map globals; // extern variables -}; -``` - -### Parsing - -For **each** `import "h"`, a separate translation unit is parsed from an in-memory file -`finch_import.c` containing `#include "h"`. Separate units give precise error attribution: a missing -header points at its own `import` line. Flags: - -``` --x c -std=gnu11 -I -isystem -CXTranslationUnit_DetailedPreprocessingRecord (to see macros) -CXTranslationUnit_SkipFunctionBodies (speed: inline bodies are irrelevant) -``` - -- **System include directories** come from running `$CC -E -v -x c /dev/null` and parsing the block - between `#include <...> search starts here:` and `End of search list.`. This is what makes - `stdio.h` resolve the same way it does for the system compiler. That matters on NixOS, where the - cc wrapper injects store paths that an unwrapped libclang knows nothing about. -- **`gnu11`** rather than `c11`: glibc hides `M_PI` and other non-ISO names under strict modes. -- **Diagnostics** of severity ≥ error abort the import. `file not found` becomes - `can't find the C header 'h'`. - -### The visitor - -`visit()` walks top-level cursors. The first declaration of a name wins. - -| Cursor | Action | +| Cursor | Result | |---|---| -| `FunctionDecl` | Map the canonical **function type** (`clang_getArgType`, so array parameters are already decayed to pointers). `static` functions are recorded as unsupported (`static inline` has no symbol to link). `FunctionNoProto` (K&R `f()`) is unsupported. Variadic-ness comes from `clang_isFunctionTypeVariadic`. | -| `VarDecl` with `extern` storage | → `CGlobal` if the type maps | -| `EnumDecl` | recurse into children | -| `EnumConstantDecl` | → `CConst` (`i64`, literal) | -| `MacroDefinition` | skip built-ins and function-like macros; `macroValue()` tries to read a number | +| `FunctionDecl` | `CFunc` (signature mapped in **signature context**; `static` and K&R functions recorded as unsupported) | +| `TypedefDecl` of a record | names the record (`typedef struct {…} CXCursor` → `CXCursor`) | +| `StructDecl` (definition) | names the record by its tag | +| `VarDecl` `extern` | `CGlobal` (mapped in **data context**) | +| `EnumConstantDecl`, numeric `MacroDefinition` | `CConst` (literal) | -### Type mapping: `mapType()` +`mapType()` works on canonical types and maps integers by size. The context decides `char *`: a `str` +in function signatures (Finch converts at the call), but `ptr[char]` inside structs, globals and behind +pointers (where the memory layout must stay a single pointer). Records by value become +`Named{name, "C"}` and are defined with `defineRecord()`: every field (`clang_Type_visitFields`) with its +byte offset, size and alignment, nested records, constant arrays (`Fixed`). Unions and bit fields are kept +as hidden bytes and mark the struct unsupported for by-value calls; pointers to anything else (void, +functions, incomplete structs) become `ptr`. -Works on the **canonical** type, so typedefs (`size_t`, `uint32_t`, `GLuint`, `FILE`) are resolved. -Integer types map **by size** (`clang_Type_getSizeOf`), not by name, so `long` becomes `i64` on LP64. +### 13.2 Laying out C structs -| C (canonical) | Finch | +`resolveStruct()` builds a **packed** LLVM struct that mirrors C byte for byte: an `[n x i8]` pad before +each field whose offset is ahead of the current position, the field itself (only if its LLVM size matches +C's), and tail padding up to C's `sizeof`. Field accesses use the recorded `llvmIndex`. Packing makes the +layout exact regardless of LLVM's alignment rules; allocas and temporaries get the C alignment explicitly. + +### 13.3 The System V x86-64 calling convention + +`classify(T)` (in `abi.cpp`) flattens a struct into scalar leaves with byte offsets (through nested +structs and fixed arrays) and: + +- **MEMORY** if it is larger than 16 bytes or has a misaligned leaf: passed as a pointer with + `byval(T) align ≥8`, returned through a hidden first `sret(T)` pointer. +- otherwise each eightbyte is **INTEGER** if any leaf in it is an integer/pointer, else **SSE**. Parts: + INTEGER → `iN` for the bytes that are left (`i32` for a 4-byte `Color`), SSE → `double`, `<2 x float>` + or `float`. One part is passed as is; two parts as two arguments, or returned as `{a, b}`. + +`makePlan()` walks the parameters counting the 6 integer and 8 SSE argument registers; a struct whose +parts don't fit any more goes to the stack (MEMORY) as a whole, like clang does. Small integer +parameters get `signext`/`zeroext`. `emitCCall()` stores each struct argument into a slot big enough for +both the struct and its register form, loads the parts at offsets 0 and 8, calls with the planned function +type and attributes, and rebuilds struct results the same way. + +`cThunk(fn)` is the reverse for `addr(fn)`: an internal function with the C signature that reassembles +struct parameters from registers or `byval` memory, calls the Finch function and lowers its result. Only +C-compatible signatures are accepted. + +`tests/run/c_structs.fn` exercises `{float,float}`, `{float,float,float}`, `{u8×4}`, `{double,int}`, +`{i64,i64}`, a 24-byte struct, a struct with a `char[8]`, nested structs, register exhaustion with 13 +struct arguments, and callbacks taking and returning structs, against a C library compiled with +`link "abi.c"`. The results match the same calls made from C. + +### 13.4 Linking C code + +`link "name"` tries `pkg-config --libs name`, then `-lname`. `link "file.c"` compiles the file (relative +to the `.fn` file) with `$CC -O2 -fPIC -c` into a temporary object; `.o` and `.a` files are passed +through. When linking fails, `linkFailed()` maps undefined symbols back to the header that declared them +and prints the `link` line to add. + +--- + +## 14. Optimization, emission, linking + +- **Target:** `sys::getDefaultTargetTriple()`, CPU `generic`, `Reloc::PIC_`. The data layout is set on the + module before any IR is built (alignment of loads/stores depends on it). +- **Optimization:** `PassBuilder::buildPerModuleDefaultPipeline(O2)`: clang's `-O2` middle end. `-O0` + skips it (`finch ir file.fn -O0` shows `Codegen`'s raw output). +- **Emission:** legacy `PassManager` + `addPassesToEmitFile(ObjectFile)`. +- **Linking:** `$CC prog.o rt.o [user objects] -lm [libs]`. On Nix the cc wrapper turns `-L` paths into + `RPATH`, so programs run without `LD_LIBRARY_PATH`. +- **`run`:** a temporary executable, run with the remaining command-line arguments, deleted afterwards; + a signal is reported as `the program crashed: ` with exit status `128 + n`. + +Performance notes: user functions are `internal`, so LLVM inlines and specializes them freely +(recursive `fib(40)`: ≈0.19 s vs. ≈0.23 s for `clang -O2` on the same C on the author's machine); +bounds-checked array code matches C on a sieve. + +--- + +## 15. Debug info + +With `-g`, `Codegen` creates a `DIBuilder`, a compile unit (`DW_LANG_C`, so gdb's C expression syntax +works for `print p.x`), a `DIFile` per source file, a `DISubprogram` per function, parameter variables +(`createParameterVariable`, so `bt` shows `length (v=…)`) and local variables (`insertDeclare` on their +allocas), and a `DILocation` per statement and call (`setLoc`). Types: basic types with Finch names +(`int`, `u8`, `float`), `str`/arrays as `{ptr, len, cap}` structs, pointers, fixed arrays, and structs with +member offsets from the LLVM `StructLayout`. Recursive structs use a replaceable forward declaration. +Generated helpers carry no locations. Use `-g -O0` for the best experience; with O2 some variables are optimized out. + +--- + +## 16. Diagnostics + +`failAt(file, line, col, msg)` prints `path:line:col: error: msg` and the source line with a caret +(tabs preserved, columns counted in characters), then exits with 1. Conventions: binary operators report +the operator, calls the callee, argument errors the argument. Messages say what is wrong and, when +there's an obvious fix, how to fix it. There are no warnings. An LLVM verifier failure prints +`internal compiler error (please report)` and exits with 2. + +--- + +## 17. The self-hosted compiler (`boot/`) + +`boot/` is a second Finch compiler written in Finch (≈3,100 lines): + +| File | Contents | |---|---| -| `void` | nothing | -| `_Bool` | `bool` | -| `char` (either signedness) | `char` | -| `signed char`, `short`, `int`, `long`, `long long`, `enum` | `i8`/`i16`/`i32`/`i64` by size | -| unsigned variants | `u8`…`u64` by size | -| `float` / `double` | `f32` / `f64` | -| `char *` (any qualifiers) | `str` | -| `T *` where T maps to a non-void type | `ptr[T]` | -| `void *`, pointer to struct/union/function/unmappable | `ptr` (opaque) | -| struct/union by value | **unsupported** ("passes a C struct by value") | -| `long double`, `__int128`, vectors, … | **unsupported** | +| `boot/lexer.fn` | the lexer (tokens are `{kind, text, line, col, nl}`; kinds are words, keywords and operators are their own kind) | +| `boot/ast.fn` | `Type` (`kind`, `elem` as a 0/1-element array, `name`, `module`), one generic `Node` for everything, `Program` | +| `boot/parser.fn` | the same grammar and newline rules as the C++ parser, precedence climbing for binary operators | +| `boot/gen.fn` | type checker + **textual LLVM IR** generator: scopes, places, coercions, copies, helpers, printing, runtime checks, array/str methods | +| `boot/main.fn` | loader for modules and the driver: writes `.ll`, runs `clang -O2 file.ll runtime/finch_rt.c` | -Unsupported functions stay in the table, so calling one produces a precise error instead of -"no such function". +It supports the core language: `int float bool char str`, `[]T`, structs (defaults, named/positional +constructors), `ptr[T]`/`addr`/`new`/`free`/`null`, all statements except `defer`, modules, and the +built-ins and methods the C++ compiler has, minus C imports, sized integers and `defer`. +It implements the same value semantics and borrow analysis (copies where the main compiler copies) but +**never frees**: as a bootstrap compiler it leaves memory to the operating system. -### Macros: `macroValue()` +The compiler state is one `Gen` struct passed as `ptr[Gen]`: Finch has no globals, and passing a +pointer is how a function changes its caller's value. Without `defer` or a map type, scopes are arrays of +arrays searched linearly; helper functions (copy/print/compare per type) are generated by temporarily +swapping the output buffers (`beginHelper`/`endHelper`). -It tokenizes the macro's extent, drops the name token and comments, strips **balanced outer -parentheses**, accepts one leading `-`, and then requires **exactly one** numeric literal. Integers -are parsed with `strtoull(…, 0)`, i.e. **C rules**: `0x` hex and leading-zero **octal**, unlike -Finch's own literals. Floats are anything containing `.`, `e` or `E` that is not hex. Trailing -`u U l L f F` suffixes are accepted. That covers `M_PI`, `EOF` (`(-1)`), `RAND_MAX`, `INT_MAX`, -`GL_*` and `GLFW_*` constants. Expressions (`(1 << 4)`), references to other macros, and strings -are skipped. +**The fixpoint.** `tests/boot.sh`: -### Calling +1. `build/finch` (C++) compiles `boot/` → **stage 1**, +2. stage 1 compiles `boot/` → **stage 2** (and its IR), +3. stage 2 compiles `boot/` → **stage 3** IR, -Imported functions are declared in the module **only when called**, through -`getOrInsertFunction`. This reuses an existing declaration if `print` already created `printf`. -Since the signatures agree (`i32 (ptr, ...)`), there is no conflict. - -### Link errors - -When the link step fails, `linkFailed()` in `main.cpp` scans the linker's output for -`undefined reference to \`sym'` (GNU ld) and `undefined symbol: sym` (lld). It groups the symbols by -`CFunc::header` and prints which header they came from, with a guessed `link` line (`guessLib()`: -basename, minus extension and trailing digits, lowercase: `GLFW/glfw3.h` → `glfw`). -`cannot find -lfoo` becomes `the library 'foo' wasn't found`. +and requires the stage 2 and stage 3 IR to be **byte-identical** (≈32,000 lines). It then compiles every +test in `tests/run` that stays within the subset with the self-hosted compiler and compares the output +with the expected one. It takes about 2.3 s for the self-hosted compiler to compile itself. --- -## 11. Optimization, emission, linking +## 18. Build system -### Target - -`hostMachine()` creates a `TargetMachine` for `sys::getDefaultTargetTriple()` with CPU -`"generic"`, no extra features, and `Reloc::PIC_`, which is required for the default PIE -executables on modern distributions. Its **data layout is installed on the module before any IR -is built**, in `Codegen`'s constructor. That matters: `IRBuilder` takes the alignment of loads and -stores from the data layout, and without it `i64` accesses would be emitted with `align 4`. - -`generic` means the binary runs on any x86-64. Adding `-march=native` behavior would be a CPU -string and feature change here. - -### Optimization - -`optimize()` builds the new pass manager pipeline: - -```cpp -PassBuilder pb(tm); -pb.register{Module,CGSCC,Function,Loop}Analyses(...); -pb.crossRegisterProxies(lam, fam, cgam, mam); -pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam); -``` - -That is clang's `-O2` middle end: SROA/mem2reg, instcombine, inlining, GVN, LICM, loop -unrolling, vectorization and so on. `-O0` skips this call, which is useful to read the IR that -`Codegen` produced (`finch ir file.fn -O0`). Back-end codegen still runs at the TargetMachine's -default level. - -### Emission - -`emitObject()` uses the **legacy** pass manager, because LLVM's machine-code pipeline is still only -reachable through it: `tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)`. - -### Linking - -``` -$CC file.o -o file -lm [pkg-config --libs NAME | -lNAME]... -``` - -The system C compiler driver (`cc` by default) is used as the linker. It knows the platform's crt -files, the dynamic loader path, libc and default library paths, which would be a lot to replicate. -`-lm` is always added. For each `link "NAME"` or `-l NAME`, `pkg-config --libs NAME` is tried -first, so `-L` paths and dependent libraries come along. On failure it falls back to `-lNAME`. -On Nix, the cc wrapper turns those `-L` paths into `RPATH` entries, so the result runs without -`LD_LIBRARY_PATH`. The object file is deleted afterwards whether linking succeeded or not. - -### `finch run` - -It compiles to a temporary file (`sys::fs::createTemporaryFile`), runs it with `system()`, deletes -it, and forwards the exit status. If the child died from a signal, it prints -`the program crashed: ` and returns `128 + signo`, like a shell. +- `find_package(LLVM CONFIG)`; **if the shared `LLVM` target exists Finch links that single library.** + `libclang.so` links `libLLVM.so` itself; linking the static component libraries as well put two copies of + LLVM in one process, and the duplicated global objects crashed at exit (a double free in a global + destructor). It also made the binary 82 MB instead of ~20 MB. +- libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`. +- `runtime/finch_rt.c` is read at configure time into `build/rt_source.inc` as a raw string literal; + `CMAKE_CONFIGURE_DEPENDS` re-runs the configure step when it changes. +- `FINCH_VERSION` from `project(VERSION 2.0.0)`. C++17, `-Wall -Wextra`, warning-free. +- `shell.nix` lists `llvmPackages.clang` before `llvmPackages.libclang`: the latter also ships an + unwrapped `clang` that can't find the system headers. --- -## 12. Diagnostics +## 19. Testing -`fail(line, col, msg)` is `[[noreturn]]`: it prints - -``` -file:line:col: error: msg - line | source text - | ^ -``` - -and exits with status 1. The excerpt is taken from `g_source`, and tabs in the prefix are kept so -the caret lines up. Conventions used across the code base: - -- **Positions:** binary operators report the **operator's** position; calls report the callee's; - type errors in arguments report the **argument's** position. -- **Messages** say what is wrong in plain words, and **how to fix it** when there is an obvious fix - (`use int(...) to convert`, `did you mean ':='?`, `add import "math.h"`). -- `headerHint()` maps about two dozen common libc/libm names to their header, for calls to unknown functions. -- An internal inconsistency, if `verifyModule` ever fails, prints `internal compiler error (please report)` - with the verifier's output and exits with status 2. - -The compiler has no warnings. Everything that would be a warning is either fine or an error. +- `tests/run.sh`: every `tests/run/*.fn` with `fn main` must print exactly its `.out` (stdin from `.in` + if present); files without `main` are modules or helpers. Every `tests/fail/*.fn` must fail with the text + from its `// expect:` line. Currently **58 passed, 0 failed**. +- `MEMCHECK=1 tests/run.sh`: the same, plus valgrind on every program (no leaks, no invalid access). +- `tests/boot.sh`: the self-hosting fixpoint and the subset run (§17). --- -## 13. Build system +## 20. Semantics vs. C -`CMakeLists.txt`: - -- `find_package(LLVM CONFIG)`. **If the `LLVM` target exists (a shared `libLLVM.so`), Finch links that - one library.** This is essential, not cosmetic: `libclang.so` itself links `libLLVM.so`. Linking - Finch against the static component libraries (`libLLVMCore.a`, …) puts a **second copy of LLVM** in - the process. Both copies register the same global objects, and the program crashed on exit in a - global destructor (`std::vector::~vector`, a double free). It also made the binary 82 MB - instead of 11 MB. -- libclang is found with `find_path(clang-c/Index.h)` and `find_library(clang)`, hinted by LLVM's dirs. -- `FINCH_VERSION` comes from `project(VERSION …)` and is printed by `finch version`, along with `LLVM_VERSION_STRING`. -- C++17, `-Wall -Wextra`, and the build is warning-free. - ---- - -## 14. Testing - -`tests/run.sh` (`FINCH=… ` overrides the compiler path): - -- `tests/run/*.fn`: run with `finch run`. Combined stdout+stderr must equal `*.out` exactly. -- `tests/fail/*.fn`: must exit non-zero, and the output must contain the text after `// expect: ` on line 1. - This covers compile errors, runtime panics (with line numbers), header and link failures. - `tests/fail/lib_hint.h` declares a function that exists in no library, to test the link-hint path. - -Current state: **34 passed, 0 failed.** Every new error message should get a `fail/` test, and every -feature a `run/` test. - ---- - -## 15. Semantics vs. C: defined and undefined behavior - -| Situation | C | Finch 1.0 | +| Situation | C | Finch 2.0 | |---|---|---| -| Signed integer overflow | UB | **Defined wraparound** (no `nsw` flags) | -| Unsigned overflow | wraps | wraps | -| Integer division by zero | UB | **Compile error** if constant, otherwise **runtime error** | -| `INT_MIN / -1` | UB | **Runtime error** | -| Load or store through null | UB | **Runtime error** for `.value` | -| `x & 1 == 0` | parses as `x & (1 == 0)` | parses as `(x & 1) == 0` | -| Implicit narrowing (`int` → `char`) | silent | **Compile error** | -| Signed/unsigned mixing | silent conversion | **Compile error** unless one side widens | -| Using a variable before it is set | UB | Impossible: every declaration initializes (zero by default) | -| Missing `return` in non-void function | UB if the value is used | **Compile error** | -| Shift by ≥ bit width | UB | **Still poison** (not checked in 1.0) | -| `float` → int out of range | UB | **Still poison** (not checked in 1.0) | -| Dangling pointer to a dead local | UB | **Still UB** (no lifetime checks yet) | -| Order of evaluation | mostly unspecified | Left to right | +| Signed integer overflow | UB | defined wraparound (no `nsw`) | +| Integer division by zero, `INT_MIN / -1` | UB | compile error if constant, otherwise runtime error | +| Array index out of range | UB | runtime error (also for `[N]T` fields of C structs) | +| Null dereference | UB | runtime error for `.value`, `.field`, `[i]` | +| Use after free / double free of arrays, strings, structs | common bugs | impossible: no manual freeing of those | +| Memory leak of arrays, strings, structs | common bug | impossible without `new` | +| `x & 1 == 0` | `x & (1 == 0)` | `(x & 1) == 0` | +| Implicit narrowing, signed/unsigned mixing | silent | compile errors | +| Uninitialized variables | UB | impossible (zero or defaults) | +| Missing return | UB if used | compile error | +| Order of evaluation | mostly unspecified | left to right; in an assignment the right side first | +| Shift ≥ width, out-of-range float→int | UB | still poison (not checked yet) | +| Dangling pointer from `addr()` / `new` + `free` | UB | still UB | --- -## 16. Function reference: who does what - -Groups of functions that sound alike, and how they differ. - -### Lexing and parsing - -| Function | Does | Differs from | -|---|---|---| -| `lex()` | text → tokens, sets `newlineBefore` | knows nothing about grammar or types | -| `tokName()` | token kind → human name for errors | | -| `Parser::cur()` / `peekTok()` / `next()` | current token / lookahead without consuming / consume | | -| `accept(k)` vs `expect(k)` | consume if it matches and return a bool / consume or **fail** with "expected …, found …" | | -| `unexpected(ctx)` | fail with "unexpected X (ctx)" at the current token | `expect` names the token it wanted; `unexpected` explains the context | -| `sameLine()` | may the current token continue the expression? | `endOfStatement()` is the statement-level counterpart (requires a newline or `}`) | -| `atDeclaration()` | `int x` / `ptr[...] p` lookahead | `type()` actually consumes a type | -| `typeFromName()` vs `isTypeName()` | name → `Type` / name → bool | | -| `statement()` vs `block()` | one statement / `{ statement* }` with terminator checks and `parenDepth` reset | | -| `callArgs()` | shared by calls and conversions: `( expr, … )` | | -| `primary()` / `postfix()` / `unary()` / `multiplicative()` / `additive()` / `compare()` / `andExpr()` / `orExpr()` | one function per precedence level | | - -### Codegen: types and values - -| Function | Does | Differs from | -|---|---|---| -| `ty(FType)` | Finch type → LLVM type | | -| `zero(FType)` | default value for an uninitialized declaration | | -| `widens(from, to)` | can every value of `from` be represented in `to`? (pure type rule) | `fits()` asks the same about one constant | -| `fits(c, from, to)` | does this literal's value fit in `to`? | | -| `coerce(v, want)` | **implicit** conversion into a known slot, or error | `convert()` is **explicit** `T(x)` and allows lossy casts; `unify()` converts **two** operands toward each other | -| `unify(l, r)` | bring binary operands to a common type | used by `arithOp()` and `compare()` | -| `intCast()` / `intToFloat()` | emit the signedness-correct cast | low-level helpers used by the three above | - -### Codegen: expressions - -| Function | Does | Differs from | -|---|---|---| -| `expr(e)` | r-value: emits code, returns `{value, type, literal}` | `place()` returns an **address** instead of loading | -| `place(e)` | l-value address of a variable, C global or `p.value` | used by `expr()` (then loads), `assign()` (then stores), `addrOf()` (returns as is) | -| `binary()` | dispatch by `BinOp` | | -| `arith()` vs `arithOp()` | `arith()` wraps `arithOp()` and computes the result's `literal` flag | `arithOp()` does `unify` and emits the instruction | -| `compare()` | all comparisons, pointer/str/float/int variants | always returns `bool` and is never a literal | -| `logic()` | `&&`/`\|\|` with branches and a φ | the only place that builds a φ by hand | -| `call()` | resolves a call name to one of the five kinds | | -| `callFinch()` vs `callC()` | see section 8.10 | | -| `print()` | builds a printf format from the types | | -| `addrOf()` | `addr(x)` → `place(x).addr` | | -| `convert()` | `T(x)` | | -| `libc()` | `getOrInsertFunction`: declare-or-reuse an external function | `declare()` creates **Finch** functions with `Function::Create` | -| `cGlobal()` | declare-or-reuse an `extern` global | | - -### Codegen: statements and control - -| Function | Does | Differs from | -|---|---|---| -| `declare()` vs `define()` | signature pass / body pass | | -| `block()` vs `blockBody()` | `block()` pushes and pops a scope around `blockBody()`; `blockBody()` emits statements and rejects unreachable ones | `define()` calls `blockBody()` directly, because parameters and the outer body share one scope | -| `stmt()` | dispatch by `StmtKind`; also handles `break`/`continue` | | -| `varDecl()` / `assign()` | declaration with optional inference / store to an existing place | | -| `addVar()` | create slot, store initial value, register name (with no-shadowing check) | `slot()` only creates the `alloca` | -| `lookup()` | search all scopes, innermost first | | -| `condition(e)` | `coerce(expr(e), bool)` with the message "a condition must be bool" | | -| `ifStmt()` / `whileStmt()` / `forStmt()` / `returnStmt()` | section 8.5 | | -| `terminated()` | does the current block already end in a terminator? | | -| `continueAt(bb, deadEnd)` | move to the merge block, or delete it if unreachable | | -| `newBlock()` | create a basic block in the current function | | - -### Runtime checks - -| Function | Does | -|---|---| -| `panicFn()` | get or create `finch.panic` | -| `panicIf(cond, msg, pos)` | conditional branch to a panic block | -| `checkDivisor(l, r, pos)` | zero and `MIN/-1` checks, or a compile-time error | +## 21. Function reference ### Driver (`main.cpp`) | Function | Does | |---|---| -| `hostMachine()` | create the `TargetMachine` | -| `optimize()` | O2 pipeline | -| `emitObject()` | write the `.o` | -| `capture()` | run a shell command, collect its output and status | -| `libFlags()` | pkg-config or `-l` for one library | -| `link()` | run the linker, delete the `.o`, explain failures | -| `linkFailed()` / `guessLib()` | translate linker errors into Finch advice | +| `Loader::load` / `findModule` | read, lex, parse a file and recursively its modules / locate `name.fn` | +| `hostMachine`, `optimize`, `emitObject` | target machine, O2 pipeline, object file | +| `runtimeObject` | compile and cache the runtime | +| `capture`, `libFlags`, `link` | run a command / pkg-config or `-l` / the link step, compiling `link "x.c"` files | +| `linkFailed`, `guessLib` | turn linker output into advice | + +### Lexer and parser + +| Function | Does | Differs from | +|---|---|---| +| `lex` / `tokName` | text → tokens / names for messages | | +| `accept` / `expect` / `unexpected` | consume if it matches / consume or fail / fail with context | | +| `sameLine` / `endOfStatement` | may this token continue the expression? / did the statement end? | | +| `atDeclaration` / `type` | lookahead for a declaration / parse a type | | +| `args` | `( [name:] expr, … )` for calls, methods and constructors | | +| `postfix` | `.field`, `.method(…)`, `[i]` chains | `primary` parses the start of a chain | + +### Codegen: program and statements (`codegen.cpp`) + +| Function | Does | Differs from | +|---|---|---| +| `declareStructs` / `declareFns` / `declare` | all types / all signatures / one signature + borrow analysis | | +| `define` / `defineMainWrapper` | one body / the C entry point | | +| `pushScope` / `popScope` | open / close a scope (`popScope` emits its cleanups if reachable) | | +| `emitCleanups(downTo)` / `emitScopeCleanups(i)` | cleanups of several scopes without popping (return/break) / of one scope | | +| `slot` / `tmpOf` / `tmp` | a variable's alloca / an anonymous one / an anonymous one holding a value | | +| `lookup` / `addVar` | find a visible variable (respects `deferLimit`) / declare one | | +| `stmt`, `varDecl`, `assign`, `ifStmt`, `whileStmt`, `forStmt`, `forEachStmt`, `returnStmt`, `jump` | statements | | +| `rootVar` / `mutates` / `mutatesExpr` | the variable an l-value starts from / does a statement or expression change it? | | + +### Codegen: expressions + +| Function | Does | Differs from | +|---|---|---| +| `expr` / `exprWant` | an r-value / the same, with an expected type for array literals | | +| `ref` / `place` | an address if any, else a value / an address or an error | `expr` loads from `ref` | +| `member` / `index` | `.x`, `.len`, `.ptr`, `.value`, auto-deref / `[i]` with bounds checks and copy-on-write | | +| `arrayLit` | `[…]` → heap array | | +| `unary`, `binary`, `arith`/`arithOp`, `compare`, `logic` | operators (`arith` adds the literal flag) | | +| `unify` / `coerce` / `convert` | two operands toward each other / one value into a slot / explicit `T(x)` | | + +### Codegen: types and ownership (`codegen_types.cpp`) + +| Function | Does | +|---|---| +| `ty`, `resolve`/`resolveT`, `findStruct`, `resolveStruct` | LLVM type; Named → Struct (deep or shallow); struct lookup; layout + cycle check | +| `owning`, `zero`, `defaultValue` | does a type own memory; zero constant; zero or the struct's defaults | +| `own` / `release` | something storable (move or copy) / drop an unkept fresh value | +| `copyValue`, `dropValue`, `copyAt`, `dropAt`, `copyFn`, `dropFn`, `typeKey` | copying and dropping by value, by address, and the per-type helpers | +| `forN` | an internal `for i in 0..n` loop used by helpers | +| `strConst`, `cstr`, `strFromC`, `elemSize` | literal str constant; str → `char*`; `char*` → borrowed str; element size | +| `emitPrint`, `printFn`, `printf_` | printing | +| `diType`, `setLoc` | debug info | + +### Codegen: calls (`codegen_builtins.cpp`) and ABI (`abi.cpp`) + +| Function | Does | +|---|---| +| `call` / `method` | resolve a call / a method or module-qualified call | +| `callFinch` / `construct` / `callC` | Finch function / struct constructor / C function | +| `print`, `addrOf`, `convert`, `arrayMethod` | built-ins | +| `rt` / `libc` | declare a runtime / libc function | +| `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | runtime checks | +| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | System V classification, register plan, declaration, call, callback thunk | --- -## 17. Extending the compiler +## 22. Extending the compiler -### A new built-in function (example: `len(str) -> int`) +**A built-in function:** add the name to `isBuiltin()`, handle it in `call()`, implement it (in IR, or as +a runtime function declared in `rt()`'s table), add `tests/run/` and `tests/fail/` cases. If it should +exist in the self-hosted compiler too, add it to `boot/gen.fn`'s `call()` and `runtimeDecls()`. -1. Add the name to `Codegen::isBuiltin()`, so it cannot be redefined. -2. In `Codegen::call()`, dispatch `if (c.callee == "len") return len(c);`. -3. Implement it: check the argument count, `coerce(expr(arg), Str, …)`, emit `strlen` via `libc()`, - return `{call, I64}`. `strlen` returns `u64`; convert with `intCast` if `int` is wanted. -4. Add `tests/run/len.fn` and its `.out`, plus a `tests/fail/` case for a wrong argument. +**An array or str method:** add a branch in `arrayMethod()` or the str part of `method()`; if it changes +the array, add its name to the `changing` sets in `method()` and `mutatesExpr()`, so borrowing stays correct. -### A new binary operator +**An operator:** lexer token + `tokName`, `BinOp`, the right precedence function in the parser, `opName`, +emission in `arithOp`/`compare`, an explanation in `badOperands`. -1. Lexer: add a `Tok`, its spelling in the operator `switch`, and `tokName()`. -2. AST: add a `BinOp`. -3. Parser: put it in the right precedence function. -4. Codegen: `opName()`, then emit it in `arithOp()` or `compare()`, and explain bad operands in `badOperands()`. +**A statement:** `StmtKind` + node, `Parser::statement`, `Codegen::stmt`; if it transfers control, respect +`terminated()`, use `continueAt()` for merge blocks, and emit cleanups (`emitCleanups`) before jumping out of scopes. -### A new statement - -Add a `StmtKind` and node in `ast.h`, recognise it in `Parser::statement()`, and handle it in -`Codegen::stmt()`. If it can transfer control, make sure the result respects `terminated()` and -uses `continueAt()` for its merge block. - -### Coding conventions - -Match the existing style: early `fail()` with a fixing hint, comments only where the *why* is not -obvious, and one test per behavior. +**A new owning type:** extend `owning()`, `dropAt`/`copyAt` (and their helpers), `emitPrint`, `diType`. --- -## 18. Known limitations and roadmap +## 23. Known limitations -- **No aggregates yet:** `struct`, arrays/slices with `.len`, and C structs by value. These are the next - milestones, in that order: arrays build on structs (pointer + length), and the memory model builds on both. -- **Memory model** (agreed design): values that own heap memory are freed automatically at the end of - the block that owns them, `defer` runs code at block exit, and `free` remains for manual control. - Implementation sketch: a per-block cleanup list, emitted at every exit edge (normal end, `return`, - `break`, `continue`). `blockBody()` and the `loops` stack already know every exit edge. -- **Strings:** concatenation, `len`, `str(x)`. These depend on the memory model. -- **Input:** `input()`. -- **Modules:** `import name` for Finch code. One `.fn` = one program today. -- **Unchecked UB:** shift amounts, float→int range, dangling pointers. Shift and float→int could be - checked like division at small cost. -- **Debug info:** no DWARF yet. `DIBuilder` would hook into `define()` and each statement's `pos`. -- **Targets:** only the host triple, with Linux/x86-64 tested. The code is target-neutral except for - the `cc -E -v` include discovery and `dprintf`. -- **Bootstrapping:** rewriting the compiler in Finch is the long-term goal. Calling the LLVM-C API - already works today: LLVM-C uses opaque handles (`LLVMModuleRef` and so on), which map to Finch's - `ptr`. `examples/llvm.fn` builds and prints a module from Finch (`import "llvm-c/Core.h"`, - `link "LLVM"`). What is still missing for a self-hosted compiler is structs, arrays, strings and file I/O. +- No methods on structs, generics, maps, `match`, closures, error values (`int("x")`, `read_file` panic). +- Strings are bytes: `.len`, `s[i]`, `upper()` are not Unicode-aware. +- Dangling pointers (`addr` of a local that went away, use after `free`) are not detected. +- Shift amounts and float→int conversions are not range-checked (LLVM poison, as in C). +- C: unions and bit-field structs by value, function-like macros, `long double`. +- One target tested: Linux x86-64 (the ABI code is System V x86-64 specific). +- The self-hosted compiler has no C imports, sized integers or `defer`, and doesn't free memory. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index 12eaaeb..282b752 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -1,9 +1,9 @@ # Finch for Technicians **For people who know their way around a computer, but little or no programming.** -This guide covers installing Finch, using the `finch` tool, the whole language, using -C libraries, and fixing common problems. It does not cover how the compiler works inside. -For that, see the [guide for engineers](for-engineers.md). +This guide covers installing Finch, using the `finch` tool, the whole language, memory, +modules, C libraries, debugging and fixing common problems. It does not cover how the +compiler works inside. For that, see the [guide for engineers](for-engineers.md). > 🇵🇱 Polska wersja: [dla-technikow.md](../pl/dla-technikow.md) @@ -20,12 +20,19 @@ For that, see the [guide for engineers](for-engineers.md). 7. [Operators](#7-operators) 8. [Control flow](#8-control-flow) 9. [Functions](#9-functions) -10. [Pointers](#10-pointers) -11. [Using C libraries](#11-using-c-libraries) -12. [Errors](#12-errors) -13. [Troubleshooting](#13-troubleshooting) -14. [Project layout and tests](#14-project-layout-and-tests) -15. [Current limits](#15-current-limits) +10. [Arrays](#10-arrays) +11. [Text (str)](#11-text-str) +12. [Structs](#12-structs) +13. [Memory: who frees what](#13-memory-who-frees-what) +14. [Pointers](#14-pointers) +15. [Built-in functions](#15-built-in-functions) +16. [Modules](#16-modules) +17. [Using C libraries](#17-using-c-libraries) +18. [Errors](#18-errors) +19. [Debugging](#19-debugging) +20. [Troubleshooting](#20-troubleshooting) +21. [Project layout and tests](#21-project-layout-and-tests) +22. [Current limits](#22-current-limits) --- @@ -37,9 +44,13 @@ file into a real program (a native executable, like the ones made from C), using What that gives you: -- **Speed close to C.** There is no interpreter, virtual machine or garbage collector. +- **Speed close to C.** No interpreter, virtual machine or garbage collector. Array bounds + checks and other safety checks cost almost nothing after optimization. - **Small, standalone programs** that you can copy and run. -- **Direct access to C libraries.** `import "stdio.h"` and call anything in it. +- **Memory handled for you.** Lists and text are freed automatically at the end of the block + that owns them, with no garbage collector involved. +- **Direct access to C libraries.** `import "stdio.h"` and call anything in it, even functions + that take structs. The design rules: one way to do each thing, C-like syntax without C's traps, and error messages that tell you how to fix the problem. @@ -56,10 +67,10 @@ Other LLVM versions will likely fail to compile, because LLVM's C++ API changes ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # downloads LLVM, libclang, cmake, ninja, pkg-config +nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config cmake -S . -B build -G Ninja ninja -C build -./build/finch version # finch 1.0.0 (LLVM 21.x) +./build/finch version # finch 2.0.0 (LLVM 21.x) ``` ### Option B: your distribution's packages @@ -73,7 +84,7 @@ libclang 21 development files, and optionally `pkg-config`. | Fedora | `gcc-c++ cmake ninja-build pkgconf llvm-devel clang-devel` | | Arch | `base-devel cmake ninja pkgconf llvm clang` | -If CMake cannot find LLVM, point it there: +These have not been tested; if CMake cannot find LLVM, point it there: ```sh cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm @@ -87,21 +98,22 @@ sudo cp build/finch /usr/local/bin/ finch version ``` -Finch needs a working C compiler (`cc`) at run time. It uses it as the **linker** that -turns compiled code into a program. Set the `CC` environment variable to use a different one. +At run time Finch needs a C compiler (`cc`). It is used to link programs, and once to build +Finch's small runtime library, which is kept in `~/.cache/finch`. Set `CC` to use a different one. --- ## 3. The finch command ``` -finch run compile and run right away +finch run [args...] compile and run right away finch build [-o name] compile into a program (default name: the file's name) finch ir print the LLVM IR (for the curious) finch version show the version options: -l link a C library, same as link "lib" in the file + -g add debug info (for gdb / lldb) -O0 skip optimizations ``` @@ -109,13 +121,13 @@ Examples: ```sh finch run hello.fn # prints the output +finch run tool.fn input.txt -v # arguments after the file go to the program finch build game.fn -o mygame # creates ./mygame -./mygame -finch run window.fn -l glfw # with a C library +finch build game.fn -g -O0 # a build for the debugger ``` -`finch run` passes on your program's exit code. If the program crashes, it tells you -how (for example `the program crashed: Segmentation fault`). +`finch run` passes on your program's exit code. If the program crashes, it says how +(for example `the program crashed: Segmentation fault`). --- @@ -126,8 +138,14 @@ how (for example `the program crashed: Segmentation fault`). /* or span several lines */ -import "math.h" // C headers (optional), always at the top -link "m" // C libraries to link (optional) +import "math.h" // C headers (optional), at the top +import shapes // Finch modules (optional): shapes.fn next to this file +link "m" // C libraries or C files to link (optional) + +struct Point { // your own types + int x + int y +} fn main() { // the program starts here print("Hello") @@ -136,13 +154,14 @@ fn main() { // the program starts here Rules worth knowing: -- **One statement per line.** There are no semicolons. To split a long expression, - break the line **after** an operator (`a +⏎ b`) or anywhere inside brackets `( )`. -- **Curly braces are always required** after `if`, `else`, `for`, `while`, and the opening - `{` goes on the same line. +- **One statement per line.** There are no semicolons. To split a long expression, break the line + **after** an operator (`a +⏎ b`) or anywhere inside brackets `( )` or `[ ]`. +- **Curly braces are always required** after `if`, `else`, `for`, `while`, and the opening `{` + goes on the same line. - **No brackets around conditions:** `if x > 5 {`, not `if (x > 5) {`. -- Functions can be in any order in the file. -- `main` is either `fn main()` or `fn main() -> int` (returns the exit code). +- Functions and structs can be in any order in the file. +- `main` is `fn main()`, `fn main() -> int` (returns the exit code) or `fn main([]str args)` + (gets the command-line arguments; `args[0]` is the program's own path). --- @@ -151,7 +170,8 @@ Rules worth knowing: ```c x := 5 // new variable, type inferred (int) int y = 10 // new variable, type written explicitly -int z // no value: starts at zero ("" for str, null for pointers) +int z // no value: starts at zero ("" for str, [] for arrays, null for pointers) +Point p // a struct with no value: zero, or the struct's default values x = 7 // assign x += 1 // also -= *= /= %= @@ -160,10 +180,10 @@ x += 1 // also -= *= /= %= - A name can be declared **once** in a function. Finch does not allow reusing a name in an inner block (no "shadowing"), because that is a common source of confusion. - Variables exist from their declaration to the end of the `{ }` block they are in. -- Names: ASCII letters, digits and `_`, not starting with a digit. Non-ASCII letters are allowed only inside strings. Type names (`int`, `u8`, …) and - keywords are reserved. +- Names: ASCII letters, digits and `_`, not starting with a digit. Non-ASCII letters are allowed only inside strings. + Type names (`int`, `u8`, …), built-in function names and keywords are reserved. -**Keywords:** `fn return if else while for in break continue true false null import link` +**Keywords:** `fn return if else while for in break continue true false null import link struct defer` --- @@ -177,7 +197,9 @@ x += 1 // also -= *= /= %= | `float` | decimal number, 64-bit (same as `f64`) | `3.14` | | `bool` | `true` or `false` | | | `char` | one byte-sized character | `'A'`, `'\n'` | -| `str` | text (a C string) | `"hello\tworld"` | +| `str` | text | `"hello\tworld"` | +| `[]T` | a list (array) of `T` | `[1, 2, 3]` | +| your structs | named groups of values | `Point(1, 2)` | String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`. @@ -192,11 +214,6 @@ String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`. `f32` is a 32-bit decimal number (about 7 significant digits), `f64` a 64-bit one (about 15–16). -### Pointers - -`ptr[T]` points to a value of type `T`. Plain `ptr` points to "something" with unknown -type, like `void*` in C. See [Pointers](#10-pointers). - ### Conversions Finch converts automatically **only when nothing can be lost**: @@ -208,32 +225,30 @@ Finch converts automatically **only when nothing can be lost**: | any whole number | `float` / `f32` | ✅ | | `f32` | `f64` | ✅ | | `null` | any pointer | ✅ | -| any pointer, `str` | `ptr` | ✅ | -| `ptr` | any `ptr[T]` | ✅ | +| any pointer | `ptr`, and `ptr` to any pointer | ✅ | | anything else | | ❌ write it: `type(value)` | **Number literals adapt.** A number written in the code fits into any type that can hold it: -`u8 b = 200` works, `u8 b = 300` is an error (`the number 300 doesn't fit in u8`). -The same applies to constants imported from C headers. +`u8 b = 200` works, `u8 b = 300` is an error. The same applies to constants imported from C headers. **Explicit conversions** use the type name like a function: ```c -int(3.99) // 3 (cuts off the fraction) -int(-3.99) // -3 -float(7) / 2 // 3.5 -u8(300) // 44 (keeps the lowest 8 bits) -i8(200) // -56 -int('A') // 65 -char(66) // 'B' -bool(0) // false (any non-zero number is true) -ptr(text) // a str as an untyped pointer -str(p) // a pointer as a str +int(3.99) // 3 (cuts off the fraction) +float(7) / 2 // 3.5 +u8(300) // 44 (keeps the lowest 8 bits) +int('A') // 65 +char(66) // 'B' +bool(0) // false (any non-zero number is true) +str(42) // "42" (also floats, bools, chars; str([]u8) turns bytes into text) +int("42") // 42 (stops the program if the text isn't a number) +float("2.5") // 2.5 +ptr(p) // a typed pointer as an untyped one +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`. -Convert first if you need more room: `int(x) + 10`. --- @@ -244,7 +259,7 @@ Convert first if you need more room: `int(x) + 10`. | Priority | Operators | Meaning | |---|---|---| | 5 | `*` `/` `%` `<<` `>>` `&` | multiply, divide, remainder, shift left/right, bitwise AND | -| 4 | `+` `-` `\|` `^` | add, subtract, bitwise OR, bitwise XOR | +| 4 | `+` `-` `\|` `^` | add (and join text), subtract, bitwise OR, bitwise XOR | | 3 | `==` `!=` `<` `<=` `>` `>=` | comparisons | | 2 | `&&` | logical AND | | 1 | `\|\|` | logical OR | @@ -252,18 +267,16 @@ Convert first if you need more room: `int(x) + 10`. Unary (in front of a value): `-x` negate, `!x` logical NOT, `~x` flip all bits. > **Different from C, on purpose:** bit operators bind tighter than comparisons, so -> `flags & 4 != 0` means `(flags & 4) != 0`. In C it silently means something else. +> `flags & 4 != 0` means `(flags & 4) != 0`. Notes: -- `/` on whole numbers rounds toward zero: `7 / 2` is `3`, `-7 / 2` is `-3`. `%` takes the sign of the left side. +- `/` on whole numbers rounds toward zero: `7 / 2` is `3`. `%` takes the sign of the left side. - Dividing by zero **stops the program** with `runtime error: division by zero` and the line number. - If the zero is written in the code, it is a compile error instead. - `&&` and `||` only evaluate the right side if needed. -- Text (`str`) can be compared with `==` and `!=` (it compares the contents). `<` and `>` do not work on text. -- Pointers can be compared with `==` and `!=`, including against `null`. -- Bit operators need whole numbers. For `bool` use `&&` and `||`. -- `>>` on signed numbers keeps the sign (`-16 >> 2` is `-4`). On unsigned numbers it fills with zeros. +- Text: `+` joins, `==` `!=` compare contents, `<` `>` compare alphabetically (byte by byte). +- Pointers can be compared with `==` and `!=`, including against `null`. A `str` is never `null`. +- Arrays and structs can't be compared with `==`; compare their parts. --- @@ -286,14 +299,32 @@ for i in 0..10 { // i = 0, 1, …, 9 (the end is not included) if i == 3 { continue } if i == 8 { break } } + +for name in names { // every element of an array (or every char of a str) + print(name) +} ``` - A condition must be a `bool`. `if x {` with a number is an error; write `if x != 0 {`. -- In `for i in a..b`, both ends are whole numbers evaluated **once** before the loop starts. - `i` is an `int` and cannot be changed inside the loop. +- In `for i in a..b`, both ends are evaluated **once**. `i` is an `int` and cannot be changed in the loop. +- In `for x in list`, `x` cannot be changed either. To change elements, loop over the indexes: + `for i in 0..list.len { list[i] = ... }`. Adding to the list inside the loop is fine. - `while true { ... }` loops until a `break` or `return`. - Code after `return`, `break` or `continue` in the same block is an error, because it can never run. +### defer + +`defer` runs a statement when the current block ends, however it ends: normally, by `return`, +`break` or `continue`. Several defers run in reverse order. Handy for cleanup: + +```c +f := fopen("data.txt", "r") +defer fclose(f) +// ... use f; it is closed when the block ends, on every path +``` + +A deferred statement can't contain `return`, `break` or `continue`. + --- ## 9. Functions @@ -309,32 +340,129 @@ fn no_result(str message) { // no "->": returns nothing } ``` -- Parameters are copies. Changing a parameter does not change the caller's variable. - Use a pointer for that (see below). +- Arguments behave like copies: changing a parameter never changes the caller's variable. + (Finch only really copies when needed: a function that doesn't change an array gets it for free.) + To change the caller's variable, pass a pointer: see [Pointers](#14-pointers). - A function with `-> type` must return a value on every path. Finch checks this. - A function cannot be defined inside another function. -- Built-in names you cannot reuse: `print`, `addr`, and all type names. - -### Built-in functions - -| Function | What it does | -|---|---| -| `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type. | -| `addr(x)` | Gives a pointer to variable `x`. | -| `int(x)`, `u8(x)`, `float(x)`, … | Converts types (see [Conversions](#conversions)). | --- -## 10. Pointers +## 10. Arrays -A pointer holds the **address** of a value: where it lives in memory. Finch uses words -instead of C's `*` and `&`: +```c +nums := [5, 3, 8] // an array of int +[]str names // an empty array of str +grid := [[1, 2], [3, 4]] // arrays of arrays +[]f32 xs = [1, 2.5] // the type decides what the numbers become +``` + +| Operation | Meaning | +|---|---| +| `a.len` | number of elements | +| `a[i]` | element `i` (from 0); out of range stops the program with a clear message | +| `a[i] = x` | change an element | +| `a.push(x)` | add at the end | +| `a.pop()` | remove the last one and give it back | +| `a.insert(i, x)` | put `x` at position `i`, moving the rest | +| `a.remove(i)` | remove position `i` and give it back | +| `a.clear()` | remove everything | +| `a.resize(n)` | make it `n` long (new elements are zero) | +| `a.contains(x)`, `a.find(x)` | is `x` there? its index, or -1 (numbers, chars, bools, str) | +| `a.sort()`, `a.reverse()` | in place (numbers, chars, str) | +| `a.slice(s, e)` | a new array with elements `s` … `e-1` | +| `a.join(sep)` | for `[]str`: one text with `sep` between the parts | +| `a.ptr` | the address of the first element (for C) | +| `print(a)` | prints `[1, 2, 3]` | + +**Assigning copies:** after `b := a`, changing `b` does not change `a`. + +--- + +## 11. Text (str) + +```c +s := "Hello" +s += ", world" // join +print(s.len, s[0]) // 12 H (.len counts bytes; ą, ż, … take 2) +s[0] = 'J' // change a character +``` + +| Method | Gives back | +|---|---| +| `s.sub(a, b)` | the part from `a` to `b-1` | +| `s.find(t)` | where `t` starts, or -1 | +| `s.contains(t)`, `s.starts_with(t)`, `s.ends_with(t)` | `bool` | +| `s.split(sep)` | `[]str`; `split("")` gives single characters | +| `s.trim()` | without spaces/newlines at both ends | +| `s.upper()`, `s.lower()` | A–Z / a–z changed (ASCII letters) | +| `s.replace(a, b)` | every `a` replaced by `b` | +| `s.repeat(n)` | `s` n times | +| `s.bytes()` | `[]u8` with the bytes | +| `s.ptr` | a `ptr[char]` for C (valid while `s` lives) | + +`str` keeps its length, so `.len` is instant, and it is always NUL-terminated, so passing it +to C as `char*` works. + +--- + +## 12. Structs + +```c +struct Player { + str name + int lives = 3 // a default value + []int scores + Point pos // structs inside structs +} + +p := Player(name: "Ola") // by name: missing fields get their default (or zero) +q := Point(1, 2) // in order: then every field must be given +p.lives -= 1 +p.scores.push(10) +p.pos.x = 5 +print(p) // Player(name: "Ola", lives: 2, scores: [10], pos: Point(x: 5, y: 0)) +``` + +- Assigning or passing a struct copies it, together with its arrays and text. +- A struct can't contain itself directly; use `ptr[Node]` or `[]Node` for that field (trees, lists). +- Structs have no methods. Write functions that take the struct: `fn heal(Player p) -> Player`. + +--- + +## 13. Memory: who frees what + +You never free arrays, text or structs. Finch does it for you, at a predictable moment: +**when the block that owns the value ends** (`}`), or earlier on `return`, `break`, `continue`. + +- A variable **owns** its value. Assigning copies it, so two variables never share one list. +- A value produced by a function call or an expression is **moved** where it's stored: no copy. +- `return list` moves the variable out of the function: no copy. +- Function parameters are **lent**: the function gets the caller's value without a copy. If the + function changes the parameter, Finch makes it a private copy first. You never notice except in speed. + +For memory you manage yourself (linked lists, trees, sharing between structures): + +```c +node := new(Node(value: 1)) // put a value on the heap, get a ptr[Node] +defer free(node) // free(...) gives it back (and frees its arrays and text) +``` + +Memory from C (`malloc`, library objects) is freed with C's functions, as in C. + +--- + +## 14. Pointers + +A pointer holds the **address** of a value. Finch uses words instead of C's `*` and `&`: | C | Finch | Meaning | |---|---|---| | `int *p` | `ptr[int] p` | p points to an int | -| `&x` | `addr(x)` | the address of x | +| `&x` | `addr(x)` | the address of x (also `addr(a[i])`, `addr(s.field)`) | | `*p` | `p.value` | the value p points to | +| `p->field` | `p.field` | a field of the struct p points to | +| `p[i]` | `p[i]` | C-style indexing (no bounds check) | | `NULL` | `null` | points nowhere | | `void *` | `ptr` | points to something of unknown type | @@ -343,38 +471,74 @@ fn double(ptr[int] p) { p.value = p.value * 2 } -fn main() { - x := 21 - double(addr(x)) - print(x) // 42 - - ptr[int] nothing = null - if nothing == null { - print("empty") - } -} +x := 21 +double(addr(x)) // x is 42 ``` -Pointers can point to pointers: `ptr[ptr[int]]`, then `pp.value.value`. +If a struct has its own field called `value`, then `p.value` on a pointer to it means that field. -Using `.value` on a `null` pointer **stops the program** with -`runtime error: used .value on a null pointer` instead of crashing randomly. +Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with a clear message. -⚠️ A pointer to a variable must not be used after the function that owns the variable -has returned. The variable no longer exists then. Finch does not check this yet. +⚠️ A pointer to a variable must not be used after the variable's block has ended. Finch does not check this. --- -## 11. Using C libraries +## 15. Built-in functions -Almost every library on Linux has a C interface: graphics, sound, networking, databases. -Finch can use them directly. +| Function | What it does | +|---|---| +| `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type, including arrays and structs. | +| `input()`, `input("question")` | Reads one line typed by the user (without the newline). Empty text at end of input. | +| `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read. | +| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. | +| `file_exists(path)` | `true` / `false` | +| `shell(command)` | Runs a shell command, gives back its exit code. | +| `exit(code)` | Ends the program right away. | +| `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). | +| `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). | +| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). | + +--- + +## 16. Modules + +Split a program into files. `import shapes` loads `shapes.fn` from the same folder (or from a +folder listed in the `FINCH_PATH` environment variable, separated by `:`). + +```c +// shapes.fn +struct Box { + float w + float h +} + +fn area(Box b) -> float { + return b.w * b.h +} +``` + +```c +// main.fn +import shapes + +fn main() { + b := shapes.Box(2, 3) + print(shapes.area(b)) + shapes.Box other // a module's type in a declaration +} +``` + +- Everything in a module is used with the module's name in front: `shapes.area`, `shapes.Box`. +- A module doesn't have `main`. Modules can import other modules. + +--- + +## 17. Using C libraries ### Step 1: import the header ```c import "stdio.h" -import "math.h" import "GLFW/glfw3.h" // a path inside the system include folders import "mylib.h" // a header next to your .fn file ``` @@ -382,33 +546,23 @@ import "mylib.h" // a header next to your .fn file Finch reads the header with **libclang** (clang's C parser) and makes these usable: - **functions**, including ones with a variable number of arguments, like `printf`, -- **`enum` values**, -- **simple `#define` constants** that are a single number, like `M_PI`, `EOF`, `GL_COLOR_BUFFER_BIT`, +- **structs**, by value and by pointer (`Vector2`, `SDL_Rect`, `CXCursor`, …), with their fields, +- **`enum` values** and **simple `#define` constants** (`M_PI`, `GL_COLOR_BUFFER_BIT`), - **global variables**, like `stdout` and `stderr`. ### Step 2: link the library -A header only *describes* functions. Their actual code is in a library file (`libNAME.so`). -Tell Finch which library to use, at the top of the file: - ```c -link "glfw" // uses libglfw.so -link "GL" // uses libGL.so +link "glfw" // uses libglfw.so (pkg-config is asked first, if installed) +link "helpers.c" // compile and link your own C file (next to the .fn file) +link "prebuilt.a" // or an object file / static library ``` -Write just the name: for `libglfw.so` that is `glfw`. If `pkg-config` knows the name, Finch uses -its settings (paths and extra libraries). Otherwise it passes `-lNAME` to the linker. -The C standard library and the math library (`libm`) are always linked. - -On the command line, `-l NAME` does the same as `link "NAME"`. - If you forget the `link` line, Finch tells you exactly what is missing: ``` error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked - the header only says the functions exist; their code lives in a library. - add this at the top of your file (with the library's real name): - + ... link "glfw" ``` @@ -422,52 +576,46 @@ error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from " | `float`, `double` | `f32`, `f64` | | `_Bool` / `bool` | `bool` | | `enum` | the matching signed integer | -| `char *`, `const char *` | `str` | -| `int *`, `double *`, … | `ptr[i32]`, `ptr[f64]`, … | -| `void *`, `struct X *`, `FILE *`, function pointers | `ptr` | +| `char *` as a **parameter or result** | `str` | +| `char *` inside a struct or behind a pointer | `ptr[char]` (use `str(p)` to read it) | +| `struct X` by value | `X`, a struct with the same fields | +| `struct X *`, `int *`, … | `ptr[X]`, `ptr[i32]`, … | +| `int arr[16]` inside a struct | `[16]i32`: index it and use `.len` | +| `void *`, function pointers, incomplete structs | `ptr` | -So C's `int` is Finch's `i32`. Literals adapt automatically (`abs(-5)` works), -but a Finch `int` variable must be converted: `abs(i32(x))`. - -### Example +### Example: structs and callbacks ```c -import "stdio.h" -import "math.h" -import "stdlib.h" +import "raylib.h" +link "raylib" fn main() { - printf("%d + %d = %d\n", 2, 3, 5) - print(sqrt(2), M_PI, RAND_MAX) - - ptr[i32] n = malloc(4) // ask C for 4 bytes of memory - n.value = 7 - print(n.value) - free(n) // and give them back - - fprintf(stderr, "this goes to the error output\n") + 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() } ``` -A complete graphics example is in `examples/window.fn`: a GLFW + OpenGL window that changes color. -`examples/llvm.fn` shows that Finch can even drive LLVM itself through its C API. +To give C a function to call back (like `qsort`'s comparator), use `addr(myFunction)`. +Its parameters must be C types (numbers, pointers, C structs). ### What does not work yet -- C functions that take or return a **`struct` by value**, such as `div()`. Finch says so - clearly if you try. Functions taking a *pointer* to a struct are fine. -- **Function-like macros** (`#define MAX(a,b) ...`) and macros that are not a single number. +- C **unions** by value, and structs with **bit fields** by value (pointers to them work). +- **Function-like macros** and macros that are not a single number. - `long double`, 128-bit integers. -- Reading the **fields** of a C struct. --- -## 12. Errors +## 18. Errors ### Compile errors -Finch stops at the first problem and shows where it is: - ``` game.fn:12:9: error: 'score' must be int, but this is str 12 | score = "high" @@ -478,64 +626,87 @@ Format: `file:line:column: error: explanation`, then the line with a `^` under t ### Runtime errors -Some problems can only be seen while the program runs. Instead of undefined behavior -(as in C), Finch stops with a clear message on the error output and exit code 1: +Instead of undefined behavior (as in C), Finch stops with a message and exit code 1: | Message | Cause | |---|---| | `runtime error: division by zero` | `/` or `%` by a variable that was 0 | | `runtime error: division overflows …` | the smallest signed number divided by −1 | -| `runtime error: used .value on a null pointer` | reading or writing `p.value` while `p` is `null` | +| `runtime error: index 5 is out of range (the length is 3)` | `a[5]` on a shorter array or str | +| `runtime error: pop() on an empty array` | | +| `runtime error: used .value on a null pointer` | also `.field` and `[i]` through `null` | +| `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` on text that isn't a number | +| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file | --- -## 13. Troubleshooting +## 19. Debugging + +```sh +finch build game.fn -g -O0 -o game +gdb ./game +(gdb) break game.fn:12 +(gdb) run +(gdb) bt # where are we, through which functions +(gdb) info locals # all variables +(gdb) print player.lives +``` + +`-g` adds debug info (lines, functions, variables, struct fields). `-O0` keeps every variable +visible; without it the optimizer may remove some. + +--- + +## 20. Troubleshooting | Problem | Solution | |---|---| -| `can't find the C header 'x.h'` | The library's development files are missing. Install them (on Debian: `libx-dev`; on Nix: add to `shell.nix`). For your own header, put it next to the `.fn` file. | +| `can't find the C header 'x.h'` | The library's development files are missing. Install them (Debian: `libx-dev`; Nix: add to `shell.nix`). | | `… 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` | The library is not installed, or the name is wrong. 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 struct by value, or another unsupported type. Look for a variant that takes pointers. | -| `no C compiler found to link with` | Install gcc or clang, or set `CC`. | -| The program runs, but a C library is not found at start | It was linked from a path the system does not search. Run it inside the same `nix-shell`, or install the library system-wide. | -| CMake: `libclang not found` | Install the libclang dev package, or run `cmake` inside `nix-shell`. | -| Build fails with LLVM API errors | You have a different LLVM version than 21. | +| `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. | +| `can't find the module 'x'` | Put `x.fn` 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. | +| `clang` inside `nix-shell` can't find `stdio.h` | You have an old `shell.nix`: `llvmPackages.clang` must be listed before `llvmPackages.libclang`. | | A program never ends | A loop condition never becomes false. Press Ctrl+C. | --- -## 14. Project layout and tests +## 21. Project layout and tests ``` Finch/ ├── src/ the compiler (C++) -├── examples/ example programs (hello, tour, types, c_import, window, llvm) +├── runtime/ finch_rt.c: the small runtime (text, arrays, input, files) +├── boot/ the Finch compiler written in Finch (see the engineers' guide) +├── examples/ hello, tour, types, structs, todo, guess, c_import, window, raylib, llvm ├── tests/ -│ ├── run/ programs + the exact output they must print (.out) +│ ├── run/ programs + the exact output they must print (.out), input (.in) │ ├── fail/ programs that must fail, with the expected error in line 1 -│ └── run.sh the test runner +│ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind) +│ └── boot.sh builds the self-hosted compiler with itself and compares ├── docs/ this documentation (en, pl) ├── shell.nix the Nix development environment └── CMakeLists.txt ``` -Run the tests after building: - ```sh -tests/run.sh # → 34 passed, 0 failed +tests/run.sh # → 58 passed, 0 failed +MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access +tests/boot.sh # the self-hosting check (needs clang) ``` --- -## 15. Current limits +## 22. Current limits -Finch 1.0 is a solid core, not a finished language. Not there yet: +Finch 2.0 is a complete small language, but not a finished one. Not there yet: -- `struct`, arrays, reading keyboard input (`input()`), string building (`"a" + "b"`). - For now, use C functions for these (`scanf`, `snprintf`, `malloc`…). -- Automatic memory management (planned: memory freed at the end of the block, plus `defer`). -- Finch modules (one program = one `.fn` file for now). +- Methods on structs, generics, maps/dictionaries, `match`. (Use arrays of structs and functions.) +- Error values: errors in `int("x")` or `read_file` stop the program. Check first (`file_exists`). +- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII. +- Threads. - Platforms other than Linux x86-64. See the [roadmap in the README](../../README.md#roadmap). diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index 263e39a..e71d9b1 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -1,10 +1,11 @@ # Finch dla inżynierów -**Kompletny opis wnętrza kompilatora Finch 1.0.** Opisuje, jak kod źródłowy staje się tokenami, -tokeny drzewem AST, a AST kodem LLVM IR, który potem jest optymalizowany, zapisywany do pliku -obiektowego i linkowany. Zawiera dokładne reguły typów, sposób tłumaczenia każdej konstrukcji na -IR (z prawdziwym wyjściem kompilatora), import nagłówków C przez libclang, szczegóły ABI -oraz opis każdej funkcji kompilatora wraz z tym, czym różni się od sąsiednich. +**Kompletny opis wnętrza Fincha 2.0.** Jak kod źródłowy staje się tokenami, tokeny drzewem AST, +a AST kodem LLVM IR, który potem jest optymalizowany, zapisywany i linkowany z małym runtime'em w C. +Dokładne reguły typów; model własności i to, jak tłumaczy się na kopie i zwolnienia; jak każda +konstrukcja staje się IR (z prawdziwym wyjściem kompilatora); obsługa nagłówków C, struktur C i ABI +System V; moduły, informacje dla debuggera i kompilator samohostujący z `boot/`. Każda funkcja +kompilatora jest opisana razem z tym, czym różni się od sąsiednich. Zakładam, że znasz C/C++ i widziałeś już LLVM IR. Język z perspektywy użytkownika opisuje [przewodnik dla techników](dla-technikow.md). @@ -20,61 +21,65 @@ Język z perspektywy użytkownika opisuje [przewodnik dla techników](dla-techni 3. [Lekser](#3-lekser) 4. [Gramatyka i parser](#4-gramatyka-i-parser) 5. [AST](#5-ast) -6. [Model semantyczny](#6-model-semantyczny) -7. [System typów](#7-system-typów) -8. [Generowanie kodu](#8-generowanie-kodu) -9. [Kontrole w czasie działania](#9-kontrole-w-czasie-działania) -10. [Współpraca z C przez libclang](#10-współpraca-z-c-przez-libclang) -11. [Optymalizacja, emisja, linkowanie](#11-optymalizacja-emisja-linkowanie) -12. [Diagnostyka](#12-diagnostyka) -13. [System budowania](#13-system-budowania) -14. [Testy](#14-testy) -15. [Semantyka a C: zachowania zdefiniowane i niezdefiniowane](#15-semantyka-a-c-zachowania-zdefiniowane-i-niezdefiniowane) -16. [Spis funkcji: kto co robi](#16-spis-funkcji-kto-co-robi) -17. [Rozbudowa kompilatora](#17-rozbudowa-kompilatora) -18. [Znane ograniczenia i plan](#18-znane-ograniczenia-i-plan) +6. [Wczytywanie: pliki i moduły](#6-wczytywanie-pliki-i-moduły) +7. [Model semantyczny](#7-model-semantyczny) +8. [System typów](#8-system-typów) +9. [Własność i pamięć](#9-własność-i-pamięć) +10. [Generowanie kodu](#10-generowanie-kodu) +11. [Kontrole w czasie działania](#11-kontrole-w-czasie-działania) +12. [Runtime w C](#12-runtime-w-c) +13. [Współpraca z C: nagłówki, struktury, ABI](#13-współpraca-z-c-nagłówki-struktury-abi) +14. [Optymalizacja, emisja, linkowanie](#14-optymalizacja-emisja-linkowanie) +15. [Informacje dla debuggera](#15-informacje-dla-debuggera) +16. [Diagnostyka](#16-diagnostyka) +17. [Kompilator samohostujący (boot/)](#17-kompilator-samohostujący-boot) +18. [System budowania](#18-system-budowania) +19. [Testy](#19-testy) +20. [Semantyka a C](#20-semantyka-a-c) +21. [Spis funkcji](#21-spis-funkcji) +22. [Rozbudowa kompilatora](#22-rozbudowa-kompilatora) +23. [Znane ograniczenia](#23-znane-ograniczenia) --- ## 1. Architektura -Finch to klasyczny jednoprzebiegowy front-end przed LLVM. Sprawdzanie typów i generowanie IR -odbywają się w jednym przejściu po AST. Nie ma osobnego przebiegu semantycznego ani własnej -reprezentacji pośredniej. +Finch to jednoprzebiegowy front-end przed LLVM: sprawdzanie typów i generowanie IR to jedno przejście +po AST, bez własnej reprezentacji pośredniej. ``` - plik.fn - │ readFile() src/main.cpp - ▼ - lex() ──────────► std::vector src/lexer.cpp - ▼ - parse() ─────────► Program (AST) src/parser.cpp, src/ast.h - │ ├─ imports ──► importHeaders() ──► CImports src/cimport.cpp (libclang) - │ └─ links - ▼ - hostMachine() ───► llvm::TargetMachine (triple, data layout) - ▼ - generate() ──────► llvm::Module (typy + IRBuilder, potem verifyModule) src/codegen.cpp - ▼ - optimize() ──────► potok PassBuilder default (pomijany przy -O0) - ▼ - emitObject() ────► plik.o (legacy::PassManager + addPassesToEmitFile) - ▼ - link() ──────────► cc plik.o -o plik -lm [-l… | pkg-config --libs …] - ▼ - plik wykonywalny (finch run: uruchamiany z pliku tymczasowego, potem usuwany) + main.fn ──► Loader::load() ── lex() ── parse() ──► Program (+ każdy importowany moduł, rekurencyjnie) + │ + wszystkie `import "x.h"` ──► importHeaders() (libclang) ──► CImports + │ + hostMachine() ──► TargetMachine (triple + data layout)│ + ▼ + generate(progs, cimports, tm, debug) ──► Codegen::run() + declareStructs() wszystkie struktury Fincha i C, ciała typów LLVM, kontrola cykli + declareFns() sygnatura każdej funkcji (+ analiza „pożyczania” parametrów) + define() każde ciało: typy + IRBuilder, zasięgi, zwalnianie, kontrole + defineMainWrapper C-owe `main(argc, argv)` + DIBuilder::finalize (z -g) ; verifyModule + ▼ + optimize() PassBuilder default (pomijane przy -O0) + emitObject() legacy PM + addPassesToEmitFile ──► prog.o + link() cc prog.o ~/.cache/finch/rt-.o [twoje .c/.o/.a] -lm [pkg-config/-l…] + ▼ + plik wykonywalny (finch run: uruchamiany z pliku tymczasowego z resztą argumentów, potem usuwany) ``` -Decyzje, które kształtują całą resztę: +Decyzje, które kształtują resztę: -- **Szybka porażka.** Pierwszy błąd wypisuje diagnostykę i wywołuje `exit(1)` (`fail()` w `src/error.h`). - Nie ma odtwarzania po błędach, więc AST i codegen nigdy nie widzą częściowo poprawnego wejścia. -- **Jedno przejście.** `Codegen` niesie stan zasięgów, pętli i bieżącej funkcji. Każde wyrażenie - zwraca jednocześnie wartość IR i swój typ Fincha (`Value_`). -- **Niech LLVM robi robotę.** Zmienne lokalne to `alloca`, a `mem2reg`/SROA zamieniają je na SSA. - Finch nigdy sam nie buduje SSA ani węzłów φ, z wyjątkiem `&&`/`||`. -- **Systemowy toolchain C jest środowiskiem uruchomieniowym.** `printf`, `strcmp`, `dprintf`, `exit` - pochodzą z libc. Finch nie ma własnej biblioteki uruchomieniowej. +- **Szybka porażka.** Pierwszy błąd wypisuje diagnostykę i kończy proces (`failAt()` w `src/error.h`). + Bez odtwarzania po błędach, więc dalsze fazy nigdy nie widzą częściowo poprawnego wejścia. +- **Jedno przejście.** `Codegen` niesie zasięgi, pętle, bieżącą funkcję i moduł. Każde wyrażenie zwraca + wartość IR, typ Fincha i dwie flagi (`Value_`, §8). +- **Niech LLVM robi robotę.** Zmienne lokalne to `alloca`, które `mem2reg`/SROA zamieniają na rejestry. + Węzły φ Finch buduje ręcznie tylko dla `&&`/`||`. +- **Wartości, nie referencje.** Tablice, teksty i struktury mają semantykę wartości: przypisanie kopiuje, + koniec bloku zwalnia. Własność rozstrzyga się w czasie kompilacji, bez liczenia referencji (§9). +- **Malutki runtime w C** (`runtime/finch_rt.c`, ok. 430 linii) robi to, co w IR byłoby żmudne: operacje na + tekstach, wzrost tablic, wejście, pliki, panic. Jest wbudowany w kompilator i trzymany w cache. --- @@ -82,1055 +87,810 @@ Decyzje, które kształtują całą resztę: | Plik | Linie | Odpowiedzialność | |---|---|---| -| `src/error.h` | ~25 | `fail(line, col, msg)`: wypisuje `plik:linia:kol: error:` i fragment kodu z `^`, kończy proces. Trzyma `g_file`, `g_source`. | -| `src/lexer.h/.cpp` | ~240 | enum `Tok`, `Token`, `lex()`, `tokName()` | -| `src/ast.h` | ~235 | `Type`, wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `Import`, `Link`, `Program` | -| `src/parser.h/.cpp` | ~430 | Parser zstępujący `Parser`, `typeFromName()` | -| `src/cimport.h/.cpp` | ~280 | Import nagłówków przez libclang: `importHeaders()` → `CImports` (funkcje, stałe, zmienne globalne) | -| `src/codegen.h/.cpp` | ~900 | `Codegen`: sprawdzanie typów + generowanie LLVM IR, `generate()` | -| `src/main.cpp` | ~260 | Sterownik: CLI, maszyna docelowa, optymalizacja, emisja, linkowanie, analiza błędów linkera, `run` | -| `tests/run.sh` | | Testy wzorcowego wyjścia i oczekiwanych błędów | +| `src/error.h` | 40 | `g_files` (wszystkie źródła), `failAt(file, line, col, msg)`, `fail(line, col, msg)` | +| `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(indeksPliku)`, `tokName()` | +| `src/ast.h` | 310 | `Type` (z Array, Fixed, Struct, Named), wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` | +| `src/parser.h/.cpp` | 540 | Parser zstępujący `Parser`, `typeFromName()` | +| `src/cimport.h/.cpp` | 420 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne | +| `src/codegen.h` | 15 | `generate()` | +| `src/codegen_impl.h` | 265 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla czterech plików poniżej | +| `src/codegen.cpp` | 1090 | Program, deklaracje, zasięgi i sprzątanie, instrukcje, wyrażenia, miejsca (`ref`), operatory, analiza modyfikacji | +| `src/codegen_types.cpp` | 500 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), wypisywanie, typy DWARF | +| `src/codegen_builtins.cpp` | 710 | Wywołania, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic | +| `src/abi.cpp` | 310 | Klasyfikacja System V x86-64, wywołania C ze strukturami przez wartość, wrappery wywoływalne z C | +| `src/main.cpp` | 390 | Sterownik: CLI, ładowanie, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, `run` | +| `runtime/finch_rt.c` | 440 | Biblioteka uruchomieniowa (§12) | +| `boot/*.fn` | 3080 | Kompilator samohostujący (§17) | --- ## 3. Lekser -`lex(src, file)` przechodzi raz, liniowo, przez źródło i zwraca `std::vector` -zakończony `Tok::End`. +`lex(file)` przechodzi raz przez `g_files[file].text` i zwraca `std::vector` zakończony `Tok::End`. ```cpp -struct Token { - Tok kind; - std::string text; // treść identyfikatora / literału (sekwencje \ już zdekodowane) - int line, col; // od 1 - bool newlineBefore; // między poprzednim tokenem a tym był znak nowej linii -}; +struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; }; ``` -### Nowe linie jako koniec instrukcji - -Finch nie ma średników. Zamiast emitować tokeny `NEWLINE`, jak robią to wewnętrznie Python i Go, -lekser **oznacza** każdy token flagą `newlineBefore`. Co ona znaczy, decyduje parser (rozdział 4). -Dzięki temu gramatyka nie zawiera tokenów nowej linii, które trzeba by wszędzie pomijać: -w listach argumentów, między `}` a `else` i tak dalej. - -### Tokeny - -- **Pozycje:** `line` i `col` liczone są od 1, a `col` liczy **znaki**, nie bajty. - Bajty kontynuacji UTF-8 jej nie zwiększają, więc `^` trafia we właściwe miejsce także w linijkach z `"Błąd"`. - Nieoczekiwany znak spoza ASCII jest pokazywany w całości (`'ż'`), z podpowiedzią, że nazwy są tylko ASCII. -- **Identyfikatory / słowa kluczowe:** `[A-Za-z_][A-Za-z0-9_]*`. Słowa kluczowe: `fn return if else while for in break continue true false null import link`. - **Nazwy typów nie są słowami kluczowymi.** `int`, `u8`, `ptr`… są leksowane jako `Ident`, a parser rozpoznaje je przez `typeFromName()`. - Lekser nie zależy więc od listy typów, a `u8(x)` parsuje się jak zwykłe wywołanie. -- **Liczby całkowite:** dziesiętne albo szesnastkowe `0x`, z separatorami `_` (usuwanymi z `text`). - Zera wiodące nic nie zmieniają: Finch nie ma ósemkowych, `010` to 10. Wartość liczy parser (`strtoull`, podstawa 10 lub 16). -- **Liczby zmiennoprzecinkowe:** `cyfry.cyfry`. Kropka należy do liczby tylko wtedy, gdy **po niej jest cyfra**, - więc `0..10` to `Int DotDot Int`, a nie `Float(0.) Dot …`. -- **Teksty / znaki:** `"…"` i `'…'`. Sekwencje `\n \t \r \0 \\ \" \'` są dekodowane do `text`. - Nieznana sekwencja, niezamknięty literał albo nowa linia w środku literału to błąd. Literał znakowy musi dać dokładnie jeden bajt. -- **Operatory:** zasada najdłuższego dopasowania: `->` przed `-`, `<<` i `<=` przed `<`, `&&` przed `&`, `..` przed `.`, `:=` (samotny `:` to błąd z podpowiedzią `:=`). -- **Komentarze:** `//` do końca linii, `/* … */` (bez zagnieżdżania; niezamknięty to błąd). +- **Nowe linie nie są tokenami.** Każdy token zapamiętuje `newlineBefore`, a o znaczeniu decyduje + parser (§4). Dzięki temu obsługa nowych linii nie zaśmieca każdej reguły listowej w gramatyce. +- **Pozycje:** od 1; `col` liczy znaki (bajty kontynuacji UTF-8 jej nie zwiększają), więc `^` trafia + pod `"Błąd"`. Zabłąkany znak spoza ASCII jest pokazywany w całości z podpowiedzią, że nazwy są tylko ASCII. +- **Identyfikatory / słowa kluczowe:** `[A-Za-z_][A-Za-z0-9_]*`. Słowa kluczowe: + `fn return if else while for in break continue true false null import link struct defer`. + **Nazwy typów nie są słowami kluczowymi**: `int`, `u8`, `ptr`… to `Ident`, rozpoznawane przez parser + za pomocą `typeFromName()`, więc `u8(x)` jest zwykłym wywołaniem, a lekser nie zależy od listy typów. +- **Liczby:** dziesiętne albo `0x` szesnastkowe z separatorami `_`. Bez ósemkowych (`010` to 10). + `.` należy do liczby tylko wtedy, gdy po niej jest cyfra, więc `0..10` to `Int DotDot Int`. +- **Teksty / znaki:** sekwencje `\n \t \r \0 \\ \" \'`. Bez nowej linii w literale. +- **Operatory:** najdłuższe dopasowanie (`->` przed `-`, `<<`/`<=` przed `<`, `&&` przed `&`, `..` przed `.`, + `:=` przed `:`). Samotny `:` to token (argumenty nazwane). +- **Komentarze:** `//` i niezagnieżdżane `/* */`. --- ## 4. Gramatyka i parser -`Parser` (`src/parser.cpp`) to ręcznie pisany parser zstępujący z jednym tokenem wyprzedzenia -(`cur()`), a w dwóch miejscach z drugim (`peekTok()`). - -### Gramatyka (EBNF) +`Parser` (`src/parser.cpp`) to ręcznie pisany parser zstępujący z jednym tokenem wyprzedzenia, a w +`atDeclaration()` z maksymalnie trzema. ```ebnf -program = { import | link | function } ; -import = "import" STRING ; -link = "link" STRING ; +program = { import | link | struct | function } ; +import = "import" ( STRING | IDENT ) ; (* "x.h" = nagłówek C, nazwa = moduł Fincha *) +link = "link" STRING ; (* "glfw" | "plik.c" | "plik.o" | "lib.a" *) +struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NOWA_LINIA } "}" ; function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ; param = type IDENT ; -type = TYPENAME | "ptr" "[" type "]" ; (* TYPENAME: int float bool char str ptr i8..u64 f32 f64 *) +type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ; block = "{" { statement TERMINATOR } "}" ; -statement = if | while | for | return | "break" | "continue" | block - | type IDENT [ "=" expr ] (* deklaracja *) - | IDENT ":=" expr (* deklaracja z wnioskowaniem *) - | target assignop expr (* target: IDENT albo postfix kończący się .value *) - | call ; -if = "if" expr block [ "else" ( if | block ) ] ; -while = "while" expr block ; -for = "for" IDENT "in" expr ".." expr block ; -return = "return" [ expr ] ; (* expr tylko w tej samej linii *) +statement = if | while | for | return | "break" | "continue" | "defer" statement | block + | type IDENT [ "=" expr ] (* deklaracja, zobacz atDeclaration *) + | IDENT ":=" expr + | target assignop expr (* cel: zmienna, pole, element, p.value *) + | call | method ; +for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *) assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; -expr = or ; -or = and { "||" and } ; -and = cmp { "&&" cmp } ; +expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ; cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ; add = mul { ( "+" | "-" | "|" | "^" ) mul } ; mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; unary = ( "-" | "!" | "~" ) unary | postfix ; -postfix = primary { "." IDENT } ; +postfix = primary { "." IDENT [ args ] | "[" expr "]" } ; primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" - | IDENT [ "(" [ expr { "," expr } ] ")" ] - | "(" expr ")" ; + | IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ; +args = "(" [ arg { "," arg } ] ")" ; arg = [ IDENT ":" ] expr ; ``` -### Reguła końca instrukcji +**Koniec instrukcji.** Po instrukcji `endOfStatement()` wymaga `}`, końca pliku albo tokenu +z `newlineBefore`. Wewnątrz wyrażeń `sameLine()` (`parenDepth > 0 || !newlineBefore`) decyduje, czy +operator, `.`, `[` albo `(` wywołania kontynuuje wyrażenie. `parenDepth` rośnie w `( )` i `[ ]`, +a przy wejściu do `{ }` jest zapamiętywany i zerowany. -`TERMINATOR` nie jest tokenem. Po każdej instrukcji `endOfStatement()` wymaga, żeby następny -token był `}`, końcem pliku albo miał `newlineBefore == true`. W przeciwnym razie zgłasza błąd -`put each statement on its own line`. +**Priorytety są jak w Go**, nie jak w C: `&` wiąże jak `*`, a `|`/`^` jak `+`, wszystkie powyżej porównań, +więc `x & MASKA == 0` znaczy `(x & MASKA) == 0`. -Wewnątrz wyrażeń `sameLine()` decyduje, czy operator binarny, `.` albo `(` wywołania -**kontynuuje** bieżące wyrażenie: +**Rozpoznawanie instrukcji.** `atDeclaration()` zwraca prawdę dla `[` `]` (typ tablicowy), `TYP IDENT`, +`ptr [` i `IDENT . IDENT IDENT` (typ z modułu). W pozostałych przypadkach parser **parsuje całe +wyrażenie, a potem patrzy na następny token**: operator przypisania robi z niego cel (musi być `Var`, +`Member` albo `Index`), a inaczej musi to być wywołanie funkcji lub metody, w przeciwnym razie +`this value is computed but never used`. -```cpp -bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; } -``` - -Czyli: - -```c -a := 1 + // '+' jest w tej linii → wyrażenie przechodzi do następnej - 2 -b := (1 // w nawiasach (parenDepth > 0) nowe linie są ignorowane - + 2) -c := 1 --5 // '-' zaczyna nową linię → instrukcja skończyła się na '1'; "-5" to wtedy błąd -``` - -`parenDepth` rośnie wokół grup `( … )`, list argumentów i list parametrów. Przy wejściu do bloku -`{ }` jest **zapamiętywany i zerowany**, więc blok wewnątrz nawiasów (dziś to się nie zdarza) -i tak używałby reguł linii. - -`(` wywołania musi być w tej samej linii co nazwa funkcji. Inaczej `f⏎(x)` po cichu stałoby się wywołaniem. - -### Priorytety: jak w Go, nie jak w C - -| Poziom | Operatory | -|---|---| -| 5 | `*` `/` `%` `<<` `>>` `&` | -| 4 | `+` `-` `\|` `^` | -| 3 | `==` `!=` `<` `<=` `>` `>=` | -| 2 | `&&` | -| 1 | `\|\|` | - -Wszystkie operatory binarne są lewostronnie łączne. Porównania też (`a < b < c` się sparsuje), -ale sprawdzanie typów odrzuci potem porównanie `bool < int`. C stawia `&`, `^`, `|` **poniżej** `==`, -przez co `x & MASKA == 0` znaczy `x & (MASKA == 0)`. Finch przyjmuje tabelę z Go, co usuwa całą tę klasę błędów. - -### Jak rozpoznawane są instrukcje - -`statement()` rozgałęzia się po pierwszym tokenie: - -1. Słowa kluczowe (`if`, `while`, `for`, `return`, `break`, `continue`, `{`) są obsługiwane wprost. - `fn`, `import` i `link` wewnątrz bloku dają celowane komunikaty. -2. **Deklaracja z typem:** `atDeclaration()` zwraca prawdę, gdy bieżący token jest nazwą typu, - a następny identyfikatorem (`int x`), albo gdy to `ptr`, a po nim `[` (`ptr[int] p`). - To jedyne miejsce wymagające dwóch tokenów wyprzedzenia. -3. **Deklaracja z wnioskowaniem:** `IDENT`, a po nim `:=`. -4. W pozostałych przypadkach parser **najpierw parsuje całe wyrażenie**, a potem patrzy na następny - token. Jeśli to operator przypisania w tej samej linii, wyrażenie staje się celem przypisania. - Musi być `VarExpr` albo `MemberExpr`, inaczej: `only a variable or p.value can be assigned to`. - Takie podejście („najpierw wyrażenie, potem decyzja”) obsługuje dowolne formy l-wartości - (`pp.value.value = 1`) bez osobnej gramatyki l-wartości. -5. Samodzielne wyrażenie jako instrukcja musi być `CallExpr`. Wszystko inne daje - `this value is computed but never used`. - -### Poziom pliku - -`program()` przechodzi przez `import`, `link` i `fn`. Element zaczynający się nazwą typu dostaje -podpowiedź `functions start with 'fn', like: fn add(int a, int b) -> int { ... }`, co łapie -definicje funkcji pisane jak w C. Argument `link` jest walidowany: `"libfoo.so"` albo ścieżka -są odrzucane z podpowiedzią samej nazwy. +**Literały struktur to wywołania**: `Punkt(1, 2)` albo `Punkt(x: 1, y: 2)`. Literały w klamrach +(`Punkt{…}`) byłyby niejednoznaczne z blokami (`if gotowe {`); wywołanie nie potrzebuje specjalnej +gramatyki, a argumenty nazwane (`IDENT ":"` w `args()`) są akceptowane tylko przez konstruktory na etapie generowania kodu. --- ## 5. AST -Wszystkie węzły są w `src/ast.h`. Wyrażenia i instrukcje to małe hierarchie klas z jawnym -znacznikiem `kind`. Kod je przetwarzający robi `switch` po `kind` i `static_cast`, co pozwala -obejść się bez RTTI i `dynamic_cast` i sprawia, że rozgałęzienia są oczywiste. - -```cpp -struct Expr { ExprKind kind; Pos pos; virtual ~Expr(); }; -using ExprPtr = std::unique_ptr; -``` +Wszystkie węzły są w `src/ast.h`: małe hierarchie klas z jawnym `kind`, rozgałęziane przez +`switch` + `static_cast`. | ExprKind | Węzeł | Pola | |---|---|---| -| `Int` | `IntExpr` | `long long value` | -| `Float` | `FloatExpr` | `double value` | -| `Bool` / `Char` / `Str` | `BoolExpr` / `CharExpr` / `StrExpr` | `value` | +| `Int` `Float` `Bool` `Char` `Str` | literały | `value` | | `Null` | `NullExpr` | | | `Var` | `VarExpr` | `name` | -| `Unary` | `UnaryExpr` | `char op` (`-` `!` `~`), `operand` | +| `Unary` | `UnaryExpr` | `op` (`- ! ~`), `operand` | | `Binary` | `BinaryExpr` | `BinOp op`, `lhs`, `rhs` | -| `Call` | `CallExpr` | `std::string callee`, `args` | -| `Member` | `MemberExpr` | `obj`, `field` (w 1.0 ma sens tylko `value`) | +| `Call` | `CallExpr` | `callee` (nazwa), `args`, `argNames` | +| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) | +| `Index` | `IndexExpr` | `obj`, `index` | +| `ArrayLit` | `ArrayLitExpr` | `elems` | +| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; także `modul.fn(...)` | -| StmtKind | Węzeł | Pola | -|---|---|---| -| `Block` | `BlockStmt` | `std::vector body` | -| `VarDecl` | `VarDeclStmt` | `hasType`, `type`, `name`, `init` (może być null, gdy `hasType`) | -| `Assign` | `AssignStmt` | `target` (Var/Member), `char op` (`= + - * / %`), `value` | -| `Expr` | `ExprStmt` | `expr` (zawsze wywołanie) | -| `If` | `IfStmt` | `cond`, `then`, `otherwise` (null, `BlockStmt` albo zagnieżdżony `IfStmt` dla `else if`) | -| `While` | `WhileStmt` | `cond`, `body` | -| `For` | `ForStmt` | `var`, `from`, `to`, `body` | -| `Return` | `ReturnStmt` | `value` (może być null) | -| `Break` / `Continue` | | | +| StmtKind | Węzeł | +|---|---| +| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | jak w nazwie; `ForEachStmt` ma `var`, `list`, `body`; `DeferStmt` trzyma jedną instrukcję | -Wywołania przechowują funkcję **po nazwie**, a nie jako wyrażenie. Finch nie ma funkcji jako -wartości, a rozwiązywanie nazw dopiero w codegenie pozwala, żeby jeden `CallExpr` oznaczał -funkcję wbudowaną (`print`, `addr`), konwersję (`u8`), funkcję Fincha albo funkcję C (rozdział 8.10). +`math.sqrt(x)` parsuje się jako `MethodExpr` na `VarExpr("math")`; to, czy `math` jest modułem, czy zmienną, +rozstrzyga się przy generowaniu kodu. Wywołania nazywają funkcję, zamiast trzymać wyrażenie: Finch nie ma +funkcji jako wartości, a późne rozwiązywanie nazwy pozwala, by jeden `CallExpr` był funkcją wbudowaną, +konwersją (`u8(x)`), konstruktorem struktury, funkcją Fincha albo funkcją C. -### Przykład - -```c -x += add(x, 2) -``` - -parsuje się do: - -``` -AssignStmt (op '+') -├─ target: VarExpr "x" -└─ value: CallExpr "add" - ├─ VarExpr "x" - └─ IntExpr 2 -``` - -**W AST nie ma węzła „load”.** To, czy `VarExpr` znaczy „adres x”, czy „wartość w x”, zależy -od miejsca, w którym występuje. Decyduje o tym codegen (`place()` kontra `expr()`, rozdział 8.2). +Nie ma węzła „load”: to, czy `x` znaczy adres, czy wartość, zależy od kontekstu (`ref()` kontra `expr()`, §10.2). ### `Type` ```cpp struct Type { - enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null }; + enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, + Str, Ptr, Null, Array, Fixed, Struct, Named }; Kind kind; - std::shared_ptr elem; // tylko Ptr: typ wskazywany; null = `ptr` bez typu + std::shared_ptr elem; // Ptr (null = `ptr` bez typu), Array, Fixed + long long count; // Fixed + std::string name, module; // Named (jak napisano) / Struct (rozwiązany) + StructInfo *info; // Struct }; ``` -- `int` i `i64` to **ten sam typ** (`I64`), a `name()` wypisuje go jako `int`. Analogicznie `float` to `F64`. -- `Null` to typ wyłącznie literału `null`. Żadna zmienna nie może go mieć. -- `Void` to „typ” wywołań, które nic nie zwracają, i funkcji bez `->`. -- Równość jest strukturalna (`ptr[ptr[int]] == ptr[ptr[int]]`). -- Rodzaje są ułożone tak, że `isInt()` to `I8..U64`, `isSigned()` to `I8..I64`, a `isUnsigned()` to `U8..U64`. +Parser tworzy `Named` dla nazw struktur; `Codegen::resolve()` zamienia je na `Struct` ze wskaźnikiem +`info`. `Fixed` (`[N]T`) pochodzi tylko z pól struktur C. `int` to `I64`, a `float` to `F64`. +Równość jest strukturalna; dwa `Struct` są równe, gdy mają to samo `info`. --- -## 6. Model semantyczny +## 6. Wczytywanie: pliki i moduły -Całe sprawdzanie semantyczne odbywa się w `Codegen` podczas emisji IR. +`Loader::load(path, module, from)` w `main.cpp` czyta plik, dopisuje go do `g_files` (pozycje niosą +indeks pliku w `Pos::file`), leksuje go i parsuje, a potem wczytuje każdy jeszcze niewidziany `import nazwa`: +`nazwa.fn` obok importującego pliku albo w folderze z `FINCH_PATH`. Wynik to `std::vector`, +z głównym plikiem na początku. Cykle nie przeszkadzają: moduł jest oznaczany jako wczytany, zanim +zaczniemy podążać za jego importami. -### Funkcje +Wszystkie pliki trafiają do **jednego modułu LLVM**. Każdy moduł Fincha ma `ModuleScope` (funkcje, +struktury, zbiór modułów, które importuje). Nazwy szuka się w bieżącym module; nazwy innego modułu +są dostępne tylko z kwalifikatorem (`geometry.length`, `geometry.Vec`) i tylko tam, gdzie moduł +zaimportowano. Funkcje są emitowane jako `finch..` (`finch.` w głównym pliku), +typy struktur jako `finch..`. -`Codegen::run()` robi dwa przejścia po `Program::fns`: +--- -1. `declare()` tworzy każdą `llvm::Function` (tylko sygnaturę), więc **kolejność definicji nie ma - znaczenia** i działa rekurencja wzajemna. Sprawdza też: duplikaty nazw, nazwy wbudowane i sygnaturę - `main` (bez parametrów; nic nie zwraca, zwraca `int` albo `i32`). -2. `define()` generuje ciała. +## 7. Model semantyczny -Bez `main` kompilacja kończy się błędem `there is no main function`. +### Kolejność deklaracji -### Zasięgi +`Codegen::run()` najpierw deklaruje **wszystkie struktury** wszystkich modułów i zaimportowane struktury C, +rozwiązuje typy pól i ustawia ciała typów LLVM (`declareStructs` → `resolveStruct`), potem deklaruje +**wszystkie sygnatury funkcji** (`declare`), a dopiero potem definiuje ciała. Kolejność w pliku i między +modułami nigdy więc nie ma znaczenia, a rekurencja (także wzajemna i między modułami) działa. -`scopes` to `std::vector>`. Nowa mapa jest wkładana dla -parametrów funkcji, dla każdego bloku `{ }` i dla każdej pętli `for` (trzyma zmienną pętli). +`resolveStruct` wykrywa cykle przez wartość trójstanową flagą (`struct A { A inner }` to błąd z radą, +by użyć `ptr[A]` albo `[]A`). Wskaźniki i tablice struktury nie potrzebują jej układu, więc +`struct Wezel { []Wezel dzieci; ptr[Wezel] rodzic }` jest w porządku: rozwiązują nazwę „płytko” +(`resolveT(…, deep=false)`). + +### Zasięgi i zmienne ```cpp -struct Var { AllocaInst *slot; FType type; bool readonly; }; +struct Var { Value *slot; FType type; bool readonly; std::string readonlyWhy; + bool owned; int order; bool moved; }; +struct Cleanup { bool isDefer; std::string var; const Stmt *body; int order; }; +struct Scope { unordered_map vars; vector cleanups; }; ``` -**Bez przesłaniania:** `addVar()` odrzuca nazwę, którą `lookup()` znajdzie w *dowolnym* -zewnętrznym zasięgu, a nie tylko w bieżącym. Odrzuca też nazwy funkcji i wbudowanych. -To świadoma reguła prostoty: w całym ciele funkcji nazwa znaczy jedną rzecz. +Zasięg jest otwierany dla parametrów funkcji, każdego `{ }`, każdego `for` (trzyma `i`), każdego `for … in` +(ukryta tymczasowa lista) i każdej jego iteracji (trzyma `x`). -**Tylko do odczytu:** zmienne `for` mają `readonly`. Przypisanie do nich albo `addr()` na nich -to błąd, więc licznika pętli nie da się zmienić za jej plecami. +- **Bez przesłaniania:** `addVar()` odrzuca nazwę znalezioną w *dowolnym* zewnętrznym zasięgu, + funkcję modułu, nazwę zaimportowanego modułu i funkcję wbudowaną. +- **Tylko do odczytu:** zmienne pętli zakresowej i pętli po elementach (`readonlyWhy` daje konkretny komunikat). +- `order` numeruje deklaracje; `defer` używa go, by ukryć zmienne zadeklarowane po nim (§9.5). -### Rozwiązywanie identyfikatorów +### Rozwiązywanie nazw -Dla `VarExpr name` w pozycji wartości kolejność jest taka: - -1. zmienna lokalna (`lookup`), -2. stała C (`cimports.consts`: wartości enum, liczby z `#define`), -3. zmienna globalna C (`cimports.globals`: `stdout`, …), -4. błąd `there is no variable named 'name'`. - -Dla wywołań: `print` → `addr` → nazwa typu (konwersja) → funkcja Fincha → funkcja C → błąd. -Ponieważ funkcje Fincha są sprawdzane przed funkcjami C, **funkcja Fincha przesłania funkcję C -o tej samej nazwie**. Funkcja Fincha jest emitowana jako `finch.`, więc oba symbole -współistnieją przy linkowaniu. +`x` w pozycji wartości: zmienna lokalna → stała C → zmienna globalna C → (nazwa modułu: błąd z podpowiedzią) → błąd. +Wywołanie `n(...)`: funkcje wbudowane (`print addr input new free exit shell read_file write_file file_exists`) → +nazwy typów (konwersja) → funkcja bieżącego modułu → struktura bieżącego modułu / struktura C +(konstruktor) → funkcja C → błąd (z podpowiedzią, jeśli funkcja istnieje w innym module, albo z +`headerHint` dla znanych nazw z libc). --- -## 7. System typów +## 8. System typów ### Reprezentacja w LLVM -| Finch | LLVM | Uwagi | -|---|---|---| -| `bool` | `i1` | | -| `char`, `i8`, `u8` | `i8` | znakowość jest w typie Fincha, nie w LLVM | -| `i16`, `u16` | `i16` | | -| `i32`, `u32` | `i32` | | -| `int`/`i64`, `u64` | `i64` | | -| `f32` | `float` | | -| `float`/`f64` | `double` | | -| `str` | `ptr` | napis C zakończony zerem | -| `ptr`, `ptr[T]` | `ptr` | LLVM 21 ma nieprzezroczyste wskaźniki; typ wskazywany istnieje tylko w typie Fincha | -| `null` | `ptr` | `ConstantPointerNull` | -| nic | `void` | | +| Finch | LLVM | +|---|---| +| `bool` | `i1` | +| `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ść) | +| `ptr`, `ptr[T]`, `null` | `ptr` (nieprzezroczysty) | +| `[N]T` | `[N x T]` | +| struktura Fincha | nazwany typ struct, naturalny układ | +| struktura C | nazwany **upakowany** (packed) struct z jawnymi bajtami wypełnienia (§13.2) | -Liczby całkowite w LLVM nie mają znaku. Każda operacja, w której znak ma znaczenie -(`sdiv`/`udiv`, `srem`/`urem`, `ashr`/`lshr`, `icmp slt`/`ult`, `sext`/`zext`, -`sitofp`/`uitofp`, `fptosi`/`fptoui`), wybiera instrukcję na podstawie typu **Fincha**. +Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`, +`sext/zext`, `sitofp/uitofp`, `fptosi/fptoui`. -### Wartości w codegenie +### Wartości ```cpp -struct Value_ { - Value *v; // wartość IR - FType type; // jej typ w Finchu - bool literal; // liczba wpisana w kodzie (albo zaimportowana stała C) -}; +struct Value_ { Value *v; FType type; bool literal; bool fresh; }; ``` -### Literały („stałe bez typu”) +- **`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ć (§9). -`literal` jest ustawiane dla `IntExpr`, `FloatExpr` i zaimportowanych stałych C. Przechodzi -przez minus jednoargumentowy i przez arytmetykę **między dwoma literałami**, która zwija się do -stałej (`arith()` ustawia `literal = l.literal && r.literal && isa(wynik)`). **Jawna -konwersja je kasuje:** `u8(250)` to stała typu `u8`, a nie literał. +### Konwersje niejawne: `coerce(v, want)` -Literał całkowity może stać się **dowolnym typem całkowitym, w którym się mieści** (`fits()` -sprawdza zakres, uwzględniając znakowość źródłowego literału). Dzięki temu `u8 b = 200`, -`malloc(4 * 10)` (do `u64`) i `glClear(GL_COLOR_BUFFER_BIT)` (makro `i64` do parametru `u32`) -działają bez rzutowań, a `u8 b = 300` jest odrzucane komunikatem `the number 300 doesn't fit in u8`. -Literał `f64` może stać się `f32` (`f32 h = 0.5`). +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. -Czemu nie wystarczy sprawdzać `isa`? `IRBuilder` zwija stałe, więc `u8(250)` też jest -`ConstantInt`. Gdyby regułą było „jest stałą”, `u8(250) + 10` po cichu rozszerzyłoby się do -`int` i dało 260, a ten sam kod ze zmienną `u8` zawinąłby się do 4. Jawna flaga sprawia, że -zachowanie zależy od tego, co *napisałeś*, a nie od tego, co zdołał zobaczyć optymalizator. +### Argumenty operatorów: `unify(l, r)` -### Konwersje niejawne: `coerce(value, want, pos, what)` +Strona-literał dopasowuje się do drugiej; 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`. -Używane wszędzie, gdzie wartość trafia do miejsca o znanym typie: inicjalizatory, przypisania, -argumenty, wartości zwracane, warunki (`want = bool`), granice zakresu (`want = int`). -Dozwolone, po kolei: +### Konwersje jawne: `convert()` -1. identyczne typy, -2. `null` → dowolny `ptr…` albo `str`, -3. dowolny `ptr…` albo `str` → `ptr` bez typu, a `ptr` bez typu → dowolny `ptr[T]` (reguły `void*`; tak działa `ptr[i32] n = malloc(4)`), -4. int → int, gdy wartość jest mieszczącym się literałem **albo** `widens(from, to)`: - - ta sama znakowość i ściśle więcej bitów, **albo** - - bez znaku → ze znakiem o ściśle większej liczbie bitów, - - (ze znakiem → bez znaku nigdy się nie rozszerza), -5. dowolna całkowita → dowolna zmiennoprzecinkowa (`sitofp`/`uitofp`), -6. `f32` → `f64` (`fpext`), -7. literał `f64` → `f32` (`fptrunc`, zwijane do stałej). - -Wszystko inne to błąd. Komunikat podpowiada jawną konwersję (`use i32(...) to convert`) -i ostrzega, gdy obetnie wartość (`it cuts off the fraction`). - -### Argumenty operatorów binarnych: `unify(l, r)` - -Przed arytmetyką i porównaniem obie strony są sprowadzane do jednego typu: - -1. ten sam typ → gotowe, -2. obie całkowite: strona-literał dopasowuje się do drugiej, jeśli się mieści; w przeciwnym razie - rozszerzana jest strona, która `widens()` do drugiej; w przeciwnym razie **błąd** (`i32` + `u32`: - żadna nie rozszerza się do drugiej, a Finch nie zgaduje), -3. całkowita i zmiennoprzecinkowa → całkowita zamieniana na ten typ zmiennoprzecinkowy, -4. `f32` i `f64` → obie `f64`. - -Typ wyniku arytmetyki to ten wspólny typ. **Nie ma promocji całkowitych jak w C:** -`u8 + u8` liczy się w `u8` i się zawija. To celowe: tego wymaga typ z rozmiarem, i tak samo działają Rust i Zig. - -### Konwersje jawne: `convert(call, to)` - -`T(x)` parsuje się jako wywołanie o nazwie typu i jest tłumaczone tutaj: - -| Z → Na | Instrukcja | +| Z → Na | IR | |---|---| -| int/char → int/char | `CreateIntCast(signed = from.isSigned())` → `trunc` / `sext` / `zext` | -| bool → int/char | `zext` | -| float → int | `fptosi` / `fptoui` (wg znakowości celu) | -| int → float | `sitofp` / `uitofp` (wg znakowości źródła) | -| float → float | `fpext` / `fptrunc` (`CreateFPCast`) | -| int → bool | `icmp ne x, 0` | -| ptr/str/null → ptr…, ptr → str | brak instrukcji (wszystko to `ptr`) | +| int/char ↔ int/char | `trunc`/`sext`/`zext` wg znakowości źródła | +| bool → int | `zext` | +| 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` (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 | -`fptosi` dla wartości spoza zakresu to w LLVM poison (w C: UB). Finch 1.0 tego nie sprawdza, zobacz rozdział 15. +### Porównania -### Porównania: `compare()` - -- **Wskaźniki:** jeśli którakolwiek strona jest `ptr…` albo `null`, dozwolone są tylko `==`/`!=`, - kompilowane do `icmp eq/ne ptr`. `str` porównywany z `null` też porównuje adresy. -- **`str == str`:** wywołanie `strcmp`, a potem `icmp eq/ne i32 %r, 0` (porównanie treści). `<`/`>` na `str` są odrzucane. -- **Liczby zmiennoprzecinkowe:** `oeq olt ole ogt oge` (uporządkowane: fałsz dla NaN) oraz `une` dla `!=` - (nieuporządkowane: `NaN != NaN` to prawda). Tak samo jak w C. -- **Całkowite / char:** `eq ne` oraz `slt sle sgt sge` albo `ult ule ugt uge` wg znakowości. `char` porównywany jest bez znaku. -- **bool:** tylko `==`/`!=`. +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. --- -## 8. Generowanie kodu +## 9. Własność i pamięć -`Codegen` posiada `llvm::IRBuilder<> b`, `Module`, tablicę funkcji `fns` -(`nazwa → {Function*, const FnDecl*}`), `scopes`, stos `loops` i `curFn`. +### 9.1 Model -### 8.1 Zmienne to miejsca na stosie +Każda wartość **typu posiadającego pamięć** (`str`, `[]T` albo struktura Fincha z takim polem) ma +dokładnie jednego właściciela: zmienną, element tablicy, pole struktury albo komórkę na stercie z `new()`. +Właściciel zwalnia wartość, gdy sam znika. Nie ma liczenia referencji ani garbage collectora; +każda decyzja zapada statycznie. -Każda zmienna, **łącznie z parametrami**, dostaje `alloca` tworzoną przez `slot()`. -`slot()` zawsze wstawia ją **na początek bloku wejściowego**, drugim `IRBuilder` ustawionym na -`entry.begin()`, niezależnie od miejsca deklaracji. Tylko takie `alloca` `mem2reg`/`SROA` umieją -zamienić na rejestry SSA, więc wyjście O2 nie ma ruchu na stosie dla wartości skalarnych. -Ponieważ każde nowe miejsce trafia na sam początek, w IR z `-O0` `alloca` są w **odwrotnej kolejności -deklaracji** (`%b2` przed `%a1` niżej). +| Sytuacja | Co się dzieje | +|---|---| +| `x := expr` / `T x = expr` / `x = expr` / pole, element, `push` | **`own(v)`**: wartość fresh jest przenoszona; pożyczona jest kopiowana w głąb | +| `x = …` (typ posiadający) | najpierw liczona jest nowa wartość, stara zwalniana po zapisie | +| koniec bloku, `return`, `break`, `continue` | posiadane zmienne każdego opuszczanego zasięgu są zwalniane (od najnowszej), uruchamiają się defery | +| `return lokalna` | zmienna jest **przenoszona na zewnątrz** (oznaczana jako `moved` na czas sprzątania tego returna) | +| przekazanie argumentu | **pożyczka**: funkcja dostaje płytką kopię i nigdy jej nie zwalnia; argumenty fresh zwalnia wywołujący po wywołaniu | +| parametr, który funkcja zmienia | kopiowany na wejściu i posiadany przez funkcję (decyduje `mutates()`, niżej) | +| wartość tymczasowa, której nikt nie trzyma (`print(a + b)`, `f(g())`, instrukcja-wyrażenie) | **`release(v)`**: zwalniana zaraz po użyciu | +| `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (fresh) | +| `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało może zmieniać `lista`, elementy są kopiowane | -Parametry są kopiowane do swoich miejsc na wejściu do funkcji (`store %a, %a1`), dzięki czemu -parametry są zwykłymi, modyfikowalnymi zmiennymi lokalnymi. +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życzane z `cap = -1` i zawsze kopiowane, zanim zostaną zatrzymane. -### 8.2 Load i store: `expr()` kontra `place()` +### 9.2 Pożyczanie parametrów: `mutates()` -To jest serce generowania dostępów do pamięci: +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. -- `place(e)` zwraca **`Place {Value *addr; FType type}`**, czyli *adres* l-wartości. - - `VarExpr` → `alloca` zmiennej (albo `GlobalVariable` zmiennej globalnej C). - - `MemberExpr` `p.value` → `expr(p)` (które **ładuje wskaźnik** z miejsca p) plus kontrola null - (rozdział 9). Adresem jest sam załadowany wskaźnik, a typem `*p.type.elem`. -- `expr(e)` zwraca **`Value_`**, czyli r-wartość. Dla `Var` i `Member` woła `place()`, a potem - emituje **jeden `load`** typu `ty(place.type)` z `place.addr`. +### 9.3 Kopiowanie i zwalnianie -Instrukcja `load` dla identyfikatora powstaje więc w `Codegen::expr()`, w przypadku `ExprKind::Var`, -jako `b.CreateLoad(ty(pl.type), pl.addr, v.name)`. Nazwa wzięta od zmiennej sprawia, że IR z `-O0` -pokazuje `%x1`, `%x2` itd. (LLVM dokleja numery do powtórzonych nazw). `p.value` kosztuje **dwa** -loady: wskaźnika i wartości wskazywanej. `pp.value.value` kosztuje trzy. +`dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach: -Instrukcje store powstają w dokładnie trzech miejscach: `addVar()` (inicjalizacja), `assign()` i krok pętli `for`. +- `str`: `finch_str_drop` (zwalnia, gdy `cap > 0`) / `finch_str_copy` (literały współdzieli, resztę duplikuje). +- `[]T` i struktury posiadające: generowane helpery `finch.drop.` i `finch.copy.`, tworzone + raz na typ (`dropFn`, `copyFn`, klucz z `typeKey()`, np. `arr_S_main_Gracz`). Tablice zwalniają każdy posiadający + element, a potem `finch_arr_free`; kopie wołają `finch_arr_clone_raw`, a potem kopiują w głąb posiadające + elementy na surowe bity. Struktury zwalniają/kopiują tylko pola posiadające. -### 8.3 Przypisanie i kolejność obliczeń +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. + +### 9.4 Jak to wygląda ```c -x += add(x, 2) -``` +fn greet(str name) -> str { + msg := "Hi, " + name + return msg +} -`assign()` **najpierw** liczy `place(target)`. Dla operatorów złożonych potem **ładuje bieżącą -wartość**, następnie oblicza prawą stronę, wywołuje `arith()`, robi `coerce()` z powrotem do typu -celu i zapisuje. Prawdziwe wyjście z `-O0`: - -```llvm -define i32 @main() { -entry: - %x = alloca i64, align 8 - store i64 5, ptr %x, align 8 ; x := 5 - %0 = load i64, ptr %x, align 8 ; bieżące x (dla +=) - %x1 = load i64, ptr %x, align 8 ; argument x - %1 = call i64 @finch.add(i64 %x1, i64 2) - %2 = add i64 %0, %1 - store i64 %2, ptr %x, align 8 - %x2 = load i64, ptr %x, align 8 - %3 = call i32 (ptr, ...) @printf(ptr @fmt, i64 %x2) - ret i32 0 +fn main() { + a := ["x"] + b := a + b.push(greet("Ola")) + print(b[1]) } ``` -Kolejność obliczeń w Finchu jest **zawsze od lewej do prawej**: cel, potem argumenty operatora -binarnego od lewej, potem argumenty wywołania od lewej. C zostawia większość tego nieokreśloną. -Zauważ, że w `x += f()` stara wartość `x` jest czytana **przed** wywołaniem `f()`. Gdyby `f` -zmieniła `x` przez wskaźnik, ta zmiana zostałaby nadpisana. To to samo co `x = x + f()` -czytane od lewej do prawej. - -Po O2 cały program powyżej zwija się do `printf(@fmt, i64 12)`. - -### 8.4 Funkcje, linkowanie i `main` +IR funkcji `main` z `-O0` (skrócony): ```llvm -define internal i64 @finch.add(i64 %a, i64 %b) { -entry: - %b2 = alloca i64, align 8 - %a1 = alloca i64, align 8 - store i64 %a, ptr %a1, align 8 - store i64 %b, ptr %b2, align 8 - %a3 = load i64, ptr %a1, align 8 - %b4 = load i64, ptr %b2, align 8 - %0 = add i64 %a3, %b4 - ret i64 %0 -} + %3 = call ptr @finch_alloc(i64 24) ; ["x"]: jeden element na stercie + store { ptr, i64, i64 } { ptr @str.1, i64 1, i64 0 }, ptr %4 ; literał, cap 0 + ... + store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (fresh) + %a1 = load { ptr, i64, i64 }, ptr %a + store { ptr, i64, i64 } %a1, ptr %1 + call void @finch.copy.arr_str(ptr %2, ptr %1) ; b := a kopiuje + %8 = load { ptr, i64, i64 }, ptr %2 + store { ptr, i64, i64 } %8, ptr %b + %9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 }) + ... + 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]: kontrola zakresu + br i1 %20, label %out.of.range, label %in.range + ... + call void @finch.drop.arr_str(ptr %b) ; koniec main: od najnowszej + call void @finch.drop.arr_str(ptr %a) + ret void ``` -- Funkcje użytkownika nazywają się **`finch.`** i mają **wiązanie wewnętrzne** (internal linkage). - Kropka nie może wystąpić w identyfikatorze C, więc funkcja Fincha nigdy nie zderzy się z symbolem - libc ani biblioteki, a wiązanie wewnętrzne pozwala LLVM swobodnie je wstawiać (inline), - specjalizować albo usuwać. W C funkcja bez `static` jest zewnętrzna i musi zostać zachowana - tak, jak ją napisano. To prawdopodobnie jeden z powodów, dla których rekurencyjne `fib(40)` - działa odrobinę szybciej niż ten sam kod C pod `clang -O2` (ok. 0,19 s wobec 0,23 s na maszynie autora). -- `main` jest emitowane jako **`i32 @main()` z wiązaniem zewnętrznym**. `fn main()` zwraca `i32 0` - na każdej ścieżce wyjścia, także przy jawnym `return`. `fn main() -> int` przycina swój wynik - `i64` przez `CreateIntCast(…, i32, signed)`. -- Brakujący return: jeśli po ciele funkcji bieżący blok nie ma terminatora, funkcje `void` dostają - `ret void` (albo `ret i32 0` dla `main`). Funkcje zwracające wartość dostają błąd kompilacji: - `function 'f' can reach its end without returning int`. Sprawdza się to na faktycznie zbudowanym - CFG (rozdział 8.5), więc `if/else` zwracające wartość w obu gałęziach jest akceptowane bez dodatkowej analizy. -- Arytmetyka jest emitowana **bez flag `nsw`/`nuw`**, więc przepełnienie liczb całkowitych to - zdefiniowane zawinięcie w kodzie uzupełnień do dwóch (rozdział 15). +a `greet` zwraca `msg` bez kopiowania (`ret … %5` po wczytaniu slotu: zmienna została przeniesiona). -### 8.5 Sterowanie przebiegiem i stan „zakończony” +### 9.5 defer -Blok, do którego wstawia builder, jest jedynym źródłem prawdy o osiągalności: +`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ą. + +### 9.6 Pamięć ręczna + +`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. + +### 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 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. + +--- + +## 10. Generowanie kodu + +### 10.1 Zmienne + +Każda zmienna, łącznie z parametrami, dostaje `alloca` na początku bloku wejściowego (`slot()`; +struktury C dostają wyrównanie z C). `addVar()` zapisuje wartość początkową, rejestruje zmienną, +dokłada sprzątanie, jeśli zmienna posiada pamięć, i (z `-g`) zgłasza ją debuggerowi. + +### 10.2 Miejsca: `ref()`, `place()`, `expr()` ```cpp -bool terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; } +struct LRef { bool isPlace; Place pl; Value_ val; }; // adres albo (jeśli go nie ma) wartość ``` -Po `return`, `break` albo `continue` bieżący blok ma terminator. -`blockBody()` sprawdza `terminated()` **przed każdą instrukcją**. Jeśli jest ustawiony, instrukcja -jest nieosiągalna i to błąd kompilacji (`this code can never run`). Martwe bloki dla kodu po skoku nigdy nie powstają. +`ref(e, forWrite)` przechodzi łańcuch `Var`/`Member`/`Index` i zwraca **adres**, jeśli istnieje: -`continueAt(bb, deadEnd)` jest używane po `if` i `while`. Jeśli blok łączący `bb` **nie ma -poprzedników** (wszystkie ścieżki wróciły albo przerwały), zostaje usunięty, a builder zostaje -w `deadEnd`, który jest zakończony. Stan „zakończony” propaguje się więc na zewnątrz przez -zagnieżdżone konstrukcje i widzi go sprawdzanie brakującego returna. +- `Var` → jej `alloca` (albo zmienna globalna C). +- `.pole` na miejscu-strukturze → `getelementptr` do pola (`llvmIndex`; struktury C pomijają elementy wypełnienia). +- przez wskaźnik (`p.value`, `p.pole`, `p[i]`) → wczytanie wskaźnika, kontrola null, adres celu. + `p.value` na wskaźniku do struktury z własnym polem `value` oznacza to pole. +- `a[i]` na miejscu-tablicy/tekście → wczytanie `{ptr,len,cap}`, `boundsCheck(i, len)`, `getelementptr`. + Przy `forWrite` na `str` najpierw `finch_str_own` (kopiowanie przy zapisie dla literałów). `[N]T` sprawdza względem `N`. +- `.len` / `.ptr` → wartości. +- wartość tymczasowa (wynik wywołania…) nie ma adresu: jej pole/element jest wyciągany, kopiowany + (jeśli posiada pamięć), a sama wartość zwalniana. -**`if / else if / else`:** bloki `then`, `else` (jeśli jest) i `endif`. `else if` to `IfStmt` -w `otherwise`, generowany rekurencyjnie wewnątrz bloku `else`. +`expr()` dla tych rodzajów woła `ref()` i wczytuje spod adresu: stąd bierze się każdy `load` zmiennej, +pola albo elementu. `place()` wymaga adresu (cele przypisań, `addr()`). -**`while`:** bloki `while.cond`, `while.body`, `while.end`. Jeśli warunek zwinie się do stałej -`true`, emitowany jest bezwarunkowy `br`, więc `while.end` ma poprzedników tylko przez `break`. -`while true` bez `break` zostawia więc funkcję „zakończoną”, a funkcja, której ostatnią instrukcją -jest taka pętla, nie potrzebuje końcowego `return`. +### 10.3 Przypisanie -**`for i in a..b`:** `a` i `b` są obliczane **raz**, przed pętlą, i zamieniane na `int`. -Bloki: `for.cond` (`icmp slt i, end`), `for.body`, `for.step` (`i + 1`), `for.end`. `continue` -skacze do `for.step`, a nie do `for.cond`, więc inkrementacja nigdy nie jest pomijana. -Zmienna pętli żyje we własnym zasięgu wokół pętli. +`x = v`: **najpierw** liczone jest `v` (może przenieść pamięć, w której żyje cel: `a[0] = f()`, gdzie `f` +robi `push` do `a`), potem `place(cel, forWrite)`, `coerce`, `own`, wczytanie starej wartości, zapis i +zwolnienie starej. Złożone `x += v`: liczenie `v`, wczytanie celu, `arith()`, zapis, zwolnienie starej +wartości (dla `str +=`). -```llvm - store i64 0, ptr %i, align 8 - br label %for.cond -for.cond: - %3 = load i64, ptr %i, align 8 - %4 = icmp slt i64 %3, 2 - br i1 %4, label %for.body, label %for.end -for.body: - ... - br label %for.step -for.step: - %6 = load i64, ptr %i, align 8 - %7 = add i64 %6, 1 - store i64 %7, ptr %i, align 8 - br label %for.cond -for.end: -``` +### 10.4 Funkcje i `main` -**`break` / `continue`:** `loops` to stos par `{continueTo, breakTo}`. `while` odkłada -`{while.cond, while.end}`, a `for` odkłada `{for.step, for.end}`. Poza pętlą to błąd. +Funkcje są `internal` z naturalną sygnaturą LLVM (agregaty przez wartość). Parametry posiadające +pamięć są kopiowane na wejściu tylko wtedy, gdy `borrowParam` jest fałszywe. `fn main` to wewnętrzna +`finch.main`; osobna zewnętrzna `i32 main(i32, ptr)` (`defineMainWrapper`) buduje `[]str args` przez +`finch_args`, jeśli trzeba, woła ją, zwalnia argumenty i zwraca kod wyjścia. Funkcja, która dochodzi do +swojego końca, dostaje sprzątanie zasięgów i `ret void` albo błąd `can reach its end without returning T`. -### 8.6 Skrócone `&&` / `||`: jedyne ręcznie budowane φ +### 10.5 Sterowanie przebiegiem + +Bieżący blok buildera jest źródłem prawdy o osiągalności (`terminated()`): instrukcje po terminatorze to +błąd (`this code can never run`). `continueAt()` usuwa blok łączący, do którego nikt nie skacze. +`if`/`while`/`for` mają klasyczny układ bloków; `while true` bez `break` zostawia funkcję zakończoną +(nie trzeba po nim `return`). `for i in a..b` liczy oba końce raz; `continue` idzie do `for.step`. +`for x in lista` czyta długość listy z jej adresu **w każdej rundzie** (`push` w środku pętli jest bezpieczny) +i trzyma indeks w slocie na stosie. `break`/`continue` emitują sprzątanie zasięgów otwartych wewnątrz pętli +(`Loop::scopeDepth`) przed skokiem. + +### 10.6 Skrócone `&&` / `||` + +Jedyne ręcznie budowane φ: lewa strona kończy się w bloku `from`, prawa w `rhsEnd`; +`phi i1 [!isAnd, from], [rhs, rhsEnd]`. + +### 10.7 Wywołania + +`callFinch()` konwertuje argumenty na typy parametrów (literały tablic przez `exprWant`), przekazuje +posiadające płytko, woła, a potem zwalnia argumenty fresh. Wyniki typów posiadających są fresh. +`construct()` buduje strukturę od `zeroinitializer` przez `insertvalue`: pozycyjnie (wszystkie pola) +albo po nazwie (brakujące biorą wyrażenie domyślne pola, liczone w module struktury z podmienionymi +zasięgami wywołującego, albo zero). Wywołania C opisuje §13. + +### 10.8 Wypisywanie + +`print` emituje `printf` dla każdego argumentu z formatem wybranym wg typu (`%lld`, `%llu`, `%g`, `%c`, +`%.*s` dla `str` z jego długością, `%p`), oddzielając spacjami, a na końcu `\n`. Tablice, tablice o stałym +rozmiarze i struktury wołają generowane helpery `finch.print.`, które wypisują `[1, 2]` / +`Punkt(x: 1, y: 2)`, biorąc teksty i znaki w środku w cudzysłów. + +### 10.9 Metody tablic i tekstów + +Tablice: `push` (`finch_arr_reserve` + zapis + len++), `pop`/`remove` (przeniesienie na zewnątrz, +`remove_gap`), `insert` (`insert_gap`), `clear`/`resize` (zwolnienie znikających elementów, +`finch_arr_resize`), `find`/`contains` (pętla z `compare(Eq)`), `slice` (nowa tablica, `copyAt` na element), +`reverse` (pętla zamian), `sort` (`qsort` z libc z generowaną porównywarką `finch.cmp.`), `join` (runtime). +Metody zmieniające tablicę wymagają adresu (zmienna/pole/element). +Teksty: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, wszystkie w runtime. + +--- + +## 11. Kontrole w czasie działania + +Wszystkie błędy w czasie działania idą przez `finch_panic(plik, linia, msg)` / +`finch_panic_index(plik, linia, i, len)` (zadeklarowane `noreturn cold`), więc gałąź błędu jest układana +jako zimna, a kontrola kosztuje porównanie i dobrze przewidywany skok. LLVM usuwa kontrole, które potrafi +udowodnić (stałe indeksy w zakresie, kontrole wyciągnięte z pętli). + +| Kontrola | Gdzie | +|---|---| +| dzielenie/reszta przez zero (stałe zero: błąd kompilacji) | `checkDivisor()` | +| `MIN / -1` dla typów ze znakiem | `checkDivisor()` | +| indeks tablicy/tekstu/tablicy stałej poza zakresem (`icmp uge`, więc także ujemne) | `boundsCheck()` | +| `.value`, `.pole`, `[i]` przez null | `member()`, `index()` | +| `pop()` na pustej, `slice()`/`sub()` poza zakresem | metody, runtime | +| zły tekst w `int(s)`/`float(s)`, nieczytelny plik w `read_file` | runtime | + +Sito na 20 milionów elementów z kontrolą zakresu przy każdym dostępie działa tak szybko jak to samo w C +(ok. 0,09 s): kontrole są wyciągane z pętli albo zwijane. + +--- + +## 12. Runtime w C + +`runtime/finch_rt.c` to zwykłe C bez stanu globalnego. CMake wbudowuje go w kompilator jako surowy +napis (`rt_source.inc`); przy pierwszym użyciu `runtimeObject()` kompiluje go przez `$CC -O2 -fPIC -c` +do `~/.cache/finch/rt-.o`, gdzie hash obejmuje źródło, wersję Fincha i kompilator C. +Jest linkowany statycznie do każdego programu. + +Wspólny układ: ```c -fn check(int a, int b) -> bool { - return a > 0 && b > 0 -} +typedef struct { char *ptr; int64_t len; int64_t cap; } FStr; // ptr zawsze zakończony zerem +typedef struct { void *ptr; int64_t len; int64_t cap; } FArr; +// cap tekstu: > 0 sterta (posiadany), 0 literał (statyczny), -1 pożyczony z C (skopiuj, zanim zatrzymasz) ``` -```llvm - %a3 = load i64, ptr %a1, align 8 - %0 = icmp sgt i64 %a3, 0 - br i1 %0, label %and.rhs, label %and.end -and.rhs: - %b4 = load i64, ptr %b2, align 8 - %1 = icmp sgt i64 %b4, 0 - br label %and.end -and.end: - %2 = phi i1 [ false, %entry ], [ %1, %and.rhs ] -``` - -`logic()` zapamiętuje blok, w którym lewa strona się **skończyła** (`from`), i blok, w którym -skończyła się prawa (`rhsEnd`). To niekoniecznie bloki, w których obliczanie się zaczęło, bo -zagnieżdżone `&&` tworzy własne bloki. φ używa `!isAnd` jako stałej skrótu (`false` dla `&&`, `true` dla `||`). - -### 8.7 `print` - -`print(a, b, …)` kompiluje się do **jednego wywołania `printf`**. Format jest budowany w czasie -kompilacji z typów argumentów, łączony spacjami i zakończony `\n`: - -| Typ Fincha | Format | Konwersja argumentu | -|---|---|---| -| całkowite ze znakiem | `%lld` | `sext` → `i64` | -| całkowite bez znaku | `%llu` | `zext` → `i64` | -| `f32`, `f64` | `%g` | `fpext` → `double` | -| `char` | `%c` | `zext` → `i32` | -| `str` | `%s` | bez zmian | -| `bool` | `%s` | `select i1, "true", "false"` | -| `ptr…` | `%p` | bez zmian (glibc wypisuje `(nil)` dla null) | -| `null` | dosłowny tekst `null` | brak | - -Format jest generowany, więc teksty użytkownika zawsze idą jako argumenty `%s`, nigdy jako -format. `%` w tekście użytkownika jest bezpieczne. O2 często zamienia `printf("%s\n", s)` na `puts(s)`. - -### 8.8 Teksty - -Literał tekstowy to prywatna stała globalna `unnamed_addr` (`CreateGlobalString`), a jego -wartością jest `ptr` na nią. Optymalizator może scalać identyczne literały. Zmienna `str` -zadeklarowana bez wartości wskazuje na wspólną stałą `""`. W 1.0 teksty są **niezmienne -i statyczne**. Nie ma sklejania ani alokacji (rozdział 18). - -### 8.9 `addr()` i wskaźniki - -`addrOf()` wymaga argumentu `Var` albo `Member` i zwraca `place(arg).addr` z typem `ptr[T]`. -Nie ma żadnego load: `addr(x)` *to jest* `alloca` zmiennej x. Takiej `alloca` mem2reg nie zamieni -na rejestr, co jest spodziewanym kosztem pobrania adresu. `addr(p.value)` daje wskaźnik już -zapisany w `p`, po kontroli null. `addr` zmiennej `for` jest odrzucane (jest tylko do odczytu). - -### 8.10 Wywołania: `callFinch()` kontra `callC()` - -Obie sprawdzają liczbę argumentów i robią `coerce()` każdego argumentu do typu parametru. Różnice: - -| | `callFinch()` | `callC()` | -|---|---|---| -| Wołana funkcja | `finch.`, utworzona w `declare()` | `getOrInsertFunction(nazwa, …)`: deklarowana leniwie przy pierwszym użyciu, potem używana ponownie | -| Zmienna liczba argumentów | nigdy | obsługiwana; dodatkowe argumenty dostają domyślne promocje C | -| Atrybuty parametrów | niepotrzebne | `signext`/`zeroext` dla parametrów i wartości zwracanych węższych niż 32 bity, na deklaracji i w miejscu wywołania | -| Nieobsługiwane sygnatury | niemożliwe | odrzucane z powodem zapisanym przez cimport (`passes a C struct by value`, …) | -| Wywołanie `main` | odrzucane | nie dotyczy | - -Domyślne promocje dla części zmiennej: `f32` → `double`; `bool`, `char` i liczby całkowite -węższe niż 32 bity → `i32` (z rozszerzeniem znaku albo zerami). Wskaźniki, `str` i liczby -32/64-bitowe przechodzą bez zmian. To odwzorowuje reguły C, których oczekuje `va_arg` w `printf`. - -Po co `signext`/`zeroext`: na x86-64 System V clang przekazuje `char`/`short`/`_Bool` już -rozszerzone do 32 bitów i tak je oznacza. Niektóre funkcje na tym polegają. Bez atrybutu LLVM -zostawiłby górne bity nieokreślone, co byłoby niezgodnością ABI z kodem C skompilowanym clangiem. +Funkcje przyjmują i zwracają te struktury **przez wskaźnik**, więc ich ABI jest trywialne (bez klasyfikacji +struktur): budowanie tekstów (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`, +`replace`, `split`, `join`, `repeat`, `from_bytes`), porównania (`eq`, `cmp`, `find`, `starts`, `ends`), +własność (`copy`, `own`, `drop`, `from_c`), tablice (`reserve`, `make`, `resize`, `clone_raw`, `free`, +`insert_gap`, `remove_gap`), `args`, `input`, pliki, `shell` i panic. Brak pamięci wypisuje +`out of memory` i kończy proces. --- -## 9. Kontrole w czasie działania +## 13. Współpraca z C: nagłówki, struktury, ABI -Finch usuwa dwa najczęstsze źródła niezdefiniowanego zachowania w C, które da się tanio sprawdzić. -Wszystkie kontrole korzystają z jednej funkcji pomocniczej na moduł: +### 13.1 Import nagłówków -```llvm -; Function Attrs: cold noinline noreturn -define internal void @finch.panic(ptr %0, i64 %1) #0 { -entry: - %2 = call i32 (i32, ptr, ...) @dprintf(i32 2, ptr @panic.fmt, ptr @panic.file, i64 %1, ptr %0) - call void @exit(i32 1) - unreachable -} -``` +`importHeaders(imports, dirs)` parsuje każdy `import "x.h"` jako osobną jednostkę translacji z pliku +w pamięci `#include "x.h"`, z `-x c -std=gnu11`, `-I` dla folderu każdego pliku `.fn` i `-isystem` dla +każdego katalogu przeszukiwanego przez systemowy kompilator C (odczytanego z `$CC -E -v -x c /dev/null`, +dzięki czemu działa to na NixOS). `gnu11`, bo glibc w trybie ścisłego ISO ukrywa `M_PI` i podobne. -`panicFn()` tworzy ją przy pierwszym użyciu. `panicIf(cond, msg, pos)` emituje -`br cond, %panic, %ok`, wywołanie `finch.panic(msg, linia)` i `unreachable` w `%panic`, -a dalej kontynuuje w `%ok`. Pisze na deskryptor 2 przez `dprintf`, więc nie zależy od symbolu -`stderr` z libc. Funkcja jest `cold` i `noinline`, więc układ skoków faworyzuje ścieżkę bez -błędu, a kontrola kosztuje jedno porównanie i dobrze przewidywany skok. +`Importer` odwiedza kursory najwyższego poziomu: -### Dzielenie i reszta całkowita: `checkDivisor()` - -```llvm -define internal i64 @finch.div(i64 %a, i64 %b) { - ... - %0 = icmp eq i64 %b4, 0 - br i1 %0, label %panic, label %ok -panic: - call void @finch.panic(ptr @panic.msg, i64 2) - unreachable -ok: - %1 = icmp eq i64 %b4, -1 - %2 = icmp eq i64 %a3, -9223372036854775808 - %3 = and i1 %2, %1 - br i1 %3, label %panic5, label %ok6 -panic5: - call void @finch.panic(ptr @panic.msg.1, i64 2) - unreachable -ok6: - %4 = sdiv i64 %a3, %b4 -``` - -- Dzielnik będący **stałym zerem** to błąd **kompilacji**. -- Stały dzielnik różny od zera (i od −1 dla typów ze znakiem) nie generuje żadnej kontroli. -- Przy dzieleniu ze znakiem `MIN / -1` się przepełnia (na x86 kończy się SIGFPE). To też jest sprawdzane. -- Gdy wszystko jest znane, O2 usuwa kontrole całkowicie: `div(10, 2)` powyżej staje się `5` w `main`. - -### `.value` na null: w `place()` - -Każde `p.value`, którego wskaźnik nie jest oczywiście niezerowy (nie jest wprost `alloca` ani -zmienną globalną), dostaje `panicIf(icmp eq p, null, "used .value on a null pointer")`. -Bez tego LLVM traktuje load z null jako UB i może go **usunąć**. Zanim dodano tę kontrolę, -`print(p.value)` przy `p` równym null wypisywało śmieciową wartość pod O2 zamiast się wysypać. - ---- - -## 10. Współpraca z C przez libclang - -`importHeaders(imports, dir)` (`src/cimport.cpp`) działa **przed** codegenem i tworzy: - -```cpp -struct CImports { - std::unordered_map fns; // nazwa, nagłówek, wynik, parametry, variadic, powód braku obsługi - std::unordered_map consts; // wartości enum + liczbowe #define - std::unordered_map globals; // zmienne extern -}; -``` - -### Parsowanie - -Dla **każdego** `import "h"` parsowana jest osobna jednostka translacji z pliku w pamięci -`finch_import.c` zawierającego `#include "h"`. Osobne jednostki dają dokładne przypisanie błędów: -brakujący nagłówek wskazuje swoją linijkę `import`. Flagi: - -``` --x c -std=gnu11 -I -isystem -CXTranslationUnit_DetailedPreprocessingRecord (żeby widzieć makra) -CXTranslationUnit_SkipFunctionBodies (szybkość: ciała funkcji inline są zbędne) -``` - -- **Systemowe katalogi nagłówków** pochodzą z uruchomienia `$CC -E -v -x c /dev/null` i odczytania - bloku między `#include <...> search starts here:` a `End of search list.`. Dzięki temu `stdio.h` - rozwiązuje się tak samo jak dla systemowego kompilatora. Ma to znaczenie na NixOS, gdzie wrapper - cc dokłada ścieżki ze store, o których „goły” libclang nic nie wie. -- **`gnu11`** zamiast `c11`: glibc w trybie ścisłym ukrywa `M_PI` i inne nazwy spoza ISO. -- **Diagnostyka** o wadze ≥ error przerywa import. `file not found` zamienia się w - `can't find the C header 'h'`. - -### Odwiedzający - -`visit()` przechodzi po kursorach najwyższego poziomu. Wygrywa pierwsza deklaracja danej nazwy. - -| Kursor | Działanie | +| Kursor | Wynik | |---|---| -| `FunctionDecl` | Mapuje kanoniczny **typ funkcji** (`clang_getArgType`, więc parametry tablicowe są już zamienione na wskaźniki). Funkcje `static` są zapisywane jako nieobsługiwane (`static inline` nie ma symbolu do zlinkowania). `FunctionNoProto` (K&R `f()`) jest nieobsługiwane. Zmienna liczba argumentów pochodzi z `clang_isFunctionTypeVariadic`. | -| `VarDecl` z `extern` | → `CGlobal`, jeśli typ się mapuje | -| `EnumDecl` | rekurencja do dzieci | -| `EnumConstantDecl` | → `CConst` (`i64`, literał) | -| `MacroDefinition` | pomija wbudowane i makra-funkcje; `macroValue()` próbuje odczytać liczbę | +| `FunctionDecl` | `CFunc` (sygnatura mapowana w **kontekście sygnatury**; funkcje `static` i K&R zapisywane jako nieobsługiwane) | +| `TypedefDecl` rekordu | nazywa rekord (`typedef struct {…} CXCursor` → `CXCursor`) | +| `StructDecl` (definicja) | nazywa rekord jego tagiem | +| `VarDecl` `extern` | `CGlobal` (mapowany w **kontekście danych**) | +| `EnumConstantDecl`, liczbowe `MacroDefinition` | `CConst` (literał) | -### Mapowanie typów: `mapType()` +`mapType()` działa na typach kanonicznych i mapuje liczby całkowite wg rozmiaru. Kontekst decyduje o +`char *`: w sygnaturach funkcji to `str` (Finch konwertuje przy wywołaniu), ale w strukturach, zmiennych +globalnych i za wskaźnikami to `ptr[char]` (tam układ pamięci musi zostać pojedynczym wskaźnikiem). +Rekordy przez wartość stają się `Named{nazwa, "C"}` i są definiowane przez `defineRecord()`: każde pole +(`clang_Type_visitFields`) z przesunięciem w bajtach, rozmiarem i wyrównaniem, zagnieżdżone rekordy, +tablice stałe (`Fixed`). Unie i pola bitowe zostają jako ukryte bajty i oznaczają strukturę jako +nieobsługiwaną przy wywołaniach przez wartość; wskaźniki na wszystko inne (void, funkcje, niekompletne +struktury) stają się `ptr`. -Działa na typie **kanonicznym**, więc typedefy (`size_t`, `uint32_t`, `GLuint`, `FILE`) są rozwijane. -Typy całkowite są mapowane **według rozmiaru** (`clang_Type_getSizeOf`), a nie nazwy, więc -`long` staje się `i64` na LP64. +### 13.2 Układ struktur C -| C (kanoniczny) | Finch | +`resolveStruct()` buduje **upakowany** struct LLVM, który odwzorowuje C bajt w bajt: wypełnienie +`[n x i8]` przed każdym polem, którego przesunięcie wyprzedza bieżącą pozycję, samo pole (tylko jeśli jego +rozmiar w LLVM zgadza się z C) i wypełnienie końcowe do `sizeof` z C. Dostęp do pól używa zapisanego +`llvmIndex`. Upakowanie czyni układ dokładnym niezależnie od reguł wyrównania LLVM; alokacje i wartości +tymczasowe dostają wyrównanie z C jawnie. + +### 13.3 Konwencja wywołań System V x86-64 + +`classify(T)` (w `abi.cpp`) spłaszcza strukturę do skalarnych liści z przesunięciami w bajtach (przez +zagnieżdżone struktury i tablice stałe) i: + +- **MEMORY**, jeśli jest większa niż 16 bajtów albo ma niewyrównany liść: przekazywana jako wskaźnik z + `byval(T) align ≥8`, zwracana przez ukryty pierwszy wskaźnik `sret(T)`. +- w przeciwnym razie każdy eightbyte jest **INTEGER**, jeśli którykolwiek liść w nim jest liczbą + całkowitą/wskaźnikiem, a inaczej **SSE**. Części: INTEGER → `iN` dla pozostałych bajtów (`i32` dla + 4-bajtowego `Color`), SSE → `double`, `<2 x float>` albo `float`. Jedna część idzie wprost; dwie jako + dwa argumenty albo zwracane jako `{a, b}`. + +`makePlan()` przechodzi parametry, licząc 6 rejestrów całkowitych i 8 SSE; struktura, której części już się +nie mieszczą, idzie w całości na stos (MEMORY), tak jak robi to clang. Małe parametry całkowite dostają +`signext`/`zeroext`. `emitCCall()` zapisuje każdy argument-strukturę do slotu wystarczająco dużego dla +struktury i jej postaci rejestrowej, wczytuje części z przesunięć 0 i 8, woła z zaplanowanym typem funkcji +i atrybutami, a wyniki-struktury odbudowuje w ten sam sposób. + +`cThunk(fn)` to odwrotność dla `addr(fn)`: wewnętrzna funkcja z sygnaturą C, która składa parametry-struktury +z rejestrów albo pamięci `byval`, woła funkcję Fincha i obniża jej wynik. Akceptowane są tylko sygnatury zgodne z C. + +`tests/run/c_structs.fn` sprawdza `{float,float}`, `{float,float,float}`, `{u8×4}`, `{double,int}`, +`{i64,i64}`, strukturę 24-bajtową, strukturę z `char[8]`, zagnieżdżone struktury, wyczerpanie rejestrów +przy 13 argumentach-strukturach oraz wywołania zwrotne przyjmujące i zwracające struktury, na bibliotece C +kompilowanej przez `link "abi.c"`. Wyniki zgadzają się z tymi samymi wywołaniami zrobionymi z C. + +### 13.4 Linkowanie kodu C + +`link "nazwa"` próbuje `pkg-config --libs nazwa`, a potem `-lnazwa`. `link "plik.c"` kompiluje plik +(względem pliku `.fn`) przez `$CC -O2 -fPIC -c` do tymczasowego obiektu; pliki `.o` i `.a` przechodzą +bez zmian. Gdy linkowanie się nie uda, `linkFailed()` mapuje niezdefiniowane symbole na nagłówek, który +je zadeklarował, i wypisuje linijkę `link` do dopisania. + +--- + +## 14. Optymalizacja, emisja, linkowanie + +- **Cel:** `sys::getDefaultTargetTriple()`, CPU `generic`, `Reloc::PIC_`. Data layout jest ustawiany na + module, zanim powstanie jakikolwiek IR (od niego zależy wyrównanie loadów i store'ów). +- **Optymalizacja:** `PassBuilder::buildPerModuleDefaultPipeline(O2)`: środkowa część `-O2` z clanga. + `-O0` ją pomija (`finch ir plik.fn -O0` pokazuje surowe wyjście `Codegen`). +- **Emisja:** stary `PassManager` + `addPassesToEmitFile(ObjectFile)`. +- **Linkowanie:** `$CC prog.o rt.o [obiekty użytkownika] -lm [biblioteki]`. Na Niksie wrapper cc zamienia + ścieżki `-L` na `RPATH`, więc programy działają bez `LD_LIBRARY_PATH`. +- **`run`:** tymczasowy plik wykonywalny uruchamiany z resztą argumentów i potem usuwany; sygnał jest + zgłaszany jako `the program crashed: ` z kodem wyjścia `128 + n`. + +Uwagi o wydajności: funkcje użytkownika są `internal`, więc LLVM swobodnie je wstawia i specjalizuje +(rekurencyjne `fib(40)`: ok. 0,19 s wobec ok. 0,23 s dla `clang -O2` na tym samym kodzie C na maszynie +autora); kod tablicowy z kontrolą zakresu dorównuje C na sicie. + +--- + +## 15. Informacje dla debuggera + +Z `-g` `Codegen` tworzy `DIBuilder`, jednostkę kompilacji (`DW_LANG_C`, więc składnia wyrażeń C w gdb +działa dla `print p.x`), `DIFile` na plik źródłowy, `DISubprogram` na funkcję, zmienne-parametry +(`createParameterVariable`, więc `bt` pokazuje `length (v=…)`), zmienne lokalne (`insertDeclare` na ich +`alloca`) i `DILocation` na każdą instrukcję i wywołanie (`setLoc`). Typy: podstawowe z nazwami Fincha +(`int`, `u8`, `float`), `str`/tablice jako struktury `{ptr, len, cap}`, wskaźniki, tablice stałe i struktury +z przesunięciami pól z `StructLayout` LLVM. Struktury rekurencyjne używają zastępowalnej deklaracji +wyprzedzającej. Generowane helpery nie mają lokacji. Najlepiej działa `-g -O0`; przy O2 część zmiennych jest usuwana. + +--- + +## 16. Diagnostyka + +`failAt(plik, linia, kol, msg)` wypisuje `ścieżka:linia:kol: error: msg` i linijkę źródła ze znakiem `^` +(tabulatory zachowane, kolumny liczone w znakach), a potem kończy proces z kodem 1. Konwencje: operatory +binarne raportują operator, wywołania nazwę funkcji, błędy argumentów pozycję argumentu. Komunikaty mówią, +co jest źle, a gdy naprawa jest oczywista, także jak to naprawić. Ostrzeżeń nie ma. Błąd weryfikatora LLVM +wypisuje `internal compiler error (please report)` i kończy proces z kodem 2. + +--- + +## 17. Kompilator samohostujący (`boot/`) + +`boot/` to drugi kompilator Fincha napisany w Finchu (ok. 3100 linii): + +| Plik | Zawartość | |---|---| -| `void` | nic | -| `_Bool` | `bool` | -| `char` (dowolna znakowość) | `char` | -| `signed char`, `short`, `int`, `long`, `long long`, `enum` | `i8`/`i16`/`i32`/`i64` wg rozmiaru | -| warianty `unsigned` | `u8`…`u64` wg rozmiaru | -| `float` / `double` | `f32` / `f64` | -| `char *` (dowolne kwalifikatory) | `str` | -| `T *`, gdzie T mapuje się na typ niebędący void | `ptr[T]` | -| `void *`, wskaźnik na struct/union/funkcję/typ niemapowalny | `ptr` (nieprzezroczysty) | -| struct/union przez wartość | **nieobsługiwane** („passes a C struct by value”) | -| `long double`, `__int128`, wektory, … | **nieobsługiwane** | +| `boot/lexer.fn` | lekser (tokeny to `{kind, text, line, col, nl}`; rodzaje to słowa, a słowa kluczowe i operatory są swoim własnym rodzajem) | +| `boot/ast.fn` | `Type` (`kind`, `elem` jako tablica 0/1 elementów, `name`, `module`), jeden ogólny `Node` na wszystko, `Program` | +| `boot/parser.fn` | ta sama gramatyka i reguły nowych linii co parser C++, wspinanie się po priorytetach dla operatorów binarnych | +| `boot/gen.fn` | sprawdzanie typów + generator **tekstowego LLVM IR**: zasięgi, miejsca, konwersje, kopie, helpery, wypisywanie, kontrole, metody tablic i tekstów | +| `boot/main.fn` | ładowanie modułów i sterownik: zapisuje `.ll`, uruchamia `clang -O2 plik.ll runtime/finch_rt.c` | -Funkcje nieobsługiwane zostają w tablicy, więc próba ich wywołania daje precyzyjny błąd zamiast -„nie ma takiej funkcji”. +Obsługuje rdzeń języka: `int float bool char str`, `[]T`, struktury (wartości domyślne, konstruktory po +nazwie i po kolei), `ptr[T]`/`addr`/`new`/`free`/`null`, wszystkie instrukcje poza `defer`, moduły oraz +funkcje wbudowane i metody kompilatora C++, bez importu C, liczb z rozmiarem i `defer`. Ma tę samą +semantykę wartości i analizę pożyczania (kopiuje tam, gdzie główny kompilator), ale **nigdy nie zwalnia**: +jako kompilator bootstrapowy zostawia pamięć systemowi operacyjnemu. -### Makra: `macroValue()` +Stan kompilatora to jedna struktura `Gen` przekazywana jako `ptr[Gen]`: Finch nie ma zmiennych globalnych, +a przekazanie wskaźnika to sposób, w jaki funkcja zmienia wartość wywołującego. Bez `defer` i typu map zasięgi +to tablice tablic przeszukiwane liniowo; funkcje pomocnicze (kopiowanie/wypisywanie/porównywanie na typ) +są generowane przez tymczasową podmianę buforów wyjścia (`beginHelper`/`endHelper`). -Tokenizuje zakres makra, odrzuca token nazwy i komentarze, zdejmuje **zrównoważone nawiasy -zewnętrzne**, akceptuje jeden wiodący `-` i wymaga **dokładnie jednego** literału liczbowego. -Liczby całkowite są czytane przez `strtoull(…, 0)`, czyli według **reguł C**: `0x` szesnastkowo -i zero wiodące jako **ósemkowe**, inaczej niż literały samego Fincha. Zmiennoprzecinkowe to -wszystko, co zawiera `.`, `e` lub `E` i nie jest szesnastkowe. Akceptowane są przyrostki -`u U l L f F`. To obejmuje `M_PI`, `EOF` (`(-1)`), `RAND_MAX`, `INT_MAX` oraz stałe `GL_*` i `GLFW_*`. -Wyrażenia (`(1 << 4)`), odwołania do innych makr i teksty są pomijane. +**Punkt stały.** `tests/boot.sh`: -### Wywoływanie +1. `build/finch` (C++) kompiluje `boot/` → **stage 1**, +2. stage 1 kompiluje `boot/` → **stage 2** (i jego IR), +3. stage 2 kompiluje `boot/` → IR **stage 3**, -Zaimportowane funkcje są deklarowane w module **dopiero przy wywołaniu**, przez -`getOrInsertFunction`. To używa istniejącej deklaracji, jeśli `print` już utworzył `printf`. -Sygnatury się zgadzają (`i32 (ptr, ...)`), więc nie ma konfliktu. - -### Błędy linkowania - -Gdy linkowanie się nie uda, `linkFailed()` w `main.cpp` przeszukuje wyjście linkera pod kątem -`undefined reference to \`sym'` (GNU ld) i `undefined symbol: sym` (lld). Grupuje symbole według -`CFunc::header` i wypisuje, z którego nagłówka pochodzą, razem z odgadniętą linijką `link` -(`guessLib()`: nazwa pliku bez rozszerzenia i końcowych cyfr, małymi literami: -`GLFW/glfw3.h` → `glfw`). `cannot find -lfoo` zamienia się w `the library 'foo' wasn't found`. +i wymaga, żeby IR ze stage 2 i stage 3 był **identyczny co do bajtu** (ok. 32 000 linii). Potem kompiluje +kompilatorem samohostującym każdy test z `tests/run`, który mieści się w podzbiorze języka, i porównuje +wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostującemu ok. 2,3 s. --- -## 11. Optymalizacja, emisja, linkowanie +## 18. System budowania -### Cel - -`hostMachine()` tworzy `TargetMachine` dla `sys::getDefaultTargetTriple()` z CPU `"generic"`, -bez dodatkowych cech i z `Reloc::PIC_`, które jest wymagane dla domyślnych plików PIE we -współczesnych dystrybucjach. Jej **data layout jest ustawiany na module, zanim powstanie -jakikolwiek IR**, w konstruktorze `Codegen`. To ważne: `IRBuilder` bierze wyrównanie loadów -i store'ów z data layoutu i bez niego dostępy do `i64` byłyby emitowane z `align 4`. - -`generic` znaczy, że plik zadziała na każdym x86-64. Zachowanie w stylu `-march=native` wymagałoby -zmiany nazwy CPU i cech w tym miejscu. - -### Optymalizacja - -`optimize()` buduje potok nowego menedżera przebiegów: - -```cpp -PassBuilder pb(tm); -pb.register{Module,CGSCC,Function,Loop}Analyses(...); -pb.crossRegisterProxies(lam, fam, cgam, mam); -pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam); -``` - -To jest środkowa część `-O2` z clanga: SROA/mem2reg, instcombine, inlining, GVN, LICM, -rozwijanie pętli, wektoryzacja i tak dalej. `-O0` pomija to wywołanie, co przydaje się do -czytania IR wyprodukowanego przez `Codegen` (`finch ir plik.fn -O0`). Generator kodu maszynowego -i tak działa na domyślnym poziomie TargetMachine. - -### Emisja - -`emitObject()` używa **starego** (legacy) menedżera przebiegów, bo potok kodu maszynowego LLVM -jest wciąż dostępny tylko przez niego: `tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)`. - -### Linkowanie - -``` -$CC plik.o -o plik -lm [pkg-config --libs NAZWA | -lNAZWA]... -``` - -Jako linker służy systemowy sterownik kompilatora C (domyślnie `cc`). Zna pliki crt platformy, -ścieżkę dynamicznego loadera, libc i domyślne ścieżki bibliotek, a odtwarzanie tego byłoby -pracochłonne. `-lm` jest dodawane zawsze. Dla każdego `link "NAZWA"` i `-l NAZWA` najpierw -próbowane jest `pkg-config --libs NAZWA`, żeby dołączyć ścieżki `-L` i biblioteki zależne. -Jeśli to się nie uda, używane jest `-lNAZWA`. Na Niksie wrapper cc zamienia ścieżki `-L` -w wpisy `RPATH`, więc wynik działa bez `LD_LIBRARY_PATH`. Plik obiektowy jest usuwany po -linkowaniu, niezależnie od wyniku. - -### `finch run` - -Kompiluje do pliku tymczasowego (`sys::fs::createTemporaryFile`), uruchamia go przez `system()`, -usuwa i przekazuje dalej kod wyjścia. Jeśli proces potomny zginął od sygnału, wypisuje -`the program crashed: ` i zwraca `128 + numer_sygnału`, jak powłoka. +- `find_package(LLVM CONFIG)`; **jeśli istnieje współdzielony cel `LLVM`, Finch linkuje tę jedną bibliotekę.** + `libclang.so` sam linkuje `libLLVM.so`; linkowanie dodatkowo statycznych bibliotek komponentów wprowadzało + do procesu dwie kopie LLVM, a zduplikowane obiekty globalne wywracały program przy wyjściu (podwójne + zwolnienie w globalnym destruktorze). Do tego binarka miała 82 MB zamiast ok. 20 MB. +- libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`. +- `runtime/finch_rt.c` jest wczytywany przy konfiguracji do `build/rt_source.inc` jako surowy literał + napisowy; `CMAKE_CONFIGURE_DEPENDS` ponawia konfigurację, gdy plik się zmieni. +- `FINCH_VERSION` z `project(VERSION 2.0.0)`. C++17, `-Wall -Wextra`, bez ostrzeżeń. +- `shell.nix` wymienia `llvmPackages.clang` przed `llvmPackages.libclang`: ten drugi dostarcza też + „gołego” `clang`, który nie widzi nagłówków systemowych. --- -## 12. Diagnostyka +## 19. Testy -`fail(line, col, msg)` jest `[[noreturn]]`: wypisuje - -``` -plik:linia:kol: error: msg - linia | tekst źródła - | ^ -``` - -i kończy proces z kodem 1. Fragment jest brany z `g_source`, a tabulatory w prefiksie są -zachowywane, żeby `^` trafiło w miejsce. Konwencje w całym kodzie: - -- **Pozycje:** operatory binarne raportują pozycję **operatora**, wywołania pozycję nazwy funkcji, - a błędy typów w argumentach pozycję **argumentu**. -- **Komunikaty** mówią prostymi słowami, co jest źle, i **jak to naprawić**, gdy naprawa jest - oczywista (`use int(...) to convert`, `did you mean ':='?`, `add import "math.h"`). -- `headerHint()` mapuje około dwóch tuzinów popularnych nazw z libc/libm na ich nagłówki, na - potrzeby wywołań nieznanych funkcji. -- Wewnętrzna niespójność, jeśli `verifyModule` kiedykolwiek zawiedzie, wypisuje - `internal compiler error (please report)` z wyjściem weryfikatora i kończy proces z kodem 2. - -Kompilator nie ma ostrzeżeń. Wszystko, co byłoby ostrzeżeniem, jest albo w porządku, albo błędem. +- `tests/run.sh`: każdy `tests/run/*.fn` z `fn main` musi wypisać dokładnie swój `.out` (stdin z `.in`, jeśli + jest); pliki bez `main` to moduły albo pliki pomocnicze. Każdy `tests/fail/*.fn` musi się nie udać z tekstem + z linijki `// expect:`. Obecnie **58 passed, 0 failed**. +- `MEMCHECK=1 tests/run.sh`: to samo plus valgrind na każdym programie (bez wycieków i złych dostępów). +- `tests/boot.sh`: punkt stały samohostowania i przebieg na podzbiorze (§17). --- -## 13. System budowania +## 20. Semantyka a C -`CMakeLists.txt`: - -- `find_package(LLVM CONFIG)`. **Jeśli istnieje cel `LLVM` (współdzielone `libLLVM.so`), Finch linkuje - tę jedną bibliotekę.** To nie kosmetyka, tylko konieczność: `libclang.so` sam linkuje `libLLVM.so`. - Linkowanie Fincha ze statycznymi bibliotekami komponentów (`libLLVMCore.a`, …) wprowadza do procesu - **drugą kopię LLVM**. Obie kopie rejestrują te same obiekty globalne i program wywracał się przy - wyjściu w globalnym destruktorze (`std::vector::~vector`, podwójne zwolnienie). - Do tego binarka miała 82 MB zamiast 11 MB. -- libclang jest szukany przez `find_path(clang-c/Index.h)` i `find_library(clang)`, z podpowiedzią katalogów LLVM. -- `FINCH_VERSION` pochodzi z `project(VERSION …)` i jest wypisywane przez `finch version` razem z `LLVM_VERSION_STRING`. -- C++17, `-Wall -Wextra`, a budowanie przechodzi bez ostrzeżeń. - ---- - -## 14. Testy - -`tests/run.sh` (ścieżkę kompilatora nadpisuje `FINCH=…`): - -- `tests/run/*.fn`: uruchamiane przez `finch run`. Połączone stdout+stderr musi być dokładnie równe `*.out`. -- `tests/fail/*.fn`: muszą zakończyć się kodem różnym od zera, a wyjście musi zawierać tekst po - `// expect: ` z pierwszej linii. To obejmuje błędy kompilacji, błędy w czasie działania (z numerami - linii) oraz problemy z nagłówkami i linkowaniem. `tests/fail/lib_hint.h` deklaruje funkcję, której - nie ma w żadnej bibliotece, żeby przetestować podpowiedź `link`. - -Stan obecny: **34 passed, 0 failed.** Każdy nowy komunikat błędu powinien dostać test w `fail/`, -a każda nowa funkcjonalność test w `run/`. - ---- - -## 15. Semantyka a C: zachowania zdefiniowane i niezdefiniowane - -| Sytuacja | C | Finch 1.0 | +| Sytuacja | C | Finch 2.0 | |---|---|---| -| Przepełnienie liczby ze znakiem | UB | **Zdefiniowane zawinięcie** (bez flag `nsw`) | -| Przepełnienie liczby bez znaku | zawija się | zawija się | -| Dzielenie całkowite przez zero | UB | **Błąd kompilacji**, jeśli stałe, w przeciwnym razie **błąd w czasie działania** | -| `INT_MIN / -1` | UB | **Błąd w czasie działania** | -| Load/store przez null | UB | **Błąd w czasie działania** dla `.value` | -| `x & 1 == 0` | parsuje się jako `x & (1 == 0)` | parsuje się jako `(x & 1) == 0` | -| Niejawne zwężenie (`int` → `char`) | po cichu | **Błąd kompilacji** | -| Mieszanie ze znakiem / bez znaku | cicha konwersja | **Błąd kompilacji**, chyba że jedna strona się rozszerza | -| Użycie zmiennej przed nadaniem wartości | UB | Niemożliwe: każda deklaracja inicjalizuje (domyślnie zerem) | -| Brak `return` w funkcji z wynikiem | UB, jeśli wynik jest użyty | **Błąd kompilacji** | -| Przesunięcie o ≥ szerokość bitową | UB | **Nadal poison** (w 1.0 niesprawdzane) | -| `float` → int poza zakresem | UB | **Nadal poison** (w 1.0 niesprawdzane) | -| Wiszący wskaźnik na martwą zmienną lokalną | UB | **Nadal UB** (brak kontroli czasu życia) | -| Kolejność obliczeń | w większości nieokreślona | Od lewej do prawej | +| 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 tablic, tekstów, struktur | częsty błąd | niemożliwy bez `new` | +| `x & 1 == 0` | `x & (1 == 0)` | `(x & 1) == 0` | +| Niejawne zwężanie, mieszanie ze znakiem / bez znaku | po cichu | błędy kompilacji | +| Niezainicjalizowane zmienne | UB | niemożliwe (zero albo wartości domyślne) | +| Brak return | UB, jeśli użyty | błąd kompilacji | +| Kolejność obliczeń | w większości nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona | +| Przesunięcie ≥ szerokość, float→int poza zakresem | UB | nadal poison (jeszcze niesprawdzane) | +| Wiszący wskaźnik z `addr()` / `new` + `free` | UB | nadal UB | --- -## 16. Spis funkcji: kto co robi - -Grupy funkcji o podobnych nazwach i czym się różnią. - -### Leksowanie i parsowanie - -| Funkcja | Robi | Różni się od | -|---|---|---| -| `lex()` | tekst → tokeny, ustawia `newlineBefore` | nic nie wie o gramatyce ani typach | -| `tokName()` | rodzaj tokenu → nazwa czytelna dla człowieka (do błędów) | | -| `Parser::cur()` / `peekTok()` / `next()` | bieżący token / podgląd bez konsumowania / konsumpcja | | -| `accept(k)` kontra `expect(k)` | konsumuje, jeśli pasuje, i zwraca bool / konsumuje albo **zgłasza błąd** „expected …, found …” | | -| `unexpected(ctx)` | błąd „unexpected X (ctx)” na bieżącym tokenie | `expect` mówi, czego chciał; `unexpected` tłumaczy kontekst | -| `sameLine()` | czy bieżący token może kontynuować wyrażenie? | `endOfStatement()` to odpowiednik na poziomie instrukcji (wymaga nowej linii albo `}`) | -| `atDeclaration()` | podgląd `int x` / `ptr[...] p` | `type()` faktycznie konsumuje typ | -| `typeFromName()` kontra `isTypeName()` | nazwa → `Type` / nazwa → bool | | -| `statement()` kontra `block()` | jedna instrukcja / `{ instrukcja* }` z kontrolą końców i zerowaniem `parenDepth` | | -| `callArgs()` | wspólne dla wywołań i konwersji: `( expr, … )` | | -| `primary()` / `postfix()` / `unary()` / `multiplicative()` / `additive()` / `compare()` / `andExpr()` / `orExpr()` | po jednej funkcji na poziom priorytetu | | - -### Codegen: typy i wartości - -| Funkcja | Robi | Różni się od | -|---|---|---| -| `ty(FType)` | typ Fincha → typ LLVM | | -| `zero(FType)` | wartość domyślna dla deklaracji bez inicjalizatora | | -| `widens(from, to)` | czy każdą wartość `from` da się przedstawić w `to`? (czysta reguła typów) | `fits()` pyta o to samo dla jednej stałej | -| `fits(c, from, to)` | czy wartość tego literału mieści się w `to`? | | -| `coerce(v, want)` | **niejawna** konwersja do miejsca o znanym typie albo błąd | `convert()` to **jawne** `T(x)` i pozwala na stratne rzutowania; `unify()` zbliża do siebie **dwa** argumenty | -| `unify(l, r)` | sprowadza argumenty binarne do wspólnego typu | używane przez `arithOp()` i `compare()` | -| `intCast()` / `intToFloat()` | emitują rzutowanie z poprawną znakowością | niskopoziomowe pomocniki trzech powyższych | - -### Codegen: wyrażenia - -| Funkcja | Robi | Różni się od | -|---|---|---| -| `expr(e)` | r-wartość: emituje kod, zwraca `{wartość, typ, literal}` | `place()` zwraca **adres**, zamiast ładować | -| `place(e)` | adres l-wartości: zmiennej, zmiennej globalnej C albo `p.value` | używane przez `expr()` (potem load), `assign()` (potem store) i `addrOf()` (zwraca bez zmian) | -| `binary()` | rozgałęzienie po `BinOp` | | -| `arith()` kontra `arithOp()` | `arith()` opakowuje `arithOp()` i liczy flagę `literal` wyniku | `arithOp()` robi `unify` i emituje instrukcję | -| `compare()` | wszystkie porównania: wskaźniki/str/float/int | zawsze zwraca `bool` i nigdy nie jest literałem | -| `logic()` | `&&`/`\|\|` ze skokami i φ | jedyne miejsce budujące φ ręcznie | -| `call()` | zamienia nazwę wywołania na jeden z pięciu rodzajów | | -| `callFinch()` kontra `callC()` | zobacz rozdział 8.10 | | -| `print()` | buduje format printf z typów | | -| `addrOf()` | `addr(x)` → `place(x).addr` | | -| `convert()` | `T(x)` | | -| `libc()` | `getOrInsertFunction`: zadeklaruj albo użyj istniejącej funkcji zewnętrznej | `declare()` tworzy funkcje **Fincha** przez `Function::Create` | -| `cGlobal()` | zadeklaruj albo użyj istniejącej zmiennej `extern` | | - -### Codegen: instrukcje i sterowanie - -| Funkcja | Robi | Różni się od | -|---|---|---| -| `declare()` kontra `define()` | przejście sygnatur / przejście ciał | | -| `block()` kontra `blockBody()` | `block()` otwiera i zamyka zasięg wokół `blockBody()`; `blockBody()` emituje instrukcje i odrzuca nieosiągalne | `define()` woła `blockBody()` bezpośrednio, bo parametry i zewnętrzne ciało dzielą jeden zasięg | -| `stmt()` | rozgałęzienie po `StmtKind`; obsługuje też `break`/`continue` | | -| `varDecl()` / `assign()` | deklaracja z opcjonalnym wnioskowaniem / zapis do istniejącego miejsca | | -| `addVar()` | tworzy miejsce, zapisuje wartość początkową, rejestruje nazwę (z kontrolą przesłaniania) | `slot()` tylko tworzy `alloca` | -| `lookup()` | szuka we wszystkich zasięgach, od najbardziej wewnętrznego | | -| `condition(e)` | `coerce(expr(e), bool)` z komunikatem „a condition must be bool” | | -| `ifStmt()` / `whileStmt()` / `forStmt()` / `returnStmt()` | rozdział 8.5 | | -| `terminated()` | czy bieżący blok kończy się już terminatorem? | | -| `continueAt(bb, deadEnd)` | przejdź do bloku łączącego albo usuń go, jeśli jest nieosiągalny | | -| `newBlock()` | tworzy blok podstawowy w bieżącej funkcji | | - -### Kontrole w czasie działania - -| Funkcja | Robi | -|---|---| -| `panicFn()` | pobiera albo tworzy `finch.panic` | -| `panicIf(cond, msg, pos)` | warunkowy skok do bloku z panic | -| `checkDivisor(l, r, pos)` | kontrole zera i `MIN/-1` albo błąd kompilacji | +## 21. Spis funkcji ### Sterownik (`main.cpp`) | Funkcja | Robi | |---|---| -| `hostMachine()` | tworzy `TargetMachine` | -| `optimize()` | potok O2 | -| `emitObject()` | zapisuje `.o` | -| `capture()` | uruchamia polecenie powłoki, zbiera wyjście i kod wyjścia | -| `libFlags()` | pkg-config albo `-l` dla jednej biblioteki | -| `link()` | uruchamia linker, usuwa `.o`, tłumaczy porażki | -| `linkFailed()` / `guessLib()` | zamieniają błędy linkera na porady w języku Fincha | +| `Loader::load` / `findModule` | czyta, leksuje i parsuje plik oraz rekurencyjnie jego moduły / szuka `nazwa.fn` | +| `hostMachine`, `optimize`, `emitObject` | maszyna docelowa, potok O2, plik obiektowy | +| `runtimeObject` | kompiluje runtime i trzyma go w cache | +| `capture`, `libFlags`, `link` | uruchamia polecenie / pkg-config albo `-l` / linkowanie, z kompilacją plików z `link "x.c"` | +| `linkFailed`, `guessLib` | zamieniają wyjście linkera na porady | + +### Lekser i parser + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `lex` / `tokName` | tekst → tokeny / nazwy do komunikatów | | +| `accept` / `expect` / `unexpected` | konsumuje, jeśli pasuje / konsumuje albo zgłasza błąd / błąd z kontekstem | | +| `sameLine` / `endOfStatement` | czy ten token może kontynuować wyrażenie? / czy instrukcja się skończyła? | | +| `atDeclaration` / `type` | podgląd deklaracji / parsowanie typu | | +| `args` | `( [nazwa:] expr, … )` dla wywołań, metod i konstruktorów | | +| `postfix` | łańcuchy `.pole`, `.metoda(…)`, `[i]` | `primary` parsuje początek łańcucha | + +### Codegen: program i instrukcje (`codegen.cpp`) + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `declareStructs` / `declareFns` / `declare` | wszystkie typy / wszystkie sygnatury / jedna sygnatura + analiza pożyczania | | +| `define` / `defineMainWrapper` | jedno ciało / punkt wejścia C | | +| `pushScope` / `popScope` | otwiera / zamyka zasięg (`popScope` emituje jego sprzątanie, jeśli osiągalne) | | +| `emitCleanups(downTo)` / `emitScopeCleanups(i)` | sprzątanie kilku zasięgów bez zamykania (return/break) / jednego zasięgu | | +| `slot` / `tmpOf` / `tmp` | `alloca` zmiennej / anonimowa / anonimowa z wartością | | +| `lookup` / `addVar` | szuka widocznej zmiennej (respektuje `deferLimit`) / deklaruje zmienną | | +| `stmt`, `varDecl`, `assign`, `ifStmt`, `whileStmt`, `forStmt`, `forEachStmt`, `returnStmt`, `jump` | instrukcje | | +| `rootVar` / `mutates` / `mutatesExpr` | zmienna, od której zaczyna się l-wartość / czy instrukcja albo wyrażenie ją zmienia? | | + +### Codegen: wyrażenia + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `expr` / `exprWant` | r-wartość / to samo z oczekiwanym typem dla literałów tablic | | +| `ref` / `place` | adres, jeśli jest, inaczej wartość / adres albo błąd | `expr` wczytuje z `ref` | +| `member` / `index` | `.x`, `.len`, `.ptr`, `.value`, auto-dereferencja / `[i]` z kontrolą zakresu i kopiowaniem przy zapisie | | +| `arrayLit` | `[…]` → tablica na stercie | | +| `unary`, `binary`, `arith`/`arithOp`, `compare`, `logic` | operatory (`arith` dokłada flagę literału) | | +| `unify` / `coerce` / `convert` | dwa argumenty do siebie / jedna wartość do miejsca / jawne `T(x)` | | + +### Codegen: typy i własność (`codegen_types.cpp`) + +| Funkcja | Robi | +|---|---| +| `ty`, `resolve`/`resolveT`, `findStruct`, `resolveStruct` | typ LLVM; Named → Struct (głęboko albo płytko); szukanie struktury; układ + kontrola cykli | +| `owning`, `zero`, `defaultValue` | czy typ posiada pamięć; stała zerowa; zero albo wartości domyślne struktury | +| `own` / `release` | coś do zapisania (przeniesienie albo kopia) / zwolnienie niezatrzymanej wartości fresh | +| `copyValue`, `dropValue`, `copyAt`, `dropAt`, `copyFn`, `dropFn`, `typeKey` | kopiowanie i zwalnianie przez wartość, przez adres i helpery na typ | +| `forN` | wewnętrzna pętla `for i in 0..n` używana przez helpery | +| `strConst`, `cstr`, `strFromC`, `elemSize` | stała-literał str; str → `char*`; `char*` → pożyczony str; rozmiar elementu | +| `emitPrint`, `printFn`, `printf_` | wypisywanie | +| `diType`, `setLoc` | informacje dla debuggera | + +### Codegen: wywołania (`codegen_builtins.cpp`) i ABI (`abi.cpp`) + +| Funkcja | Robi | +|---|---| +| `call` / `method` | rozwiązuje wywołanie / metodę albo wywołanie z modułu | +| `callFinch` / `construct` / `callC` | funkcja Fincha / konstruktor struktury / funkcja C | +| `print`, `addrOf`, `convert`, `arrayMethod` | funkcje wbudowane | +| `rt` / `libc` | deklaruje funkcję runtime'u / libc | +| `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | kontrole w czasie działania | +| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | klasyfikacja System V, plan rejestrów, deklaracja, wywołanie, wrapper dla callbacków | --- -## 17. Rozbudowa kompilatora +## 22. Rozbudowa kompilatora -### Nowa funkcja wbudowana (przykład: `len(str) -> int`) +**Funkcja wbudowana:** dodaj nazwę do `isBuiltin()`, obsłuż ją w `call()`, zaimplementuj (w IR albo jako +funkcję runtime'u zadeklarowaną w tabeli `rt()`), dodaj przypadki w `tests/run/` i `tests/fail/`. Jeśli ma +istnieć też w kompilatorze samohostującym, dodaj ją do `call()` i `runtimeDecls()` w `boot/gen.fn`. -1. Dodaj nazwę do `Codegen::isBuiltin()`, żeby nie dało się jej zdefiniować ponownie. -2. W `Codegen::call()` dodaj rozgałęzienie `if (c.callee == "len") return len(c);`. -3. Zaimplementuj: sprawdź liczbę argumentów, `coerce(expr(arg), Str, …)`, wyemituj `strlen` przez `libc()`, - zwróć `{call, I64}`. `strlen` zwraca `u64`; jeśli chcesz `int`, przekonwertuj przez `intCast`. -4. Dodaj `tests/run/len.fn` z plikiem `.out` i przypadek w `tests/fail/` dla złego argumentu. +**Metoda tablicy albo tekstu:** dodaj gałąź w `arrayMethod()` albo w części `str` w `method()`; jeśli +zmienia tablicę, dopisz jej nazwę do zbiorów `changing` w `method()` i `mutatesExpr()`, żeby pożyczanie +dalej było poprawne. -### Nowy operator binarny +**Operator:** token leksera + `tokName`, `BinOp`, właściwa funkcja priorytetu w parserze, `opName`, emisja +w `arithOp`/`compare` i wyjaśnienie w `badOperands`. -1. Lekser: nowy `Tok`, jego zapis w `switch` operatorów i w `tokName()`. -2. AST: nowy `BinOp`. -3. Parser: umieść go w funkcji właściwego poziomu priorytetu. -4. Codegen: `opName()`, emisja w `arithOp()` albo `compare()` i wyjaśnienie złych argumentów w `badOperands()`. +**Instrukcja:** `StmtKind` + węzeł, `Parser::statement`, `Codegen::stmt`; jeśli przenosi sterowanie, +respektuj `terminated()`, używaj `continueAt()` dla bloków łączących i emituj sprzątanie (`emitCleanups`) +przed wyskokiem z zasięgów. -### Nowa instrukcja - -Dodaj `StmtKind` i węzeł w `ast.h`, rozpoznaj ją w `Parser::statement()` i obsłuż w `Codegen::stmt()`. -Jeśli może przenosić sterowanie, upewnij się, że wynik respektuje `terminated()` i używa `continueAt()` -dla swojego bloku łączącego. - -### Konwencje - -Trzymaj się istniejącego stylu: wczesne `fail()` z podpowiedzią naprawy, komentarze tylko tam, -gdzie *dlaczego* nie jest oczywiste, i jeden test na jedno zachowanie. +**Nowy typ posiadający pamięć:** rozszerz `owning()`, `dropAt`/`copyAt` (i ich helpery), `emitPrint`, `diType`. --- -## 18. Znane ograniczenia i plan +## 23. Znane ograniczenia -- **Brak typów złożonych:** `struct`, tablice/wycinki z `.len` i struktury C przez wartość. To kolejne - kamienie milowe, w tej kolejności: tablice opierają się na strukturach (wskaźnik + długość), - a model pamięci na obu. -- **Model pamięci** (uzgodniony projekt): wartości posiadające pamięć na stercie są zwalniane - automatycznie na końcu bloku, który je posiada, `defer` uruchamia kod przy wyjściu z bloku, - a `free` zostaje do ręcznej kontroli. Szkic implementacji: lista sprzątania na blok, emitowana na - każdej krawędzi wyjścia (normalny koniec, `return`, `break`, `continue`). `blockBody()` i stos - `loops` już znają każdą krawędź wyjścia. -- **Teksty:** sklejanie, `len`, `str(x)`. Zależą od modelu pamięci. -- **Wejście:** `input()`. -- **Moduły:** `import nazwa` dla kodu w Finchu. Dziś jeden `.fn` = jeden program. -- **Niesprawdzane UB:** wielkość przesunięcia, zakres float→int, wiszące wskaźniki. Przesunięcia - i float→int dałoby się sprawdzać jak dzielenie, niewielkim kosztem. -- **Informacje dla debuggera:** brak DWARF. `DIBuilder` podpiąłby się w `define()` i przy `pos` każdej instrukcji. -- **Platformy:** tylko triple hosta, testowane na Linux/x86-64. Kod jest niezależny od platformy - z wyjątkiem wykrywania nagłówków przez `cc -E -v` i użycia `dprintf`. -- **Bootstrap:** długoterminowym celem jest przepisanie kompilatora w Finchu. Wołanie API LLVM-C już - działa: LLVM-C używa nieprzezroczystych uchwytów (`LLVMModuleRef` itd.), które mapują się na `ptr` - Fincha. `examples/llvm.fn` buduje i wypisuje moduł z poziomu Fincha (`import "llvm-c/Core.h"`, - `link "LLVM"`). Do samodzielnego kompilatora brakuje jeszcze struktur, tablic, tekstów i operacji na plikach. +- Brak metod w strukturach, typów generycznych, map, `match`, domknięć i błędów jako wartości (`int("x")`, `read_file` robią panic). +- Teksty to bajty: `.len`, `s[i]`, `upper()` nie znają Unicode. +- Wiszące wskaźniki (`addr` zmiennej, która zniknęła, użycie po `free`) nie są wykrywane. +- Wielkość przesunięć i konwersje float→int nie są sprawdzane (poison w LLVM, jak w C). +- C: unie i struktury z polami bitowymi przez wartość, makra-funkcje, `long double`. +- Testowana jedna platforma: Linux x86-64 (kod ABI jest specyficzny dla System V x86-64). +- Kompilator samohostujący nie ma importu C, liczb z rozmiarem ani `defer` i nie zwalnia pamięci. diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md index bd2659d..4ff632e 100644 --- a/docs/pl/dla-poczatkujacych.md +++ b/docs/pl/dla-poczatkujacych.md @@ -20,10 +20,15 @@ Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. T 8. [Podejmowanie decyzji: if](#8-podejmowanie-decyzji-if) 9. [Powtarzanie: pętle](#9-powtarzanie-pętle) 10. [Własne polecenia: funkcje](#10-własne-polecenia-funkcje) -11. [Kiedy coś pójdzie nie tak](#11-kiedy-coś-pójdzie-nie-tak) -12. [Małe projekty do zrobienia](#12-małe-projekty-do-zrobienia) -13. [Ściąga](#13-ściąga) -14. [Słowniczek](#14-słowniczek) +11. [Listy: wiele wartości w jednym pudełku](#11-listy-wiele-wartości-w-jednym-pudełku) +12. [Zabawa tekstem](#12-zabawa-tekstem) +13. [Pytanie użytkownika](#13-pytanie-użytkownika) +14. [Własne rodzaje wartości: struktury](#14-własne-rodzaje-wartości-struktury) +15. [Zapisywanie do pliku](#15-zapisywanie-do-pliku) +16. [Kiedy coś pójdzie nie tak](#16-kiedy-coś-pójdzie-nie-tak) +17. [Małe projekty do zrobienia](#17-małe-projekty-do-zrobienia) +18. [Ściąga](#18-ściąga) +19. [Słowniczek](#19-słowniczek) --- @@ -72,7 +77,7 @@ Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy. ./build/finch version ``` -Powinieneś zobaczyć coś w stylu `finch 1.0.0`. +Powinieneś zobaczyć coś w stylu `finch 2.0.0`. --- @@ -446,7 +451,202 @@ Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad czy pod `main`. --- -## 11. Kiedy coś pójdzie nie tak +## 11. Listy: wiele wartości w jednym pudełku + +Czasem jedno pudełko nie wystarczy. Na liście zakupów jest wiele rzeczy. +**Lista** (programiści mówią też **tablica**) trzyma wiele wartości po kolei: + +```c +fn main() { + owoce := ["jabłko", "banan", "wiśnia"] + print(owoce) + print("Mam", owoce.len, "owoce") + print("Pierwszy to", owoce[0]) +} +``` + +``` +["jabłko", "banan", "wiśnia"] +Mam 3 owoce +Pierwszy to jabłko +``` + +- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddzielamy przecinkami. +- `.len` mówi, ile rzeczy jest na liście. +- `owoce[0]` to **pierwsza** rzecz. Liczymy od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia. + +Jeśli zapytasz o miejsce, którego nie ma, np. `owoce[10]`, program się zatrzyma i powie: +`index 10 is out of range (the length is 3)`, czyli „indeks 10 jest poza zakresem (długość to 3)”. + +### Dodawanie i zabieranie + +```c +fn main() { + []int wyniki // pusta lista liczb całkowitych + wyniki.push(10) // dołóż 10 na koniec + wyniki.push(25) + wyniki.push(7) + print(wyniki) // [10, 25, 7] + + ostatni := wyniki.pop() // zabierz ostatni + print(ostatni, wyniki) // 7 [10, 25] + + wyniki.sort() // posortuj + print(wyniki) +} +``` + +### Przechodzenie po liście + +`for ... in` odwiedza każdą wartość po kolei: + +```c +fn main() { + suma := 0 + for n in [3, 5, 2] { + suma += n + } + print("Suma:", suma) // Suma: 10 +} +``` + +Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń), +`sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#tablice). + +--- + +## 12. Zabawa tekstem + +Teksty sklejasz znakiem `+`: + +```c +fn main() { + imie := "Ola" + powitanie := "Cześć, " + imie + "!" + print(powitanie) +} +``` + +Żeby dokleić liczbę do tekstu, najpierw zamień ją na tekst przez `str(...)`: + +```c +wiek := 10 +print("Wiek: " + str(wiek)) +``` + +Teksty mają też przydatne narzędzia: + +```c +fn main() { + slowo := "Finch" + print(slowo.len) // 5 liter + print(slowo[0]) // F (pierwsza litera) + print(slowo.upper()) // FINCH (wielkimi literami) + print(slowo.lower()) // finch (małymi literami) + print(slowo.contains("in")) // true (czy zawiera "in") + for litera in slowo { + print(litera) // F, i, n, c, h, każda w osobnej linijce + } +} +``` + +⚠️ `.len` liczy bajty, a polskie litery (ą, ę, ż…) zajmują po 2 bajty. `"żółw".len` to 7, a nie 4. + +--- + +## 13. Pytanie użytkownika + +`input` pokazuje pytanie i czeka, aż ktoś wpisze odpowiedź i naciśnie Enter: + +```c +fn main() { + imie := input("Jak masz na imię? ") + print("Miło cię poznać,", imie) +} +``` + +Odpowiedź jest zawsze **tekstem**. Jeśli potrzebujesz liczby, zamień ją przez `int(...)`: + +```c +fn main() { + odpowiedz := input("Ile masz lat? ") + wiek := int(odpowiedz) + print("Za rok będziesz mieć", wiek + 1) +} +``` + +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 + +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 { + str tytul + str autor + int rok +} + +fn main() { + k := Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1937) + print(k.tytul, "napisał", k.autor) + k.rok = 1938 + print(k) +} +``` + +``` +Hobbit napisał Tolkien +Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1938) +``` + +- `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ęść, a `k.rok = 1938` ją zmienia. + +Częściom możesz nadać wartość początkową. Wtedy nie trzeba ich podawać: + +```c +struct Gracz { + str imie + int zycia = 3 +} + +fn main() { + g := Gracz(imie: "Ola") + print(g.zycia) // 3 +} +``` + +Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. + +--- + +## 15. Zapisywanie do pliku + +Program zapomina wszystko, kiedy się kończy. Żeby coś zapamiętać, zapisz to do pliku: + +```c +fn main() { + write_file("notatka.txt", "Kupić mleko") + print(read_file("notatka.txt")) +} +``` + +- `write_file(nazwa, tekst)` zapisuje tekst w pliku (i zastępuje to, co tam było). +- `read_file(nazwa)` oddaje tekst z pliku. +- `file_exists(nazwa)` mówi, czy plik istnieje (`true` albo `false`). + +Plik pojawi się w folderze, w którym uruchomiłeś program. + +--- + +## 16. Kiedy coś pójdzie nie tak Każdy się myli przy programowaniu, nawet eksperci, codziennie. Finch stara się tłumaczyć, co poszło nie tak, prostymi słowami (na razie po angielsku). Przykład: @@ -485,10 +685,13 @@ Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o | `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(...)` | --- -## 12. Małe projekty do zrobienia +## 17. Małe projekty do zrobienia Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero, gdy utkniesz. @@ -595,9 +798,59 @@ fn main() { ``` +### Projekt 6: Zgadnij liczbę + +Komputer ma tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem mówi „więcej” albo „mniej”. + +
Rozwiązanie + +```c +fn main() { + tajna := 42 + proby := 0 + while true { + strzal := int(input("Twój strzał: ")) + proby += 1 + if strzal < tajna { + print("Więcej!") + } else if strzal > tajna { + print("Mniej!") + } else { + print("Tak! Potrzebowałeś", proby, "prób") + return + } + } +} +``` + +`return` w `main` kończy program. (Wersja z losową liczbą jest w `examples/guess.fn`.) +
+ +### Projekt 7: Średnia ocen + +Zapytaj o 3 oceny, zapisz je na liście, a potem pokaż listę i średnią. + +
Rozwiązanie + +```c +fn main() { + []int oceny + for i in 0..3 { + oceny.push(int(input("Ocena: "))) + } + suma := 0 + for o in oceny { + suma += o + } + print("Oceny:", oceny) + print("Średnia:", float(suma) / oceny.len) +} +``` +
+ --- -## 13. Ściąga +## 18. Ściąga ```c // komentarz @@ -630,11 +883,21 @@ fn main() { // tu zaczyna się program fn dodaj(int a, int b) -> int { // własne polecenie return a + b } + +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 { } +// 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") ``` --- -## 14. Słowniczek +## 19. Słowniczek | Słowo | Znaczenie | |---|---| @@ -647,6 +910,10 @@ fn dodaj(int a, int b) -> int { // własne polecenie | **funkcja** | Nazwana grupa kroków, której można używać wiele razy | | **pętla** | Kroki, które się powtarzają | | **warunek** | Pytanie tak/nie, np. `x > 5` | +| **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` | +| **indeks** | Pozycja wartości na liście, liczona od 0 | +| **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części | +| **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 | @@ -662,6 +929,10 @@ fn dodaj(int a, int b) -> int { // własne polecenie | `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 | --- diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md index 69ac141..e638005 100644 --- a/docs/pl/dla-technikow.md +++ b/docs/pl/dla-technikow.md @@ -1,9 +1,9 @@ # Finch dla techników **Dla osób, które dobrze znają komputery, ale programować nie umieją albo umieją niewiele.** -Ten przewodnik opisuje instalację Fincha, obsługę polecenia `finch`, cały język, -korzystanie z bibliotek C 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). +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) @@ -20,26 +20,37 @@ działa w środku. Do tego służy [przewodnik dla inżynierów](dla-inzynierow. 7. [Operatory](#7-operatory) 8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem) 9. [Funkcje](#9-funkcje) -10. [Wskaźniki](#10-wskaźniki) -11. [Biblioteki z C](#11-biblioteki-z-c) -12. [Błędy](#12-błędy) -13. [Rozwiązywanie problemów](#13-rozwiązywanie-problemów) -14. [Struktura projektu i testy](#14-struktura-projektu-i-testy) -15. [Obecne ograniczenia](#15-obecne-ograniczenia) +10. [Tablice](#10-tablice) +11. [Tekst (str)](#11-tekst-str) +12. [Struktury](#12-struktury) +13. [Pamięć: kto co zwalnia](#13-pamięć-kto-co-zwalnia) +14. [Wskaźniki](#14-wskaźniki) +15. [Funkcje wbudowane](#15-funkcje-wbudowane) +16. [Moduły](#16-moduły) +17. [Biblioteki z C](#17-biblioteki-z-c) +18. [Błędy](#18-błędy) +19. [Debugowanie](#19-debugowanie) +20. [Rozwiązywanie problemów](#20-rozwiązywanie-problemów) +21. [Struktura projektu i testy](#21-struktura-projektu-i-testy) +22. [Obecne ograniczenia](#22-obecne-ograniczenia) --- ## 1. Czym jest Finch Finch to język **kompilowany**. Narzędzie `finch` tłumaczy plik `.fn` na prawdziwy program -(natywny plik wykonywalny, taki sam jak te z C). Korzysta przy tym z **LLVM**, tego samego -zaplecza kompilatorów, na którym działają clang, Rust i Swift. +(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 to daje: - **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ć. -- **Bezpośredni dostęp do bibliotek C.** Piszesz `import "stdio.h"` i wołasz, co chcesz. +- **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: 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ć. @@ -56,10 +67,10 @@ Z innymi wersjami LLVM prawdopodobnie się nie skompiluje, bo API C++ LLVM zmien ```sh git clone https://github.com/Nexoniarz/Finch.git cd Finch -nix-shell # pobiera LLVM, libclang, cmake, ninja, pkg-config +nix-shell # pobiera LLVM, libclang, clang, cmake, ninja, pkg-config cmake -S . -B build -G Ninja ninja -C build -./build/finch version # finch 1.0.0 (LLVM 21.x) +./build/finch version # finch 2.0.0 (LLVM 21.x) ``` ### Opcja B: pakiety z twojej dystrybucji @@ -73,7 +84,7 @@ i libclang 21, a opcjonalnie `pkg-config`. | Fedora | `gcc-c++ cmake ninja-build pkgconf llvm-devel clang-devel` | | Arch | `base-devel cmake ninja pkgconf llvm clang` | -Jeśli CMake nie znajduje LLVM, wskaż mu katalog: +Ta opcja nie była testowana. Jeśli CMake nie znajduje LLVM, wskaż mu katalog: ```sh cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm @@ -87,21 +98,23 @@ sudo cp build/finch /usr/local/bin/ finch version ``` -Podczas pracy Finch potrzebuje działającego kompilatora C (`cc`). Używa go jako **linkera**, -który skleja skompilowany kod w gotowy program. Inny kompilator wskażesz zmienną środowiskową `CC`. +Podczas pracy Finch potrzebuje kompilatora C (`cc`). Używa go do linkowania programów i jednorazowo +do zbudowania swojej małej biblioteki uruchomieniowej, którą trzyma w `~/.cache/finch`. +Inny kompilator wskażesz zmienną `CC`. --- ## 3. Polecenie finch ``` -finch run skompiluj i od razu uruchom -finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) -finch ir pokaż LLVM IR (dla ciekawskich) -finch version pokaż wersję +finch run [argumenty...] skompiluj i od razu uruchom +finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) +finch ir pokaż LLVM IR (dla ciekawskich) +finch version pokaż wersję opcje: -l dolinkuj bibliotekę C, to samo co link "lib" w pliku + -g dodaj informacje dla debuggera (gdb / lldb) -O0 bez optymalizacji ``` @@ -109,13 +122,13 @@ Przykłady: ```sh finch run hello.fn # wypisuje wynik +finch run narzedzie.fn dane.txt -v # argumenty po nazwie pliku trafiają do programu finch build gra.fn -o mojagra # tworzy ./mojagra -./mojagra -finch run okno.fn -l glfw # z biblioteką C +finch build gra.fn -g -O0 # wersja dla debuggera ``` -`finch run` przekazuje dalej kod wyjścia twojego programu. Jeśli program się wysypie, -powie jak (np. `the program crashed: Segmentation fault`). +`finch run` przekazuje dalej kod wyjścia programu. Jeśli program się wysypie, powie jak +(np. `the program crashed: Segmentation fault`). --- @@ -126,8 +139,14 @@ powie jak (np. `the program crashed: Segmentation fault`). /* albo zajmuje kilka linijek */ -import "math.h" // nagłówki C (opcjonalne), zawsze na górze -link "m" // biblioteki C do dolinkowania (opcjonalne) +import "math.h" // nagłówki C (opcjonalne), na górze +import ksztalty // moduły Fincha (opcjonalne): ksztalty.fn obok tego pliku +link "m" // biblioteki albo pliki C do dolinkowania (opcjonalne) + +struct Punkt { // własne typy + int x + int y +} fn main() { // tu zaczyna się program print("Cześć") @@ -136,12 +155,13 @@ fn main() { // tu zaczyna się program Zasady, które warto znać: -- **Jedna instrukcja na linijkę.** Średników nie ma. Długie wyrażenie możesz złamać - **po** operatorze (`a +⏎ b`) albo w dowolnym miejscu wewnątrz nawiasów `( )`. +- **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 mogą być w pliku w dowolnej kolejności. -- `main` to `fn main()` albo `fn main() -> int` (zwraca kod wyjścia). +- 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 linii poleceń; `args[0]` to ścieżka samego programu). --- @@ -150,19 +170,20 @@ Zasady, które warto znać: ```c x := 5 // nowa zmienna, typ zgadnięty (int) int y = 10 // nowa zmienna, typ podany jawnie -int z // bez wartości: startuje od zera ("" dla str, null dla wskaźników) +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ć **raz** w funkcji. Finch nie pozwala użyć tej samej nazwy ponownie - w bloku wewnętrznym (nie ma „przesłaniania”), bo to częste źródło pomyłek. +- 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`, …) i słowa kluczowe są zarezerwowane. + 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` +**Słowa kluczowe:** `fn return if else while for in break continue true false null import link struct defer` --- @@ -176,7 +197,9 @@ x += 1 // także -= *= /= %= | `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 (napis w stylu C) | `"cześć\tświecie"` | +| `str` | tekst | `"cześć\tświecie"` | +| `[]T` | lista (tablica) elementów `T` | `[1, 2, 3]` | +| twoje struktury | nazwane grupy wartości | `Punkt(1, 2)` | Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`. @@ -191,48 +214,41 @@ Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, ` `f32` to 32-bitowa liczba z przecinkiem (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16). -### Wskaźniki - -`ptr[T]` wskazuje na wartość typu `T`. Samo `ptr` wskazuje na „coś” nieznanego typu, -jak `void*` w C. Szczegóły w rozdziale [Wskaźniki](#10-wskaźniki). - ### 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 tej samej „znakowości” (`i8`→`i32`, `u8`→`u64`) | ✅ | +| 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, `str` | `ptr` | ✅ | -| `ptr` | dowolny `ptr[T]` | ✅ | +| dowolny wskaźnik | `ptr`, a `ptr` na dowolny wskaźnik | ✅ | | wszystko inne | | ❌ napisz to jawnie: `typ(wartość)` | **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 (`the number 300 doesn't fit in u8`). -To samo dotyczy stałych z nagłówków C. +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. **Jawna zamiana** wygląda jak wywołanie funkcji o nazwie typu: ```c -int(3.99) // 3 (obcina część ułamkową) -int(-3.99) // -3 -float(7) / 2 // 3.5 -u8(300) // 44 (zostawia najniższe 8 bitów) -i8(200) // -56 -int('A') // 65 -char(66) // 'B' -bool(0) // false (każda liczba różna od zera to true) -ptr(tekst) // str jako wskaźnik bez typu -str(p) // wskaźnik jako str +int(3.99) // 3 (obcina część ułamkową) +float(7) / 2 // 3.5 +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" (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 +str(p) // wskaźnik na znaki z C jako tekst ``` **Liczby o stałym rozmiarze się „zawijają”**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0. Działania liczą się w typie użytych wartości, więc `u8(250) + 10` daje `4`, a nie `260`. -Jeśli potrzebujesz więcej miejsca, najpierw zamień typ: `int(x) + 10`. --- @@ -242,8 +258,8 @@ Jeśli potrzebujesz więcej miejsca, najpierw zamień typ: `int(x) + 10`. | Priorytet | Operatory | Znaczenie | |---|---|---| -| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcie bitów w lewo/prawo, bitowe AND | -| 4 | `+` `-` `\|` `^` | dodawanie, 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 I | | 1 | `\|\|` | logiczne LUB | @@ -251,18 +267,16 @@ Jeśli potrzebujesz więcej miejsca, najpierw zamień typ: `int(x) + 10`. 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`. W C to samo po cichu znaczy co innego. +> `flagi & 4 != 0` znaczy `(flagi & 4) != 0`. Uwagi: -- `/` na liczbach całkowitych zaokrągla w stronę zera: `7 / 2` to `3`, `-7 / 2` to `-3`. `%` ma znak lewej strony. +- `/` na liczbach całkowitych zaokrągla w stronę zera: `7 / 2` to `3`. `%` ma znak lewej strony. - Dzielenie przez zero **zatrzymuje program** komunikatem `runtime error: division by zero` z numerem linijki. - Jeśli zero jest wpisane wprost w kod, jest to błąd już przy kompilacji. - `&&` i `||` liczą prawą stronę tylko wtedy, gdy to potrzebne. -- Tekst (`str`) można porównywać przez `==` i `!=` (porównuje zawartość). `<` i `>` na tekstach nie działają. -- Wskaźniki porównuje się przez `==` i `!=`, także z `null`. -- Operatory bitowe wymagają liczb całkowitych. Dla `bool` używaj `&&` i `||`. -- `>>` na liczbach ze znakiem zachowuje znak (`-16 >> 2` to `-4`). Na liczbach bez znaku wypełnia zerami. +- 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. --- @@ -285,14 +299,33 @@ 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 str) + print(imie) +} ``` - 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ą liczbami całkowitymi obliczanymi **raz**, przed startem pętli. - `i` jest typu `int` i nie można go zmieniać wewnątrz pętli. +- 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` lub `continue` w tym samym bloku to błąd, bo nigdy się nie wykona. +### defer + +`defer` uruchamia instrukcję, gdy bieżący blok się kończy, niezależnie od tego, jak się kończy: +normalnie, przez `return`, `break` lub `continue`. Kilka `defer` wykonuje się w odwrotnej kolejności. +Przydaje się do sprzątania: + +```c +f := fopen("dane.txt", "r") +defer fclose(f) +// ... używaj f; zostanie zamknięty na końcu bloku, na każdej ścieżce +``` + +Instrukcja w `defer` nie może zawierać `return`, `break` ani `continue`. + --- ## 9. Funkcje @@ -308,32 +341,129 @@ fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca } ``` -- Parametry są kopiami. Zmiana parametru nie zmienia zmiennej u wywołującego. - Do tego służą wskaźniki (niżej). +- 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, 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. -- Wbudowane nazwy, których nie możesz użyć: `print`, `addr` i wszystkie nazwy typów. - -### Funkcje wbudowane - -| Funkcja | Co robi | -|---|---| -| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem. | -| `addr(x)` | Daje wskaźnik na zmienną `x`. | -| `int(x)`, `u8(x)`, `float(x)`, … | Zamienia typy (zobacz [Zamiana typów](#zamiana-typów)). | --- -## 10. Wskaźniki +## 10. Tablice -Wskaźnik przechowuje **adres** wartości, czyli miejsce w pamięci, gdzie ona leży. Finch zamiast -`*` i `&` z C używa słów: +```c +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 +``` + +| Operacja | Znaczenie | +|---|---| +| `a.len` | liczba elementów | +| `a[i]` | element `i` (od 0); poza zakresem program zatrzymuje się z czytelnym komunikatem | +| `a[i] = x` | zmiana elementu | +| `a.push(x)` | dodaj na końcu | +| `a.pop()` | zabierz ostatni i go oddaj | +| `a.insert(i, x)` | wstaw `x` na pozycję `i`, przesuwając resztę | +| `a.remove(i)` | usuń pozycję `i` i ją oddaj | +| `a.clear()` | usuń wszystko | +| `a.resize(n)` | zmień długość na `n` (nowe elementy to zera) | +| `a.contains(x)`, `a.find(x)` | czy `x` jest w środku? jego indeks albo -1 (liczby, znaki, bool, str) | +| `a.sort()`, `a.reverse()` | w miejscu (liczby, znaki, str) | +| `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]` | + +**Przypisanie kopiuje:** po `b := a` zmiana `b` nie zmienia `a`. + +--- + +## 11. Tekst (str) + +```c +s := "Cześć" +s += ", świecie" // sklejanie +print(s.len, s[0]) // .len liczy bajty; ą, ś, ż… zajmują po 2 +s[0] = 'D' // zmiana znaku +``` + +| 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 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` n razy | +| `s.bytes()` | `[]u8` z bajtami | +| `s.ptr` | `ptr[char]` dla C (ważny, dopóki żyje `s`) | + +`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. + +--- + +## 12. Struktury + +```c +struct Gracz { + str imie + int zycia = 3 // wartość domyślna + []int wyniki + Punkt pozycja // struktury w strukturach +} + +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 +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 ją kopiuje, razem z jej tablicami i tekstami. +- Struktura nie może zawierać samej siebie wprost; dla takiego pola użyj `ptr[Wezel]` albo `[]Wezel` (drzewa, listy). +- Struktury nie mają metod. Pisz funkcje, które je przyjmują: `fn wylecz(Gracz g) -> Gracz`. + +--- + +## 13. Pamięć: kto co zwalnia + +Tablic, tekstów i struktur nie zwalniasz nigdy. Finch robi to za ciebie, w przewidywalnym momencie: +**gdy kończy się blok, do którego wartość należy** (`}`), albo wcześniej przy `return`, `break`, `continue`. + +- 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. + +Do pamięci zarządzanej samodzielnie (listy wiązane, drzewa, dzielenie między strukturami): + +```c +wezel := new(Wezel(wartosc: 1)) // wstaw wartość na stertę, dostajesz ptr[Wezel] +defer free(wezel) // free(...) ją oddaje (razem z jej tablicami i tekstami) +``` + +Pamięć z C (`malloc`, obiekty bibliotek) zwalniasz funkcjami z C, 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 | +| `&x` | `addr(x)` | adres x (także `addr(a[i])`, `addr(s.pole)`) | | `*p` | `p.value` | wartość, na którą wskazuje p | +| `p->pole` | `p.pole` | pole struktury, na którą wskazuje p | +| `p[i]` | `p[i]` | indeksowanie jak w C (bez kontroli zakresu) | | `NULL` | `null` | wskazuje donikąd | | `void *` | `ptr` | wskazuje na coś nieznanego typu | @@ -342,72 +472,98 @@ fn podwoj(ptr[int] p) { p.value = p.value * 2 } -fn main() { - x := 21 - podwoj(addr(x)) - print(x) // 42 - - ptr[int] nic = null - if nic == null { - print("pusty") - } -} +x := 21 +podwoj(addr(x)) // x to 42 ``` -Wskaźnik może wskazywać na wskaźnik: `ptr[ptr[int]]`, a potem `pp.value.value`. +Jeśli struktura ma własne pole o nazwie `value`, to `p.value` na wskaźniku do niej oznacza to pole. -Użycie `.value` na wskaźniku `null` **zatrzymuje program** komunikatem -`runtime error: used .value on a null pointer`, zamiast losowo go wysypać. +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 funkcji, do której ta zmienna należała, -bo zmienna wtedy już nie istnieje. Finch jeszcze tego nie sprawdza. +⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu jej bloku. Finch tego nie sprawdza. --- -## 11. Biblioteki z C +## 15. Funkcje wbudowane -Prawie każda biblioteka na Linuksie ma interfejs w C: grafika, dźwięk, sieć, bazy danych. -Finch może z nich korzystać bezpośrednio. +| Funkcja | Co robi | +|---|---| +| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. | +| `input()`, `input("pytanie")` | Czyta jedną linijkę wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia daje pusty tekst. | +| `read_file(sciezka)` | Cały plik jako `str`. Jeśli nie da się go przeczytać, zatrzymuje program z czytelnym komunikatem. | +| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. | +| `file_exists(sciezka)` | `true` / `false` | +| `shell(polecenie)` | Uruchamia polecenie powłoki i oddaje jego kod wyjścia. | +| `exit(kod)` | Natychmiast kończy program. | +| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (callback). | +| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zobacz [Pamięć](#13-pamięć-kto-co-zwalnia)). | +| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zobacz [Zamiana typów](#zamiana-typów)). | + +--- + +## 16. Moduły + +Program możesz podzielić na pliki. `import ksztalty` wczytuje `ksztalty.fn` z tego samego folderu +(albo z folderu wymienionego w zmiennej środowiskowej `FINCH_PATH`, rozdzielonych `:`). + +```c +// ksztalty.fn +struct Pudlo { + float w + float h +} + +fn pole(Pudlo p) -> float { + return p.w * p.h +} +``` + +```c +// main.fn +import ksztalty + +fn main() { + p := ksztalty.Pudlo(2, 3) + print(ksztalty.pole(p)) + ksztalty.Pudlo inne // typ z modułu w deklaracji +} +``` + +- 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. + +--- + +## 17. Biblioteki z C ### Krok 1: zaimportuj nagłówek ```c import "stdio.h" -import "math.h" import "GLFW/glfw3.h" // ścieżka w systemowych katalogach nagłówków -import "mojalib.h" // nagłówek leżący obok twojego pliku .fn +import "mojalib.h" // nagłówek obok twojego pliku .fn ``` Finch czyta nagłówek przez **libclang** (parser C z clanga) i udostępnia: - **funkcje**, także te ze zmienną liczbą argumentów, jak `printf`, -- **wartości `enum`**, -- **proste stałe `#define`**, które są pojedynczą liczbą, np. `M_PI`, `EOF`, `GL_COLOR_BUFFER_BIT`, +- **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ę -Nagłówek tylko *opisuje* funkcje. Ich właściwy kod jest w pliku biblioteki (`libNAZWA.so`). -Powiedz Finchowi, której biblioteki użyć, na górze pliku: - ```c -link "glfw" // używa libglfw.so -link "GL" // używa libGL.so +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 .fn) +link "gotowe.a" // albo plik obiektowy / bibliotekę statyczną ``` -Wpisz samą nazwę: dla `libglfw.so` to `glfw`. Jeśli `pkg-config` zna tę nazwę, Finch użyje jego -ustawień (ścieżek i dodatkowych bibliotek). W przeciwnym razie przekaże linkerowi `-lNAZWA`. -Biblioteka standardowa C i matematyczna (`libm`) są dolinkowane zawsze. - -Na linii poleceń `-l NAZWA` robi to samo co `link "NAZWA"`. - 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 - the header only says the functions exist; their code lives in a library. - add this at the top of your file (with the library's real name): - + ... link "glfw" ``` @@ -421,122 +577,138 @@ error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from " | `float`, `double` | `f32`, `f64` | | `_Bool` / `bool` | `bool` | | `enum` | odpowiadająca liczba całkowita ze znakiem | -| `char *`, `const char *` | `str` | -| `int *`, `double *`, … | `ptr[i32]`, `ptr[f64]`, … | -| `void *`, `struct X *`, `FILE *`, wskaźniki na funkcje | `ptr` | +| `char *` jako **parametr albo wynik** | `str` | +| `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 arr[16]` w strukturze | `[16]i32`: indeksujesz i używasz `.len` | +| `void *`, wskaźniki na funkcje, niekompletne struktury | `ptr` | -Czyli `int` z C to `i32` w Finchu. Liczby wpisane w kod dopasowują się same (`abs(-5)` działa), -ale zmienną typu `int` z Fincha trzeba zamienić: `abs(i32(x))`. - -### Przykład +### Przykład: struktury i raylib ```c -import "stdio.h" -import "math.h" -import "stdlib.h" +import "raylib.h" +link "raylib" fn main() { - printf("%d + %d = %d\n", 2, 3, 5) - print(sqrt(2), M_PI, RAND_MAX) - - ptr[i32] n = malloc(4) // poproś C o 4 bajty pamięci - n.value = 7 - print(n.value) - free(n) // i oddaj je - - fprintf(stderr, "to idzie na wyjście błędów\n") + 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() } ``` -Pełny przykład z grafiką jest w `examples/window.fn`: okno GLFW + OpenGL, które zmienia kolor. -`examples/llvm.fn` pokazuje, że Finch potrafi nawet sterować samym LLVM przez jego API w C. +Ż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). ### Co jeszcze nie działa -- Funkcje C, które przyjmują lub zwracają **`struct` przez wartość**, np. `div()`. Finch mówi o tym - wprost, jeśli spróbujesz. Funkcje przyjmujące *wskaźnik* na strukturę działają. -- **Makra-funkcje** (`#define MAX(a,b) ...`) i makra, które nie są pojedynczą liczbą. +- **Unie** C przez wartość i struktury z **polami bitowymi** przez wartość (wskaźniki do nich działają). +- **Makra-funkcje** i makra, które nie są pojedynczą liczbą. - `long double`, liczby 128-bitowe. -- Odczytywanie **pól** struktury C. --- -## 12. Błędy +## 18. Błędy ### Błędy kompilacji -Finch zatrzymuje się na pierwszym problemie i pokazuje, gdzie jest: - ``` gra.fn:12:9: error: 'wynik' must be int, but this is str 12 | wynik = "wysoki" | ^ ``` -Format: `plik:linijka:kolumna: error: wyjaśnienie`, a pod spodem linijka ze znakiem `^` 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 -Niektóre problemy widać dopiero, gdy program działa. Zamiast niezdefiniowanego zachowania -(jak w C) Finch zatrzymuje się z czytelnym komunikatem na wyjściu błędów i kodem wyjścia 1: +Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się 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: used .value on a null pointer` | odczyt lub zapis `p.value`, gdy `p` jest `null` | +| `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 | --- -## 13. Rozwiązywanie problemów +## 19. Debugowanie + +```sh +finch build gra.fn -g -O0 -o gra +gdb ./gra +(gdb) break gra.fn:12 +(gdb) run +(gdb) bt # gdzie jesteśmy, przez które funkcje +(gdb) info locals # wszystkie zmienne +(gdb) print gracz.zycia +``` + +`-g` dodaje informacje dla debuggera (linijki, funkcje, zmienne, pola struktur). `-O0` zostawia wszystkie +zmienne widoczne; bez niego optymalizator może część z nich usunąć. + +--- + +## 20. 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`). Własny nagłówek połóż obok pliku `.fn`. | +| `can't find the C header 'x.h'` | Brakuje plików deweloperskich biblioteki. Zainstaluj je (Debian: `libx-dev`; Nix: dopisz do `shell.nix`). | | `… come from "x.h", but its library isn't linked` | Dopisz `link "nazwa"` na górze pliku. | -| `the library 'x' wasn't found` | Biblioteka nie jest zainstalowana albo nazwa jest zła. Na Niksie dopisz ją do `shell.nix` i pracuj wewnątrz `nix-shell`. | -| `the C function 'f' can't be used from Finch yet` | Używa struktury przez wartość albo innego nieobsługiwanego typu. Poszukaj wariantu, który przyjmuje wskaźniki. | -| `no C compiler found to link with` | Zainstaluj gcc lub clang albo ustaw `CC`. | -| Program się kompiluje, ale przy starcie nie znajduje biblioteki C | Został zlinkowany ze ścieżki, której system nie przeszukuje. Uruchamiaj go w tym samym `nix-shell` albo zainstaluj bibliotekę systemowo. | -| `unexpected character 'ż'` | Polska litera w nazwie. W nazwach używaj tylko a–z. | -| CMake: `libclang not found` | Zainstaluj pakiet deweloperski libclang albo uruchom `cmake` w `nix-shell`. | -| Budowanie kończy się błędami API LLVM | Masz inną wersję LLVM niż 21. | +| `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.fn` obok importującego pliku albo ustaw `FINCH_PATH`. | +| `couldn't build the Finch runtime` | Brak działającego kompilatora C. Zainstaluj gcc lub clang albo ustaw `CC`. | +| Biblioteka nie znajduje się przy starcie programu | Zlinkowano ją z folderu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją systemowo. | +| `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. | --- -## 14. Struktura projektu i testy +## 21. Struktura projektu i testy ``` Finch/ ├── src/ kompilator (C++) -├── examples/ przykładowe programy (hello, tour, types, c_import, window, llvm) +├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, wejście, pliki) +├── boot/ kompilator Fincha napisany w Finchu (zobacz przewodnik dla inżynierów) +├── examples/ hello, tour, types, structs, todo, guess, c_import, window, raylib, llvm ├── tests/ -│ ├── run/ programy + dokładny wynik, który muszą wypisać (.out) +│ ├── run/ programy + dokładny wynik (.out) i wejście (.in) │ ├── fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce -│ └── run.sh skrypt uruchamiający testy +│ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem) +│ └── boot.sh buduje kompilator samohostujący nim samym i porównuje ├── docs/ ta dokumentacja (en, pl) ├── shell.nix środowisko deweloperskie Nix └── CMakeLists.txt ``` -Testy uruchomisz po zbudowaniu: - ```sh -tests/run.sh # → 34 passed, 0 failed +tests/run.sh # → 58 passed, 0 failed +MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci +tests/boot.sh # sprawdzenie samohostowania (wymaga clanga) ``` --- -## 15. Obecne ograniczenia +## 22. Obecne ograniczenia -Finch 1.0 to solidny rdzeń, a nie skończony język. Jeszcze nie ma: +Finch 2.0 to kompletny mały język, ale nie skończony. Jeszcze nie ma: -- `struct`, tablic, czytania z klawiatury (`input()`), sklejania tekstów (`"a" + "b"`). - Na razie użyj do tego funkcji z C (`scanf`, `snprintf`, `malloc`…). -- Automatycznego zarządzania pamięcią (w planach: zwalnianie na końcu bloku i `defer`). -- Modułów Fincha (na razie jeden program = jeden plik `.fn`). +- Metod w strukturach, typów generycznych, słowników (map), `match`. (Używaj tablic struktur i funkcji.) +- Błędów jako wartości: błąd w `int("x")` albo `read_file` zatrzymuje program. Sprawdzaj wcześniej (`file_exists`). +- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII. +- Wątków. - Platform innych niż Linux x86-64. Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju). diff --git a/examples/raylib.fn b/examples/raylib.fn new file mode 100644 index 0000000..fe44ba5 --- /dev/null +++ b/examples/raylib.fn @@ -0,0 +1,15 @@ +// A raylib window with a circle: C structs (Color, Vector2) passed by value. +// Needs raylib (in shell.nix). +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() +} diff --git a/notatka.txt b/notatka.txt new file mode 100644 index 0000000..1044ac2 --- /dev/null +++ b/notatka.txt @@ -0,0 +1 @@ +Kupić mleko \ No newline at end of file diff --git a/note.txt b/note.txt new file mode 100644 index 0000000..7d2e267 --- /dev/null +++ b/note.txt @@ -0,0 +1 @@ +Buy milk \ No newline at end of file diff --git a/shell.nix b/shell.nix index d340bbc..7087ecf 100644 --- a/shell.nix +++ b/shell.nix @@ -10,8 +10,9 @@ pkgs.mkShell { ninja pkg-config # finds the right flags for `link "..."` - # for examples/window.fn + # for examples/window.fn and examples/raylib.fn glfw libGL + raylib ]; } diff --git a/src/codegen_types.cpp b/src/codegen_types.cpp index 33b4158..dd57a0a 100644 --- a/src/codegen_types.cpp +++ b/src/codegen_types.cpp @@ -230,7 +230,8 @@ Value *Codegen::own(const Value_ &v) { } void Codegen::release(const Value_ &v) { - if (v.fresh && v.v && owning(v.type)) dropValue(v.v, v.type); + // constants (string literals, empty arrays) own no memory: nothing to free + if (v.fresh && v.v && !isa(v.v) && owning(v.type)) dropValue(v.v, v.type); } Value *Codegen::copyValue(Value *v, const FType &t) {