mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
Finch 2.4.0: methods, maps, errors as values; str() of any value; macOS and ARM64 docs; Kate support
- fn Struct.method(...) with self by reference, also on C structs
- map[K]V: insertion-ordered hash maps, literals, m[k] += 1, has/get/remove/keys/values
- -> T! / -> !, return error(...), or <value>, or { err }, try; fallible int/float/read_file/write_file/delete_file
- for i, x in list / for k, v in map
- print and str() share one text formatter
- LSP: method and map completion, method signatures; grammar and snippets for VS Code and Kate
- tests: 74 (methods, errors, maps + 11 failure cases); docs at all three levels, en and pl
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fd86eb885c
commit
b872be3176
55 changed files with 3400 additions and 336 deletions
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
|
@ -69,6 +69,19 @@ jobs:
|
|||
run: CC=/usr/bin/clang tests/boot.sh
|
||||
- name: Language server
|
||||
run: python3 tests/lsp_test.py
|
||||
- name: Apple frameworks
|
||||
run: |
|
||||
cat > fw.fch <<'EOF'
|
||||
import "CoreFoundation/CoreFoundation.h"
|
||||
link "CoreFoundation.framework"
|
||||
|
||||
fn main() {
|
||||
print(CFAbsoluteTimeGetCurrent() > 0.0)
|
||||
}
|
||||
EOF
|
||||
out=$(CC=/usr/bin/clang build/finch run fw.fch)
|
||||
echo "$out"
|
||||
test "$out" = "true"
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
project(finch VERSION 2.3.0 LANGUAGES C CXX) # C: LLVM's CMake config checks C headers on some systems
|
||||
project(finch VERSION 2.4.0 LANGUAGES C CXX) # C: LLVM's CMake config checks C headers on some systems
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
|
@ -15,6 +15,7 @@ add_executable(finch
|
|||
src/codegen.cpp
|
||||
src/codegen_types.cpp
|
||||
src/codegen_builtins.cpp
|
||||
src/codegen_map.cpp
|
||||
src/abi.cpp
|
||||
src/cimport.cpp
|
||||
src/target.cpp
|
||||
|
|
|
|||
42
README.md
42
README.md
|
|
@ -38,6 +38,8 @@ fn main() {
|
|||
- **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Out-of-range
|
||||
indexes, division by zero and null pointers stop with a clear runtime error instead of undefined behavior.
|
||||
Operator precedence that does not bite (`x & 1 == 0` means what it says).
|
||||
- **Practical.** Maps, methods, errors as values (`int(s) or 0`, `try read_file(p)`), modules, any C
|
||||
library, and a language server for your editor. Runs on Linux, macOS and Windows, on x86-64 and ARM64.
|
||||
- **Errors that talk like a person:**
|
||||
```
|
||||
game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
|
||||
|
|
@ -51,11 +53,18 @@ fn main() {
|
|||
|
||||
## Quick start
|
||||
|
||||
**Windows:** download `finch-windows-x64.zip` from the [releases](https://github.com/Nexoniarz/Finch/releases),
|
||||
install LLVM 21 and Visual Studio Build Tools (C++), and see the
|
||||
[install steps](docs/en/for-technicians.md#windows). **VS Code:** install the `finch-lang` `.vsix` from the same release.
|
||||
Ready builds are in the [releases](https://github.com/Nexoniarz/Finch/releases):
|
||||
|
||||
**Linux** x86-64, LLVM 21:
|
||||
| System | Download | Also needs |
|
||||
|---|---|---|
|
||||
| Windows x64 | `finch-windows-x64.zip` | LLVM 21, Visual Studio Build Tools (C++); [steps](docs/en/for-technicians.md#windows) |
|
||||
| macOS (Apple Silicon) | `finch-macos-arm64.tar.gz` | `brew install llvm@21`; [steps](docs/en/for-technicians.md#macos) |
|
||||
| Linux x64 / ARM64 | `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` | LLVM 21 libraries, a C compiler; [steps](docs/en/for-technicians.md#linux) |
|
||||
|
||||
Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any
|
||||
editor with LSP support (`finch lsp`).
|
||||
|
||||
**Building it yourself** (Linux or macOS, LLVM 21):
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git
|
||||
|
|
@ -66,7 +75,7 @@ ninja -C build
|
|||
|
||||
./build/finch run examples/hello.fch
|
||||
./build/finch build examples/tour.fch -o tour && ./tour
|
||||
tests/run.sh # 59 passed, 0 failed
|
||||
tests/run.sh # 74 passed, 0 failed
|
||||
tests/boot.sh # the compiler written in Finch builds itself
|
||||
```
|
||||
|
||||
|
|
@ -86,17 +95,20 @@ Pick the guide that fits you:
|
|||
| | |
|
||||
|---|---|
|
||||
| Variables | `x := 5` (inferred) or `int x = 5` |
|
||||
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, pointers `ptr[T]` |
|
||||
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]` |
|
||||
| Functions | `fn add(int a, int b) -> int { return a + b }` |
|
||||
| Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` |
|
||||
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for n in nums { }` |
|
||||
| Methods | `fn Point.move(int dx) { self.x += dx }` → `p.move(3)` |
|
||||
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for i, n in nums { }` |
|
||||
| Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` |
|
||||
| Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` |
|
||||
| Text | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
|
||||
| Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
|
||||
| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures |
|
||||
| Modules | `import shapes` → `shapes.area(b)` |
|
||||
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks |
|
||||
| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
|
||||
| Tools | `finch run/build/ir`, `-g` for gdb, `-O0` |
|
||||
| Tools | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` for gdb/lldb, `-O0` |
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -106,6 +118,7 @@ Pick the guide that fits you:
|
|||
| [`examples/tour.fch`](examples/tour.fch) | variables, functions, conditions, loops |
|
||||
| [`examples/guess.fch`](examples/guess.fch) | a guessing game: input, loops, C's `rand` |
|
||||
| [`examples/todo.fch`](examples/todo.fch) | structs, arrays, strings and files |
|
||||
| [`examples/wordcount.fch`](examples/wordcount.fch) | maps, methods and errors as values: the most frequent words in a file |
|
||||
| [`examples/structs.fch`](examples/structs.fch) | structs, arrays of structs, a linked list with `new`/`free` |
|
||||
| [`examples/types.fch`](examples/types.fch) | sized numbers and pointers |
|
||||
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
|
||||
|
|
@ -132,19 +145,20 @@ Pick the guide that fits you:
|
|||
- [x] Bootstrap: a Finch compiler written in Finch that builds itself
|
||||
- [x] Windows: `finch.exe`, `.exe` programs, the Microsoft x64 ABI; cross-building from Linux (`--target windows`)
|
||||
- [x] Language server (`finch lsp`) and a VS Code extension
|
||||
- [ ] Maps, methods on structs, `match`
|
||||
- [ ] Error values instead of stopping the program
|
||||
- [ ] More platforms (macOS, ARM64)
|
||||
- [x] macOS and ARM64 (Linux and Apple Silicon), with their C calling conventions; Kate support
|
||||
- [x] Methods on structs, maps, errors as values (`!`, `or`, `try`)
|
||||
- [ ] Generics, `match`, enums
|
||||
- [ ] A standard library in Finch (JSON, paths, time, processes)
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/ compiler: lexer, parser, AST, codegen, C import + ABI, driver (C++17, ~4.5k lines)
|
||||
src/ compiler: lexer, parser, AST, codegen, C import + ABI, language server, driver (C++17, ~7k lines)
|
||||
runtime/ the small C runtime linked into every program
|
||||
boot/ the Finch compiler written in Finch (~3k lines)
|
||||
examples/ example programs
|
||||
editors/ the VS Code extension
|
||||
tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, windows.sh, lsp_test.py
|
||||
editors/ VS Code extension, Kate highlighting and LSP setup
|
||||
tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, cross.sh, lsp_test.py
|
||||
docs/ en/ and pl/ guides for three audiences
|
||||
```
|
||||
|
||||
|
|
|
|||
42
README.pl.md
42
README.pl.md
|
|
@ -38,6 +38,8 @@ fn main() {
|
|||
- **Bezpieczniejszy niż C tam, gdzie to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem
|
||||
i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast
|
||||
powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać).
|
||||
- **Praktyczny.** Mapy, metody, błędy jako wartości (`int(s) or 0`, `try read_file(p)`), moduły, każda
|
||||
biblioteka C i serwer języka dla twojego edytora. Działa na Linuksie, macOS i Windowsie, na x86-64 i ARM64.
|
||||
- **Błędy mówią po ludzku:**
|
||||
```
|
||||
gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
|
||||
|
|
@ -51,11 +53,18 @@ fn main() {
|
|||
|
||||
## Szybki start
|
||||
|
||||
**Windows:** pobierz `finch-windows-x64.zip` z [wydań](https://github.com/Nexoniarz/Finch/releases),
|
||||
zainstaluj LLVM 21 i Visual Studio Build Tools (C++), a potem przejdź przez
|
||||
[kroki instalacji](docs/pl/dla-technikow.md#windows). **VS Code:** zainstaluj plik `.vsix` `finch-lang` z tego samego wydania.
|
||||
Gotowe wersje są w [wydaniach](https://github.com/Nexoniarz/Finch/releases):
|
||||
|
||||
**Linux** x86-64, LLVM 21:
|
||||
| System | Plik | Potrzebuje też |
|
||||
|---|---|---|
|
||||
| Windows x64 | `finch-windows-x64.zip` | LLVM 21, Visual Studio Build Tools (C++); [kroki](docs/pl/dla-technikow.md#windows) |
|
||||
| macOS (Apple Silicon) | `finch-macos-arm64.tar.gz` | `brew install llvm@21`; [kroki](docs/pl/dla-technikow.md#macos) |
|
||||
| Linux x64 / ARM64 | `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` | biblioteki LLVM 21, kompilator C; [kroki](docs/pl/dla-technikow.md#linux) |
|
||||
|
||||
Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo każdy
|
||||
edytor obsługujący LSP (`finch lsp`).
|
||||
|
||||
**Samodzielne budowanie** (Linux albo macOS, LLVM 21):
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git
|
||||
|
|
@ -66,7 +75,7 @@ ninja -C build
|
|||
|
||||
./build/finch run examples/hello.fch
|
||||
./build/finch build examples/tour.fch -o tour && ./tour
|
||||
tests/run.sh # 59 passed, 0 failed
|
||||
tests/run.sh # 74 passed, 0 failed
|
||||
tests/boot.sh # kompilator napisany w Finchu buduje sam siebie
|
||||
```
|
||||
|
||||
|
|
@ -86,17 +95,20 @@ Wybierz przewodnik dla siebie:
|
|||
| | |
|
||||
|---|---|
|
||||
| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` |
|
||||
| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, wskaźniki `ptr[T]` |
|
||||
| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]` |
|
||||
| Funkcje | `fn add(int a, int b) -> int { return a + b }` |
|
||||
| Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
|
||||
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for n in liczby { }` |
|
||||
| Metody | `fn Punkt.przesun(int dx) { self.x += dx }` → `p.przesun(3)` |
|
||||
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` |
|
||||
| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cy")`, `ile[s] += 1`, `for k, v in wiek { }` |
|
||||
| Błędy | `fn parsuj(str s) -> int! { … return error("zły") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` |
|
||||
| Tekst | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
|
||||
| Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
|
||||
| Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie |
|
||||
| Moduły | `import ksztalty` → `ksztalty.pole(p)` |
|
||||
| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków |
|
||||
| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
|
||||
| Narzędzia | `finch run/build/ir`, `-g` dla gdb, `-O0` |
|
||||
| Narzędzia | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` dla gdb/lldb, `-O0` |
|
||||
|
||||
## Przykłady
|
||||
|
||||
|
|
@ -106,6 +118,7 @@ Wybierz przewodnik dla siebie:
|
|||
| [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle |
|
||||
| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C |
|
||||
| [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki |
|
||||
| [`examples/wordcount.fch`](examples/wordcount.fch) | mapy, metody i błędy jako wartości: najczęstsze słowa w pliku |
|
||||
| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` |
|
||||
| [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki |
|
||||
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
|
||||
|
|
@ -132,19 +145,20 @@ Wybierz przewodnik dla siebie:
|
|||
- [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie
|
||||
- [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`)
|
||||
- [x] Serwer języka (`finch lsp`) i rozszerzenie VS Code
|
||||
- [ ] Mapy, metody w strukturach, `match`
|
||||
- [ ] Błędy jako wartości zamiast zatrzymywania programu
|
||||
- [ ] Więcej platform (macOS, ARM64)
|
||||
- [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate
|
||||
- [x] Metody w strukturach, mapy, błędy jako wartości (`!`, `or`, `try`)
|
||||
- [ ] Typy generyczne, `match`, enumy
|
||||
- [ ] Biblioteka standardowa w Finchu (JSON, ścieżki, czas, procesy)
|
||||
|
||||
## Struktura
|
||||
|
||||
```
|
||||
src/ kompilator: lekser, parser, AST, codegen, import C + ABI, sterownik (C++17, ok. 4,5 tys. linii)
|
||||
src/ kompilator: lekser, parser, AST, codegen, import C + ABI, serwer języka, sterownik (C++17, ok. 7 tys. linii)
|
||||
runtime/ mały runtime w C dołączany do każdego programu
|
||||
boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii)
|
||||
examples/ przykładowe programy
|
||||
editors/ rozszerzenie VS Code
|
||||
tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh, windows.sh, lsp_test.py
|
||||
editors/ rozszerzenie VS Code, podświetlanie i konfiguracja LSP dla Kate
|
||||
tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh, cross.sh, lsp_test.py
|
||||
docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README
|
|||
| `finch lsp` | the language server (editors use it) |
|
||||
| `finch version` | version |
|
||||
| `-l lib` | link a C library (like `link "lib"`) |
|
||||
| `--target windows` / `linux` / triple | build for another system |
|
||||
| `--target windows` / `linux` / `arm64` / `macos` / triple | build for another system |
|
||||
| `-g` | debug info (gdb, lldb, Visual Studio) |
|
||||
| `-O0` | no optimizations |
|
||||
|
||||
|
|
@ -71,6 +71,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around
|
|||
| `u8 u16 u32 u64` | unsigned, sized |
|
||||
| `f32 f64` | floats |
|
||||
| `[]T` | array: `[1, 2, 3]`, `[]str names` |
|
||||
| `map[K]V` | map: `["a": 1]`, `map[str]int counts`, `[:]` |
|
||||
| `ptr[T]`, `ptr` | pointer, untyped pointer |
|
||||
| `Name` / `module.Name` | a struct |
|
||||
|
||||
|
|
@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … }
|
|||
while x > 0 { … }
|
||||
for i in 0..10 { … } // 0 … 9
|
||||
for item in list { … } // every element (or char of a str)
|
||||
for i, item in list { … } // with its index
|
||||
for key, value in m { … } // every map entry, in insertion order
|
||||
break continue return value
|
||||
defer cleanup() // runs when the block ends
|
||||
```
|
||||
|
|
@ -121,6 +124,47 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
|
|||
|
||||
---
|
||||
|
||||
## Methods
|
||||
|
||||
```c
|
||||
fn Point.move(int dx, int dy) { // self is the Point it's called on (not a copy)
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
}
|
||||
fn Point.dist() -> float { … }
|
||||
|
||||
p.move(1, 2) list[0].move(1, 1) ptr_to_point.move(0, 1)
|
||||
```
|
||||
|
||||
Also on structs from C: `fn Vector2.length() -> f32 { … }`.
|
||||
|
||||
---
|
||||
|
||||
## Errors as values
|
||||
|
||||
```c
|
||||
fn parse(str s) -> int! { // ! = can fail
|
||||
if s.len == 0 {
|
||||
return error("empty")
|
||||
}
|
||||
return int(s) or { return error("not a number: " + s) }
|
||||
}
|
||||
fn save(str p, str t) -> ! { // can fail, no result
|
||||
try write_file(p, t)
|
||||
}
|
||||
```
|
||||
|
||||
| Code | On failure |
|
||||
|---|---|
|
||||
| `x := parse(s) or 0` | use 0 |
|
||||
| `x := parse(s) or { print(err); return }` | run the block (`err`: the message) |
|
||||
| `x := try parse(s)` | fail the current function too |
|
||||
| `fn main() -> !` | a failure prints `error: …`, exit code 1 |
|
||||
|
||||
Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
|
||||
|
||||
---
|
||||
|
||||
## Structs
|
||||
|
||||
| Code | Meaning |
|
||||
|
|
@ -155,6 +199,23 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
|
|||
|
||||
---
|
||||
|
||||
## Maps `map[K]V`
|
||||
|
||||
| Code | Gives |
|
||||
|---|---|
|
||||
| `m := ["a": 1, "b": 2]` | a map (keys: numbers, char, bool, str) |
|
||||
| `m[k]` | value (missing key: the program stops) |
|
||||
| `m[k] = v` | add / replace |
|
||||
| `m[k] += 1`, `m[k].push(x)` | a missing key starts at the default (0, `[]`, …) |
|
||||
| `m.len` | number of keys |
|
||||
| `m.has(k)` | bool |
|
||||
| `m.get(k, default)` | value or default |
|
||||
| `m.remove(k)` | bool: was it there |
|
||||
| `m.clear()` | empty it |
|
||||
| `m.keys()`, `m.values()` | arrays, insertion order |
|
||||
|
||||
---
|
||||
|
||||
## Text `str`
|
||||
|
||||
| Code | Gives |
|
||||
|
|
@ -189,6 +250,8 @@ Parameters behave like copies. Order in the file doesn't matter. A comma after t
|
|||
| `char(66)` | `'B'` |
|
||||
| `bool(0)` | `false` |
|
||||
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text |
|
||||
| `str([1, 2])`, `str(m)`, `str(point)` | the same text `print` shows |
|
||||
| `int("x") or 0`, `float(s) or 0.0` | a number, or the fallback |
|
||||
| `str(bytes)` | `[]u8` → text |
|
||||
| `str(p)` | C `char*` → text |
|
||||
| `ptr(p)`, `ptr(16)` | untyped pointer, from a pointer or a number |
|
||||
|
|
@ -210,6 +273,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa
|
|||
| `delete_file(path)` | bool |
|
||||
| `shell(command)` | run a command, its exit code |
|
||||
| `exit(code)` | stop the program now |
|
||||
| `error(message)` | `return error("…")` in a `-> T!` function |
|
||||
| `addr(x)` | pointer to x; `addr(fn)` = C callback |
|
||||
| `new(value)` | value on the heap → `ptr[T]` |
|
||||
| `free(p)` | give it back |
|
||||
|
|
@ -281,11 +345,17 @@ shapes.Box other // in declarations
|
|||
| `pop() on an empty array` | |
|
||||
| `used .value on a null pointer` | `.value` / `.field` / `[i]` through `null` |
|
||||
| `can't turn "x" into int` | `int("x")` |
|
||||
| `can't read the file "…"` | `read_file` |
|
||||
| `can't read the file "…"` | `read_file` (without `or`) |
|
||||
| `the key "x" is not in the map` | `m["x"]` |
|
||||
|
||||
---
|
||||
|
||||
## VS Code (extension "Finch")
|
||||
## Editors
|
||||
|
||||
Kate: `editors/kate/install.sh`, then enable the **LSP Client** plugin; run with **Build & Run** → `finch run %f`.
|
||||
Any LSP editor: `finch lsp`.
|
||||
|
||||
### VS Code (extension "Finch")
|
||||
|
||||
| Key / action | Does |
|
||||
|---|---|
|
||||
|
|
@ -295,11 +365,12 @@ shapes.Box other // in declarations
|
|||
| hover | type / signature / docs |
|
||||
| `Ctrl+Space` | completion (after `.`: fields, methods, module members) |
|
||||
| `Ctrl+Shift+O` | functions and structs in the file |
|
||||
| `main`, `fn`, `fnr`, `struct`, `for`, `foreach`, `if`, `ife`, `while`, `input` + Tab | snippets |
|
||||
| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | snippets |
|
||||
| setting `finch.path` | where finch / finch.exe is |
|
||||
|
||||
---
|
||||
|
||||
## Keywords
|
||||
|
||||
`fn return if else while for in break continue true false null import link struct defer`
|
||||
`fn return if else while for in break continue true false null import link struct defer or try`
|
||||
(and `self` inside methods)
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ To check it works, type:
|
|||
./build/finch version
|
||||
```
|
||||
|
||||
You should see something like `finch 2.3.0`.
|
||||
You should see something like `finch 2.4.0`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -505,8 +505,68 @@ fn main() {
|
|||
}
|
||||
```
|
||||
|
||||
If you also want to know *where* you are in the list, give `for` two names: the first one counts
|
||||
0, 1, 2…, the second one is the value:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
for i, fruit in ["apple", "banana", "cherry"] {
|
||||
print(i + 1, fruit)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
1 apple
|
||||
2 banana
|
||||
3 cherry
|
||||
```
|
||||
|
||||
Lists can do more: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`.
|
||||
The [guide for technicians](for-technicians.md#arrays) lists them all.
|
||||
The [guide for technicians](for-technicians.md#10-arrays-and-maps) lists them all.
|
||||
|
||||
### Looking things up by name: maps
|
||||
|
||||
A list finds things by their place: `fruits[0]`. Sometimes you want to find things by a **name**
|
||||
instead, like looking up a word in a dictionary, or a phone number in a phone book.
|
||||
That's a **map**: each **key** (the name) has a **value**:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
phone := ["Ola": "555-1234", "Jan": "555-9876"]
|
||||
print(phone["Ola"]) // 555-1234
|
||||
|
||||
phone["Ewa"] = "555-0000" // add a new name
|
||||
print(phone.len) // 3
|
||||
|
||||
if phone.has("Tom") {
|
||||
print("Tom is here")
|
||||
} else {
|
||||
print("no Tom")
|
||||
}
|
||||
|
||||
for name, number in phone { // every name with its number
|
||||
print(name, number)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `["key": value, ...]` makes a map. Keys are often text, but numbers work too.
|
||||
- `phone["Ola"]` gets the value for a key. Asking for a key that isn't there stops the program,
|
||||
so check first with `phone.has("Tom")`, or use `phone.get("Tom", "unknown")`.
|
||||
- `for name, number in phone` visits every key with its value, in the order you added them.
|
||||
|
||||
Maps are great for **counting**. A new key starts at 0, so `+= 1` just works:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
map[str]int votes // an empty map: text keys, whole-number values
|
||||
for v in ["cat", "dog", "cat", "cat", "fish"] {
|
||||
votes[v] += 1
|
||||
}
|
||||
print(votes) // {"cat": 3, "dog": 1, "fish": 1}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -618,6 +678,36 @@ fn main() {
|
|||
|
||||
And you can keep many of them in a list: `[]Book shelf`, then `shelf.push(b)`.
|
||||
|
||||
### Commands for your struct: methods
|
||||
|
||||
A struct can have its own commands. Write the struct's name, a dot, and the command's name.
|
||||
Inside, `self` is the struct the command was used on:
|
||||
|
||||
```c
|
||||
struct Player {
|
||||
str name
|
||||
int lives = 3
|
||||
}
|
||||
|
||||
fn Player.hit() {
|
||||
self.lives -= 1
|
||||
print(self.name, "has", self.lives, "lives left")
|
||||
}
|
||||
|
||||
fn Player.is_alive() -> bool {
|
||||
return self.lives > 0
|
||||
}
|
||||
|
||||
fn main() {
|
||||
p := Player(name: "Ola")
|
||||
p.hit() // Ola has 2 lives left
|
||||
p.hit() // Ola has 1 lives left
|
||||
print(p.is_alive()) // true
|
||||
}
|
||||
```
|
||||
|
||||
`p.hit()` changes `p` itself, so after two hits `p.lives` really is 1.
|
||||
|
||||
---
|
||||
|
||||
## 15. Saving to a file
|
||||
|
|
@ -680,6 +770,57 @@ Here we simply misspelled `age`.
|
|||
| `index 5 is out of range (the length is 3)` | You asked a list for a place it doesn't have | Remember counting starts at 0; check `.len` |
|
||||
| `can't turn "abc" into int` | `int(...)` got text that isn't a number | Check what the person typed |
|
||||
| `can't use '+' on str and int` | Text plus a number | Use `str(...)` on the number |
|
||||
| `'f' can fail, so say what happens then` | You used a command that can fail without saying what to do | Add `or ...` (see below) |
|
||||
|
||||
### Things that can fail: `or` and `try`
|
||||
|
||||
Some things can go wrong *while the program runs*: the person types `abc` where you wanted a number,
|
||||
or a file isn't there. Instead of stopping the program, you can say what to do then, with **`or`**:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
age := int(input("Your age? ")) or 0 // not a number? use 0
|
||||
print("Next year you'll be", age + 1)
|
||||
|
||||
note := read_file("note.txt") or "(no note yet)"
|
||||
print(note)
|
||||
}
|
||||
```
|
||||
|
||||
`or` can also be followed by a block. Inside it, `err` is the message that says what went wrong:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
while true {
|
||||
n := int(input("Pick a number: ")) or {
|
||||
print("That's not a number, try again")
|
||||
continue
|
||||
}
|
||||
print("You picked", n)
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your own functions can fail too. Put `!` after the result type, and use `return error("...")`:
|
||||
|
||||
```c
|
||||
fn safe_divide(int a, int b) -> int! {
|
||||
if b == 0 {
|
||||
return error("can't divide by zero")
|
||||
}
|
||||
return a / b
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print(safe_divide(10, 2) or -1) // 5
|
||||
print(safe_divide(10, 0) or -1) // -1
|
||||
}
|
||||
```
|
||||
|
||||
Finch won't let you forget: calling `safe_divide(10, 0)` without `or` is an error that tells you
|
||||
to handle it. (You can also pass the problem on with `try`; the
|
||||
[guide for technicians](for-technicians.md#errors-as-values) explains it.)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -882,10 +1023,13 @@ struct Pet { // your own kind of value
|
|||
int age = 1
|
||||
}
|
||||
|
||||
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { }
|
||||
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } for i, n in nums { }
|
||||
// maps: ages := ["Ola": 9] ages["Jan"] = 10 ages["Ola"] ages.has("Tom") for name, age in ages { }
|
||||
// text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
|
||||
// ask: name := input("Name? ")
|
||||
// files: write_file("f.txt", text) read_file("f.txt")
|
||||
// methods: fn Pet.grow() { self.age += 1 } p.grow()
|
||||
// failing: n := int(text) or 0 x := read_file("f.txt") or { print(err) ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -905,7 +1049,9 @@ struct Pet { // your own kind of value
|
|||
| **condition** | A yes/no question, like `x > 5` |
|
||||
| **list / array** | Many values kept in order, like `[1, 2, 3]` |
|
||||
| **index** | The position of a value in a list, starting at 0 |
|
||||
| **struct** | Your own kind of value, made of named parts |
|
||||
| **struct** | Your own kind of value made of named parts |
|
||||
| **method** | A command that belongs to a struct, like `p.hit()` |
|
||||
| **map** | Values you find by a key (a name), like a dictionary |
|
||||
| **input** | What the person types into the program |
|
||||
| **error** | A message saying something is wrong, and where |
|
||||
| **bug** | A mistake in a program |
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ For the language from a user's point of view, see the [technicians' guide](for-t
|
|||
20. [Semantics vs. C](#20-semantics-vs-c)
|
||||
21. [Function reference](#21-function-reference)
|
||||
22. [Extending the compiler](#22-extending-the-compiler)
|
||||
23. [Targets and Windows](#23-targets-and-windows)
|
||||
23. [Targets: Windows, macOS, ARM64](#23-targets-windows-macos-arm64)
|
||||
24. [The language server](#24-the-language-server)
|
||||
25. [The VS Code extension](#25-the-vs-code-extension)
|
||||
25. [Editor integrations](#25-editor-integrations)
|
||||
26. [Known limitations](#26-known-limitations)
|
||||
|
||||
---
|
||||
|
|
@ -90,20 +90,24 @@ Design decisions that shape the rest:
|
|||
|
||||
| File | Lines | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/error.h` | 40 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)` |
|
||||
| `src/error.h` | 50 | `g_files` (all sources), `failAt(file, line, col, msg)`, `fail(line, col, msg)`, `FinchError` |
|
||||
| `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(fileIndex)`, `tokName()` |
|
||||
| `src/ast.h` | 310 | `Type` (incl. Array, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
|
||||
| `src/parser.h/.cpp` | 540 | Recursive-descent `Parser`, `typeFromName()` |
|
||||
| `src/cimport.h/.cpp` | 420 | libclang import: functions, C structs (fields, offsets), constants, globals |
|
||||
| `src/ast.h` | 360 | `Type` (incl. Array, Map, Fixed, Struct, Named), all `Expr`/`Stmt` nodes, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
|
||||
| `src/parser.h/.cpp` | 610 | Recursive-descent `Parser`, `typeFromName()` |
|
||||
| `src/loader.h/.cpp` | 90 | Reading files and modules (`Loader`), `FINCH_PATH` |
|
||||
| `src/target.h/.cpp` | 210 | `g_target`: triple, C compiler, processes, paths; backend initialisation |
|
||||
| `src/cimport.h/.cpp` | 430 | libclang import: functions, C structs (fields, offsets), constants, globals |
|
||||
| `src/codegen.h` | 15 | `generate()` |
|
||||
| `src/codegen_impl.h` | 265 | The `Codegen` class and its helper structs, shared by the four files below |
|
||||
| `src/codegen.cpp` | 1090 | Program, declarations, scopes and cleanups, statements, expressions, places (`ref`), operators, mutation analysis |
|
||||
| `src/codegen_types.cpp` | 500 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), printing, DWARF types |
|
||||
| `src/codegen_builtins.cpp` | 710 | Calls, constructors, built-ins, conversions, array and str methods, runtime declarations, panics |
|
||||
| `src/abi.cpp` | 310 | System V x86-64 classification, C calls with structs by value, C-callable thunks |
|
||||
| `src/main.cpp` | 390 | Driver: CLI, loader, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` |
|
||||
| `runtime/finch_rt.c` | 440 | The runtime library (§12) |
|
||||
| `boot/*.fch` | 3080 | The self-hosted compiler (§17) |
|
||||
| `src/codegen_impl.h` | 310 | The `Codegen` class and its helper structs, shared by the files below |
|
||||
| `src/codegen.cpp` | 1460 | Program, declarations (functions, methods), scopes and cleanups, statements, expressions, places (`ref`), operators, errors as values, mutation analysis |
|
||||
| `src/codegen_types.cpp` | 580 | LLVM types, type resolution, struct layout, coercions, ownership (copy/drop helpers), text formatting, debug types |
|
||||
| `src/codegen_builtins.cpp` | 890 | Calls, methods, constructors, built-ins, conversions, array and str methods, runtime declarations, panics |
|
||||
| `src/codegen_map.cpp` | 240 | Maps: indexing, literals, methods, iteration |
|
||||
| `src/abi.cpp` | 370 | C calling conventions (System V x86-64, Microsoft x64, AAPCS64), C calls with structs by value, C-callable thunks |
|
||||
| `src/lsp.cpp`, `src/index.h`, `src/builtins_doc.h` | 700 | The language server (§24) |
|
||||
| `src/main.cpp` | 350 | Driver: CLI, target machine, O2, object emission, runtime cache, linking, link-error advice, `run` |
|
||||
| `runtime/finch_rt.c` | 740 | The runtime library (§12) |
|
||||
| `boot/*.fch` | 3100 | The self-hosted compiler (§17) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -120,7 +124,8 @@ struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; };
|
|||
- **Positions:** 1-based; `col` counts characters (UTF-8 continuation bytes don't advance it), so
|
||||
carets line up under `"Błąd"`. A stray non-ASCII character is reported whole, with a hint that names are ASCII-only.
|
||||
- **Identifiers / keywords:** `[A-Za-z_][A-Za-z0-9_]*`. Keywords:
|
||||
`fn return if else while for in break continue true false null import link struct defer`.
|
||||
`fn return if else while for in break continue true false null import link struct defer or try`.
|
||||
(`self` and `map` are ordinary identifiers with a meaning in context: inside a method, before `[`.)
|
||||
**Type names are not keywords**: `int`, `u8`, `ptr`… are `Ident`s recognised by the parser through
|
||||
`typeFromName()`, so `u8(x)` is an ordinary call and the lexer doesn't depend on the type list.
|
||||
- **Numbers:** decimal or `0x` hex with `_` separators. No octal (`010` is 10). A `.` belongs to a number
|
||||
|
|
@ -142,27 +147,31 @@ program = { import | link | struct | function } ;
|
|||
import = "import" ( STRING | IDENT ) ; (* "x.h" = C header, name = Finch module *)
|
||||
link = "link" STRING ; (* "glfw" | "file.c" | "file.o" | "lib.a" *)
|
||||
struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NEWLINE } "}" ;
|
||||
function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ;
|
||||
function = "fn" IDENT [ "." IDENT ] "(" [ param { "," param } ] ")" [ "->" ( type [ "!" ] | "!" ) ] block ;
|
||||
(* fn Point.move: a method; "!": can fail *)
|
||||
param = type IDENT ;
|
||||
type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ;
|
||||
type = "[" "]" type | "map" "[" type "]" type | "ptr" [ "[" type "]" ]
|
||||
| TYPENAME | IDENT [ "." IDENT ] ;
|
||||
|
||||
block = "{" { statement TERMINATOR } "}" ;
|
||||
statement = if | while | for | return | "break" | "continue" | "defer" statement | block
|
||||
| type IDENT [ "=" expr ] (* declaration, see atDeclaration *)
|
||||
| IDENT ":=" expr
|
||||
| target assignop expr (* target: var, field, element, p.value *)
|
||||
| call | method ;
|
||||
for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* range | for-each *)
|
||||
| call | method | try | orelse ;
|
||||
for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* range | for-each *)
|
||||
assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
|
||||
|
||||
expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ;
|
||||
expr = or [ "or" ( block | expr ) ] ; (* f(x) or 0, f(x) or { ... }: lowest, right to left *)
|
||||
or = and { "||" and } ; and = cmp { "&&" cmp } ;
|
||||
cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ;
|
||||
add = mul { ( "+" | "-" | "|" | "^" ) mul } ;
|
||||
mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ;
|
||||
unary = ( "-" | "!" | "~" ) unary | postfix ;
|
||||
unary = ( "-" | "!" | "~" ) unary | "try" postfix | postfix ;
|
||||
postfix = primary { "." IDENT [ args ] | "[" expr "]" } ;
|
||||
primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null"
|
||||
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ;
|
||||
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]"
|
||||
| "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* maps *)
|
||||
args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ;
|
||||
```
|
||||
|
||||
|
|
@ -201,11 +210,16 @@ All nodes are in `src/ast.h`: small class hierarchies with an explicit `kind`, d
|
|||
| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) |
|
||||
| `Index` | `IndexExpr` | `obj`, `index` |
|
||||
| `ArrayLit` | `ArrayLitExpr` | `elems` |
|
||||
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fch(...)` |
|
||||
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; also `module.fn(...)` and `p.method(...)` |
|
||||
| `MapLit` | `MapLitExpr` | `keys`, `values` (`[:]` has none) |
|
||||
| `Try` | `TryExpr` | `call` |
|
||||
| `OrElse` | `OrElseExpr` | `call`, and either `fallback` (an expression) or `block` |
|
||||
|
||||
| StmtKind | Node |
|
||||
|---|---|
|
||||
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `list`, `body`; `DeferStmt` holds one statement |
|
||||
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | as named; `ForEachStmt` has `var`, `var2` (`for i, x` / `for k, v`), `list`, `body`; `DeferStmt` holds one statement |
|
||||
|
||||
`FnDecl` has `recv` (the struct of a method, `""` otherwise) and `fallible` (`-> T!`).
|
||||
|
||||
`math.sqrt(x)` parses as a `MethodExpr` on `VarExpr("math")`; whether `math` is a module or a variable
|
||||
is decided during code generation. Calls name their callee instead of holding an expression: Finch has
|
||||
|
|
@ -220,9 +234,10 @@ There is no "load" node: whether `x` means its address or its value depends on c
|
|||
```cpp
|
||||
struct Type {
|
||||
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
|
||||
Str, Ptr, Null, Array, Fixed, Struct, Named };
|
||||
Str, Ptr, Null, Array, Map, Fixed, Struct, Named };
|
||||
Kind kind;
|
||||
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed
|
||||
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value
|
||||
std::shared_ptr<Type> key; // Map
|
||||
long long count; // Fixed
|
||||
std::string name, module; // Named (as written) / Struct (resolved)
|
||||
StructInfo *info; // Struct
|
||||
|
|
@ -299,9 +314,12 @@ type names (conversion) → function of the current module → struct of the cur
|
|||
| `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` |
|
||||
| `f32` / `float` | `float` / `double` |
|
||||
| `str`, `[]T` | `{ ptr, i64, i64 }` (pointer, length, capacity) |
|
||||
| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; §10.12) |
|
||||
| `ptr`, `ptr[T]`, `null` | `ptr` (opaque) |
|
||||
| `[N]T` | `[N x T]` |
|
||||
| Finch struct | named struct type, natural layout |
|
||||
| a method's `self` | `ptr` (the first parameter) |
|
||||
| the result of `-> T!` | `{ i1 ok, T value, str error }` (`i8` in place of `T` for `-> !`) |
|
||||
| C struct | named **packed** struct with explicit padding bytes (§13.2) |
|
||||
|
||||
Signedness lives only in the Finch type and selects `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`,
|
||||
|
|
@ -375,6 +393,8 @@ value when it goes away. There is no reference counting and no garbage collector
|
|||
| a temporary nobody keeps (`print(a + b)`, `f(g())`, an expression statement) | **`release(v)`**: dropped right after use |
|
||||
| `a.pop()`, `a.remove(i)` | the element is moved out (fresh) |
|
||||
| `for x in list` | `x` borrows each element; if the body may change `list`, each element is copied instead |
|
||||
| a method call `v.m()` | `self` **is** `v` (its address is passed): no copy; changes are visible to the caller |
|
||||
| maps | own their keys and values like arrays own elements; `m[k] = v` moves/copies `v` in and drops the old value |
|
||||
|
||||
A string literal is a fresh value with `cap = 0`: moving it costs nothing, dropping it is skipped
|
||||
(`release()` ignores constants), and writing into it (`s[i] = c`) first makes a heap copy
|
||||
|
|
@ -389,6 +409,11 @@ parameters are borrowed (`Fn::borrowParam`); changed ones are copied once at ent
|
|||
syntactic and conservative: anything that *might* change the parameter makes a copy, which is always correct.
|
||||
The same analysis decides whether `for x in list` must copy elements.
|
||||
|
||||
Methods take part: `declareFns()` first finds which methods change `self` (`mutates("self", body)`),
|
||||
repeating until nothing changes, because calling a changing method on `self` changes `self` too. Their
|
||||
names go into `mutatingMethods`, which `mutatesExpr()` treats like `push`: `p.move(1)` changes `p`.
|
||||
Only then is `borrowParam` computed for every function and method.
|
||||
|
||||
### 9.3 Copy and drop
|
||||
|
||||
`dropAt(addr, T)` / `copyAt(dst, src, T)` work on addresses:
|
||||
|
|
@ -507,7 +532,9 @@ one. Compound `x += v`: evaluate `v`, load the target, `arith()`, store, drop th
|
|||
### 10.4 Functions and `main`
|
||||
|
||||
Functions are `internal` with their natural LLVM signature (aggregates by value). Owning parameters are
|
||||
copied at entry only if `borrowParam` is false. `fn main` is the internal `finch.main`; a separate
|
||||
copied at entry only if `borrowParam` is false. Methods (§10.10) get `self`'s address as an extra first
|
||||
parameter; fallible functions (§10.11) return `{ i1, T, str }`. `fn main() -> !` / `-> int!` makes the
|
||||
wrapper print `error: <message>` to stderr and exit with 1 on failure. `fn main` is the internal `finch.main`; a separate
|
||||
external `i32 main(i32, ptr)` (`defineMainWrapper`) builds `[]str args` with `finch_args` if requested,
|
||||
calls it, drops the arguments and returns the exit code. A function that falls off its end gets the
|
||||
scope cleanups and `ret void`, or the error `can reach its end without returning T`.
|
||||
|
|
@ -537,9 +564,12 @@ scopes swapped out, or zero). C calls are in §13.
|
|||
|
||||
### 10.8 Printing
|
||||
|
||||
`print` emits `printf` per argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`, `%.*s` for
|
||||
`str` with its length, `%p`), separated by spaces, then `\n`. Arrays, fixed arrays and structs call
|
||||
generated helpers `finch.print.<key>` that print `[1, 2]` / `Point(x: 1, y: 2)`, quoting strings and chars inside.
|
||||
`print` emits `printf` per scalar argument with a format chosen by type (`%lld`, `%llu`, `%g`, `%c`,
|
||||
`%.*s` for `str` with its length), separated by spaces, then `\n`. Everything else (pointers, arrays,
|
||||
fixed arrays, maps, structs) is first **formatted** into a heap `str` buffer by `emitFormat()` and
|
||||
printed with `finch_buf_print`. Composite types get a generated helper `finch.format.<key>(buf, value*)`
|
||||
that appends `[1, 2]` / `{"a": 1}` / `Point(x: 1, y: 2)`, quoting strings and chars inside. `str(x)` of
|
||||
an array, map or struct runs the same helper, so `str(x)` is always what `print(x)` shows.
|
||||
|
||||
### 10.9 Array and str methods
|
||||
|
||||
|
|
@ -549,6 +579,65 @@ Arrays: `push` (`finch_arr_reserve` + store + len++), `pop`/`remove` (move out,
|
|||
(swap loop), `sort` (libc `qsort` with a generated comparator `finch.cmp.<key>`), `join` (runtime).
|
||||
Methods that change the array require an address (a variable/field/element).
|
||||
Strings: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, all in the runtime.
|
||||
`for i, x in list` adds a read-only `int` index variable next to the element.
|
||||
|
||||
### 10.10 Methods
|
||||
|
||||
`fn Point.move(int dx)` is declared like a function named `finch.[module.]Point.move` with a leading
|
||||
`ptr` parameter and stored in `Codegen::methods[StructInfo*]` (so methods follow the struct into every
|
||||
module that uses it, and C structs can have methods too). In `define()`, `self` is a `Var` whose `slot`
|
||||
**is that pointer argument**: `self.x` is a GEP from the caller's own struct, and `self = …` writes the
|
||||
caller's value. No copy happens, ever.
|
||||
|
||||
At a call `obj.m(args)` (`structMethod()`), `ref(obj)` gives an address when `obj` is a place (a variable,
|
||||
field, element, `m[key]`), the loaded pointer for a `ptr[T]` (null-checked), or a stack temporary for a
|
||||
value (`Point(1, 2).length()`), which is dropped after the call. Two safety rules come from the mutation
|
||||
analysis: a method that changes `self` can't be called on a read-only loop variable, and if such a method
|
||||
is given an argument that lives inside the receiver (`c.merge(c)`, `p.add(p.items)`), the argument is
|
||||
copied first, so `self` can't free memory the argument still points to.
|
||||
|
||||
### 10.11 Errors as values
|
||||
|
||||
A fallible function (`-> T!`) returns `{ i1 ok, T value, str error }`; on failure `value` is zero, so the
|
||||
caller never has to drop it. `return v` wraps `{1, v, zero}` (`retValue()`); `return error(msg)` evaluates
|
||||
the message, runs the cleanups and returns `{0, zero, msg}` (`retError()`).
|
||||
|
||||
Calling a fallible function is only allowed as the operand of `or` / `try`. `fallible(call)` sets
|
||||
`fallibleTarget` to the call expression; `callFinch()` (and the fallible built-ins) compare their own
|
||||
`CallExpr`/`MethodExpr` against it, and if it matches, leave `fallibleFailed` (`i1`) and `fallibleMsg`
|
||||
(`str`) for the caller; if a fallible call doesn't match, it's the compile error *'f' can fail, so say what
|
||||
happens then*. Nested handlers save and restore the three fields.
|
||||
|
||||
| Form | Lowering |
|
||||
|---|---|
|
||||
| `f(x) or fallback` | `br failed, or.failed, or.ok`; the failed block drops the message and evaluates the fallback (only there); a φ joins the two values |
|
||||
| `f(x) or { … }` | the failed block declares `err` (owning the message) and runs the block. If the value is used, the block must end in a terminator; as a statement it may fall through, and the success path releases the unused value |
|
||||
| `try f(x)` | the failed block runs `emitCleanups(0)` and returns `{0, zero, msg}` from the current function, which must be fallible |
|
||||
|
||||
Built-ins with a failure mode check `&call == fallibleTarget` and switch to `finch_str_try_int`,
|
||||
`finch_str_try_float`, `finch_try_read_file`, `finch_try_write_file`, `finch_try_delete_file`, which
|
||||
return 1 / 0 and fill an error `str` instead of panicking. The cost on success is one predictable branch;
|
||||
nothing is allocated unless a failure happens.
|
||||
|
||||
### 10.12 Maps
|
||||
|
||||
The runtime (§12) keeps an insertion-ordered hash table, in the style of CPython's `dict`: an `entries`
|
||||
array of `{ i64 hash, K key, V value }` slots in insertion order (hash 0 = removed), plus an `index` of
|
||||
`icap` slots (power of two, ≥ 2 × cap) with linear probing (−1 empty, −2 removed, else an entry number).
|
||||
The runtime only hashes (FNV-1a + a 64-bit mix; text keys by content, others by bytes), finds and places
|
||||
entries; the compiler stores, copies and drops keys and values, exactly like array elements.
|
||||
|
||||
| Code | Lowering |
|
||||
|---|---|
|
||||
| `m[k]` read | `finch_map_find`; −1 → `finch_map_missing` (noreturn, with the key as text); else a place at `entry.value` |
|
||||
| `m[k]` write (`=`, `+=`, a changing method, `addr`) | `mapSlot()`: `finch_map_slot` returns the entry, adding it if missing; a new entry gets a copy of the key and the value type's default |
|
||||
| `["a": 1, …]` | a zeroed map + `mapSlot` per entry (the first entry decides the types; number values are all `float` if one has a dot) |
|
||||
| `has`, `get`, `remove`, `clear`, `keys`, `values` | `codegen_map.cpp`; `get`'s default is evaluated only when the key is missing |
|
||||
| `for k, v in m` | a loop over `0..used` that skips removed slots, re-reading `entries` and `used` every round |
|
||||
| copy / drop / format | helpers like arrays': `finch_map_clone_raw` then deep copies of owning keys and values; drop each live entry, then `finch_map_free` |
|
||||
|
||||
Removing marks the slot (and the index entry) removed; slots are packed when the table would grow while
|
||||
at least a quarter of them are removed. Removing the last key resets the table.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -565,7 +654,9 @@ well-predicted branch. LLVM removes checks it can prove (constant indexes in ran
|
|||
| array/str/fixed-array index out of range (`icmp uge`, so negative indexes too) | `boundsCheck()` |
|
||||
| `.value`, `.field`, `[i]` through null | `member()`, `index()` |
|
||||
| `pop()` on empty, `slice()`/`sub()` out of range | methods, runtime |
|
||||
| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` | runtime |
|
||||
| bad text in `int(s)`/`float(s)`, unreadable file in `read_file` (without `or` / `try`) | runtime |
|
||||
| `m[key]` read with a missing key | `mapIndex()` |
|
||||
| a method called through a null `ptr[T]` | `structMethod()` |
|
||||
|
||||
A 20-million-element sieve with a bounds check on every access runs as fast as the same C (≈0.09 s):
|
||||
the checks are hoisted or folded.
|
||||
|
|
@ -591,8 +682,11 @@ Functions take and return these **by pointer**, so their C ABI is trivial (no st
|
|||
string building (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`, `replace`,
|
||||
`split`, `join`, `repeat`, `from_bytes`), comparison (`eq`, `cmp`, `find`, `starts`, `ends`), ownership
|
||||
(`copy`, `own`, `drop`, `from_c`), arrays (`reserve`, `make`, `resize`, `clone_raw`, `free`,
|
||||
`insert_gap`, `remove_gap`), `args`, `input`, files, `shell`, and the panics. Allocation failure prints
|
||||
`out of memory` and exits.
|
||||
`insert_gap`, `remove_gap`), maps (`map_find`, `map_slot`, `map_remove_at`, `map_clear`, `map_free`,
|
||||
`map_clone_raw`, `map_missing`; §10.12), text buffers for `print`/`str` (`buf_add`, `buf_int`, …,
|
||||
`buf_print`), the fallible variants (`str_try_int`, `str_try_float`, `try_read_file`, `try_write_file`,
|
||||
`try_delete_file`, `report_error`), `args`, `input`, files, `shell`, and the panics. Allocation failure
|
||||
prints `out of memory` and exits.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -749,7 +843,7 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp
|
|||
- libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`.
|
||||
- `runtime/finch_rt.c` is read at configure time into `build/rt_source.inc` as a raw string literal;
|
||||
`CMAKE_CONFIGURE_DEPENDS` re-runs the configure step when it changes.
|
||||
- `FINCH_VERSION` from `project(VERSION 2.3.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), warning-free.
|
||||
- `FINCH_VERSION` from `project(VERSION 2.4.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), warning-free.
|
||||
- `shell.nix` lists `llvmPackages.clang` before `llvmPackages.libclang`: the latter also ships an
|
||||
unwrapped `clang` that can't find the system headers.
|
||||
|
||||
|
|
@ -759,9 +853,12 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp
|
|||
|
||||
- `tests/run.sh`: every `tests/run/*.fch` with `fn main` must print exactly its `.out` (stdin from `.in`
|
||||
if present); files without `main` are modules or helpers. Every `tests/fail/*.fch` must fail with the text
|
||||
from its `// expect:` line. Currently **59 passed, 0 failed**.
|
||||
from its `// expect:` line. Currently **74 passed, 0 failed**.
|
||||
- `MEMCHECK=1 tests/run.sh`: the same, plus valgrind on every program (no leaks, no invalid access).
|
||||
- `tests/boot.sh`: the self-hosting fixpoint and the subset run (§17).
|
||||
- `tests/cross.sh windows|arm64`: every test built with `--target` and run under Wine / QEMU.
|
||||
- `python3 tests/lsp_test.py`: the language server, driven like an editor.
|
||||
- CI (`.github/workflows/ci.yml`) runs all of it on Linux x86-64, Linux ARM64, macOS ARM64 and Windows.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -850,11 +947,14 @@ with the expected one. It takes about 2.3 s for the self-hosted compiler to comp
|
|||
| Function | Does |
|
||||
|---|---|
|
||||
| `call` / `method` | resolve a call / a method or module-qualified call |
|
||||
| `callFinch` / `construct` / `callC` | Finch function / struct constructor / C function |
|
||||
| `callFinch` / `construct` / `callC` | Finch function or method / struct constructor / C function |
|
||||
| `structMethod` | `obj.m(...)`: the receiver's address, safety checks, the call |
|
||||
| `print`, `addrOf`, `convert`, `arrayMethod` | built-ins |
|
||||
| `fallible`, `tryExpr`, `orElse`, `retValue`, `retError` (`codegen.cpp`) | errors as values (§10.11) |
|
||||
| `mapIndex`, `mapLit`, `mapMethod`, `mapSlot`, `forEachEntry` (`codegen_map.cpp`) | maps (§10.12) |
|
||||
| `rt` / `libc` | declare a runtime / libc function |
|
||||
| `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | runtime checks |
|
||||
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | System V classification, register plan, declaration, call, callback thunk |
|
||||
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | ABI classification (System V, Microsoft x64, AAPCS64), register plan, declaration, call, callback thunk |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -877,12 +977,15 @@ emission in `arithOp`/`compare`, an explanation in `badOperands`.
|
|||
|
||||
---
|
||||
|
||||
## 23. Targets and Windows
|
||||
## 23. Targets: Windows, macOS, ARM64
|
||||
|
||||
`src/target.h` holds `g_target`: the LLVM triple, `windows` / `msvc` / `cross` flags, the C compiler (`cc`)
|
||||
and the executable suffix. `setTarget()` picks the C compiler: `FINCH_CC`, else `CC` (not when
|
||||
cross-compiling), else `x86_64-w64-mingw32-gcc` for `--target windows` from Linux, `clang` on Windows,
|
||||
`cc` on Unix. Everything platform-dependent goes through it:
|
||||
`src/target.h` holds `g_target`: the LLVM triple, `windows` / `msvc` / `darwin` / `cross` flags, the C
|
||||
compiler (`cc`) and the executable suffix. `setTarget()` maps `windows`, `linux`, `arm64`, `macos` (or a full
|
||||
triple) to a triple and picks the C compiler: `FINCH_CC`, else `CC` (not when cross-compiling), else
|
||||
`x86_64-w64-mingw32-gcc` / `aarch64-linux-gnu-gcc` for Windows / ARM64 Linux from another system,
|
||||
`clang --target=<triple>` for other cross targets, `clang` on Windows, `cc` on Unix. `initTargets()`
|
||||
registers the X86 and AArch64 backends (whichever this LLVM has; CMake defines `FINCH_TARGET_X86` /
|
||||
`FINCH_TARGET_AARCH64` and links their libraries). Everything platform-dependent goes through it:
|
||||
|
||||
- **Headers:** libclang parses with `--target=<triple>` (so `long` is 32 bits on Windows and the right
|
||||
headers are used) and, on Windows, `-D_USE_MATH_DEFINES`. Include folders come from `<cc> -E -v`.
|
||||
|
|
@ -893,7 +996,12 @@ cross-compiling), else `x86_64-w64-mingw32-gcc` for `--target windows` from Linu
|
|||
- **Linking:** no `-lm` with MSVC; `.exe` names; the runtime cache key includes the triple and the C compiler.
|
||||
MSVC linker messages (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) are
|
||||
translated like GNU ld's.
|
||||
- **Running:** a Windows program built on Linux runs with `wine`.
|
||||
- **Running:** `runPrefix()`: a Windows program built on Linux runs with `wine`, an ARM64 Linux one
|
||||
elsewhere with `qemu-aarch64`.
|
||||
- **macOS:** `<cc> -E -v` lists `(framework directory)` entries; they become `-iframework` for libclang,
|
||||
so `import "OpenGL/gl.h"` works. `link "Cocoa.framework"` becomes `-framework Cocoa`. Apple's linker
|
||||
messages (`Undefined symbols for architecture arm64: "_f"`, `ld: library 'x' not found`,
|
||||
`framework 'X' not found`) are translated like the others, without Mach-O's leading `_`.
|
||||
- **Debug info:** CodeView for MSVC targets, DWARF elsewhere.
|
||||
- **Runtime:** no `getline` or `sys/wait.h` on Windows; `stdin`/`stdout`/`stderr` are macros there, so
|
||||
`finch_std_stream(i)` gives them to Finch when a header has no `extern` variable for them.
|
||||
|
|
@ -908,10 +1016,30 @@ receives a plain pointer). Results of 1/2/4/8 bytes come back in `rax` as an int
|
|||
through a hidden `sret` pointer. Each argument takes one slot, so there is no register counting.
|
||||
`cThunk()` mirrors this for callbacks.
|
||||
|
||||
`tests/windows.sh` builds every test with `--target windows`, runs it with Wine and compares the output:
|
||||
`tests/cross.sh windows` builds every test with `--target windows`, runs it with Wine and compares the output:
|
||||
all of them pass, including `c_structs` (which then exercises the Microsoft rules against a C library built
|
||||
with MinGW). CI additionally builds `finch.exe` with MSVC on Windows Server and runs `tests/run.sh` there.
|
||||
|
||||
### AAPCS64 (ARM64 Linux and Apple)
|
||||
|
||||
`classify()` mirrors clang's AArch64 lowering:
|
||||
|
||||
- A **homogeneous floating-point aggregate** (1–4 members, all `float` or all `double`, nested structs
|
||||
and arrays flattened) goes in vector registers: passed as `[n x float]` / `[n x double]`, returned as
|
||||
`{ float, … }`. On Linux the argument also gets `alignstack(8)`, as clang does; Apple doesn't.
|
||||
- Any other struct of up to 16 bytes goes in general registers: `i64` (≤ 8 bytes) or `[2 x i64]`
|
||||
(`i128` if its alignment is 16); it is returned as an integer of its exact size or `[2 x i64]`.
|
||||
- Bigger structs are **Indirect**: a copy on the caller's stack, passed by address (no `byval`); returned
|
||||
through `sret`.
|
||||
- There is no register counting: the backend places each part.
|
||||
- C's plain `char` is **unsigned** on ARM64 Linux and signed on Apple and x86, which matters for the
|
||||
sign/zero extension of `char` arguments and results (`charIsSigned()`).
|
||||
- Apple passes the variadic part of `printf(...)` on the stack; LLVM does that from the `...` in the
|
||||
function type.
|
||||
|
||||
`tests/cross.sh arm64` runs the whole suite (including the struct ABI test against a C library built by
|
||||
GCC) under QEMU, and CI runs it natively on ARM64 Linux and Apple Silicon.
|
||||
|
||||
### Building finch.exe
|
||||
|
||||
CI uses Visual Studio 2022, Ninja and the official `clang+llvm-21.x-x86_64-pc-windows-msvc` package
|
||||
|
|
@ -957,7 +1085,19 @@ signature help (and mirrored by the cheat sheets).
|
|||
|
||||
---
|
||||
|
||||
## 25. The VS Code extension
|
||||
## 25. Editor integrations
|
||||
|
||||
### Kate
|
||||
|
||||
`editors/kate/finch.xml` is a KSyntaxHighlighting definition (the engine of Kate, KWrite and KDevelop):
|
||||
keyword lists that match `builtins_doc.h`, a context for `fn Type.method`, strings with escapes, and
|
||||
folding regions for `{ }` and block comments. `lspclient.json` is the entry for Kate's LSP
|
||||
client (`highlightingModeRegex: ^Finch$`); `install.sh` copies the definition and merges the entry into
|
||||
`~/.config/kate/lspclient/settings.json`. Kate sends more requests than VS Code (`documentHighlight`,
|
||||
`foldingRange`, …); the server answers every request it doesn't implement with `MethodNotFound`, so no
|
||||
client waits.
|
||||
|
||||
### VS Code
|
||||
|
||||
`editors/vscode` is a plain JavaScript extension: `extension.js` starts `finch lsp` through
|
||||
`vscode-languageclient` (path from the `finch.path` setting) and adds **Run** / **Build** as
|
||||
|
|
@ -969,10 +1109,15 @@ templates. CI packages it with `vsce` into `finch-lang.vsix`.
|
|||
|
||||
## 26. Known limitations
|
||||
|
||||
- No methods on structs, generics, maps, `match`, closures, error values (`int("x")`, `read_file` panic).
|
||||
- No generics, interfaces, `match`, closures or Finch-side enums.
|
||||
- Strings are bytes: `.len`, `s[i]`, `upper()` are not Unicode-aware.
|
||||
- Changing a map's keys inside a `for` over the same map is not detected (it is memory-safe, but entries
|
||||
may be skipped or seen twice).
|
||||
- A `try` / `or` failure in the middle of an expression doesn't release fresh temporaries computed
|
||||
earlier in that expression (a small leak on the failure path only).
|
||||
- Dangling pointers (`addr` of a local that went away, use after `free`) are not detected.
|
||||
- Shift amounts and float→int conversions are not range-checked (LLVM poison, as in C).
|
||||
- C: unions and bit-field structs by value, function-like macros, `long double`.
|
||||
- Two systems: Linux and Windows, both x86-64 (System V and Microsoft x64 ABIs). No macOS or ARM yet.
|
||||
- The self-hosted compiler has no C imports, sized integers or `defer`, and doesn't free memory.
|
||||
- Targets: x86-64 and ARM64 (Linux, macOS, Windows on x86-64). No Windows on ARM64, 32-bit, or WebAssembly.
|
||||
- The self-hosted compiler has no C imports, sized integers, `defer`, maps, methods or errors as values,
|
||||
and doesn't free memory.
|
||||
|
|
|
|||
|
|
@ -20,16 +20,16 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md)
|
|||
7. [Operators](#7-operators)
|
||||
8. [Control flow](#8-control-flow)
|
||||
9. [Functions](#9-functions)
|
||||
10. [Arrays](#10-arrays)
|
||||
10. [Arrays and maps](#10-arrays-and-maps)
|
||||
11. [Text (str)](#11-text-str)
|
||||
12. [Structs](#12-structs)
|
||||
12. [Structs and methods](#12-structs-and-methods)
|
||||
13. [Memory: who frees what](#13-memory-who-frees-what)
|
||||
14. [Pointers](#14-pointers)
|
||||
15. [Built-in functions](#15-built-in-functions)
|
||||
16. [Modules](#16-modules)
|
||||
17. [Using C libraries](#17-using-c-libraries)
|
||||
18. [Errors](#18-errors)
|
||||
19. [Editor: VS Code](#19-editor-vs-code)
|
||||
19. [Editors: VS Code, Kate and others](#19-editors-vs-code-kate-and-others)
|
||||
20. [Debugging](#20-debugging)
|
||||
21. [Troubleshooting](#21-troubleshooting)
|
||||
22. [Project layout and tests](#22-project-layout-and-tests)
|
||||
|
|
@ -60,7 +60,9 @@ messages that tell you how to fix the problem.
|
|||
|
||||
## 2. Installing
|
||||
|
||||
Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**.
|
||||
Finch runs on **Linux** (x86-64 and ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64),
|
||||
and is built with **LLVM 21**. Each [release](https://github.com/Nexoniarz/Finch/releases) has a ready
|
||||
`finch` for Windows, Linux x86-64, Linux ARM64 and macOS ARM64.
|
||||
|
||||
### Windows
|
||||
|
||||
|
|
@ -76,8 +78,30 @@ Finch runs on **Linux and Windows, x86-64**, and is built with **LLVM 21**.
|
|||
Programs are normal `.exe` files. To build from source on Windows instead, follow `.github/workflows/ci.yml`
|
||||
(Visual Studio 2022 + the `clang+llvm-21.x-x86_64-pc-windows-msvc` package from LLVM's releases).
|
||||
|
||||
### macOS
|
||||
|
||||
```sh
|
||||
brew install llvm@21 # Finch's LLVM; programs are linked with Apple's clang
|
||||
```
|
||||
|
||||
Then either unpack **`finch-macos-arm64.tar.gz`** from the releases (it uses Homebrew's `llvm@21`),
|
||||
or build it yourself (Xcode command line tools: `xcode-select --install`):
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
|
||||
ninja -C build
|
||||
./build/finch version
|
||||
```
|
||||
|
||||
Apple frameworks link with `link "Cocoa.framework"` (`-framework Cocoa`), and their headers import as usual:
|
||||
`import "OpenGL/gl.h"`.
|
||||
|
||||
### Linux
|
||||
|
||||
The ready `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` need LLVM 21's libraries
|
||||
(Debian/Ubuntu: `libllvm21 libclang1-21` from [apt.llvm.org](https://apt.llvm.org)) and a C compiler (`cc`).
|
||||
Building it yourself works the same on x86-64 and ARM64 (Raspberry Pi 4/5, Graviton, …).
|
||||
Other LLVM versions will likely fail to compile, because LLVM's C++ API changes between releases.
|
||||
|
||||
### Option A: Nix (recommended, nothing to install by hand)
|
||||
|
|
@ -88,7 +112,7 @@ cd Finch
|
|||
nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config
|
||||
cmake -S . -B build -G Ninja
|
||||
ninja -C build
|
||||
./build/finch version # finch 2.3.0 (LLVM 21.x)
|
||||
./build/finch version # finch 2.4.0 (LLVM 21.x)
|
||||
```
|
||||
|
||||
### Option B: your distribution's packages
|
||||
|
|
@ -109,16 +133,27 @@ cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
|
|||
ninja -C build
|
||||
```
|
||||
|
||||
### Building Windows programs on Linux
|
||||
### Building for another system
|
||||
|
||||
With the MinGW-w64 cross compiler installed (`x86_64-w64-mingw32-gcc`; on Nix
|
||||
`pkgsCross.mingwW64.buildPackages.gcc`):
|
||||
`--target` builds a program for another system. Finch needs that system's C compiler to link:
|
||||
|
||||
| `--target` | Builds | C compiler used | `finch run` uses |
|
||||
|---|---|---|---|
|
||||
| `windows` | a Windows `.exe` | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine |
|
||||
| `arm64` | an ARM64 Linux program | `aarch64-linux-gnu-gcc` | `qemu-aarch64` |
|
||||
| `linux` | an x86-64 Linux program | `clang --target=…` | |
|
||||
| `macos` | a macOS program (on a Mac: for the other processor) | `clang --target=…` | |
|
||||
| any LLVM triple | e.g. `aarch64-unknown-linux-gnu` | `clang --target=<triple>` | |
|
||||
|
||||
```sh
|
||||
finch build game.fch --target windows # game.exe
|
||||
finch run game.fch --target windows # runs it with Wine, if installed
|
||||
finch run game.fch --target arm64 # builds for ARM64 and runs it with QEMU
|
||||
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build tool.fch --target arm64 # another compiler name
|
||||
```
|
||||
|
||||
On Nix the compilers are `pkgsCross.mingwW64.buildPackages.gcc` and
|
||||
`pkgsCross.aarch64-multiplatform.buildPackages.gcc`.
|
||||
|
||||
### Putting `finch` on your PATH (optional)
|
||||
|
||||
```sh
|
||||
|
|
@ -137,11 +172,13 @@ Finch's small runtime library, which is kept in `~/.cache/finch`. Set `CC` to us
|
|||
finch run <file.fch> [args...] compile and run right away
|
||||
finch build <file.fch> [-o name] compile into a program (default name: the file's name)
|
||||
finch ir <file.fch> print the LLVM IR (for the curious)
|
||||
finch lsp the language server, for editors
|
||||
finch version show the version
|
||||
|
||||
options:
|
||||
-l <lib> link a C library, same as link "lib" in the file
|
||||
-g add debug info (for gdb / lldb)
|
||||
--target <name> build for another system: windows, linux, arm64, macos, or an LLVM triple
|
||||
-g add debug info (for gdb / lldb / Visual Studio)
|
||||
-O0 skip optimizations
|
||||
```
|
||||
|
||||
|
|
@ -227,6 +264,7 @@ x += 1 // also -= *= /= %=
|
|||
| `char` | one byte-sized character | `'A'`, `'\n'` |
|
||||
| `str` | text | `"hello\tworld"` |
|
||||
| `[]T` | a list (array) of `T` | `[1, 2, 3]` |
|
||||
| `map[K]V` | values of type `V` found by a key of type `K` | `["a": 1, "b": 2]` |
|
||||
| your structs | named groups of values | `Point(1, 2)` |
|
||||
|
||||
String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`.
|
||||
|
|
@ -333,6 +371,14 @@ for i in 0..10 { // i = 0, 1, …, 9 (the end is not included)
|
|||
for name in names { // every element of an array (or every char of a str)
|
||||
print(name)
|
||||
}
|
||||
|
||||
for i, name in names { // with its index: 0, 1, 2, ...
|
||||
print(i, name)
|
||||
}
|
||||
|
||||
for key, value in ages { // every entry of a map, in the order the keys were added
|
||||
print(key, value) // (for key in ages: just the keys)
|
||||
}
|
||||
```
|
||||
|
||||
- A condition must be a `bool`. `if x {` with a number is an error; write `if x != 0 {`.
|
||||
|
|
@ -375,11 +421,13 @@ fn no_result(str message) { // no "->": returns nothing
|
|||
To change the caller's variable, pass a pointer: see [Pointers](#14-pointers).
|
||||
- A function with `-> type` must return a value on every path. Finch checks this.
|
||||
- A function cannot be defined inside another function.
|
||||
- A function that can fail is written `-> int!` (or `-> !` with no result); see
|
||||
[Errors as values](#errors-as-values).
|
||||
- Long calls can be split over several lines inside the brackets; a comma after the last argument is fine.
|
||||
|
||||
---
|
||||
|
||||
## 10. Arrays
|
||||
## 10. Arrays and maps
|
||||
|
||||
```c
|
||||
nums := [5, 3, 8] // an array of int
|
||||
|
|
@ -408,6 +456,44 @@ grid := [[1, 2], [3, 4]] // arrays of arrays
|
|||
|
||||
**Assigning copies:** after `b := a`, changing `b` does not change `a`.
|
||||
|
||||
### Maps
|
||||
|
||||
A `map[K]V` finds values by a key. Keys can be whole numbers, `char`, `bool` or `str`; values can be
|
||||
anything (arrays, structs, other maps). Iterating goes in the order the keys were first added.
|
||||
|
||||
```c
|
||||
ages := ["anna": 31, "bob": 25] // a map literal: map[str]int
|
||||
map[str][]str groups // an empty map
|
||||
map[int]str names = [:] // [:] is an empty map where the type is known
|
||||
```
|
||||
|
||||
| Operation | Meaning |
|
||||
|---|---|
|
||||
| `m[k]` | the value for `k`; a missing key stops the program with `the key "k" is not in the map` |
|
||||
| `m[k] = v` | add or replace |
|
||||
| `m[k] += 1`, `m[k].push(x)`, `m[k].field = …` | changing a missing key adds it first, with the value type's default (0, `""`, `[]`, …) |
|
||||
| `m.len` | the number of keys |
|
||||
| `m.has(k)` | is `k` there? |
|
||||
| `m.get(k, default)` | the value, or `default` (only computed when needed) |
|
||||
| `m.remove(k)` | removes `k`; `true` if it was there |
|
||||
| `m.clear()` | removes everything |
|
||||
| `m.keys()`, `m.values()` | new arrays, in insertion order |
|
||||
| `print(m)`, `str(m)` | `{"anna": 31, "bob": 25}` |
|
||||
|
||||
```c
|
||||
map[str]int counts
|
||||
for word in text.split(" ") {
|
||||
counts[word] += 1 // counting: a new word starts at 0
|
||||
}
|
||||
for word, n in counts {
|
||||
print(word, n)
|
||||
}
|
||||
```
|
||||
|
||||
Lookups are hash-based (average constant time). Like arrays, maps own their keys and values,
|
||||
copy on assignment, and are freed at the end of their block. Don't add or remove keys of a map
|
||||
inside a `for` over that same map; collect them in an array and change the map after the loop.
|
||||
|
||||
---
|
||||
|
||||
## 11. Text (str)
|
||||
|
|
@ -437,7 +523,7 @@ to C as `char*` works.
|
|||
|
||||
---
|
||||
|
||||
## 12. Structs
|
||||
## 12. Structs and methods
|
||||
|
||||
```c
|
||||
struct Player {
|
||||
|
|
@ -457,7 +543,34 @@ print(p) // Player(name: "Ola", lives: 2, scores: [10], pos: Point(x: 5, y
|
|||
|
||||
- Assigning or passing a struct copies it, together with its arrays and text.
|
||||
- A struct can't contain itself directly; use `ptr[Node]` or `[]Node` for that field (trees, lists).
|
||||
- Structs have no methods. Write functions that take the struct: `fn heal(Player p) -> Player`.
|
||||
|
||||
### Methods
|
||||
|
||||
A method is a function that belongs to a struct: `fn Struct.name(...)`. Inside, `self` is the struct
|
||||
it was called on, and it is the caller's value itself, not a copy: changes to `self` stay.
|
||||
|
||||
```c
|
||||
fn Player.heal(int amount) {
|
||||
self.lives += amount
|
||||
}
|
||||
|
||||
fn Player.is_alive() -> bool {
|
||||
return self.lives > 0
|
||||
}
|
||||
|
||||
p := Player(name: "Ola")
|
||||
p.heal(2) // p.lives is now 5
|
||||
if p.is_alive() { ... }
|
||||
|
||||
team[0].heal(1) // on an array element, a field, m[key], through a ptr[Player] ...
|
||||
```
|
||||
|
||||
- A method goes in the same file (module) as its struct, and is available wherever the struct is.
|
||||
- Methods can also be added to **structs from C headers**: `fn Vector2.length() -> f32 { ... }`.
|
||||
- A method can't have the name of a field.
|
||||
- Calling a method that changes `self` on a loop variable (`for p in team { p.heal(1) }`) is an error,
|
||||
because the loop variable is read-only; use `for i in 0..team.len { team[i].heal(1) }`.
|
||||
- Like functions, methods can fail (`-> !`); see [Errors as values](#errors-as-values).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -520,14 +633,16 @@ Using `.value`, `.field` or `[i]` on a `null` pointer **stops the program** with
|
|||
|---|---|
|
||||
| `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type, including arrays and structs. |
|
||||
| `input()`, `input("question")` | Reads one line typed by the user (without the newline). Empty text at end of input. |
|
||||
| `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read. |
|
||||
| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. |
|
||||
| `read_file(path)` | The whole file as `str`. Stops the program with a clear message if it can't be read, unless handled: `read_file(p) or ...`. |
|
||||
| `write_file(path, text)` | Writes (replaces) the file; gives back `true` if it worked. With `or` / `try`, the failure carries the reason. |
|
||||
| `file_exists(path)` | `true` / `false` |
|
||||
| `delete_file(path)` | Deletes the file; `true` if it worked (or the reason, with `or` / `try`). |
|
||||
| `error(message)` | The failure a fallible function returns: `return error("...")`. |
|
||||
| `shell(command)` | Runs a shell command, gives back its exit code. |
|
||||
| `exit(code)` | Ends the program right away. |
|
||||
| `addr(x)` | A pointer to `x`; `addr(function)` gives C a function pointer (callback). |
|
||||
| `new(value)`, `free(p)` | Manual heap memory (see [Memory](#13-memory-who-frees-what)). |
|
||||
| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). |
|
||||
| `int(x)`, `str(x)`, `u8(x)`, … | Conversions (see [Conversions](#conversions)). `str(x)` works on every type and gives the same text `print` shows. `int(text) or 0` handles text that isn't a number. |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -669,6 +784,54 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete
|
|||
|
||||
## 18. Errors
|
||||
|
||||
### Errors as values
|
||||
|
||||
Some failures are normal: the user types `abc`, a file is missing. Finch handles them without
|
||||
exceptions and without hidden control flow. A function that can fail says so with `!` after its result
|
||||
type, and fails with `return error("message")`:
|
||||
|
||||
```c
|
||||
fn parse_port(str text) -> int! {
|
||||
n := int(text) or { return error("'" + text + "' is not a number") }
|
||||
if n < 1 || n > 65535 {
|
||||
return error("port " + str(n) + " is out of range")
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn save(str path, str text) -> ! { // can fail, gives back nothing
|
||||
try write_file(path, text)
|
||||
}
|
||||
```
|
||||
|
||||
The caller **must** handle the failure, in one of three ways (forgetting is a compile error):
|
||||
|
||||
| Code | On failure |
|
||||
|---|---|
|
||||
| `port := parse_port(s) or 8080` | use this value instead (it's only computed on failure; it can be another call with `or`) |
|
||||
| `port := parse_port(s) or { print(err); return }` | run the block; `err` is the message (a `str`). The block must leave (`return`, `break`, `continue`, `exit`) unless the result isn't used. |
|
||||
| `port := try parse_port(s)` | fail the current function with the same error (it must be fallible too) |
|
||||
|
||||
```c
|
||||
fn load(str path) -> Config! {
|
||||
text := try read_file(path)
|
||||
port := try parse_port(text.trim())
|
||||
return Config(port: port)
|
||||
}
|
||||
|
||||
fn main() -> ! { // main can fail too: the error is printed, and the exit code is 1
|
||||
config := try load("app.conf")
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Built-ins that can fail work the same way: `int(text)`, `float(text)`, `read_file`, `write_file`,
|
||||
`delete_file`. Without `or` / `try` they behave as before (`int("x")` stops the program,
|
||||
`write_file` gives back a `bool`).
|
||||
|
||||
Under the hood a fallible function returns its value plus a flag and the message: no allocation on
|
||||
success, no unwinding, and the cost of a failure is building its message.
|
||||
|
||||
### Compile errors
|
||||
|
||||
```
|
||||
|
|
@ -691,11 +854,30 @@ Instead of undefined behavior (as in C), Finch stops with a message and exit cod
|
|||
| `runtime error: pop() on an empty array` | |
|
||||
| `runtime error: used .value on a null pointer` | also `.field` and `[i]` through `null` |
|
||||
| `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` on text that isn't a number |
|
||||
| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file |
|
||||
| `runtime error: can't read the file "…"` | `read_file` on a missing or unreadable file (handle it with `or`) |
|
||||
| `runtime error: the key "x" is not in the map …` | `m["x"]` when the map has no such key (use `.has` or `.get`) |
|
||||
| `runtime error: called .f() on a null pointer` | a method called through a `null` `ptr[T]` |
|
||||
|
||||
---
|
||||
|
||||
## 19. Editor: VS Code
|
||||
## 19. Editors: VS Code, Kate and others
|
||||
|
||||
Every editor feature comes from the same language server, `finch lsp`, which uses the real compiler:
|
||||
errors as you type, types and docs on hover, go to definition, completion (fields, methods, map and array
|
||||
methods after `.`, module members, names from C headers), the file's outline, and parameter hints.
|
||||
|
||||
### Kate (and KWrite, KDevelop)
|
||||
|
||||
```sh
|
||||
editors/kate/install.sh
|
||||
```
|
||||
|
||||
It installs the highlighting (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) and adds `finch`
|
||||
to Kate's LSP client settings, keeping your other servers. Restart Kate and enable the **LSP Client** plugin.
|
||||
To run programs, add `finch run %f` as a target in the **Build & Run** plugin; Finch's
|
||||
`file:line:column: error:` messages are then clickable. Details: `editors/kate/README.md`.
|
||||
|
||||
### VS Code
|
||||
|
||||
The **Finch** extension (`editors/vscode`, also attached to each release as a `.vsix` file) gives you:
|
||||
highlighting, errors as you type, completion (also fields and methods after `.`), types on hover,
|
||||
|
|
@ -703,8 +885,12 @@ go to definition (F12), an outline of the file, parameter hints, snippets, and a
|
|||
(`Ctrl+F5`). Errors from **Finch: Build This File** appear in the Problems panel.
|
||||
|
||||
Install: Extensions → `…` → *Install from VSIX…* → pick `finch-lang-*.vsix`. The extension runs `finch`
|
||||
from your PATH; if it is somewhere else, set **Finch: Path** in the settings. Other editors that speak the
|
||||
Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) can use `finch lsp` directly.
|
||||
from your PATH; if it is somewhere else, set **Finch: Path** in the settings.
|
||||
|
||||
### Others
|
||||
|
||||
Neovim, Helix, Zed, Emacs, Sublime and any other editor that speaks the Language Server Protocol:
|
||||
run `finch lsp` for files ending in `.fch` (stdin/stdout, no options).
|
||||
|
||||
## 20. Debugging
|
||||
|
||||
|
|
@ -737,6 +923,9 @@ Visual Studio debugger and WinDbg read.
|
|||
| A library is not found when the program starts | It was linked from a folder the system doesn't search. Run inside the same `nix-shell`, or install it system-wide. |
|
||||
| Windows: `finch` can't link (`LNK…` errors about `libcmt`, `kernel32`) | Install Visual Studio Build Tools with *Desktop development with C++*. |
|
||||
| Windows: `finch.exe` doesn't start (missing `libclang.dll`) | Keep `libclang.dll` next to `finch.exe`. |
|
||||
| macOS: `finch` doesn't start (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
|
||||
| macOS: `can't find the C header 'stdio.h'` | Install the command line tools: `xcode-select --install`. |
|
||||
| `--target arm64`: `no C compiler found to link with` | Install `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`), or set `FINCH_CC`. |
|
||||
| `clang` inside `nix-shell` can't find `stdio.h` | You have an old `shell.nix`: `llvmPackages.clang` must be listed before `llvmPackages.libclang`. |
|
||||
| A program never ends | A loop condition never becomes false. Press Ctrl+C. |
|
||||
|
||||
|
|
@ -755,17 +944,18 @@ Finch/
|
|||
│ ├── fail/ programs that must fail, with the expected error in line 1
|
||||
│ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind)
|
||||
│ └── boot.sh builds the self-hosted compiler with itself and compares
|
||||
├── editors/vscode/ the VS Code extension
|
||||
├── editors/ the VS Code extension (vscode/), Kate highlighting and LSP setup (kate/)
|
||||
├── docs/ this documentation (en, pl), including the cheat sheet
|
||||
├── shell.nix the Nix development environment
|
||||
└── CMakeLists.txt
|
||||
```
|
||||
|
||||
```sh
|
||||
tests/run.sh # → 59 passed, 0 failed
|
||||
tests/run.sh # → 74 passed, 0 failed
|
||||
MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access
|
||||
tests/boot.sh # the self-hosting check (needs clang)
|
||||
tests/windows.sh # every test built for Windows and run with Wine
|
||||
tests/cross.sh windows # every test built for Windows and run with Wine
|
||||
tests/cross.sh arm64 # every test built for ARM64 Linux and run with QEMU
|
||||
python3 tests/lsp_test.py # the language server
|
||||
```
|
||||
|
||||
|
|
@ -773,12 +963,11 @@ python3 tests/lsp_test.py # the language server
|
|||
|
||||
## 23. Current limits
|
||||
|
||||
Finch 2.0 is a complete small language, but not a finished one. Not there yet:
|
||||
Not there yet:
|
||||
|
||||
- Methods on structs, generics, maps/dictionaries, `match`. (Use arrays of structs and functions.)
|
||||
- Error values: errors in `int("x")` or `read_file` stop the program. Check first (`file_exists`).
|
||||
- Generics (your own `List[T]`), interfaces, `match`, enums written in Finch.
|
||||
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
|
||||
- Threads.
|
||||
- Platforms other than Linux and Windows on x86-64 (macOS, ARM).
|
||||
- Windows on ARM64, 32-bit systems, WebAssembly.
|
||||
|
||||
See the [roadmap in the README](../../README.md#roadmap).
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ Język z perspektywy użytkownika opisuje [przewodnik dla techników](dla-techni
|
|||
20. [Semantyka a C](#20-semantyka-a-c)
|
||||
21. [Spis funkcji](#21-spis-funkcji)
|
||||
22. [Rozbudowa kompilatora](#22-rozbudowa-kompilatora)
|
||||
23. [Platformy i Windows](#23-platformy-i-windows)
|
||||
23. [Platformy: Windows, macOS, ARM64](#23-platformy-windows-macos-arm64)
|
||||
24. [Serwer języka](#24-serwer-języka)
|
||||
25. [Rozszerzenie VS Code](#25-rozszerzenie-vs-code)
|
||||
25. [Integracje z edytorami](#25-integracje-z-edytorami)
|
||||
26. [Znane ograniczenia](#26-znane-ograniczenia)
|
||||
|
||||
---
|
||||
|
|
@ -90,20 +90,24 @@ Decyzje, które kształtują resztę:
|
|||
|
||||
| Plik | Linie | Odpowiedzialność |
|
||||
|---|---|---|
|
||||
| `src/error.h` | 40 | `g_files` (wszystkie źródła), `failAt(file, line, col, msg)`, `fail(line, col, msg)` |
|
||||
| `src/error.h` | 50 | `g_files` (wszystkie źródła), `failAt(file, line, col, msg)`, `fail(line, col, msg)`, `FinchError` |
|
||||
| `src/lexer.h/.cpp` | 250 | `Tok`, `Token`, `lex(indeksPliku)`, `tokName()` |
|
||||
| `src/ast.h` | 310 | `Type` (z Array, Fixed, Struct, Named), wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
|
||||
| `src/parser.h/.cpp` | 540 | Parser zstępujący `Parser`, `typeFromName()` |
|
||||
| `src/cimport.h/.cpp` | 420 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne |
|
||||
| `src/ast.h` | 360 | `Type` (z Array, Map, Fixed, Struct, Named), wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `StructDecl`, `Import`, `Link`, `Program` |
|
||||
| `src/parser.h/.cpp` | 610 | Parser zstępujący `Parser`, `typeFromName()` |
|
||||
| `src/loader.h/.cpp` | 90 | Wczytywanie plików i modułów (`Loader`), `FINCH_PATH` |
|
||||
| `src/target.h/.cpp` | 210 | `g_target`: triple, kompilator C, procesy, ścieżki; inicjalizacja backendów |
|
||||
| `src/cimport.h/.cpp` | 430 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne |
|
||||
| `src/codegen.h` | 15 | `generate()` |
|
||||
| `src/codegen_impl.h` | 265 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla czterech plików poniżej |
|
||||
| `src/codegen.cpp` | 1090 | Program, deklaracje, zasięgi i sprzątanie, instrukcje, wyrażenia, miejsca (`ref`), operatory, analiza modyfikacji |
|
||||
| `src/codegen_types.cpp` | 500 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), wypisywanie, typy DWARF |
|
||||
| `src/codegen_builtins.cpp` | 710 | Wywołania, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic |
|
||||
| `src/abi.cpp` | 310 | Klasyfikacja System V x86-64, wywołania C ze strukturami przez wartość, wrappery wywoływalne z C |
|
||||
| `src/main.cpp` | 390 | Sterownik: CLI, ładowanie, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, `run` |
|
||||
| `runtime/finch_rt.c` | 440 | Biblioteka uruchomieniowa (§12) |
|
||||
| `boot/*.fch` | 3080 | Kompilator samohostujący (§17) |
|
||||
| `src/codegen_impl.h` | 310 | Klasa `Codegen` i jej struktury pomocnicze, wspólne dla plików poniżej |
|
||||
| `src/codegen.cpp` | 1460 | Program, deklaracje (funkcje, metody), zasięgi i sprzątanie, instrukcje, wyrażenia, miejsca (`ref`), operatory, błędy jako wartości, analiza modyfikacji |
|
||||
| `src/codegen_types.cpp` | 580 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), zamiana na tekst, typy dla debuggera |
|
||||
| `src/codegen_builtins.cpp` | 890 | Wywołania, metody, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic |
|
||||
| `src/codegen_map.cpp` | 240 | Mapy: indeksowanie, literały, metody, przechodzenie |
|
||||
| `src/abi.cpp` | 370 | Konwencje wywołań C (System V x86-64, Microsoft x64, AAPCS64), wywołania C ze strukturami przez wartość, wrappery wywoływalne z C |
|
||||
| `src/lsp.cpp`, `src/index.h`, `src/builtins_doc.h` | 700 | Serwer języka (§24) |
|
||||
| `src/main.cpp` | 350 | Sterownik: CLI, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, `run` |
|
||||
| `runtime/finch_rt.c` | 740 | Biblioteka uruchomieniowa (§12) |
|
||||
| `boot/*.fch` | 3100 | Kompilator samohostujący (§17) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -120,7 +124,8 @@ struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; };
|
|||
- **Pozycje:** od 1; `col` liczy znaki (bajty kontynuacji UTF-8 jej nie zwiększają), więc `^` trafia
|
||||
pod `"Błąd"`. Zabłąkany znak spoza ASCII jest pokazywany w całości z podpowiedzią, że nazwy są tylko ASCII.
|
||||
- **Identyfikatory / słowa kluczowe:** `[A-Za-z_][A-Za-z0-9_]*`. Słowa kluczowe:
|
||||
`fn return if else while for in break continue true false null import link struct defer`.
|
||||
`fn return if else while for in break continue true false null import link struct defer or try`.
|
||||
(`self` i `map` to zwykłe identyfikatory ze znaczeniem zależnym od miejsca: w metodzie, przed `[`.)
|
||||
**Nazwy typów nie są słowami kluczowymi**: `int`, `u8`, `ptr`… to `Ident`, rozpoznawane przez parser
|
||||
za pomocą `typeFromName()`, więc `u8(x)` jest zwykłym wywołaniem, a lekser nie zależy od listy typów.
|
||||
- **Liczby:** dziesiętne albo `0x` szesnastkowe z separatorami `_`. Bez ósemkowych (`010` to 10).
|
||||
|
|
@ -142,27 +147,31 @@ program = { import | link | struct | function } ;
|
|||
import = "import" ( STRING | IDENT ) ; (* "x.h" = nagłówek C, nazwa = moduł Fincha *)
|
||||
link = "link" STRING ; (* "glfw" | "plik.c" | "plik.o" | "lib.a" *)
|
||||
struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NOWA_LINIA } "}" ;
|
||||
function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ;
|
||||
function = "fn" IDENT [ "." IDENT ] "(" [ param { "," param } ] ")" [ "->" ( type [ "!" ] | "!" ) ] block ;
|
||||
(* fn Punkt.przesun: metoda; "!": może się nie udać *)
|
||||
param = type IDENT ;
|
||||
type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ;
|
||||
type = "[" "]" type | "map" "[" type "]" type | "ptr" [ "[" type "]" ]
|
||||
| TYPENAME | IDENT [ "." IDENT ] ;
|
||||
|
||||
block = "{" { statement TERMINATOR } "}" ;
|
||||
statement = if | while | for | return | "break" | "continue" | "defer" statement | block
|
||||
| type IDENT [ "=" expr ] (* deklaracja, zobacz atDeclaration *)
|
||||
| IDENT ":=" expr
|
||||
| target assignop expr (* cel: zmienna, pole, element, p.value *)
|
||||
| call | method ;
|
||||
for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *)
|
||||
| call | method | try | orelse ;
|
||||
for = "for" IDENT [ "," IDENT ] "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *)
|
||||
assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
|
||||
|
||||
expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ;
|
||||
expr = or [ "or" ( block | expr ) ] ; (* f(x) or 0, f(x) or { ... }: najniższy, od prawej *)
|
||||
or = and { "||" and } ; and = cmp { "&&" cmp } ;
|
||||
cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ;
|
||||
add = mul { ( "+" | "-" | "|" | "^" ) mul } ;
|
||||
mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ;
|
||||
unary = ( "-" | "!" | "~" ) unary | postfix ;
|
||||
unary = ( "-" | "!" | "~" ) unary | "try" postfix | postfix ;
|
||||
postfix = primary { "." IDENT [ args ] | "[" expr "]" } ;
|
||||
primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null"
|
||||
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ;
|
||||
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]"
|
||||
| "[" ":" "]" | "[" expr ":" expr { "," expr ":" expr } [","] "]" ; (* mapy *)
|
||||
args = "(" [ arg { "," arg } [","] ] ")" ; arg = [ IDENT ":" ] expr ;
|
||||
```
|
||||
|
||||
|
|
@ -202,11 +211,16 @@ Wszystkie węzły są w `src/ast.h`: małe hierarchie klas z jawnym `kind`, rozg
|
|||
| `Member` | `MemberExpr` | `obj`, `field` (`.x`, `.len`, `.ptr`, `.value`) |
|
||||
| `Index` | `IndexExpr` | `obj`, `index` |
|
||||
| `ArrayLit` | `ArrayLitExpr` | `elems` |
|
||||
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; także `modul.fch(...)` |
|
||||
| `Method` | `MethodExpr` | `obj`, `name`, `args`, `argNames`; także `modul.fn(...)` i `p.metoda(...)` |
|
||||
| `MapLit` | `MapLitExpr` | `keys`, `values` (`[:]` nie ma żadnych) |
|
||||
| `Try` | `TryExpr` | `call` |
|
||||
| `OrElse` | `OrElseExpr` | `call` oraz `fallback` (wyrażenie) albo `block` |
|
||||
|
||||
| StmtKind | Węzeł |
|
||||
|---|---|
|
||||
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | jak w nazwie; `ForEachStmt` ma `var`, `list`, `body`; `DeferStmt` trzyma jedną instrukcję |
|
||||
| `Block` `VarDecl` `Assign` `Expr` `If` `While` `For` `ForEach` `Return` `Break` `Continue` `Defer` | jak w nazwie; `ForEachStmt` ma `var`, `var2` (`for i, x` / `for k, v`), `list`, `body`; `DeferStmt` trzyma jedną instrukcję |
|
||||
|
||||
`FnDecl` ma `recv` (struktura metody, w innych przypadkach `""`) i `fallible` (`-> T!`).
|
||||
|
||||
`math.sqrt(x)` parsuje się jako `MethodExpr` na `VarExpr("math")`; to, czy `math` jest modułem, czy zmienną,
|
||||
rozstrzyga się przy generowaniu kodu. Wywołania nazywają funkcję, zamiast trzymać wyrażenie: Finch nie ma
|
||||
|
|
@ -220,9 +234,10 @@ Nie ma węzła „load”: to, czy `x` znaczy adres, czy wartość, zależy od k
|
|||
```cpp
|
||||
struct Type {
|
||||
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
|
||||
Str, Ptr, Null, Array, Fixed, Struct, Named };
|
||||
Str, Ptr, Null, Array, Map, Fixed, Struct, Named };
|
||||
Kind kind;
|
||||
std::shared_ptr<Type> elem; // Ptr (null = `ptr` bez typu), Array, Fixed
|
||||
std::shared_ptr<Type> elem; // Ptr (null = `ptr` bez typu), Array, Fixed; Map: wartość
|
||||
std::shared_ptr<Type> key; // Map
|
||||
long long count; // Fixed
|
||||
std::string name, module; // Named (jak napisano) / Struct (rozwiązany)
|
||||
StructInfo *info; // Struct
|
||||
|
|
@ -302,9 +317,12 @@ nazwy typów (konwersja) → funkcja bieżącego modułu → struktura bieżące
|
|||
| `char` `i8` `u8` / `i16` `u16` / `i32` `u32` / `int` `u64` | `i8` / `i16` / `i32` / `i64` |
|
||||
| `f32` / `float` | `float` / `double` |
|
||||
| `str`, `[]T` | `{ ptr, i64, i64 }` (wskaźnik, długość, pojemność) |
|
||||
| `map[K]V` | `{ ptr, i64, i64, i64, ptr, i64 }` (entries, len, used, cap, index, icap; §10.12) |
|
||||
| `ptr`, `ptr[T]`, `null` | `ptr` (nieprzezroczysty) |
|
||||
| `[N]T` | `[N x T]` |
|
||||
| struktura Fincha | nazwany typ struct, naturalny układ |
|
||||
| `self` metody | `ptr` (pierwszy parametr) |
|
||||
| wynik `-> T!` | `{ i1 ok, T value, str error }` (`i8` w miejscu `T` dla `-> !`) |
|
||||
| struktura C | nazwany **upakowany** (packed) struct z jawnymi bajtami wypełnienia (§13.2) |
|
||||
|
||||
Znakowość istnieje tylko w typie Fincha i wybiera `sdiv/udiv`, `ashr/lshr`, `icmp s*/u*`,
|
||||
|
|
@ -380,6 +398,8 @@ każda decyzja zapada statycznie.
|
|||
| wartość tymczasowa, której nikt nie trzyma (`print(a + b)`, `f(g())`, instrukcja-wyrażenie) | **`release(v)`**: zwalniana zaraz po użyciu |
|
||||
| `a.pop()`, `a.remove(i)` | element jest przenoszony na zewnątrz (fresh) |
|
||||
| `for x in lista` | `x` pożycza kolejne elementy; jeśli ciało może zmieniać `lista`, elementy są kopiowane |
|
||||
| wywołanie metody `v.m()` | `self` **to jest** `v` (przekazywany jest jego adres): bez kopii; zmiany widzi wywołujący |
|
||||
| mapy | są właścicielami kluczy i wartości tak jak tablice elementów; `m[k] = v` przenosi/kopiuje `v` i zwalnia starą wartość |
|
||||
|
||||
Literał tekstowy to wartość fresh z `cap = 0`: przeniesienie nic nie kosztuje, zwalnianie jest pomijane
|
||||
(`release()` ignoruje stałe), a zapis do niego (`s[i] = c`) najpierw robi kopię na stercie (`finch_str_own`).
|
||||
|
|
@ -394,6 +414,11 @@ parametry są pożyczane (`Fn::borrowParam`), zmieniane kopiowane raz na wejści
|
|||
i zachowawcza: wszystko, co *może* zmienić parametr, powoduje kopię, co zawsze jest poprawne.
|
||||
Ta sama analiza decyduje, czy `for x in lista` musi kopiować elementy.
|
||||
|
||||
Metody biorą w tym udział: `declareFns()` najpierw ustala, które metody zmieniają `self`
|
||||
(`mutates("self", ciało)`), powtarzając, aż nic się nie zmieni, bo wywołanie zmieniającej metody na `self`
|
||||
też zmienia `self`. Ich nazwy trafiają do `mutatingMethods`, które `mutatesExpr()` traktuje jak `push`:
|
||||
`p.przesun(1)` zmienia `p`. Dopiero potem liczone jest `borrowParam` dla każdej funkcji i metody.
|
||||
|
||||
### 9.3 Kopiowanie i zwalnianie
|
||||
|
||||
`dropAt(addr, T)` / `copyAt(dst, src, T)` działają na adresach:
|
||||
|
|
@ -513,7 +538,10 @@ wartości (dla `str +=`).
|
|||
### 10.4 Funkcje i `main`
|
||||
|
||||
Funkcje są `internal` z naturalną sygnaturą LLVM (agregaty przez wartość). Parametry posiadające
|
||||
pamięć są kopiowane na wejściu tylko wtedy, gdy `borrowParam` jest fałszywe. `fn main` to wewnętrzna
|
||||
pamięć są kopiowane na wejściu tylko wtedy, gdy `borrowParam` jest fałszywe. Metody (§10.10) dostają adres
|
||||
`self` jako dodatkowy pierwszy parametr; funkcje, które mogą się nie udać (§10.11), zwracają `{ i1, T, str }`.
|
||||
`fn main() -> !` / `-> int!` sprawia, że wrapper przy porażce wypisuje `error: <komunikat>` na stderr
|
||||
i kończy z kodem 1. `fn main` to wewnętrzna
|
||||
`finch.main`; osobna zewnętrzna `i32 main(i32, ptr)` (`defineMainWrapper`) buduje `[]str args` przez
|
||||
`finch_args`, jeśli trzeba, woła ją, zwalnia argumenty i zwraca kod wyjścia. Funkcja, która dochodzi do
|
||||
swojego końca, dostaje sprzątanie zasięgów i `ret void` albo błąd `can reach its end without returning T`.
|
||||
|
|
@ -543,10 +571,13 @@ zasięgami wywołującego, albo zero). Wywołania C opisuje §13.
|
|||
|
||||
### 10.8 Wypisywanie
|
||||
|
||||
`print` emituje `printf` dla każdego argumentu z formatem wybranym wg typu (`%lld`, `%llu`, `%g`, `%c`,
|
||||
`%.*s` dla `str` z jego długością, `%p`), oddzielając spacjami, a na końcu `\n`. Tablice, tablice o stałym
|
||||
rozmiarze i struktury wołają generowane helpery `finch.print.<klucz>`, które wypisują `[1, 2]` /
|
||||
`Punkt(x: 1, y: 2)`, biorąc teksty i znaki w środku w cudzysłów.
|
||||
`print` emituje `printf` dla każdego argumentu skalarnego z formatem wybranym wg typu (`%lld`, `%llu`,
|
||||
`%g`, `%c`, `%.*s` dla `str` z jego długością), oddzielając spacjami, a na końcu `\n`. Wszystko inne
|
||||
(wskaźniki, tablice, tablice o stałym rozmiarze, mapy, struktury) jest najpierw **zamieniane na tekst**
|
||||
w buforze `str` na stercie przez `emitFormat()` i wypisywane przez `finch_buf_print`. Typy złożone dostają
|
||||
generowany helper `finch.format.<klucz>(buf, wartość*)`, który dopisuje `[1, 2]` / `{"a": 1}` /
|
||||
`Punkt(x: 1, y: 2)`, biorąc teksty i znaki w środku w cudzysłów. `str(x)` tablicy, mapy albo struktury
|
||||
używa tego samego helpera, więc `str(x)` to zawsze to, co pokazuje `print(x)`.
|
||||
|
||||
### 10.9 Metody tablic i tekstów
|
||||
|
||||
|
|
@ -556,6 +587,66 @@ Tablice: `push` (`finch_arr_reserve` + zapis + len++), `pop`/`remove` (przeniesi
|
|||
`reverse` (pętla zamian), `sort` (`qsort` z libc z generowaną porównywarką `finch.cmp.<klucz>`), `join` (runtime).
|
||||
Metody zmieniające tablicę wymagają adresu (zmienna/pole/element).
|
||||
Teksty: `sub find contains starts_with ends_with split trim upper lower replace repeat bytes`, wszystkie w runtime.
|
||||
`for i, x in lista` dodaje obok elementu zmienną-indeks typu `int` tylko do odczytu.
|
||||
|
||||
### 10.10 Metody
|
||||
|
||||
`fn Punkt.przesun(int dx)` jest deklarowana jak funkcja `finch.[modul.]Punkt.przesun` z dodatkowym
|
||||
pierwszym parametrem `ptr` i trzymana w `Codegen::methods[StructInfo*]` (więc metody idą za strukturą do
|
||||
każdego modułu, który jej używa, a struktury z C też mogą mieć metody). W `define()` `self` to `Var`, którego
|
||||
`slot` **jest tym argumentem-wskaźnikiem**: `self.x` to GEP od struktury samego wywołującego, a `self = …`
|
||||
zapisuje wartość wywołującego. Nigdy nie ma kopii.
|
||||
|
||||
Przy wywołaniu `obj.m(args)` (`structMethod()`) `ref(obj)` daje adres, gdy `obj` jest miejscem (zmienna,
|
||||
pole, element, `m[klucz]`), wczytany wskaźnik dla `ptr[T]` (sprawdzany na null) albo tymczasowe miejsce na
|
||||
stosie dla wartości (`Punkt(1, 2).dlugosc()`), zwalniane po wywołaniu. Z analizy modyfikacji wynikają dwie
|
||||
reguły bezpieczeństwa: metody zmieniającej `self` nie można wywołać na zmiennej pętli (tylko do odczytu),
|
||||
a jeśli taka metoda dostaje argument żyjący wewnątrz odbiorcy (`c.merge(c)`, `p.add(p.items)`), argument
|
||||
jest najpierw kopiowany, żeby `self` nie mógł zwolnić pamięci, na którą argument wciąż wskazuje.
|
||||
|
||||
### 10.11 Błędy jako wartości
|
||||
|
||||
Funkcja, która może się nie udać (`-> T!`), zwraca `{ i1 ok, T value, str error }`; przy porażce `value`
|
||||
jest zerem, więc wywołujący nigdy nie musi go zwalniać. `return v` opakowuje `{1, v, zero}` (`retValue()`);
|
||||
`return error(msg)` liczy komunikat, wykonuje sprzątanie i zwraca `{0, zero, msg}` (`retError()`).
|
||||
|
||||
Wywołanie takiej funkcji jest dozwolone tylko jako argument `or` / `try`. `fallible(wywołanie)` ustawia
|
||||
`fallibleTarget` na wyrażenie wywołania; `callFinch()` (i funkcje wbudowane, które mogą się nie udać)
|
||||
porównują z nim swoje `CallExpr`/`MethodExpr` i jeśli pasuje, zostawiają `fallibleFailed` (`i1`)
|
||||
i `fallibleMsg` (`str`) dla wywołującego; jeśli wywołanie takiej funkcji nie pasuje, to błąd kompilacji
|
||||
*'f' can fail, so say what happens then*. Zagnieżdżone obsługi zapisują i przywracają te trzy pola.
|
||||
|
||||
| Forma | Tłumaczenie |
|
||||
|---|---|
|
||||
| `f(x) or zapasowa` | `br failed, or.failed, or.ok`; blok porażki zwalnia komunikat i liczy wartość zapasową (tylko tam); φ łączy obie wartości |
|
||||
| `f(x) or { … }` | blok porażki deklaruje `err` (właściciel komunikatu) i wykonuje blok. Jeśli wartość jest używana, blok musi kończyć się skokiem; jako instrukcja może przejść dalej, a ścieżka sukcesu zwalnia nieużytą wartość |
|
||||
| `try f(x)` | blok porażki wykonuje `emitCleanups(0)` i zwraca `{0, zero, msg}` z bieżącej funkcji, która też musi móc się nie udać |
|
||||
|
||||
Funkcje wbudowane z trybem porażki sprawdzają `&wywołanie == fallibleTarget` i przełączają się na
|
||||
`finch_str_try_int`, `finch_str_try_float`, `finch_try_read_file`, `finch_try_write_file`,
|
||||
`finch_try_delete_file`, które zwracają 1 / 0 i wypełniają `str` z błędem zamiast robić panic. Koszt przy
|
||||
sukcesie to jedna przewidywalna gałąź; nic nie jest alokowane, dopóki nie zdarzy się porażka.
|
||||
|
||||
### 10.12 Mapy
|
||||
|
||||
Runtime (§12) trzyma tablicę haszującą zachowującą kolejność wstawiania, w stylu `dict` z CPythona:
|
||||
tablica `entries` z miejscami `{ i64 hash, K key, V value }` w kolejności wstawiania (hash 0 = usunięte)
|
||||
oraz `index` z `icap` miejscami (potęga dwójki, ≥ 2 × cap) z sondowaniem liniowym (−1 puste, −2 usunięte,
|
||||
w innym razie numer wpisu). Runtime tylko haszuje (FNV-1a + 64-bitowe mieszanie; klucze tekstowe po treści,
|
||||
pozostałe po bajtach), znajduje i umieszcza wpisy; kompilator zapisuje, kopiuje i zwalnia klucze i wartości,
|
||||
dokładnie tak jak elementy tablic.
|
||||
|
||||
| Kod | Tłumaczenie |
|
||||
|---|---|
|
||||
| odczyt `m[k]` | `finch_map_find`; −1 → `finch_map_missing` (noreturn, z kluczem jako tekst); w innym razie miejsce `entry.value` |
|
||||
| zapis `m[k]` (`=`, `+=`, metoda zmieniająca, `addr`) | `mapSlot()`: `finch_map_slot` zwraca wpis, dodając go, jeśli go nie ma; nowy wpis dostaje kopię klucza i wartość domyślną typu |
|
||||
| `["a": 1, …]` | wyzerowana mapa + `mapSlot` dla każdego wpisu (pierwszy wpis ustala typy; liczbowe wartości są wszystkie `float`, jeśli któraś ma kropkę) |
|
||||
| `has`, `get`, `remove`, `clear`, `keys`, `values` | `codegen_map.cpp`; wartość domyślna `get` jest liczona tylko wtedy, gdy klucza brak |
|
||||
| `for k, v in m` | pętla po `0..used` pomijająca usunięte miejsca, wczytująca `entries` i `used` w każdym obrocie |
|
||||
| kopia / zwolnienie / tekst | helpery jak dla tablic: `finch_map_clone_raw`, a potem głębokie kopie posiadających kluczy i wartości; zwolnienie każdego żywego wpisu, potem `finch_map_free` |
|
||||
|
||||
Usunięcie oznacza miejsce (i wpis w indeksie) jako usunięte; miejsca są upychane, gdy tablica miałaby
|
||||
urosnąć, a co najmniej ćwierć z nich jest usunięta. Usunięcie ostatniego klucza resetuje tablicę.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -573,7 +664,9 @@ udowodnić (stałe indeksy w zakresie, kontrole wyciągnięte z pętli).
|
|||
| indeks tablicy/tekstu/tablicy stałej poza zakresem (`icmp uge`, więc także ujemne) | `boundsCheck()` |
|
||||
| `.value`, `.pole`, `[i]` przez null | `member()`, `index()` |
|
||||
| `pop()` na pustej, `slice()`/`sub()` poza zakresem | metody, runtime |
|
||||
| zły tekst w `int(s)`/`float(s)`, nieczytelny plik w `read_file` | runtime |
|
||||
| zły tekst w `int(s)`/`float(s)`, nieczytelny plik w `read_file` (bez `or` / `try`) | runtime |
|
||||
| odczyt `m[klucz]` z brakującym kluczem | `mapIndex()` |
|
||||
| metoda wywołana przez `null` w `ptr[T]` | `structMethod()` |
|
||||
|
||||
Sito na 20 milionów elementów z kontrolą zakresu przy każdym dostępie działa tak szybko jak to samo w C
|
||||
(ok. 0,09 s): kontrole są wyciągane z pętli albo zwijane.
|
||||
|
|
@ -599,8 +692,11 @@ Funkcje przyjmują i zwracają te struktury **przez wskaźnik**, więc ich ABI j
|
|||
struktur): budowanie tekstów (`concat`, `from_int/uint/float/char/bool`, `sub`, `trim`, `upper`, `lower`,
|
||||
`replace`, `split`, `join`, `repeat`, `from_bytes`), porównania (`eq`, `cmp`, `find`, `starts`, `ends`),
|
||||
własność (`copy`, `own`, `drop`, `from_c`), tablice (`reserve`, `make`, `resize`, `clone_raw`, `free`,
|
||||
`insert_gap`, `remove_gap`), `args`, `input`, pliki, `shell` i panic. Brak pamięci wypisuje
|
||||
`out of memory` i kończy proces.
|
||||
`insert_gap`, `remove_gap`), mapy (`map_find`, `map_slot`, `map_remove_at`, `map_clear`, `map_free`,
|
||||
`map_clone_raw`, `map_missing`; §10.12), bufory tekstu dla `print`/`str` (`buf_add`, `buf_int`, …,
|
||||
`buf_print`), warianty mogące się nie udać (`str_try_int`, `str_try_float`, `try_read_file`,
|
||||
`try_write_file`, `try_delete_file`, `report_error`), `args`, `input`, pliki, `shell` i panic. Brak pamięci
|
||||
wypisuje `out of memory` i kończy proces.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -759,7 +855,7 @@ wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostują
|
|||
- libclang: `find_path(clang-c/Index.h)` + `find_library(clang)`.
|
||||
- `runtime/finch_rt.c` jest wczytywany przy konfiguracji do `build/rt_source.inc` jako surowy literał
|
||||
napisowy; `CMAKE_CONFIGURE_DEPENDS` ponawia konfigurację, gdy plik się zmieni.
|
||||
- `FINCH_VERSION` z `project(VERSION 2.3.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), bez ostrzeżeń.
|
||||
- `FINCH_VERSION` z `project(VERSION 2.4.0)`. C++17, `-Wall -Wextra` (MSVC: `/W3`), bez ostrzeżeń.
|
||||
- `shell.nix` wymienia `llvmPackages.clang` przed `llvmPackages.libclang`: ten drugi dostarcza też
|
||||
„gołego” `clang`, który nie widzi nagłówków systemowych.
|
||||
|
||||
|
|
@ -769,9 +865,12 @@ wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostują
|
|||
|
||||
- `tests/run.sh`: każdy `tests/run/*.fch` z `fn main` musi wypisać dokładnie swój `.out` (stdin z `.in`, jeśli
|
||||
jest); pliki bez `main` to moduły albo pliki pomocnicze. Każdy `tests/fail/*.fch` musi się nie udać z tekstem
|
||||
z linijki `// expect:`. Obecnie **59 passed, 0 failed**.
|
||||
z linijki `// expect:`. Obecnie **74 passed, 0 failed**.
|
||||
- `MEMCHECK=1 tests/run.sh`: to samo plus valgrind na każdym programie (bez wycieków i złych dostępów).
|
||||
- `tests/boot.sh`: punkt stały samohostowania i przebieg na podzbiorze (§17).
|
||||
- `tests/cross.sh windows|arm64`: każdy test zbudowany z `--target` i uruchomiony w Wine / QEMU.
|
||||
- `python3 tests/lsp_test.py`: serwer języka, sterowany jak przez edytor.
|
||||
- CI (`.github/workflows/ci.yml`) uruchamia to wszystko na Linuksie x86-64, Linuksie ARM64, macOS ARM64 i Windowsie.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -860,11 +959,14 @@ wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostują
|
|||
| Funkcja | Robi |
|
||||
|---|---|
|
||||
| `call` / `method` | rozwiązuje wywołanie / metodę albo wywołanie z modułu |
|
||||
| `callFinch` / `construct` / `callC` | funkcja Fincha / konstruktor struktury / funkcja C |
|
||||
| `callFinch` / `construct` / `callC` | funkcja lub metoda Fincha / konstruktor struktury / funkcja C |
|
||||
| `structMethod` | `obj.m(...)`: adres odbiorcy, kontrole bezpieczeństwa, wywołanie |
|
||||
| `print`, `addrOf`, `convert`, `arrayMethod` | funkcje wbudowane |
|
||||
| `fallible`, `tryExpr`, `orElse`, `retValue`, `retError` (`codegen.cpp`) | błędy jako wartości (§10.11) |
|
||||
| `mapIndex`, `mapLit`, `mapMethod`, `mapSlot`, `forEachEntry` (`codegen_map.cpp`) | mapy (§10.12) |
|
||||
| `rt` / `libc` | deklaruje funkcję runtime'u / libc |
|
||||
| `fileName`, `panicIf`, `boundsCheck`, `checkDivisor` | kontrole w czasie działania |
|
||||
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | klasyfikacja System V, plan rejestrów, deklaracja, wywołanie, wrapper dla callbacków |
|
||||
| `classify`, `makePlan`, `declareC`, `emitCCall`, `cThunk` | klasyfikacja ABI (System V, Microsoft x64, AAPCS64), plan rejestrów, deklaracja, wywołanie, wrapper dla callbacków |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -889,12 +991,16 @@ przed wyskokiem z zasięgów.
|
|||
|
||||
---
|
||||
|
||||
## 23. Platformy i Windows
|
||||
## 23. Platformy: Windows, macOS, ARM64
|
||||
|
||||
`src/target.h` trzyma `g_target`: triple LLVM, flagi `windows` / `msvc` / `cross`, kompilator C (`cc`)
|
||||
i rozszerzenie plików wykonywalnych. `setTarget()` wybiera kompilator C: `FINCH_CC`, potem `CC` (nie przy
|
||||
kompilacji krzyżowej), potem `x86_64-w64-mingw32-gcc` dla `--target windows` z Linuksa, `clang` na
|
||||
Windowsie, `cc` na Uniksie. Przez tę warstwę idzie wszystko, co zależy od platformy:
|
||||
`src/target.h` trzyma `g_target`: triple LLVM, flagi `windows` / `msvc` / `darwin` / `cross`, kompilator C
|
||||
(`cc`) i rozszerzenie plików wykonywalnych. `setTarget()` zamienia `windows`, `linux`, `arm64`, `macos`
|
||||
(albo pełny triple) na triple i wybiera kompilator C: `FINCH_CC`, potem `CC` (nie przy kompilacji
|
||||
krzyżowej), potem `x86_64-w64-mingw32-gcc` / `aarch64-linux-gnu-gcc` dla Windowsa / Linuksa ARM64 z innego
|
||||
systemu, `clang --target=<triple>` dla pozostałych celów krzyżowych, `clang` na Windowsie, `cc` na Uniksie.
|
||||
`initTargets()` rejestruje backendy X86 i AArch64 (te, które ma dany LLVM; CMake definiuje
|
||||
`FINCH_TARGET_X86` / `FINCH_TARGET_AARCH64` i linkuje ich biblioteki). Przez tę warstwę idzie wszystko,
|
||||
co zależy od platformy:
|
||||
|
||||
- **Nagłówki:** libclang parsuje z `--target=<triple>` (więc `long` ma 32 bity na Windowsie i używane są
|
||||
właściwe nagłówki), a na Windowsie z `-D_USE_MATH_DEFINES`. Katalogi nagłówków pochodzą z `<cc> -E -v`.
|
||||
|
|
@ -905,7 +1011,12 @@ Windowsie, `cc` na Uniksie. Przez tę warstwę idzie wszystko, co zależy od pla
|
|||
- **Linkowanie:** bez `-lm` przy MSVC; nazwy `.exe`; klucz cache runtime'u obejmuje triple i kompilator C.
|
||||
Komunikaty linkera MSVC (`LNK2019 unresolved external symbol`, `LNK1104`/`LNK1181 cannot open file`) są
|
||||
tłumaczone tak jak komunikaty GNU ld.
|
||||
- **Uruchamianie:** program dla Windowsa zbudowany na Linuksie uruchamia się przez `wine`.
|
||||
- **Uruchamianie:** `runPrefix()`: program dla Windowsa zbudowany na Linuksie uruchamia się przez `wine`,
|
||||
program dla Linuksa ARM64 na innym procesorze przez `qemu-aarch64`.
|
||||
- **macOS:** `<cc> -E -v` wypisuje wpisy `(framework directory)`; zamieniają się w `-iframework` dla
|
||||
libclang, więc `import "OpenGL/gl.h"` działa. `link "Cocoa.framework"` daje `-framework Cocoa`. Komunikaty
|
||||
linkera Apple (`Undefined symbols for architecture arm64: "_f"`, `ld: library 'x' not found`,
|
||||
`framework 'X' not found`) są tłumaczone jak pozostałe, bez wiodącego `_` z Mach-O.
|
||||
- **Informacje dla debuggera:** CodeView dla celów MSVC, DWARF w pozostałych.
|
||||
- **Runtime:** na Windowsie nie ma `getline` ani `sys/wait.h`; `stdin`/`stdout`/`stderr` są tam makrami,
|
||||
więc `finch_std_stream(i)` udostępnia je Finchowi, gdy nagłówek nie ma dla nich zmiennej `extern`.
|
||||
|
|
@ -920,10 +1031,30 @@ kopii (bez `byval`: funkcja dostaje zwykły wskaźnik). Wyniki 1/2/4/8-bajtowe w
|
|||
wszystko inne przez ukryty wskaźnik `sret`. Każdy argument zajmuje jedno miejsce, więc nie ma liczenia
|
||||
rejestrów. `cThunk()` robi to samo w drugą stronę dla callbacków.
|
||||
|
||||
`tests/windows.sh` buduje każdy test z `--target windows`, uruchamia go w Wine i porównuje wynik: przechodzą
|
||||
`tests/cross.sh windows` buduje każdy test z `--target windows`, uruchamia go w Wine i porównuje wynik: przechodzą
|
||||
wszystkie, łącznie z `c_structs` (który wtedy sprawdza reguły Microsoftu na bibliotece C zbudowanej MinGW).
|
||||
CI dodatkowo buduje `finch.exe` przez MSVC na Windows Server i uruchamia tam `tests/run.sh`.
|
||||
|
||||
### AAPCS64 (Linux ARM64 i Apple)
|
||||
|
||||
`classify()` odwzorowuje to, jak clang tłumaczy wywołania na AArch64:
|
||||
|
||||
- **Jednorodny agregat zmiennoprzecinkowy** (1–4 elementy, wszystkie `float` albo wszystkie `double`,
|
||||
zagnieżdżone struktury i tablice spłaszczone) idzie w rejestrach wektorowych: przekazywany jako
|
||||
`[n x float]` / `[n x double]`, zwracany jako `{ float, … }`. Na Linuksie argument dostaje też
|
||||
`alignstack(8)`, tak jak u clanga; Apple tego nie robi.
|
||||
- Każda inna struktura do 16 bajtów idzie w rejestrach ogólnych: `i64` (≤ 8 bajtów) albo `[2 x i64]`
|
||||
(`i128`, jeśli jej wyrównanie to 16); zwracana jest jako liczba całkowita dokładnie jej rozmiaru albo `[2 x i64]`.
|
||||
- Większe struktury są **pośrednie (Indirect)**: kopia na stosie wywołującego, przekazana przez adres
|
||||
(bez `byval`); zwracane przez `sret`.
|
||||
- Nie ma liczenia rejestrów: każdą część rozmieszcza backend.
|
||||
- Zwykły `char` w C jest **bez znaku** na Linuksie ARM64, a ze znakiem na Apple i x86, co ma znaczenie przy
|
||||
rozszerzaniu argumentów i wyników typu `char` (`charIsSigned()`).
|
||||
- Apple przekazuje zmienną część `printf(...)` przez stos; LLVM robi to na podstawie `...` w typie funkcji.
|
||||
|
||||
`tests/cross.sh arm64` uruchamia cały zestaw testów (łącznie z testem ABI struktur na bibliotece C
|
||||
zbudowanej GCC) w QEMU, a CI robi to natywnie na Linuksie ARM64 i Apple Silicon.
|
||||
|
||||
### Budowanie finch.exe
|
||||
|
||||
CI używa Visual Studio 2022, Ninja i oficjalnego pakietu `clang+llvm-21.x-x86_64-pc-windows-msvc`
|
||||
|
|
@ -970,7 +1101,19 @@ i podpowiedziach parametrów (i odwzorowany w ściągach).
|
|||
|
||||
---
|
||||
|
||||
## 25. Rozszerzenie VS Code
|
||||
## 25. Integracje z edytorami
|
||||
|
||||
### Kate
|
||||
|
||||
`editors/kate/finch.xml` to definicja KSyntaxHighlighting (silnika Kate, KWrite i KDevelop): listy słów
|
||||
kluczowych zgodne z `builtins_doc.h`, kontekst dla `fn Typ.metoda`, teksty ze znakami specjalnymi oraz
|
||||
regiony zwijania dla `{ }` i komentarzy blokowych. `lspclient.json` to wpis dla klienta LSP w Kate
|
||||
(`highlightingModeRegex: ^Finch$`); `install.sh` kopiuje definicję i dołącza wpis do
|
||||
`~/.config/kate/lspclient/settings.json`. Kate wysyła więcej zapytań niż VS Code (`documentHighlight`,
|
||||
`foldingRange`, …); serwer odpowiada na każde nieobsługiwane zapytanie błędem `MethodNotFound`, więc żaden
|
||||
klient nie czeka.
|
||||
|
||||
### VS Code
|
||||
|
||||
`editors/vscode` to zwykłe rozszerzenie w JavaScripcie: `extension.js` uruchamia `finch lsp` przez
|
||||
`vscode-languageclient` (ścieżka z ustawienia `finch.path`) i dodaje **Run** / **Build** jako zadania
|
||||
|
|
@ -982,10 +1125,15 @@ szablony. CI pakuje je przez `vsce` do `finch-lang.vsix`.
|
|||
|
||||
## 26. Znane ograniczenia
|
||||
|
||||
- Brak metod w strukturach, typów generycznych, map, `match`, domknięć i błędów jako wartości (`int("x")`, `read_file` robią panic).
|
||||
- Brak typów generycznych, interfejsów, `match`, domknięć i enumów pisanych w Finchu.
|
||||
- Teksty to bajty: `.len`, `s[i]`, `upper()` nie znają Unicode.
|
||||
- Zmiana kluczy mapy wewnątrz `for` po tej samej mapie nie jest wykrywana (jest bezpieczna dla pamięci,
|
||||
ale wpisy mogą zostać pominięte albo odwiedzone dwa razy).
|
||||
- Porażka `try` / `or` w środku wyrażenia nie zwalnia świeżych wartości tymczasowych policzonych wcześniej
|
||||
w tym wyrażeniu (mały wyciek, tylko na ścieżce porażki).
|
||||
- Wiszące wskaźniki (`addr` zmiennej, która zniknęła, użycie po `free`) nie są wykrywane.
|
||||
- Wielkość przesunięć i konwersje float→int nie są sprawdzane (poison w LLVM, jak w C).
|
||||
- C: unie i struktury z polami bitowymi przez wartość, makra-funkcje, `long double`.
|
||||
- Dwa systemy: Linux i Windows, oba x86-64 (ABI System V i Microsoft x64). Jeszcze bez macOS i ARM.
|
||||
- Kompilator samohostujący nie ma importu C, liczb z rozmiarem ani `defer` i nie zwalnia pamięci.
|
||||
- Cele: x86-64 i ARM64 (Linux, macOS; Windows na x86-64). Bez Windowsa na ARM64, systemów 32-bitowych i WebAssembly.
|
||||
- Kompilator samohostujący nie ma importu C, liczb z rozmiarem, `defer`, map, metod ani błędów jako
|
||||
wartości i nie zwalnia pamięci.
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.
|
|||
./build/finch version
|
||||
```
|
||||
|
||||
Powinieneś zobaczyć coś w stylu `finch 2.3.0`.
|
||||
Powinieneś zobaczyć coś w stylu `finch 2.4.0`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -510,8 +510,68 @@ fn main() {
|
|||
}
|
||||
```
|
||||
|
||||
Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, daj `for` dwie nazwy: pierwsza liczy
|
||||
0, 1, 2…, druga to wartość:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
for i, owoc in ["jabłko", "banan", "wiśnia"] {
|
||||
print(i + 1, owoc)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
1 jabłko
|
||||
2 banan
|
||||
3 wiśnia
|
||||
```
|
||||
|
||||
Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń),
|
||||
`sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#tablice).
|
||||
`sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#10-tablice-i-mapy).
|
||||
|
||||
### Szukanie po nazwie: mapy
|
||||
|
||||
Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem chcesz znaleźć coś po **nazwie**, tak jak
|
||||
słowo w słowniku albo numer w książce telefonicznej. Do tego służy **mapa** (`map`): każdy
|
||||
**klucz** (nazwa) ma swoją **wartość**:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
telefon := ["Ola": "555-1234", "Jan": "555-9876"]
|
||||
print(telefon["Ola"]) // 555-1234
|
||||
|
||||
telefon["Ewa"] = "555-0000" // nowa osoba
|
||||
print(telefon.len) // 3
|
||||
|
||||
if telefon.has("Tomek") {
|
||||
print("Tomek jest")
|
||||
} else {
|
||||
print("nie ma Tomka")
|
||||
}
|
||||
|
||||
for imie, numer in telefon { // każde imię z jego numerem
|
||||
print(imie, numer)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `["klucz": wartość, ...]` tworzy mapę. Klucze to zwykle tekst, ale mogą być też liczby.
|
||||
- `telefon["Ola"]` daje wartość dla klucza. Pytanie o klucz, którego nie ma, zatrzymuje program,
|
||||
więc najpierw sprawdź `telefon.has("Tomek")` (czy ma) albo użyj `telefon.get("Tomek", "nie wiem")`.
|
||||
- `for imie, numer in telefon` odwiedza każdy klucz z wartością, w kolejności dodawania.
|
||||
|
||||
Mapy świetnie nadają się do **liczenia**. Nowy klucz zaczyna od 0, więc `+= 1` po prostu działa:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
map[str]int glosy // pusta mapa: klucze to tekst, wartości to liczby
|
||||
for g in ["kot", "pies", "kot", "kot", "rybka"] {
|
||||
glosy[g] += 1
|
||||
}
|
||||
print(glosy) // {"kot": 3, "pies": 1, "rybka": 1}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -625,6 +685,36 @@ fn main() {
|
|||
|
||||
Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`.
|
||||
|
||||
### Polecenia twojej struktury: metody
|
||||
|
||||
Struktura może mieć własne polecenia (**metody**). Napisz nazwę struktury, kropkę i nazwę polecenia.
|
||||
W środku `self` (ang. „sam”) to struktura, na której użyto polecenia:
|
||||
|
||||
```c
|
||||
struct Gracz {
|
||||
str imie
|
||||
int zycia = 3
|
||||
}
|
||||
|
||||
fn Gracz.trafiony() {
|
||||
self.zycia -= 1
|
||||
print(self.imie, "ma jeszcze", self.zycia, "życia")
|
||||
}
|
||||
|
||||
fn Gracz.zyje() -> bool {
|
||||
return self.zycia > 0
|
||||
}
|
||||
|
||||
fn main() {
|
||||
g := Gracz(imie: "Ola")
|
||||
g.trafiony() // Ola ma jeszcze 2 życia
|
||||
g.trafiony() // Ola ma jeszcze 1 życia
|
||||
print(g.zyje()) // true
|
||||
}
|
||||
```
|
||||
|
||||
`g.trafiony()` zmienia samo `g`, więc po dwóch trafieniach `g.zycia` naprawdę wynosi 1.
|
||||
|
||||
---
|
||||
|
||||
## 15. Zapisywanie do pliku
|
||||
|
|
@ -688,6 +778,57 @@ Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o
|
|||
| `index 5 is out of range (the length is 3)` | Zapytałeś listę o miejsce, którego nie ma | Pamiętaj, że liczymy od 0; sprawdź `.len` |
|
||||
| `can't turn "abc" into int` | `int(...)` dostał tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
|
||||
| `can't use '+' on str and int` | Tekst plus liczba | Zamień liczbę na tekst przez `str(...)` |
|
||||
| `'f' can fail, so say what happens then` | Użyłeś polecenia, które może się nie udać, i nie napisałeś, co wtedy | Dopisz `or ...` (niżej) |
|
||||
|
||||
### Rzeczy, które mogą się nie udać: `or` i `try`
|
||||
|
||||
Niektóre rzeczy mogą pójść źle *w trakcie działania programu*: ktoś wpisze `abc` zamiast liczby
|
||||
albo pliku nie ma. Zamiast zatrzymywać program, możesz powiedzieć, co wtedy zrobić, słowem **`or`** („albo”):
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0
|
||||
print("Za rok będziesz mieć", wiek + 1)
|
||||
|
||||
notatka := read_file("notatka.txt") or "(jeszcze brak notatki)"
|
||||
print(notatka)
|
||||
}
|
||||
```
|
||||
|
||||
Po `or` może też być blok. W środku `err` to komunikat, co poszło nie tak:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
while true {
|
||||
n := int(input("Podaj liczbę: ")) or {
|
||||
print("To nie jest liczba, spróbuj jeszcze raz")
|
||||
continue
|
||||
}
|
||||
print("Wybrałeś", n)
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Twoje własne funkcje też mogą się nie udać. Dopisz `!` po typie wyniku i użyj `return error("...")`:
|
||||
|
||||
```c
|
||||
fn bezpieczne_dzielenie(int a, int b) -> int! {
|
||||
if b == 0 {
|
||||
return error("nie można dzielić przez zero")
|
||||
}
|
||||
return a / b
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print(bezpieczne_dzielenie(10, 2) or -1) // 5
|
||||
print(bezpieczne_dzielenie(10, 0) or -1) // -1
|
||||
}
|
||||
```
|
||||
|
||||
Finch nie pozwoli ci zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który
|
||||
przypomina, żeby to obsłużyć. (Można też przekazać problem dalej przez `try`; wyjaśnia to
|
||||
[przewodnik dla techników](dla-technikow.md#błędy-jako-wartości).)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -889,10 +1030,13 @@ struct Zwierzak { // własny rodzaj wartości
|
|||
int wiek = 1
|
||||
}
|
||||
|
||||
// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { }
|
||||
// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } for i, n in liczby { }
|
||||
// mapy: wiek := ["Ola": 9] wiek["Jan"] = 10 wiek["Ola"] wiek.has("Tomek") for imie, lata in wiek { }
|
||||
// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
|
||||
// pytanie: imie := input("Imię? ")
|
||||
// pliki: write_file("p.txt", tekst) read_file("p.txt")
|
||||
// metody: fn Zwierzak.urosnij() { self.wiek += 1 } z.urosnij()
|
||||
// porażki: n := int(tekst) or 0 x := read_file("p.txt") or { print(err) ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -913,6 +1057,8 @@ struct Zwierzak { // własny rodzaj wartości
|
|||
| **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` |
|
||||
| **indeks** | Pozycja wartości na liście, liczona od 0 |
|
||||
| **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części |
|
||||
| **metoda** | Polecenie należące do struktury, np. `g.trafiony()` |
|
||||
| **mapa** (map) | Wartości znajdowane po kluczu (nazwie), jak w słowniku |
|
||||
| **wejście** (input) | To, co użytkownik wpisuje do programu |
|
||||
| **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie |
|
||||
| **bug** | Pomyłka w programie |
|
||||
|
|
@ -934,6 +1080,11 @@ struct Zwierzak { // własny rodzaj wartości
|
|||
| `input` | wejście (pytanie) |
|
||||
| `len` (od *length*) | długość |
|
||||
| `error` | błąd |
|
||||
| `or` | albo (co zrobić, gdy się nie uda) |
|
||||
| `try` | spróbuj (a jak się nie uda, przekaż błąd dalej) |
|
||||
| `self` | sam (struktura, na której użyto metody) |
|
||||
| `map` | mapa (słownik) |
|
||||
| `has` / `get` | ma / weź |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -20,16 +20,16 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-
|
|||
7. [Operatory](#7-operatory)
|
||||
8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem)
|
||||
9. [Funkcje](#9-funkcje)
|
||||
10. [Tablice](#10-tablice)
|
||||
10. [Tablice i mapy](#10-tablice-i-mapy)
|
||||
11. [Tekst (str)](#11-tekst-str)
|
||||
12. [Struktury](#12-struktury)
|
||||
12. [Struktury i metody](#12-struktury-i-metody)
|
||||
13. [Pamięć: kto co zwalnia](#13-pamięć-kto-co-zwalnia)
|
||||
14. [Wskaźniki](#14-wskaźniki)
|
||||
15. [Funkcje wbudowane](#15-funkcje-wbudowane)
|
||||
16. [Moduły](#16-moduły)
|
||||
17. [Biblioteki z C](#17-biblioteki-z-c)
|
||||
18. [Błędy](#18-błędy)
|
||||
19. [Edytor: VS Code](#19-edytor-vs-code)
|
||||
19. [Edytory: VS Code, Kate i inne](#19-edytory-vs-code-kate-i-inne)
|
||||
20. [Debugowanie](#20-debugowanie)
|
||||
21. [Rozwiązywanie problemów](#21-rozwiązywanie-problemów)
|
||||
22. [Struktura projektu i testy](#22-struktura-projektu-i-testy)
|
||||
|
|
@ -60,7 +60,9 @@ i komunikaty błędów, które mówią, jak problem naprawić.
|
|||
|
||||
## 2. Instalacja
|
||||
|
||||
Finch działa na **Linuksie i Windowsie, x86-64**, i jest budowany z **LLVM 21**.
|
||||
Finch działa na **Linuksie** (x86-64 i ARM64), **macOS** (Apple Silicon i Intel) i **Windowsie** (x86-64),
|
||||
i jest budowany z **LLVM 21**. Każde [wydanie](https://github.com/Nexoniarz/Finch/releases) ma gotowy
|
||||
`finch` dla Windowsa, Linuksa x86-64, Linuksa ARM64 i macOS ARM64.
|
||||
|
||||
### Windows
|
||||
|
||||
|
|
@ -75,8 +77,30 @@ Finch działa na **Linuksie i Windowsie, x86-64**, i jest budowany z **LLVM 21**
|
|||
Programy to zwykłe pliki `.exe`. Żeby zbudować Fincha ze źródeł na Windowsie, idź za `.github/workflows/ci.yml`
|
||||
(Visual Studio 2022 + pakiet `clang+llvm-21.x-x86_64-pc-windows-msvc` z wydań LLVM).
|
||||
|
||||
### macOS
|
||||
|
||||
```sh
|
||||
brew install llvm@21 # LLVM dla Fincha; programy linkuje clang od Apple
|
||||
```
|
||||
|
||||
Potem rozpakuj **`finch-macos-arm64.tar.gz`** z wydań (korzysta z `llvm@21` z Homebrew)
|
||||
albo zbuduj go sam (narzędzia wiersza poleceń Xcode: `xcode-select --install`):
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
|
||||
ninja -C build
|
||||
./build/finch version
|
||||
```
|
||||
|
||||
Frameworki Apple linkuje się przez `link "Cocoa.framework"` (`-framework Cocoa`), a ich nagłówki importuje
|
||||
się normalnie: `import "OpenGL/gl.h"`.
|
||||
|
||||
### Linux
|
||||
|
||||
Gotowe `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` potrzebują bibliotek LLVM 21
|
||||
(Debian/Ubuntu: `libllvm21 libclang1-21` z [apt.llvm.org](https://apt.llvm.org)) i kompilatora C (`cc`).
|
||||
Samodzielne budowanie wygląda tak samo na x86-64 i ARM64 (Raspberry Pi 4/5, Graviton, …).
|
||||
Z innymi wersjami LLVM Finch prawdopodobnie się nie skompiluje, bo API C++ LLVM zmienia się między wydaniami.
|
||||
|
||||
### Opcja A: Nix (zalecana, nic nie instalujesz ręcznie)
|
||||
|
|
@ -87,7 +111,7 @@ cd Finch
|
|||
nix-shell # pobiera LLVM, libclang, clang, cmake, ninja, pkg-config
|
||||
cmake -S . -B build -G Ninja
|
||||
ninja -C build
|
||||
./build/finch version # finch 2.3.0 (LLVM 21.x)
|
||||
./build/finch version # finch 2.4.0 (LLVM 21.x)
|
||||
```
|
||||
|
||||
### Opcja B: pakiety z twojej dystrybucji
|
||||
|
|
@ -108,16 +132,27 @@ cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
|
|||
ninja -C build
|
||||
```
|
||||
|
||||
### Budowanie programów na Windowsa pod Linuksem
|
||||
### Budowanie na inny system
|
||||
|
||||
Z zainstalowanym kompilatorem krzyżowym MinGW-w64 (`x86_64-w64-mingw32-gcc`; na Niksie
|
||||
`pkgsCross.mingwW64.buildPackages.gcc`):
|
||||
`--target` buduje program na inny system. Do linkowania Finch potrzebuje kompilatora C dla tego systemu:
|
||||
|
||||
| `--target` | Buduje | Kompilator C | `finch run` używa |
|
||||
|---|---|---|---|
|
||||
| `windows` | `.exe` dla Windowsa | `x86_64-w64-mingw32-gcc` (MinGW-w64) | Wine |
|
||||
| `arm64` | program dla Linuksa ARM64 | `aarch64-linux-gnu-gcc` | `qemu-aarch64` |
|
||||
| `linux` | program dla Linuksa x86-64 | `clang --target=…` | |
|
||||
| `macos` | program dla macOS (na Macu: dla drugiego procesora) | `clang --target=…` | |
|
||||
| dowolny triple LLVM | np. `aarch64-unknown-linux-gnu` | `clang --target=<triple>` | |
|
||||
|
||||
```sh
|
||||
finch build gra.fch --target windows # gra.exe
|
||||
finch run gra.fch --target windows # uruchamia ją w Wine, jeśli jest zainstalowane
|
||||
finch run gra.fch --target arm64 # buduje na ARM64 i uruchamia w QEMU
|
||||
FINCH_CC=aarch64-unknown-linux-gnu-gcc finch build narzedzie.fch --target arm64 # inna nazwa kompilatora
|
||||
```
|
||||
|
||||
Na Niksie te kompilatory to `pkgsCross.mingwW64.buildPackages.gcc` i
|
||||
`pkgsCross.aarch64-multiplatform.buildPackages.gcc`.
|
||||
|
||||
### Dodanie `finch` do PATH (opcjonalnie)
|
||||
|
||||
```sh
|
||||
|
|
@ -137,11 +172,13 @@ Inny kompilator wskażesz zmienną `CC`.
|
|||
finch run <plik.fch> [argumenty...] skompiluj i od razu uruchom
|
||||
finch build <plik.fch> [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku)
|
||||
finch ir <plik.fch> pokaż LLVM IR (dla ciekawskich)
|
||||
finch lsp serwer języka, dla edytorów
|
||||
finch version pokaż wersję
|
||||
|
||||
opcje:
|
||||
-l <lib> dolinkuj bibliotekę C, to samo co link "lib" w pliku
|
||||
-g dodaj informacje dla debuggera (gdb / lldb)
|
||||
--target <nazwa> zbuduj na inny system: windows, linux, arm64, macos albo triple LLVM
|
||||
-g dodaj informacje dla debuggera (gdb / lldb / Visual Studio)
|
||||
-O0 bez optymalizacji
|
||||
```
|
||||
|
||||
|
|
@ -226,6 +263,7 @@ x += 1 // także -= *= /= %=
|
|||
| `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` |
|
||||
| `str` | tekst | `"cześć\tświecie"` |
|
||||
| `[]T` | lista (tablica) elementów `T` | `[1, 2, 3]` |
|
||||
| `map[K]V` | wartości typu `V` znajdowane po kluczu typu `K` | `["a": 1, "b": 2]` |
|
||||
| twoje struktury | nazwane grupy wartości | `Punkt(1, 2)` |
|
||||
|
||||
Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`.
|
||||
|
|
@ -332,6 +370,14 @@ for i in 0..10 { // i = 0, 1, …, 9 (koniec nie jest wliczany)
|
|||
for imie in imiona { // każdy element tablicy (albo każdy znak str)
|
||||
print(imie)
|
||||
}
|
||||
|
||||
for i, imie in imiona { // razem z indeksem: 0, 1, 2, ...
|
||||
print(i, imie)
|
||||
}
|
||||
|
||||
for klucz, wartosc in wiek { // każdy wpis mapy, w kolejności dodawania kluczy
|
||||
print(klucz, wartosc) // (for klucz in wiek: same klucze)
|
||||
}
|
||||
```
|
||||
|
||||
- Warunek musi być typu `bool`. `if x {` z liczbą to błąd; napisz `if x != 0 {`.
|
||||
|
|
@ -375,11 +421,13 @@ fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca
|
|||
Żeby zmienić zmienną wywołującego, przekaż wskaźnik, zobacz [Wskaźniki](#14-wskaźniki).
|
||||
- Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza.
|
||||
- Funkcji nie można definiować wewnątrz innej funkcji.
|
||||
- Funkcję, która może się nie udać, zapisuje się `-> int!` (albo `-> !` bez wyniku); zobacz
|
||||
[Błędy jako wartości](#błędy-jako-wartości).
|
||||
- Długie wywołania możesz rozbić na kilka linijek wewnątrz nawiasów; przecinek po ostatnim argumencie jest dozwolony.
|
||||
|
||||
---
|
||||
|
||||
## 10. Tablice
|
||||
## 10. Tablice i mapy
|
||||
|
||||
```c
|
||||
liczby := [5, 3, 8] // tablica int
|
||||
|
|
@ -408,6 +456,45 @@ siatka := [[1, 2], [3, 4]] // tablice tablic
|
|||
|
||||
**Przypisanie kopiuje:** po `b := a` zmiana `b` nie zmienia `a`.
|
||||
|
||||
### Mapy
|
||||
|
||||
`map[K]V` znajduje wartości po kluczu. Kluczem może być liczba całkowita, `char`, `bool` albo `str`;
|
||||
wartością cokolwiek (tablice, struktury, inne mapy). Przechodzenie pętlą idzie w kolejności, w jakiej
|
||||
klucze zostały dodane po raz pierwszy.
|
||||
|
||||
```c
|
||||
wiek := ["anna": 31, "bob": 25] // literał mapy: map[str]int
|
||||
map[str][]str grupy // pusta mapa
|
||||
map[int]str nazwy = [:] // [:] to pusta mapa tam, gdzie typ jest znany
|
||||
```
|
||||
|
||||
| Operacja | Znaczenie |
|
||||
|---|---|
|
||||
| `m[k]` | wartość dla `k`; brak klucza zatrzymuje program komunikatem `the key "k" is not in the map` |
|
||||
| `m[k] = v` | dodaj albo zastąp |
|
||||
| `m[k] += 1`, `m[k].push(x)`, `m[k].pole = …` | zmiana brakującego klucza najpierw go dodaje, z wartością domyślną typu (0, `""`, `[]`, …) |
|
||||
| `m.len` | liczba kluczy |
|
||||
| `m.has(k)` | czy `k` jest w mapie? |
|
||||
| `m.get(k, domyslna)` | wartość albo `domyslna` (liczona tylko w razie potrzeby) |
|
||||
| `m.remove(k)` | usuwa `k`; `true`, jeśli był |
|
||||
| `m.clear()` | usuwa wszystko |
|
||||
| `m.keys()`, `m.values()` | nowe tablice, w kolejności dodawania |
|
||||
| `print(m)`, `str(m)` | `{"anna": 31, "bob": 25}` |
|
||||
|
||||
```c
|
||||
map[str]int ile
|
||||
for slowo in tekst.split(" ") {
|
||||
ile[slowo] += 1 // liczenie: nowe słowo zaczyna od 0
|
||||
}
|
||||
for slowo, n in ile {
|
||||
print(slowo, n)
|
||||
}
|
||||
```
|
||||
|
||||
Wyszukiwanie używa haszowania (średnio stały czas). Tak jak tablice, mapy są właścicielami kluczy
|
||||
i wartości, kopiują się przy przypisaniu i zwalniają na końcu bloku. Nie dodawaj ani nie usuwaj kluczy
|
||||
mapy wewnątrz `for` po tej samej mapie; zbierz je do tablicy i zmień mapę po pętli.
|
||||
|
||||
---
|
||||
|
||||
## 11. Tekst (str)
|
||||
|
|
@ -437,7 +524,7 @@ przekazanie go do C jako `char*` działa.
|
|||
|
||||
---
|
||||
|
||||
## 12. Struktury
|
||||
## 12. Struktury i metody
|
||||
|
||||
```c
|
||||
struct Gracz {
|
||||
|
|
@ -457,7 +544,34 @@ print(g) // Gracz(imie: "Ola", zycia: 2, wyniki: [10], pozycja: Punkt(x: 5,
|
|||
|
||||
- Przypisanie albo przekazanie struktury ją kopiuje, razem z jej tablicami i tekstami.
|
||||
- Struktura nie może zawierać samej siebie wprost; dla takiego pola użyj `ptr[Wezel]` albo `[]Wezel` (drzewa, listy).
|
||||
- Struktury nie mają metod. Pisz funkcje, które je przyjmują: `fn wylecz(Gracz g) -> Gracz`.
|
||||
|
||||
### Metody
|
||||
|
||||
Metoda to funkcja należąca do struktury: `fn Struktura.nazwa(...)`. W środku `self` to struktura,
|
||||
na której ją wywołano, i to jest sama wartość wywołującego, nie kopia: zmiany w `self` zostają.
|
||||
|
||||
```c
|
||||
fn Gracz.wylecz(int ile) {
|
||||
self.zycia += ile
|
||||
}
|
||||
|
||||
fn Gracz.zyje() -> bool {
|
||||
return self.zycia > 0
|
||||
}
|
||||
|
||||
g := Gracz(imie: "Ola")
|
||||
g.wylecz(2) // g.zycia wynosi teraz 5
|
||||
if g.zyje() { ... }
|
||||
|
||||
druzyna[0].wylecz(1) // na elemencie tablicy, polu, m[klucz], przez ptr[Gracz] ...
|
||||
```
|
||||
|
||||
- Metoda jest w tym samym pliku (module) co jej struktura i jest dostępna wszędzie, gdzie ta struktura.
|
||||
- Metody można też dopisywać do **struktur z nagłówków C**: `fn Vector2.length() -> f32 { ... }`.
|
||||
- Metoda nie może nazywać się tak jak pole.
|
||||
- Wywołanie metody zmieniającej `self` na zmiennej pętli (`for g in druzyna { g.wylecz(1) }`) to błąd,
|
||||
bo zmienna pętli jest tylko do odczytu; użyj `for i in 0..druzyna.len { druzyna[i].wylecz(1) }`.
|
||||
- Tak jak funkcje, metody mogą się nie udać (`-> !`); zobacz [Błędy jako wartości](#błędy-jako-wartości).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -520,14 +634,16 @@ Użycie `.value`, `.pole` albo `[i]` na wskaźniku `null` **zatrzymuje program**
|
|||
|---|---|
|
||||
| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem, także z tablicami i strukturami. |
|
||||
| `input()`, `input("pytanie")` | Czyta jedną linijkę wpisaną przez użytkownika (bez znaku nowej linii). Na końcu wejścia daje pusty tekst. |
|
||||
| `read_file(sciezka)` | Cały plik jako `str`. Jeśli nie da się go przeczytać, zatrzymuje program z czytelnym komunikatem. |
|
||||
| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. |
|
||||
| `read_file(sciezka)` | Cały plik jako `str`. Jeśli nie da się go przeczytać, zatrzymuje program z czytelnym komunikatem, chyba że obsłużysz to: `read_file(p) or ...`. |
|
||||
| `write_file(sciezka, tekst)` | Zapisuje (zastępuje) plik; oddaje `true`, jeśli się udało. Z `or` / `try` porażka niesie przyczynę. |
|
||||
| `file_exists(sciezka)` | `true` / `false` |
|
||||
| `delete_file(sciezka)` | Usuwa plik; `true`, jeśli się udało (albo przyczyna, z `or` / `try`). |
|
||||
| `error(komunikat)` | Porażka, którą zwraca funkcja mogąca się nie udać: `return error("...")`. |
|
||||
| `shell(polecenie)` | Uruchamia polecenie powłoki i oddaje jego kod wyjścia. |
|
||||
| `exit(kod)` | Natychmiast kończy program. |
|
||||
| `addr(x)` | Wskaźnik na `x`; `addr(funkcja)` daje C wskaźnik na funkcję (callback). |
|
||||
| `new(wartosc)`, `free(p)` | Ręczna pamięć na stercie (zobacz [Pamięć](#13-pamięć-kto-co-zwalnia)). |
|
||||
| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zobacz [Zamiana typów](#zamiana-typów)). |
|
||||
| `int(x)`, `str(x)`, `u8(x)`, … | Zamiany typów (zobacz [Zamiana typów](#zamiana-typów)). `str(x)` działa na każdym typie i daje ten sam tekst, co `print`. `int(tekst) or 0` obsługuje tekst, który nie jest liczbą. |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -669,6 +785,54 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k
|
|||
|
||||
## 18. Błędy
|
||||
|
||||
### Błędy jako wartości
|
||||
|
||||
Niektóre porażki są normalne: użytkownik wpisze `abc`, pliku nie ma. Finch obsługuje je bez wyjątków
|
||||
i bez ukrytych skoków. Funkcja, która może się nie udać, mówi to znakiem `!` po typie wyniku i kończy się
|
||||
porażką przez `return error("komunikat")`:
|
||||
|
||||
```c
|
||||
fn port_z_tekstu(str tekst) -> int! {
|
||||
n := int(tekst) or { return error("'" + tekst + "' to nie liczba") }
|
||||
if n < 1 || n > 65535 {
|
||||
return error("port " + str(n) + " jest poza zakresem")
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn zapisz(str sciezka, str tekst) -> ! { // może się nie udać, nic nie oddaje
|
||||
try write_file(sciezka, tekst)
|
||||
}
|
||||
```
|
||||
|
||||
Wywołujący **musi** obsłużyć porażkę, na jeden z trzech sposobów (zapomnienie to błąd kompilacji):
|
||||
|
||||
| Kod | W razie porażki |
|
||||
|---|---|
|
||||
| `port := port_z_tekstu(s) or 8080` | użyj tej wartości (liczonej tylko przy porażce; może to być kolejne wywołanie z `or`) |
|
||||
| `port := port_z_tekstu(s) or { print(err); return }` | wykonaj blok; `err` to komunikat (`str`). Blok musi wyjść (`return`, `break`, `continue`, `exit`), chyba że wynik nie jest używany. |
|
||||
| `port := try port_z_tekstu(s)` | zakończ bieżącą funkcję tym samym błędem (musi też móc się nie udać) |
|
||||
|
||||
```c
|
||||
fn wczytaj(str sciezka) -> Konfig! {
|
||||
tekst := try read_file(sciezka)
|
||||
port := try port_z_tekstu(tekst.trim())
|
||||
return Konfig(port: port)
|
||||
}
|
||||
|
||||
fn main() -> ! { // main też może się nie udać: błąd zostanie wypisany, a kod wyjścia to 1
|
||||
konfig := try wczytaj("app.conf")
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Funkcje wbudowane, które mogą się nie udać, działają tak samo: `int(tekst)`, `float(tekst)`, `read_file`,
|
||||
`write_file`, `delete_file`. Bez `or` / `try` zachowują się jak wcześniej (`int("x")` zatrzymuje program,
|
||||
`write_file` oddaje `bool`).
|
||||
|
||||
Pod spodem funkcja, która może się nie udać, zwraca wartość z flagą i komunikatem: bez alokacji przy
|
||||
sukcesie, bez zwijania stosu, a porażka kosztuje tyle, co zbudowanie komunikatu.
|
||||
|
||||
### Błędy kompilacji
|
||||
|
||||
```
|
||||
|
|
@ -692,11 +856,31 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikat
|
|||
| `runtime error: pop() on an empty array` | |
|
||||
| `runtime error: used .value on a null pointer` | także `.pole` i `[i]` przez `null` |
|
||||
| `runtime error: can't turn "x" into int` | `int(...)` / `float(...)` na tekście, który nie jest liczbą |
|
||||
| `runtime error: can't read the file "…"` | `read_file` na brakującym albo nieczytelnym pliku |
|
||||
| `runtime error: can't read the file "…"` | `read_file` na brakującym albo nieczytelnym pliku (obsłuż to przez `or`) |
|
||||
| `runtime error: the key "x" is not in the map …` | `m["x"]`, gdy mapa nie ma takiego klucza (użyj `.has` albo `.get`) |
|
||||
| `runtime error: called .f() on a null pointer` | metoda wywołana przez `null` w `ptr[T]` |
|
||||
|
||||
---
|
||||
|
||||
## 19. Edytor: VS Code
|
||||
## 19. Edytory: VS Code, Kate i inne
|
||||
|
||||
Wszystkie funkcje edytorów pochodzą z jednego serwera języka, `finch lsp`, który używa prawdziwego
|
||||
kompilatora: błędy na bieżąco, typy i opisy po najechaniu myszą, idź do definicji, podpowiedzi (pola,
|
||||
metody, metody map i tablic po `.`, zawartość modułów, nazwy z nagłówków C), konspekt pliku i podpowiedzi
|
||||
parametrów.
|
||||
|
||||
### Kate (oraz KWrite, KDevelop)
|
||||
|
||||
```sh
|
||||
editors/kate/install.sh
|
||||
```
|
||||
|
||||
Instaluje podświetlanie (`~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml`) i dopisuje `finch`
|
||||
do ustawień klienta LSP w Kate, zachowując twoje pozostałe serwery. Uruchom Kate ponownie i włącz wtyczkę
|
||||
**LSP Client**. Żeby uruchamiać programy, dodaj `finch run %f` jako cel we wtyczce **Build & Run**; komunikaty
|
||||
Fincha `plik:linia:kolumna: error:` są wtedy klikalne. Szczegóły: `editors/kate/README.md`.
|
||||
|
||||
### VS Code
|
||||
|
||||
Rozszerzenie **Finch** (`editors/vscode`, dołączane też do każdego wydania jako plik `.vsix`) daje:
|
||||
podświetlanie składni, błędy na bieżąco, podpowiedzi (także pola i metody po `.`), typy po najechaniu
|
||||
|
|
@ -704,8 +888,12 @@ myszą, idź do definicji (F12), konspekt pliku, podpowiedzi parametrów, szablo
|
|||
(`Ctrl+F5`). Błędy z **Finch: Build This File** trafiają do panelu Problems.
|
||||
|
||||
Instalacja: Rozszerzenia → `…` → *Install from VSIX…* → wybierz `finch-lang-*.vsix`. Rozszerzenie uruchamia
|
||||
`finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach. Inne edytory obsługujące
|
||||
Language Server Protocol (Neovim, Helix, Zed, Emacs, Sublime) mogą używać `finch lsp` bezpośrednio.
|
||||
`finch` z PATH; jeśli jest gdzie indziej, ustaw **Finch: Path** w ustawieniach.
|
||||
|
||||
### Inne
|
||||
|
||||
Neovim, Helix, Zed, Emacs, Sublime i każdy inny edytor obsługujący Language Server Protocol:
|
||||
uruchamiaj `finch lsp` dla plików `.fch` (stdin/stdout, bez opcji).
|
||||
|
||||
## 20. Debugowanie
|
||||
|
||||
|
|
@ -738,6 +926,9 @@ CodeView, który czyta debugger Visual Studio i WinDbg.
|
|||
| Biblioteka nie znajduje się przy starcie programu | Zlinkowano ją z folderu, którego system nie przeszukuje. Uruchamiaj w tym samym `nix-shell` albo zainstaluj ją systemowo. |
|
||||
| Windows: `finch` nie linkuje (błędy `LNK…` o `libcmt`, `kernel32`) | Zainstaluj Visual Studio Build Tools z *Desktop development with C++*. |
|
||||
| Windows: `finch.exe` się nie uruchamia (brak `libclang.dll`) | Trzymaj `libclang.dll` obok `finch.exe`. |
|
||||
| macOS: `finch` się nie uruchamia (`Library not loaded: …libLLVM…`) | `brew install llvm@21`. |
|
||||
| macOS: `can't find the C header 'stdio.h'` | Zainstaluj narzędzia wiersza poleceń: `xcode-select --install`. |
|
||||
| `--target arm64`: `no C compiler found to link with` | Zainstaluj `aarch64-linux-gnu-gcc` (Debian: `gcc-aarch64-linux-gnu`) albo ustaw `FINCH_CC`. |
|
||||
| `clang` w `nix-shell` nie widzi `stdio.h` | Masz stary `shell.nix`: `llvmPackages.clang` musi być przed `llvmPackages.libclang`. |
|
||||
| Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. |
|
||||
|
||||
|
|
@ -756,17 +947,18 @@ Finch/
|
|||
│ ├── fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce
|
||||
│ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem)
|
||||
│ └── boot.sh buduje kompilator samohostujący nim samym i porównuje
|
||||
├── editors/vscode/ rozszerzenie VS Code
|
||||
├── editors/ rozszerzenie VS Code (vscode/), podświetlanie i konfiguracja LSP dla Kate (kate/)
|
||||
├── docs/ ta dokumentacja (en, pl), łącznie ze ściągą
|
||||
├── shell.nix środowisko deweloperskie Nix
|
||||
└── CMakeLists.txt
|
||||
```
|
||||
|
||||
```sh
|
||||
tests/run.sh # → 59 passed, 0 failed
|
||||
tests/run.sh # → 74 passed, 0 failed
|
||||
MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci
|
||||
tests/boot.sh # sprawdzenie samohostowania (wymaga clanga)
|
||||
tests/windows.sh # każdy test zbudowany na Windowsa i uruchomiony w Wine
|
||||
tests/cross.sh windows # każdy test zbudowany na Windowsa i uruchomiony w Wine
|
||||
tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomiony w QEMU
|
||||
python3 tests/lsp_test.py # serwer języka
|
||||
```
|
||||
|
||||
|
|
@ -774,12 +966,11 @@ python3 tests/lsp_test.py # serwer języka
|
|||
|
||||
## 23. Obecne ograniczenia
|
||||
|
||||
Finch 2.0 to kompletny mały język, ale nie skończony. Jeszcze nie ma:
|
||||
Jeszcze nie ma:
|
||||
|
||||
- Metod w strukturach, typów generycznych, słowników (map), `match`. (Używaj tablic struktur i funkcji.)
|
||||
- Błędów jako wartości: błąd w `int("x")` albo `read_file` zatrzymuje program. Sprawdzaj wcześniej (`file_exists`).
|
||||
- Typów generycznych (własnego `Lista[T]`), interfejsów, `match`, enumów pisanych w Finchu.
|
||||
- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII.
|
||||
- Wątków.
|
||||
- Platform innych niż Linux i Windows na x86-64 (macOS, ARM).
|
||||
- Windowsa na ARM64, systemów 32-bitowych, WebAssembly.
|
||||
|
||||
Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju).
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.p
|
|||
| `finch lsp` | serwer języka (korzystają z niego edytory) |
|
||||
| `finch version` | wersja |
|
||||
| `-l lib` | dolinkuj bibliotekę C (jak `link "lib"`) |
|
||||
| `--target windows` / `linux` / triple | zbuduj dla innego systemu |
|
||||
| `--target windows` / `linux` / `arm64` / `macos` / triple | zbuduj dla innego systemu |
|
||||
| `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) |
|
||||
| `-O0` | bez optymalizacji |
|
||||
|
||||
|
|
@ -71,6 +71,7 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi
|
|||
| `u8 u16 u32 u64` | bez znaku, z rozmiarem |
|
||||
| `f32 f64` | zmiennoprzecinkowe |
|
||||
| `[]T` | tablica: `[1, 2, 3]`, `[]str imiona` |
|
||||
| `map[K]V` | mapa: `["a": 1]`, `map[str]int ile`, `[:]` |
|
||||
| `ptr[T]`, `ptr` | wskaźnik, wskaźnik bez typu |
|
||||
| `Nazwa` / `modul.Nazwa` | struktura |
|
||||
|
||||
|
|
@ -100,6 +101,8 @@ if a > b { … } else if a == b { … } else { … }
|
|||
while x > 0 { … }
|
||||
for i in 0..10 { … } // 0 … 9
|
||||
for el in lista { … } // każdy element (albo znak str)
|
||||
for i, el in lista { … } // razem z indeksem
|
||||
for klucz, wart in m { … } // każdy wpis mapy, w kolejności dodawania
|
||||
break continue return wartosc
|
||||
defer sprzatanie() // uruchomi się na końcu bloku
|
||||
```
|
||||
|
|
@ -121,6 +124,47 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec
|
|||
|
||||
---
|
||||
|
||||
## Metody
|
||||
|
||||
```c
|
||||
fn Punkt.przesun(int dx, int dy) { // self to Punkt, na którym wywołano (nie kopia)
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
}
|
||||
fn Punkt.odleglosc() -> float { … }
|
||||
|
||||
p.przesun(1, 2) lista[0].przesun(1, 1) wsk_na_punkt.przesun(0, 1)
|
||||
```
|
||||
|
||||
Także na strukturach z C: `fn Vector2.length() -> f32 { … }`.
|
||||
|
||||
---
|
||||
|
||||
## Błędy jako wartości
|
||||
|
||||
```c
|
||||
fn parsuj(str s) -> int! { // ! = może się nie udać
|
||||
if s.len == 0 {
|
||||
return error("pusty")
|
||||
}
|
||||
return int(s) or { return error("to nie liczba: " + s) }
|
||||
}
|
||||
fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku
|
||||
try write_file(p, t)
|
||||
}
|
||||
```
|
||||
|
||||
| Kod | Przy porażce |
|
||||
|---|---|
|
||||
| `x := parsuj(s) or 0` | użyj 0 |
|
||||
| `x := parsuj(s) or { print(err); return }` | wykonaj blok (`err`: komunikat) |
|
||||
| `x := try parsuj(s)` | zakończ porażką też bieżącą funkcję |
|
||||
| `fn main() -> !` | porażka wypisuje `error: …`, kod wyjścia 1 |
|
||||
|
||||
Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
|
||||
|
||||
---
|
||||
|
||||
## Struktury
|
||||
|
||||
| Kod | Znaczenie |
|
||||
|
|
@ -155,6 +199,23 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec
|
|||
|
||||
---
|
||||
|
||||
## Mapy `map[K]V`
|
||||
|
||||
| Kod | Daje |
|
||||
|---|---|
|
||||
| `m := ["a": 1, "b": 2]` | mapa (klucze: liczby, char, bool, str) |
|
||||
| `m[k]` | wartość (brak klucza: program się zatrzymuje) |
|
||||
| `m[k] = v` | dodaj / zastąp |
|
||||
| `m[k] += 1`, `m[k].push(x)` | brakujący klucz zaczyna od wartości domyślnej (0, `[]`, …) |
|
||||
| `m.len` | liczba kluczy |
|
||||
| `m.has(k)` | czy jest |
|
||||
| `m.get(k, domyslna)` | wartość albo domyślna |
|
||||
| `m.remove(k)` | bool: czy był |
|
||||
| `m.clear()` | wyczyść |
|
||||
| `m.keys()`, `m.values()` | tablice, w kolejności dodawania |
|
||||
|
||||
---
|
||||
|
||||
## Tekst `str`
|
||||
|
||||
| Kod | Daje |
|
||||
|
|
@ -189,6 +250,8 @@ Parametry zachowują się jak kopie. Kolejność w pliku nie ma znaczenia. Przec
|
|||
| `char(66)` | `'B'` |
|
||||
| `bool(0)` | `false` |
|
||||
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst |
|
||||
| `str([1, 2])`, `str(m)`, `str(punkt)` | ten sam tekst, który pokazuje `print` |
|
||||
| `int("x") or 0`, `float(s) or 0.0` | liczba albo wartość zapasowa |
|
||||
| `str(bajty)` | `[]u8` → tekst |
|
||||
| `str(p)` | `char*` z C → tekst |
|
||||
| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo liczby |
|
||||
|
|
@ -210,6 +273,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo
|
|||
| `delete_file(sciezka)` | usuń plik |
|
||||
| `shell(polecenie)` | uruchom polecenie, jego kod wyjścia |
|
||||
| `exit(kod)` | zakończ program od razu |
|
||||
| `error(komunikat)` | `return error("…")` w funkcji `-> T!` |
|
||||
| `addr(x)` | wskaźnik na x; `addr(fn)` = callback dla C |
|
||||
| `new(wartosc)` | wartość na stercie → `ptr[T]` |
|
||||
| `free(p)` | oddaj ją |
|
||||
|
|
@ -281,11 +345,17 @@ ksztalty.Pudlo inne // w deklaracjach
|
|||
| `pop() on an empty array` | `pop()` na pustej tablicy |
|
||||
| `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` |
|
||||
| `can't turn "x" into int` | `int("x")` |
|
||||
| `can't read the file "…"` | `read_file` |
|
||||
| `can't read the file "…"` | `read_file` (bez `or`) |
|
||||
| `the key "x" is not in the map` | `m["x"]` |
|
||||
|
||||
---
|
||||
|
||||
## VS Code (rozszerzenie „Finch”)
|
||||
## Edytory
|
||||
|
||||
Kate: `editors/kate/install.sh`, potem włącz wtyczkę **LSP Client**; uruchamianie przez **Build & Run** → `finch run %f`.
|
||||
Każdy edytor z LSP: `finch lsp`.
|
||||
|
||||
### VS Code (rozszerzenie „Finch”)
|
||||
|
||||
| Klawisz / akcja | Robi |
|
||||
|---|---|
|
||||
|
|
@ -295,7 +365,7 @@ ksztalty.Pudlo inne // w deklaracjach
|
|||
| najechanie myszą | typ / sygnatura / opis |
|
||||
| `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) |
|
||||
| `Ctrl+Shift+O` | funkcje i struktury w pliku |
|
||||
| `main`, `fn`, `fnr`, `struct`, `for`, `foreach`, `if`, `ife`, `while`, `input` + Tab | szablony |
|
||||
| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | szablony |
|
||||
| ustawienie `finch.path` | gdzie jest finch / finch.exe |
|
||||
|
||||
---
|
||||
|
|
@ -314,3 +384,7 @@ ksztalty.Pudlo inne // w deklaracjach
|
|||
| `import` / `link` | zaimportuj / dolinkuj |
|
||||
| `struct` | struktura |
|
||||
| `defer` | odłóż (na koniec bloku) |
|
||||
| `or` | albo (co zrobić przy porażce) |
|
||||
| `try` | spróbuj (porażkę przekaż dalej) |
|
||||
| `self` | sam (w metodzie: struktura, na której ją wywołano) |
|
||||
| `map` | mapa |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml
|
||||
(or run editors/kate/install.sh), then restart Kate.
|
||||
-->
|
||||
<language name="Finch" version="1" kateversion="5.0" section="Sources" extensions="*.fch"
|
||||
<language name="Finch" version="2" kateversion="5.0" section="Sources" extensions="*.fch"
|
||||
mimetype="text/x-finch" indenter="cstyle" author="The Finch authors" license="Apache-2.0">
|
||||
<highlighting>
|
||||
<list name="keywords">
|
||||
|
|
@ -22,6 +22,8 @@
|
|||
<item>continue</item>
|
||||
<item>return</item>
|
||||
<item>defer</item>
|
||||
<item>or</item>
|
||||
<item>try</item>
|
||||
</list>
|
||||
<list name="imports">
|
||||
<item>import</item>
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
<item>true</item>
|
||||
<item>false</item>
|
||||
<item>null</item>
|
||||
<item>self</item>
|
||||
</list>
|
||||
<list name="types">
|
||||
<item>int</item>
|
||||
|
|
@ -39,6 +42,7 @@
|
|||
<item>char</item>
|
||||
<item>str</item>
|
||||
<item>ptr</item>
|
||||
<item>map</item>
|
||||
<item>i8</item>
|
||||
<item>i16</item>
|
||||
<item>i32</item>
|
||||
|
|
@ -62,6 +66,7 @@
|
|||
<item>addr</item>
|
||||
<item>new</item>
|
||||
<item>free</item>
|
||||
<item>error</item>
|
||||
</list>
|
||||
|
||||
<contexts>
|
||||
|
|
@ -99,8 +104,11 @@
|
|||
<AnyChar String="()[],.:" attribute="Symbol"/>
|
||||
</context>
|
||||
|
||||
<!-- fn name( or fn Type.method( -->
|
||||
<context name="FunctionName" attribute="Normal Text" lineEndContext="#pop" fallthroughContext="#pop">
|
||||
<DetectSpaces/>
|
||||
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*(?=\.)" attribute="Data Type"/>
|
||||
<DetectChar char="." attribute="Symbol"/>
|
||||
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*" attribute="Function Definition" context="#pop"/>
|
||||
</context>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## 2.4.0
|
||||
|
||||
- Highlighting for methods (`fn Point.move`), maps (`map[K]V`), `or`, `try`, `self` and `error(...)`.
|
||||
- Completion for struct methods and map methods; parameter hints for method calls.
|
||||
- Snippets: `method`, `fnf` (a function that can fail), `map`, `forkv`, `orb`.
|
||||
|
||||
## 2.3.0
|
||||
|
||||
- First release: highlighting, snippets, language server, Run and Build.
|
||||
|
|
|
|||
4
editors/vscode/package-lock.json
generated
4
editors/vscode/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "finch-lang",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "finch-lang",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "finch-lang",
|
||||
"displayName": "Finch",
|
||||
"description": "The Finch programming language: highlighting, errors as you type, completion, hover, go to definition, and a Run button.",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"publisher": "nexoniarz",
|
||||
"license": "Apache-2.0",
|
||||
"icon": "images/icon.png",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,160 @@
|
|||
{
|
||||
"main": { "prefix": "main", "body": ["fn main() {", "\t$0", "}"], "description": "The program's start" },
|
||||
"main with arguments": { "prefix": "mainargs", "body": ["fn main([]str args) {", "\t$0", "}"], "description": "main that gets the command-line arguments" },
|
||||
"function": { "prefix": "fn", "body": ["fn ${1:name}(${2}) {", "\t$0", "}"], "description": "A function" },
|
||||
"function with result": { "prefix": "fnr", "body": ["fn ${1:name}(${2}) -> ${3:int} {", "\t$0", "}"], "description": "A function that returns a value" },
|
||||
"struct": { "prefix": "struct", "body": ["struct ${1:Name} {", "\t${2:int} ${3:field}", "}"], "description": "A struct" },
|
||||
"if": { "prefix": "if", "body": ["if ${1:condition} {", "\t$0", "}"] },
|
||||
"if else": { "prefix": "ife", "body": ["if ${1:condition} {", "\t$2", "} else {", "\t$0", "}"] },
|
||||
"for range": { "prefix": "for", "body": ["for ${1:i} in ${2:0}..${3:n} {", "\t$0", "}"], "description": "Count from start to end-1" },
|
||||
"for each": { "prefix": "foreach", "body": ["for ${1:item} in ${2:list} {", "\t$0", "}"], "description": "Every element of an array or str" },
|
||||
"while": { "prefix": "while", "body": ["while ${1:condition} {", "\t$0", "}"] },
|
||||
"print": { "prefix": "print", "body": ["print(${1})"] },
|
||||
"input": { "prefix": "input", "body": ["${1:answer} := input(\"${2:Question? }\")"] },
|
||||
"import C header": { "prefix": "importc", "body": ["import \"${1:stdio.h}\""] },
|
||||
"defer": { "prefix": "defer", "body": ["defer ${1}"], "description": "Run this when the block ends" }
|
||||
"main": {
|
||||
"prefix": "main",
|
||||
"body": [
|
||||
"fn main() {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "The program's start"
|
||||
},
|
||||
"main with arguments": {
|
||||
"prefix": "mainargs",
|
||||
"body": [
|
||||
"fn main([]str args) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "main that gets the command-line arguments"
|
||||
},
|
||||
"function": {
|
||||
"prefix": "fn",
|
||||
"body": [
|
||||
"fn ${1:name}(${2}) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "A function"
|
||||
},
|
||||
"function with result": {
|
||||
"prefix": "fnr",
|
||||
"body": [
|
||||
"fn ${1:name}(${2}) -> ${3:int} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "A function that returns a value"
|
||||
},
|
||||
"struct": {
|
||||
"prefix": "struct",
|
||||
"body": [
|
||||
"struct ${1:Name} {",
|
||||
"\t${2:int} ${3:field}",
|
||||
"}"
|
||||
],
|
||||
"description": "A struct"
|
||||
},
|
||||
"if": {
|
||||
"prefix": "if",
|
||||
"body": [
|
||||
"if ${1:condition} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"if else": {
|
||||
"prefix": "ife",
|
||||
"body": [
|
||||
"if ${1:condition} {",
|
||||
"\t$2",
|
||||
"} else {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"for range": {
|
||||
"prefix": "for",
|
||||
"body": [
|
||||
"for ${1:i} in ${2:0}..${3:n} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "Count from start to end-1"
|
||||
},
|
||||
"for each": {
|
||||
"prefix": "foreach",
|
||||
"body": [
|
||||
"for ${1:item} in ${2:list} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "Every element of an array or str"
|
||||
},
|
||||
"while": {
|
||||
"prefix": "while",
|
||||
"body": [
|
||||
"while ${1:condition} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"print": {
|
||||
"prefix": "print",
|
||||
"body": [
|
||||
"print(${1})"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"prefix": "input",
|
||||
"body": [
|
||||
"${1:answer} := input(\"${2:Question? }\")"
|
||||
]
|
||||
},
|
||||
"import C header": {
|
||||
"prefix": "importc",
|
||||
"body": [
|
||||
"import \"${1:stdio.h}\""
|
||||
]
|
||||
},
|
||||
"defer": {
|
||||
"prefix": "defer",
|
||||
"body": [
|
||||
"defer ${1}"
|
||||
],
|
||||
"description": "Run this when the block ends"
|
||||
},
|
||||
"method": {
|
||||
"prefix": "method",
|
||||
"body": [
|
||||
"fn ${1:Type}.${2:name}(${3}) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "A method: self is the struct it is called on"
|
||||
},
|
||||
"fallible function": {
|
||||
"prefix": "fnf",
|
||||
"body": [
|
||||
"fn ${1:name}(${2}) -> ${3:int}! {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "A function that can fail: return error(\"...\")"
|
||||
},
|
||||
"map": {
|
||||
"prefix": "map",
|
||||
"body": [
|
||||
"map[${1:str}]${2:int} ${3:name}"
|
||||
],
|
||||
"description": "An empty map"
|
||||
},
|
||||
"or block": {
|
||||
"prefix": "orb",
|
||||
"body": [
|
||||
"or {",
|
||||
"\t${1:print(err)}",
|
||||
"\t${2:return}",
|
||||
"}"
|
||||
],
|
||||
"description": "Handle a failure: err is the message"
|
||||
},
|
||||
"for key, value": {
|
||||
"prefix": "forkv",
|
||||
"body": [
|
||||
"for ${1:key}, ${2:value} in ${3:map} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "Every key and value of a map (or index and element of an array)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,12 +85,18 @@
|
|||
}
|
||||
},
|
||||
"function": {
|
||||
"match": "\\b(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"match": "\\b(fn)\\s+(?:([A-Za-z_][A-Za-z0-9_]*)(\\.))?([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "storage.type.function.finch"
|
||||
},
|
||||
"2": {
|
||||
"name": "entity.name.type.finch"
|
||||
},
|
||||
"3": {
|
||||
"name": "punctuation.accessor.finch"
|
||||
},
|
||||
"4": {
|
||||
"name": "entity.name.function.finch"
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +117,7 @@
|
|||
},
|
||||
{
|
||||
"name": "keyword.control.finch",
|
||||
"match": "\\b(if|else|while|for|in|break|continue|return|defer)\\b"
|
||||
"match": "\\b(if|else|while|for|in|break|continue|return|defer|or|try)\\b"
|
||||
},
|
||||
{
|
||||
"name": "storage.type.function.finch",
|
||||
|
|
@ -126,11 +132,15 @@
|
|||
"match": "\\b(true|false|null)\\b"
|
||||
},
|
||||
{
|
||||
"name": "storage.type.primitive.finch",
|
||||
"match": "\\b(int|float|bool|char|str|ptr|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b"
|
||||
"name": "variable.language.self.finch",
|
||||
"match": "\\bself\\b"
|
||||
},
|
||||
{
|
||||
"match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free)\\s*(?=\\()",
|
||||
"name": "storage.type.primitive.finch",
|
||||
"match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b"
|
||||
},
|
||||
{
|
||||
"match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "support.function.builtin.finch"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ const code = [
|
|||
' p := Point(x: 1)',
|
||||
' return a + p.x',
|
||||
'}',
|
||||
'fn Point.move(int dx) -> int! {',
|
||||
' map[str]int m',
|
||||
' n := try parse(s) or 0',
|
||||
' self.x += dx',
|
||||
' return error("no")',
|
||||
'}',
|
||||
];
|
||||
const expect = [ // [line, token text, scope that must be present]
|
||||
[0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'],
|
||||
|
|
@ -32,6 +38,11 @@ const expect = [ // [line, token text, scope that must be present]
|
|||
[7, 'c', 'string.quoted.single'], [7, '3.5', 'constant.numeric.float'], [7, 'true', 'constant.language'], [7, 'null', 'constant.language'],
|
||||
[8, 'Point', 'entity.name.type'],
|
||||
[9, 'return', 'keyword.control'],
|
||||
[11, 'Point', 'entity.name.type'], [11, 'move', 'entity.name.function'], [11, '!', 'keyword.operator'],
|
||||
[12, 'map', 'storage.type.primitive'],
|
||||
[13, 'try', 'keyword.control'], [13, 'or', 'keyword.control'],
|
||||
[14, 'self', 'variable.language.self'],
|
||||
[15, 'error', 'support.function.builtin'],
|
||||
];
|
||||
registry.loadGrammar('source.finch').then((g) => {
|
||||
let state = vsctm.INITIAL, toks = [];
|
||||
|
|
|
|||
62
examples/wordcount.fch
Normal file
62
examples/wordcount.fch
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Counts the words in a file: maps, methods and errors as values.
|
||||
// finch run examples/wordcount.fch README.md
|
||||
|
||||
struct Counter {
|
||||
map[str]int counts
|
||||
int total
|
||||
}
|
||||
|
||||
// lower-case letters and digits only: "Hello," -> "hello"
|
||||
fn clean(str word) -> str {
|
||||
out := ""
|
||||
for c in word.lower() {
|
||||
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
|
||||
out += str(c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn Counter.add(str text) {
|
||||
for word in text.split(" ") {
|
||||
w := clean(word)
|
||||
if w.len > 0 {
|
||||
self.counts[w] += 1 // a new word starts at 0
|
||||
self.total += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the n most frequent words, most frequent first
|
||||
fn Counter.top(int n) -> []str {
|
||||
words := self.counts.keys()
|
||||
for i in 0..words.len {
|
||||
for j in i + 1..words.len {
|
||||
if self.counts[words[j]] > self.counts[words[i]] {
|
||||
w := words[i]
|
||||
words[i] = words[j]
|
||||
words[j] = w
|
||||
}
|
||||
}
|
||||
}
|
||||
if words.len > n {
|
||||
words.resize(n)
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
fn main([]str args) -> ! {
|
||||
if args.len < 2 {
|
||||
return error("usage: wordcount <file>")
|
||||
}
|
||||
text := try read_file(args[1]) // a missing file ends the program with a clear message
|
||||
|
||||
Counter c
|
||||
for line in text.split("\n") {
|
||||
c.add(line)
|
||||
}
|
||||
print(c.total, "words,", c.counts.len, "different")
|
||||
for i, w in c.top(10) {
|
||||
print(i + 1, w, c.counts[w])
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,11 @@
|
|||
// cap == 0 : a string literal, valid forever, never written to
|
||||
// cap == -1: borrowed from C, must be copied before it is kept
|
||||
// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0
|
||||
// map[K]V: see "maps" below
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -148,33 +150,87 @@ void finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }
|
|||
|
||||
void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); }
|
||||
|
||||
static void bad_number(const FStr *s, const char *kind, const char *file, int64_t line) {
|
||||
char msg[200];
|
||||
snprintf(msg, sizeof msg, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr, kind);
|
||||
finch_panic(file, line, msg);
|
||||
static void number_error(char *msg, size_t size, const FStr *s, const char *kind) {
|
||||
snprintf(msg, size, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : "", kind);
|
||||
}
|
||||
|
||||
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
|
||||
const char *p = s->ptr;
|
||||
static int parse_int(const FStr *s, int64_t *out) {
|
||||
const char *p = s->ptr ? s->ptr : "";
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
char *end;
|
||||
errno = 0;
|
||||
long long v = strtoll(p, &end, 10);
|
||||
while (isspace((unsigned char)*end)) end++;
|
||||
if (end == p || *end || errno == ERANGE) bad_number(s, "int", file, line);
|
||||
return v;
|
||||
*out = v;
|
||||
return !(end == p || *end || errno == ERANGE);
|
||||
}
|
||||
|
||||
double finch_str_to_float(const FStr *s, const char *file, int64_t line) {
|
||||
const char *p = s->ptr;
|
||||
static int parse_float(const FStr *s, double *out) {
|
||||
const char *p = s->ptr ? s->ptr : "";
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
char *end;
|
||||
double v = strtod(p, &end);
|
||||
while (isspace((unsigned char)*end)) end++;
|
||||
if (end == p || *end) bad_number(s, "float", file, line);
|
||||
*out = v;
|
||||
return !(end == p || *end);
|
||||
}
|
||||
|
||||
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
|
||||
int64_t v;
|
||||
if (!parse_int(s, &v)) {
|
||||
char msg[200];
|
||||
number_error(msg, sizeof msg, s, "int");
|
||||
finch_panic(file, line, msg);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
double finch_str_to_float(const FStr *s, const char *file, int64_t line) {
|
||||
double v;
|
||||
if (!parse_float(s, &v)) {
|
||||
char msg[200];
|
||||
number_error(msg, sizeof msg, s, "float");
|
||||
finch_panic(file, line, msg);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------
|
||||
// Each gives back 1 on success; on failure 0, with the message in *err.
|
||||
|
||||
static void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }
|
||||
|
||||
int finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {
|
||||
if (parse_int(s, out)) return 1;
|
||||
char msg[200];
|
||||
number_error(msg, sizeof msg, s, "int");
|
||||
set_error(err, msg);
|
||||
*out = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int finch_str_try_float(const FStr *s, double *out, FStr *err) {
|
||||
if (parse_float(s, out)) return 1;
|
||||
char msg[200];
|
||||
number_error(msg, sizeof msg, s, "float");
|
||||
set_error(err, msg);
|
||||
*out = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void file_error(FStr *err, const char *what, const FStr *path) {
|
||||
char msg[300];
|
||||
snprintf(msg, sizeof msg, "can't %s the file \"%.*s\": %s", what, (int)(path->len > 200 ? 200 : path->len),
|
||||
path->ptr ? path->ptr : "", strerror(errno));
|
||||
set_error(err, msg);
|
||||
}
|
||||
|
||||
// when `fn main() -> !` fails
|
||||
void finch_report_error(const FStr *msg) {
|
||||
fflush(stdout);
|
||||
fprintf(stderr, "error: %.*s\n", (int)msg->len, msg->ptr ? msg->ptr : "");
|
||||
}
|
||||
|
||||
void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {
|
||||
if (start < 0 || end > s->len || start > end) {
|
||||
char msg[160];
|
||||
|
|
@ -303,6 +359,58 @@ void finch_str_repeat(FStr *out, const FStr *s, int64_t n) {
|
|||
|
||||
void finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }
|
||||
|
||||
// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------
|
||||
// A buffer starts as an empty str and grows on the heap.
|
||||
|
||||
void finch_buf_add(FStr *b, const char *p, int64_t n) {
|
||||
if (b->len + n + 1 > b->cap) {
|
||||
int64_t cap = b->cap > 0 ? b->cap * 2 : 32;
|
||||
while (cap < b->len + n + 1) cap *= 2;
|
||||
b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));
|
||||
b->cap = cap;
|
||||
}
|
||||
if (n) memcpy(b->ptr + b->len, p, (size_t)n);
|
||||
b->len += n;
|
||||
b->ptr[b->len] = 0;
|
||||
}
|
||||
|
||||
static void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
static void buf_fmt(FStr *b, const char *fmt, ...) {
|
||||
char t[64];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
int n = vsnprintf(t, sizeof t, fmt, ap);
|
||||
va_end(ap);
|
||||
finch_buf_add(b, t, n);
|
||||
}
|
||||
|
||||
void finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, "%lld", (long long)v); }
|
||||
void finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, "%llu", (unsigned long long)v); }
|
||||
void finch_buf_float(FStr *b, double v) { buf_fmt(b, "%g", v); }
|
||||
void finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? "true" : "false", v ? 4 : 5); }
|
||||
void finch_buf_ptr(FStr *b, const void *p) {
|
||||
if (p) buf_fmt(b, "0x%llx", (unsigned long long)(uintptr_t)p);
|
||||
else finch_buf_add(b, "null", 4);
|
||||
}
|
||||
|
||||
void finch_buf_char(FStr *b, char c, int quoted) {
|
||||
if (quoted) finch_buf_add(b, "'", 1);
|
||||
finch_buf_add(b, &c, 1);
|
||||
if (quoted) finch_buf_add(b, "'", 1);
|
||||
}
|
||||
|
||||
void finch_buf_str(FStr *b, const FStr *s, int quoted) {
|
||||
if (quoted) finch_buf_add(b, "\"", 1);
|
||||
finch_buf_add(b, s->ptr, s->len);
|
||||
if (quoted) finch_buf_add(b, "\"", 1);
|
||||
}
|
||||
|
||||
// print the buffer and free it
|
||||
void finch_buf_print(FStr *b) {
|
||||
if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);
|
||||
if (b->cap > 0) free(b->ptr);
|
||||
}
|
||||
|
||||
// ---------- arrays ----------
|
||||
|
||||
void finch_arr_reserve(FArr *a, int64_t need, int64_t es) {
|
||||
|
|
@ -409,8 +517,27 @@ int finch_file_exists(const FStr *path) {
|
|||
return f != NULL;
|
||||
}
|
||||
|
||||
int finch_try_read_file(FStr *out, const FStr *path, FStr *err) {
|
||||
FILE *f = fopen(path->ptr ? path->ptr : "", "rb");
|
||||
if (!f) {
|
||||
file_error(err, "read", path);
|
||||
return 0;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
char *m = finch_alloc(size + 1);
|
||||
size_t got = fread(m, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
m[got] = 0;
|
||||
out->ptr = m;
|
||||
out->len = (int64_t)got;
|
||||
out->cap = size + 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {
|
||||
FILE *f = fopen(path->ptr, "rb");
|
||||
FILE *f = fopen(path->ptr ? path->ptr : "", "rb");
|
||||
if (!f) {
|
||||
char msg[300];
|
||||
snprintf(msg, sizeof msg, "can't read the file \"%.*s\": %s", (int)(path->len > 200 ? 200 : path->len),
|
||||
|
|
@ -444,12 +571,174 @@ int64_t finch_shell(const FStr *cmd) {
|
|||
|
||||
int finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : "") == 0; }
|
||||
|
||||
int finch_try_delete_file(const FStr *path, FStr *err) {
|
||||
if (remove(path->ptr ? path->ptr : "") == 0) return 1;
|
||||
file_error(err, "delete", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here
|
||||
void *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }
|
||||
|
||||
int finch_write_file(const FStr *path, const FStr *text) {
|
||||
FILE *f = fopen(path->ptr, "wb");
|
||||
FILE *f = fopen(path->ptr ? path->ptr : "", "wb");
|
||||
if (!f) return 0;
|
||||
size_t put = fwrite(text->ptr, 1, (size_t)text->len, f);
|
||||
size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;
|
||||
return fclose(f) == 0 && put == (size_t)text->len;
|
||||
}
|
||||
|
||||
int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {
|
||||
errno = 0;
|
||||
if (finch_write_file(path, text)) return 1;
|
||||
if (!errno) errno = EIO;
|
||||
file_error(err, "write", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------- maps ----------
|
||||
// map[K]V = { entries; len; used; cap; index; icap }
|
||||
// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.
|
||||
// A removed entry keeps its slot (hash 0) until the next resize packs the slots.
|
||||
// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.
|
||||
// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.
|
||||
// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).
|
||||
|
||||
typedef struct {
|
||||
char *entries;
|
||||
int64_t len, used, cap;
|
||||
int64_t *index;
|
||||
int64_t icap;
|
||||
} FMap;
|
||||
|
||||
static uint64_t map_hash(const void *key, int64_t ks, int kstr) {
|
||||
const unsigned char *p;
|
||||
size_t n;
|
||||
if (kstr) {
|
||||
const FStr *s = key;
|
||||
p = (const unsigned char *)s->ptr;
|
||||
n = (size_t)s->len;
|
||||
} else {
|
||||
p = key;
|
||||
n = (size_t)ks;
|
||||
}
|
||||
uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= p[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
h ^= h >> 31;
|
||||
h *= 0xbf58476d1ce4e5b9ULL;
|
||||
h ^= h >> 29;
|
||||
return h | 1; // 0 marks a removed entry
|
||||
}
|
||||
|
||||
static int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {
|
||||
if (!kstr) return memcmp(a, b, (size_t)ks) == 0;
|
||||
const FStr *x = (const FStr *)a, *y = b;
|
||||
return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);
|
||||
}
|
||||
|
||||
static void map_reindex(FMap *m, int64_t es) {
|
||||
if (m->len != m->used) { // pack the slots of removed entries away
|
||||
int64_t j = 0;
|
||||
for (int64_t i = 0; i < m->used; i++) {
|
||||
char *e = m->entries + i * es;
|
||||
if (*(uint64_t *)e == 0) continue;
|
||||
if (i != j) memmove(m->entries + j * es, e, (size_t)es);
|
||||
j++;
|
||||
}
|
||||
m->used = j;
|
||||
}
|
||||
int64_t icap = 16;
|
||||
while (icap < m->cap * 2) icap *= 2;
|
||||
free(m->index);
|
||||
m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));
|
||||
memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));
|
||||
m->icap = icap;
|
||||
for (int64_t i = 0; i < m->used; i++) {
|
||||
uint64_t h = *(uint64_t *)(m->entries + i * es);
|
||||
int64_t s = (int64_t)(h & (uint64_t)(icap - 1));
|
||||
while (m->index[s] != -1) s = (s + 1) & (icap - 1);
|
||||
m->index[s] = i;
|
||||
}
|
||||
}
|
||||
|
||||
// the entry number of `key`, or -1
|
||||
int64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {
|
||||
if (m->len == 0) return -1;
|
||||
uint64_t h = map_hash(key, ks, kstr);
|
||||
for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {
|
||||
int64_t i = m->index[s];
|
||||
if (i == -1) return -1;
|
||||
if (i < 0) continue;
|
||||
char *e = m->entries + i * es;
|
||||
if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;
|
||||
}
|
||||
}
|
||||
|
||||
// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.
|
||||
int64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {
|
||||
int64_t i = finch_map_find(m, key, es, ks, kstr);
|
||||
*added = i < 0;
|
||||
if (i >= 0) return i;
|
||||
if (m->used == m->cap) {
|
||||
if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing
|
||||
map_reindex(m, es);
|
||||
} else {
|
||||
m->cap = m->cap ? m->cap * 2 : 8;
|
||||
m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));
|
||||
map_reindex(m, es);
|
||||
}
|
||||
}
|
||||
uint64_t h = map_hash(key, ks, kstr);
|
||||
i = m->used++;
|
||||
char *e = m->entries + i * es;
|
||||
memset(e, 0, (size_t)es);
|
||||
*(uint64_t *)e = h;
|
||||
int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));
|
||||
while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);
|
||||
m->index[s] = i;
|
||||
m->len++;
|
||||
return i;
|
||||
}
|
||||
|
||||
// Forget entry i (its key and value were already dropped by the compiler).
|
||||
void finch_map_remove_at(FMap *m, int64_t i, int64_t es) {
|
||||
char *e = m->entries + i * es;
|
||||
uint64_t h = *(uint64_t *)e;
|
||||
int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));
|
||||
while (m->index[s] != i) s = (s + 1) & (m->icap - 1);
|
||||
m->index[s] = -2;
|
||||
*(uint64_t *)e = 0;
|
||||
if (--m->len == 0) {
|
||||
m->used = 0;
|
||||
memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));
|
||||
}
|
||||
}
|
||||
|
||||
void finch_map_clear(FMap *m) {
|
||||
m->len = m->used = 0;
|
||||
if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));
|
||||
}
|
||||
|
||||
void finch_map_free(FMap *m) {
|
||||
free(m->entries);
|
||||
free(m->index);
|
||||
}
|
||||
|
||||
// A copy of the slots and the index; the compiler then copies the keys and values that own memory.
|
||||
void finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {
|
||||
*out = *m;
|
||||
if (!m->cap) return;
|
||||
out->entries = finch_alloc(m->cap * es);
|
||||
memcpy(out->entries, m->entries, (size_t)(m->used * es));
|
||||
out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));
|
||||
memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));
|
||||
}
|
||||
|
||||
__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {
|
||||
char msg[200];
|
||||
snprintf(msg, sizeof msg, "the key %.*s is not in the map (check with .has(key), or use .get(key, default))",
|
||||
(int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : "");
|
||||
finch_panic(file, line, msg);
|
||||
}
|
||||
|
|
|
|||
44
src/ast.h
44
src/ast.h
|
|
@ -11,12 +11,14 @@ struct Type {
|
|||
Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
|
||||
Str, Ptr, Null,
|
||||
Array, // []T: growable, owns its elements
|
||||
Map, // map[K]V: a hash map that keeps insertion order, owns its keys and values
|
||||
Fixed, // [N]T: only inside C structs
|
||||
Struct, // a resolved struct
|
||||
Named, // a struct name as written, before codegen resolves it
|
||||
};
|
||||
Kind kind = Void;
|
||||
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed
|
||||
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value
|
||||
std::shared_ptr<Type> key; // Map
|
||||
long long count = 0; // Fixed
|
||||
std::string name; // Named / Struct
|
||||
std::string module; // Named: written qualifier ("" = none); Struct: defining module
|
||||
|
|
@ -34,6 +36,12 @@ struct Type {
|
|||
a.elem = std::make_shared<Type>(t);
|
||||
return a;
|
||||
}
|
||||
static Type mapOf(const Type &k, const Type &v) {
|
||||
Type m(Map);
|
||||
m.key = std::make_shared<Type>(k);
|
||||
m.elem = std::make_shared<Type>(v);
|
||||
return m;
|
||||
}
|
||||
|
||||
bool isInt() const { return kind >= I8 && kind <= U64; }
|
||||
bool isSigned() const { return kind >= I8 && kind <= I64; }
|
||||
|
|
@ -58,6 +66,7 @@ struct Type {
|
|||
if (!elem || !o.elem) return !elem && !o.elem;
|
||||
return *elem == *o.elem;
|
||||
case Array: return *elem == *o.elem;
|
||||
case Map: return *key == *o.key && *elem == *o.elem;
|
||||
case Fixed: return count == o.count && *elem == *o.elem;
|
||||
case Struct: return info == o.info;
|
||||
case Named: return name == o.name && module == o.module;
|
||||
|
|
@ -86,6 +95,7 @@ struct Type {
|
|||
case Ptr: return elem ? "ptr[" + elem->show() + "]" : "ptr";
|
||||
case Null: return "null";
|
||||
case Array: return "[]" + elem->show();
|
||||
case Map: return "map[" + key->show() + "]" + elem->show();
|
||||
case Fixed: return "[" + std::to_string(count) + "]" + elem->show();
|
||||
case Struct:
|
||||
case Named: return module.empty() ? name : module + "." + name;
|
||||
|
|
@ -104,7 +114,8 @@ struct Pos {
|
|||
|
||||
// ---------- expressions ----------
|
||||
|
||||
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method };
|
||||
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method,
|
||||
MapLit, Try, OrElse };
|
||||
|
||||
struct Expr {
|
||||
ExprKind kind;
|
||||
|
|
@ -179,7 +190,11 @@ struct ArrayLitExpr : Expr { // [1, 2, 3]
|
|||
std::vector<ExprPtr> elems;
|
||||
explicit ArrayLitExpr(Pos p) : Expr(ExprKind::ArrayLit, p) {}
|
||||
};
|
||||
// obj.name(args): a.push(x), s.find("x"), and math.sqrt(2) / math.Vec(1, 2) for modules
|
||||
struct MapLitExpr : Expr { // ["a": 1, "b": 2], or [:] for an empty map
|
||||
std::vector<ExprPtr> keys, values;
|
||||
explicit MapLitExpr(Pos p) : Expr(ExprKind::MapLit, p) {}
|
||||
};
|
||||
// obj.name(args): a.push(x), s.find("x"), p.move(1, 2), and math.sqrt(2) / math.Vec(1, 2) for modules
|
||||
struct MethodExpr : Expr {
|
||||
ExprPtr obj;
|
||||
std::string name;
|
||||
|
|
@ -188,6 +203,21 @@ struct MethodExpr : Expr {
|
|||
MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {}
|
||||
};
|
||||
|
||||
struct BlockStmt;
|
||||
// try f(x): if f fails, the current function fails with the same error
|
||||
struct TryExpr : Expr {
|
||||
ExprPtr call;
|
||||
TryExpr(Pos p, ExprPtr c) : Expr(ExprKind::Try, p), call(std::move(c)) {}
|
||||
};
|
||||
// f(x) or fallback, f(x) or { ...err... }
|
||||
struct OrElseExpr : Expr {
|
||||
ExprPtr call;
|
||||
ExprPtr fallback; // or a value
|
||||
std::unique_ptr<BlockStmt> block; // or a block that sees `err`
|
||||
OrElseExpr(Pos p, ExprPtr c) : Expr(ExprKind::OrElse, p), call(std::move(c)) {}
|
||||
~OrElseExpr() override;
|
||||
};
|
||||
|
||||
// ---------- statements ----------
|
||||
|
||||
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, ForEach, Return, Break, Continue, Block, Defer };
|
||||
|
|
@ -206,6 +236,7 @@ struct BlockStmt : Stmt {
|
|||
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
|
||||
};
|
||||
using BlockPtr = std::unique_ptr<BlockStmt>;
|
||||
inline OrElseExpr::~OrElseExpr() = default;
|
||||
|
||||
struct VarDeclStmt : Stmt {
|
||||
bool hasType; // `int x = 5` vs `x := 5`
|
||||
|
|
@ -246,7 +277,9 @@ struct ForStmt : Stmt {
|
|||
struct ForEachStmt : Stmt {
|
||||
std::string var;
|
||||
Pos varPos;
|
||||
ExprPtr list; // for var in list (an array or a str)
|
||||
std::string var2; // for i, x in list / for k, v in map ("" = one variable)
|
||||
Pos var2Pos;
|
||||
ExprPtr list; // for var in list (an array, a str or a map)
|
||||
BlockPtr body;
|
||||
ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, p) {}
|
||||
};
|
||||
|
|
@ -278,7 +311,10 @@ struct FnDecl {
|
|||
Pos pos;
|
||||
Pos namePos;
|
||||
std::string name;
|
||||
std::string recv; // fn Point.move(...): "Point" (a method; `self` is the Point it is called on)
|
||||
Pos recvPos;
|
||||
Type ret; // Void when there is no `-> type`
|
||||
bool fallible = false; // -> int! / -> ! : can fail with an error
|
||||
std::vector<Param> params;
|
||||
BlockPtr body;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,18 +11,19 @@ struct BuiltinDoc {
|
|||
inline const BuiltinDoc kBuiltins[] = {
|
||||
{"print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."},
|
||||
{"input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."},
|
||||
{"read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read."},
|
||||
{"write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked."},
|
||||
{"read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read, unless handled: read_file(p) or { ... }, try read_file(p)."},
|
||||
{"write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked; with or / try, the reason it didn't."},
|
||||
{"file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"},
|
||||
{"delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked."},
|
||||
{"delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."},
|
||||
{"error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."},
|
||||
{"shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."},
|
||||
{"exit", "exit(int code)", "Ends the program right away with this exit code."},
|
||||
{"addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."},
|
||||
{"new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."},
|
||||
{"free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."},
|
||||
{"str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."},
|
||||
{"int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number)."},
|
||||
{"float", "float(x) -> float", "Converts to a decimal number; parses text."},
|
||||
{"int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number, unless handled: int(s) or 0)."},
|
||||
{"float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."},
|
||||
{"char", "char(int n) -> char", "The character with this code."},
|
||||
{"bool", "bool(int n) -> bool", "true for any number but 0."},
|
||||
{"ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."},
|
||||
|
|
@ -62,8 +63,18 @@ inline const BuiltinDoc kStrMethods[] = {
|
|||
{"bytes", "bytes() -> []u8", "The bytes of the text."},
|
||||
};
|
||||
|
||||
inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true",
|
||||
"false", "null", "import", "link", "struct", "defer"};
|
||||
inline const BuiltinDoc kMapMethods[] = {
|
||||
{"len", ".len -> int", "The number of keys."},
|
||||
{"has", "has(K key) -> bool", "Is the key in the map?"},
|
||||
{"get", "get(K key, V default) -> V", "The value for key, or default if the key isn't there."},
|
||||
{"remove", "remove(K key) -> bool", "Removes the key and its value; true if it was there."},
|
||||
{"clear", "clear()", "Removes everything."},
|
||||
{"keys", "keys() -> []K", "The keys, in the order they were added."},
|
||||
{"values", "values() -> []V", "The values, in the order their keys were added."},
|
||||
};
|
||||
|
||||
inline const char *kTypeNames[] = {"int", "float", "bool", "char", "str", "ptr", "i8", "i16", "i32", "i64",
|
||||
inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true",
|
||||
"false", "null", "import", "link", "struct", "defer", "or", "try", "self"};
|
||||
|
||||
inline const char *kTypeNames[] = {"int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64",
|
||||
"u8", "u16", "u32", "u64", "f32", "f64"};
|
||||
|
|
|
|||
340
src/codegen.cpp
340
src/codegen.cpp
|
|
@ -39,6 +39,8 @@ std::unique_ptr<Module> Codegen::run() {
|
|||
if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')");
|
||||
for (auto &[name, m] : modules)
|
||||
for (auto &[fname, fn] : m.fns) define(fn);
|
||||
for (auto &[s, ms] : methods)
|
||||
for (auto &[mname, fn] : ms) define(fn);
|
||||
defineMainWrapper();
|
||||
if (di) di->finalize();
|
||||
|
||||
|
|
@ -102,19 +104,72 @@ void Codegen::declareStructs() {
|
|||
void Codegen::declareFns() {
|
||||
for (const Program &p : progs)
|
||||
for (const FnDecl &fn : p.fns) declare(p, fn);
|
||||
|
||||
// Which methods change `self`? Calling one of them on something changes it too, so repeat until stable.
|
||||
for (bool changed = true; changed;) {
|
||||
changed = false;
|
||||
for (auto &[s, ms] : methods)
|
||||
for (auto &[name, f] : ms)
|
||||
if (!f.mutatesSelf && mutates("self", *f.decl->body)) {
|
||||
f.mutatesSelf = true;
|
||||
mutatingMethods.insert(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
// Owning parameters the function never changes are lent to it instead of copied.
|
||||
auto borrow = [&](Fn &f) {
|
||||
for (size_t i = 0; i < f.params.size(); i++)
|
||||
f.borrowParam.push_back(owning(f.params[i]) && !mutates(f.decl->params[i].name, *f.decl->body));
|
||||
};
|
||||
for (auto &[name, m] : modules)
|
||||
for (auto &[fname, f] : m.fns) borrow(f);
|
||||
for (auto &[s, ms] : methods)
|
||||
for (auto &[name, f] : ms) borrow(f);
|
||||
}
|
||||
|
||||
FType Codegen::structType(StructInfo *s) {
|
||||
FType t(FType::Struct);
|
||||
t.info = s;
|
||||
t.name = s->name;
|
||||
t.module = s->module == "C" ? "" : s->module;
|
||||
return t;
|
||||
}
|
||||
|
||||
llvm::Type *Codegen::retType(const Fn &f) {
|
||||
if (!f.fallible) return ty(f.ret);
|
||||
llvm::Type *v = f.ret.kind == FType::Void ? (llvm::Type *)b.getInt8Ty() : ty(f.ret);
|
||||
return StructType::get(ctx, {b.getInt1Ty(), v, ty(FType::Str)});
|
||||
}
|
||||
|
||||
void Codegen::declare(const Program &p, const FnDecl &fn) {
|
||||
ModuleScope &m = modules[p.module];
|
||||
Pos pos = fn.pos;
|
||||
curModule = p.module;
|
||||
StructInfo *recv = nullptr;
|
||||
if (!fn.recv.empty()) { // fn Point.move(...)
|
||||
Pos rp = fn.recvPos;
|
||||
recv = findStruct("", fn.recv, false);
|
||||
if (!recv)
|
||||
failAt(rp.file, rp.line, rp.col, "there is no struct named '" + fn.recv + "' here (a method goes in the same file as its struct, or on a struct from C)");
|
||||
resolveStruct(recv);
|
||||
for (auto &f : recv->fields)
|
||||
if (f.name == fn.name && !f.hidden)
|
||||
failAt(fn.namePos.file, fn.namePos.line, fn.namePos.col, "the struct " + recv->name + " already has a field named '" + fn.name + "'");
|
||||
if (methods[recv].count(fn.name)) failAt(pos.file, pos.line, pos.col, "the method " + fn.recv + "." + fn.name + " is defined twice");
|
||||
for (const Param &prm : fn.params)
|
||||
if (prm.name == "self")
|
||||
failAt(prm.namePos.file, prm.namePos.line, prm.namePos.col, "'self' is already the " + recv->name + " the method is called on");
|
||||
} else {
|
||||
if (m.fns.count(fn.name)) failAt(pos.file, pos.line, pos.col, "function '" + fn.name + "' is defined twice");
|
||||
if (isBuiltin(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is a built-in function, pick another name");
|
||||
if (m.structs.count(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is already the name of a struct");
|
||||
}
|
||||
|
||||
curModule = p.module;
|
||||
Fn f;
|
||||
f.decl = &fn;
|
||||
f.module = p.module;
|
||||
f.recv = recv;
|
||||
f.fallible = fn.fallible;
|
||||
f.ret = resolve(fn.ret, pos);
|
||||
for (const Param &prm : fn.params) f.params.push_back(resolve(prm.type, prm.pos));
|
||||
|
||||
|
|
@ -128,14 +183,13 @@ void Codegen::declare(const Program &p, const FnDecl &fn) {
|
|||
}
|
||||
|
||||
std::vector<llvm::Type *> params;
|
||||
for (size_t i = 0; i < f.params.size(); i++) {
|
||||
params.push_back(ty(f.params[i]));
|
||||
f.borrowParam.push_back(owning(f.params[i]) && !mutates(fn.params[i].name, *fn.body));
|
||||
}
|
||||
if (recv) params.push_back(b.getPtrTy()); // self
|
||||
for (size_t i = 0; i < f.params.size(); i++) params.push_back(ty(f.params[i]));
|
||||
// User functions get a prefix so they never clash with C library names.
|
||||
std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + fn.name;
|
||||
f.llvm = Function::Create(FunctionType::get(ty(f.ret), params, false), Function::InternalLinkage, name, mod.get());
|
||||
m.fns[fn.name] = f;
|
||||
std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + (recv ? recv->name + "." : "") + fn.name;
|
||||
f.llvm = Function::Create(FunctionType::get(retType(f), params, false), Function::InternalLinkage, name, mod.get());
|
||||
if (recv) methods[recv][fn.name] = f;
|
||||
else m.fns[fn.name] = f;
|
||||
}
|
||||
|
||||
// The real C `main` calls the Finch one and turns its result into an exit code.
|
||||
|
|
@ -155,6 +209,17 @@ void Codegen::defineMainWrapper() {
|
|||
}
|
||||
Value *r = b.CreateCall(fm.llvm, args);
|
||||
if (argsArr) dropValue(argsArr, fm.params[0]);
|
||||
if (fm.fallible) { // fn main() -> !: a failure is printed, and the exit code is 1
|
||||
BasicBlock *bad = BasicBlock::Create(ctx, "main.failed", w), *good = BasicBlock::Create(ctx, "main.ok", w);
|
||||
b.CreateCondBr(b.CreateExtractValue(r, 0), good, bad);
|
||||
b.SetInsertPoint(bad);
|
||||
Value *msg = tmp(b.CreateExtractValue(r, 2));
|
||||
b.CreateCall(rt("finch_report_error"), {msg});
|
||||
b.CreateCall(rt("finch_str_drop"), {msg});
|
||||
b.CreateRet(b.getInt32(1));
|
||||
b.SetInsertPoint(good);
|
||||
if (fm.ret.kind != FType::Void) r = b.CreateExtractValue(r, 1);
|
||||
}
|
||||
if (fm.ret.kind == FType::Void) b.CreateRet(b.getInt32(0));
|
||||
else b.CreateRet(b.CreateIntCast(r, b.getInt32Ty(), true));
|
||||
}
|
||||
|
|
@ -164,7 +229,8 @@ void Codegen::define(Fn &fn) {
|
|||
curFn = &fn;
|
||||
curModule = fn.module;
|
||||
if (g_index) {
|
||||
FnInfo fi{fn.module, d.name, signature(fn), "", {}, d.namePos.file, d.namePos.line, d.namePos.col, d.body->end.line};
|
||||
std::string shown = fn.recv ? fn.recv->name + "." + d.name : d.name;
|
||||
FnInfo fi{fn.module, shown, signature(fn), "", {}, d.namePos.file, d.namePos.line, d.namePos.col, d.body->end.line};
|
||||
for (size_t i = 0; i < fn.params.size(); i++) fi.params.push_back(fn.params[i].show() + " " + d.params[i].name);
|
||||
g_index->fns.push_back(fi);
|
||||
note(d.namePos, d.name.size(), "```finch\n" + signature(fn) + "\n```", d.namePos);
|
||||
|
|
@ -186,12 +252,33 @@ void Codegen::define(Fn &fn) {
|
|||
varOrder = 0;
|
||||
pushScope();
|
||||
unsigned i = 0;
|
||||
for (Argument &arg : f->args()) {
|
||||
auto argIt = f->arg_begin();
|
||||
if (fn.recv) { // `self` is the caller's struct itself, reached through its address
|
||||
Argument &selfArg = *argIt++;
|
||||
selfArg.setName("self");
|
||||
Var v;
|
||||
v.slot = &selfArg;
|
||||
v.type = structType(fn.recv);
|
||||
v.order = ++varOrder;
|
||||
v.pos = d.recvPos;
|
||||
scopes.back().vars["self"] = v;
|
||||
if (g_index) g_index->vars.push_back({"self", v.type.show(), d.pos.file, d.pos.line, d.pos.col, d.pos.file, d.pos.line, d.body->end.line});
|
||||
if (di) {
|
||||
FType pt = FType::ptrTo(v.type);
|
||||
AllocaInst *a = slot(pt, "self.addr");
|
||||
b.CreateStore(&selfArg, a);
|
||||
DILocalVariable *dv = di->createParameterVariable(diFn, "self", 1, diFiles[d.pos.file], d.pos.line, diType(pt));
|
||||
di->insertDeclare(a, dv, di->createExpression(), DILocation::get(ctx, d.pos.line, d.pos.col, diFn), b.GetInsertBlock());
|
||||
}
|
||||
}
|
||||
for (; argIt != f->arg_end(); ++argIt) {
|
||||
Argument &arg = *argIt;
|
||||
const Param &p = d.params[i];
|
||||
const FType &t = fn.params[i];
|
||||
arg.setName(p.name);
|
||||
if (owning(t) && !fn.borrowParam[i]) addVar(p.namePos, p.name, t, copyValue(&arg, t), true, false, i + 1); // changed inside: own a copy
|
||||
else addVar(p.namePos, p.name, t, &arg, false, false, i + 1);
|
||||
unsigned argNo = i + 1 + (fn.recv ? 1 : 0);
|
||||
if (owning(t) && !fn.borrowParam[i]) addVar(p.namePos, p.name, t, copyValue(&arg, t), true, false, argNo); // changed inside: own a copy
|
||||
else addVar(p.namePos, p.name, t, &arg, false, false, argNo);
|
||||
i++;
|
||||
}
|
||||
blockBody(*d.body);
|
||||
|
|
@ -199,7 +286,7 @@ void Codegen::define(Fn &fn) {
|
|||
if (fn.ret.kind != FType::Void)
|
||||
failAt(d.pos.file, d.pos.line, d.pos.col, "function '" + d.name + "' can reach its end without returning " + fn.ret.show());
|
||||
emitCleanups(0);
|
||||
b.CreateRetVoid();
|
||||
retValue(nullptr);
|
||||
}
|
||||
scopes.clear();
|
||||
diFn = nullptr;
|
||||
|
|
@ -315,7 +402,11 @@ void Codegen::stmt(const Stmt &s) {
|
|||
case StmtKind::Block: block(static_cast<const BlockStmt &>(s)); break;
|
||||
case StmtKind::VarDecl: varDecl(static_cast<const VarDeclStmt &>(s)); break;
|
||||
case StmtKind::Assign: assign(static_cast<const AssignStmt &>(s)); break;
|
||||
case StmtKind::Expr: release(expr(*static_cast<const ExprStmt &>(s).expr)); break;
|
||||
case StmtKind::Expr: {
|
||||
const Expr &x = *static_cast<const ExprStmt &>(s).expr;
|
||||
release(x.kind == ExprKind::OrElse ? orElse(static_cast<const OrElseExpr &>(x), true) : expr(x));
|
||||
break;
|
||||
}
|
||||
case StmtKind::If: ifStmt(static_cast<const IfStmt &>(s)); break;
|
||||
case StmtKind::While: whileStmt(static_cast<const WhileStmt &>(s)); break;
|
||||
case StmtKind::For: forStmt(static_cast<const ForStmt &>(s)); break;
|
||||
|
|
@ -486,7 +577,8 @@ void Codegen::forStmt(const ForStmt &s) {
|
|||
popScope();
|
||||
}
|
||||
|
||||
// for x in list: x is each element (or each char of a str)
|
||||
// for x in list: x is each element (or each char of a str); for i, x in list: also its index;
|
||||
// for key in map / for key, value in map: in the order the keys were added
|
||||
void Codegen::forEachStmt(const ForEachStmt &s) {
|
||||
pushScope();
|
||||
LRef r = ref(*s.list);
|
||||
|
|
@ -500,9 +592,50 @@ void Codegen::forEachStmt(const ForEachStmt &s) {
|
|||
lt = r.val.type;
|
||||
addr = addVar(s.pos, "$list" + std::to_string(varOrder), lt, own(r.val), true).slot;
|
||||
}
|
||||
if (lt.kind == FType::Map) {
|
||||
FType kt = *lt.key, vt = *lt.elem;
|
||||
StructType *ET = mapEntryTy(lt);
|
||||
llvm::Type *MT = ty(lt);
|
||||
// if the loop changes the map, keys and values are copied, so they can't disappear under us
|
||||
bool copyKV = live && mutates(rootVar(*s.list), *s.body);
|
||||
Value *i = slot(FType::I64, "$i");
|
||||
b.CreateStore(b.getInt64(0), i);
|
||||
BasicBlock *condBB = newBlock("map.cond"), *check = newBlock("map.check"), *body = newBlock("map.body"),
|
||||
*step = newBlock("map.step"), *end = newBlock("map.end");
|
||||
b.CreateBr(condBB);
|
||||
b.SetInsertPoint(condBB);
|
||||
Value *iv = b.CreateLoad(b.getInt64Ty(), i);
|
||||
b.CreateCondBr(b.CreateICmpSLT(iv, b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(MT, addr, 2))), check, end);
|
||||
b.SetInsertPoint(check);
|
||||
Value *entry = b.CreateInBoundsGEP(ET, b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(MT, addr, 0)), iv);
|
||||
Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ET, entry, 0));
|
||||
b.CreateCondBr(b.CreateICmpEQ(hash, b.getInt64(0)), step, body); // a removed entry
|
||||
b.SetInsertPoint(body);
|
||||
loops.push_back({step, end, scopes.size()});
|
||||
pushScope();
|
||||
std::string why = " can't be changed (to change the map, use m[key] = ...)";
|
||||
Value *kv = b.CreateLoad(ty(kt), b.CreateStructGEP(ET, entry, 1));
|
||||
Var &k = addVar(s.varPos, s.var, kt, copyKV ? copyValue(kv, kt) : kv, copyKV, true);
|
||||
k.readonlyWhy = "the loop variable '" + s.var + "'" + why;
|
||||
if (!s.var2.empty()) {
|
||||
Value *vv = b.CreateLoad(ty(vt), b.CreateStructGEP(ET, entry, 2));
|
||||
Var &v = addVar(s.var2Pos, s.var2, vt, copyKV ? copyValue(vv, vt) : vv, copyKV, true);
|
||||
v.readonlyWhy = "the loop variable '" + s.var2 + "'" + why;
|
||||
}
|
||||
block(*s.body);
|
||||
popScope();
|
||||
loops.pop_back();
|
||||
if (!terminated()) b.CreateBr(step);
|
||||
b.SetInsertPoint(step);
|
||||
b.CreateStore(b.CreateAdd(b.CreateLoad(b.getInt64Ty(), i), b.getInt64(1)), i);
|
||||
b.CreateBr(condBB);
|
||||
b.SetInsertPoint(end);
|
||||
popScope();
|
||||
return;
|
||||
}
|
||||
if (lt.kind != FType::Array && lt.kind != FType::Str && lt.kind != FType::Fixed)
|
||||
failAt(s.list->pos.file, s.list->pos.line, s.list->pos.col,
|
||||
"for ... in needs an array or a str, but this is " + lt.show() + " (for a range of numbers write 0..n)");
|
||||
"for ... in needs an array, a str or a map, but this is " + lt.show() + " (for a range of numbers write 0..n)");
|
||||
FType et = lt.kind == FType::Str ? FType(FType::Char) : *lt.elem;
|
||||
// If the loop changes the list, each element is copied, so it can't disappear under us.
|
||||
bool copyElems = live && owning(et) && mutates(rootVar(*s.list), *s.body);
|
||||
|
|
@ -531,8 +664,16 @@ void Codegen::forEachStmt(const ForEachStmt &s) {
|
|||
Value *elem = b.CreateLoad(ty(et), elemAddr);
|
||||
loops.push_back({step, end, scopes.size()});
|
||||
pushScope();
|
||||
Var &xv = addVar(s.varPos, s.var, et, copyElems ? copyValue(elem, et) : elem, copyElems, true);
|
||||
xv.readonlyWhy = "the loop variable '" + s.var + "' can't be changed (to change the list, loop with for i in 0..list.len and use list[i])";
|
||||
std::string elemVar = s.var;
|
||||
Pos elemPos = s.varPos;
|
||||
if (!s.var2.empty()) { // for i, x in list
|
||||
Var &index = addVar(s.varPos, s.var, FType::I64, iv, false, true);
|
||||
index.readonlyWhy = "the loop variable '" + s.var + "' can't be changed";
|
||||
elemVar = s.var2;
|
||||
elemPos = s.var2Pos;
|
||||
}
|
||||
Var &xv = addVar(elemPos, elemVar, et, copyElems ? copyValue(elem, et) : elem, copyElems, true);
|
||||
xv.readonlyWhy = "the loop variable '" + elemVar + "' can't be changed (to change the list, use list[i] = ...)";
|
||||
block(*s.body);
|
||||
popScope();
|
||||
loops.pop_back();
|
||||
|
|
@ -546,14 +687,46 @@ void Codegen::forEachStmt(const ForEachStmt &s) {
|
|||
popScope();
|
||||
}
|
||||
|
||||
void Codegen::retValue(Value *v) {
|
||||
if (!curFn->fallible) {
|
||||
if (v) b.CreateRet(v);
|
||||
else b.CreateRetVoid();
|
||||
return;
|
||||
}
|
||||
Value *r = Constant::getNullValue(curFn->llvm->getReturnType());
|
||||
r = b.CreateInsertValue(r, b.getInt1(true), 0);
|
||||
if (v) r = b.CreateInsertValue(r, v, 1);
|
||||
b.CreateRet(r);
|
||||
}
|
||||
|
||||
void Codegen::retError(Value *msg) {
|
||||
Value *r = Constant::getNullValue(curFn->llvm->getReturnType());
|
||||
b.CreateRet(b.CreateInsertValue(r, msg, 2));
|
||||
}
|
||||
|
||||
void Codegen::returnStmt(const ReturnStmt &s) {
|
||||
const FnDecl &d = *curFn->decl;
|
||||
if (inDefer) failAt(s.pos.file, s.pos.line, s.pos.col, "return can't be used inside defer");
|
||||
// return error("...")
|
||||
if (s.value && s.value->kind == ExprKind::Call && static_cast<const CallExpr &>(*s.value).callee == "error") {
|
||||
auto &c = static_cast<const CallExpr &>(*s.value);
|
||||
if (!curFn->fallible)
|
||||
failAt(c.pos.file, c.pos.line, c.pos.col, "'" + d.name + "' can't fail, so it can't return an error (make it fallible: -> " +
|
||||
(curFn->ret.kind == FType::Void ? std::string("!") : curFn->ret.show() + "!") + ")");
|
||||
if (g_index)
|
||||
for (const BuiltinDoc &bd : kBuiltins)
|
||||
if (std::string(bd.name) == "error") note(c.pos, 5, "```finch\n" + std::string(bd.signature) + "\n```\n" + bd.doc);
|
||||
checkArgs(c.args, c.argNames, 1, c.pos, "error");
|
||||
Value *msg = own(coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the error message"));
|
||||
emitCleanups(0);
|
||||
retError(msg);
|
||||
return;
|
||||
}
|
||||
if (curFn->ret.kind == FType::Void) {
|
||||
if (s.value)
|
||||
failAt(s.value->pos.file, s.value->pos.line, s.value->pos.col, "'" + d.name + "' doesn't return a value (it has no '-> type')");
|
||||
emitCleanups(0);
|
||||
b.CreateRetVoid();
|
||||
retValue(nullptr);
|
||||
return;
|
||||
}
|
||||
if (!s.value) failAt(s.pos.file, s.pos.line, s.pos.col, "'" + d.name + "' must return " + curFn->ret.show());
|
||||
|
|
@ -569,7 +742,7 @@ void Codegen::returnStmt(const ReturnStmt &s) {
|
|||
it->second.moved = true;
|
||||
emitCleanups(0);
|
||||
scopes[idx].vars[name].moved = false;
|
||||
b.CreateRet(v);
|
||||
retValue(v);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -578,7 +751,7 @@ void Codegen::returnStmt(const ReturnStmt &s) {
|
|||
Value_ v = coerce(exprWant(*s.value, curFn->ret), curFn->ret, s.value->pos, "the returned value");
|
||||
Value *out = own(v);
|
||||
emitCleanups(0);
|
||||
b.CreateRet(out);
|
||||
retValue(out);
|
||||
}
|
||||
|
||||
// ---------- mutation analysis (does a function or loop change a variable?) ----------
|
||||
|
|
@ -595,6 +768,17 @@ std::string Codegen::rootVar(const Expr &e) {
|
|||
bool Codegen::mutatesExpr(const std::string &name, const Expr &e) {
|
||||
static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"};
|
||||
switch (e.kind) {
|
||||
case ExprKind::Try: return mutatesExpr(name, *static_cast<const TryExpr &>(e).call);
|
||||
case ExprKind::OrElse: {
|
||||
auto &x = static_cast<const OrElseExpr &>(e);
|
||||
return mutatesExpr(name, *x.call) || (x.fallback && mutatesExpr(name, *x.fallback)) || (x.block && mutates(name, *x.block));
|
||||
}
|
||||
case ExprKind::MapLit: {
|
||||
auto &x = static_cast<const MapLitExpr &>(e);
|
||||
for (size_t k = 0; k < x.keys.size(); k++)
|
||||
if (mutatesExpr(name, *x.keys[k]) || mutatesExpr(name, *x.values[k])) return true;
|
||||
return false;
|
||||
}
|
||||
case ExprKind::Unary: return mutatesExpr(name, *static_cast<const UnaryExpr &>(e).operand);
|
||||
case ExprKind::Binary: {
|
||||
auto &x = static_cast<const BinaryExpr &>(e);
|
||||
|
|
@ -618,7 +802,7 @@ bool Codegen::mutatesExpr(const std::string &name, const Expr &e) {
|
|||
return false;
|
||||
case ExprKind::Method: {
|
||||
auto &m = static_cast<const MethodExpr &>(e);
|
||||
if (changing.count(m.name) && rootVar(*m.obj) == name) return true;
|
||||
if ((changing.count(m.name) || mutatingMethods.count(m.name)) && rootVar(*m.obj) == name) return true;
|
||||
if (mutatesExpr(name, *m.obj)) return true;
|
||||
for (auto &a : m.args)
|
||||
if (mutatesExpr(name, *a)) return true;
|
||||
|
|
@ -667,9 +851,96 @@ bool Codegen::mutates(const std::string &name, const Stmt &s) {
|
|||
|
||||
Value_ Codegen::exprWant(const Expr &e, const FType &want) {
|
||||
if (e.kind == ExprKind::ArrayLit) return arrayLit(static_cast<const ArrayLitExpr &>(e), &want);
|
||||
if (e.kind == ExprKind::MapLit) return mapLit(static_cast<const MapLitExpr &>(e), &want);
|
||||
return expr(e);
|
||||
}
|
||||
|
||||
// ---------- errors as values ----------
|
||||
|
||||
// Emit a call that may fail; `failed` (i1) and `msg` (str) tell how it went.
|
||||
Value_ Codegen::fallible(const Expr &call, Value *&failed, Value *&msg) {
|
||||
Pos p = call.pos;
|
||||
if (call.kind != ExprKind::Call && call.kind != ExprKind::Method)
|
||||
failAt(p.file, p.line, p.col, "only a function call can fail; 'or' and 'try' go after one, like int(text) or 0");
|
||||
const Expr *savedTarget = fallibleTarget;
|
||||
Value *savedFailed = fallibleFailed, *savedMsg = fallibleMsg;
|
||||
fallibleTarget = &call;
|
||||
fallibleFailed = fallibleMsg = nullptr;
|
||||
Value_ v = expr(call);
|
||||
failed = fallibleFailed;
|
||||
msg = fallibleMsg;
|
||||
fallibleTarget = savedTarget;
|
||||
fallibleFailed = savedFailed;
|
||||
fallibleMsg = savedMsg;
|
||||
if (!failed) failAt(p.file, p.line, p.col, "this call can't fail, so it needs no 'or' or 'try'");
|
||||
return v;
|
||||
}
|
||||
|
||||
// try f(x): on failure, the current function fails with the same error
|
||||
Value_ Codegen::tryExpr(const TryExpr &t) {
|
||||
Pos p = t.pos;
|
||||
if (inDefer) failAt(p.file, p.line, p.col, "try can't be used inside defer");
|
||||
if (!curFn->fallible)
|
||||
failAt(p.file, p.line, p.col, "try passes the error on, so '" + curFn->decl->name + "' must be able to fail too (-> " +
|
||||
(curFn->ret.kind == FType::Void ? std::string("!") : curFn->ret.show() + "!") +
|
||||
"); or handle it here: ... or <value> / ... or { ... }");
|
||||
Value *failed, *msg;
|
||||
Value_ v = fallible(*t.call, failed, msg);
|
||||
BasicBlock *bad = newBlock("try.failed"), *ok = newBlock("try.ok");
|
||||
b.CreateCondBr(failed, bad, ok);
|
||||
b.SetInsertPoint(bad);
|
||||
emitCleanups(0);
|
||||
retError(msg);
|
||||
b.SetInsertPoint(ok);
|
||||
return v;
|
||||
}
|
||||
|
||||
// f(x) or fallback / f(x) or { ... err ... }
|
||||
Value_ Codegen::orElse(const OrElseExpr &e, bool discard) {
|
||||
Value *failed, *msg;
|
||||
Value_ v = fallible(*e.call, failed, msg);
|
||||
BasicBlock *bad = newBlock("or.failed"), *ok = newBlock("or.ok");
|
||||
b.CreateCondBr(failed, bad, ok);
|
||||
b.SetInsertPoint(bad);
|
||||
if (e.block) {
|
||||
pushScope();
|
||||
addVar(e.block->pos, "err", FType::Str, msg, true);
|
||||
blockBody(*e.block);
|
||||
bool left = terminated();
|
||||
if (!left && v.type.kind != FType::Void && !discard)
|
||||
failAt(e.block->end.file, e.block->end.line, e.block->end.col,
|
||||
"this block must leave with return, break, continue or exit(...): after a failure there is no value to go on with (or give one: ... or <value>)");
|
||||
popScope();
|
||||
if (discard && v.type.kind != FType::Void) { // read_file(p) or { ... } as a statement: drop the unused value
|
||||
BasicBlock *join = newBlock("or.end");
|
||||
if (!left) b.CreateBr(join);
|
||||
b.SetInsertPoint(ok);
|
||||
release(v);
|
||||
b.CreateBr(join);
|
||||
b.SetInsertPoint(join);
|
||||
return {nullptr, FType::Void};
|
||||
}
|
||||
if (!left) b.CreateBr(ok);
|
||||
b.SetInsertPoint(ok);
|
||||
return v;
|
||||
}
|
||||
Pos fp = e.fallback->pos;
|
||||
if (v.type.kind == FType::Void)
|
||||
failAt(fp.file, fp.line, fp.col, "this call gives back no value, so there is nothing to replace; handle the failure with or { ... }");
|
||||
dropValue(msg, FType::Str);
|
||||
Value *fv = own(coerce(exprWant(*e.fallback, v.type), v.type, fp, "the value after 'or'"));
|
||||
BasicBlock *badEnd = b.GetInsertBlock();
|
||||
BasicBlock *join = newBlock("or.end");
|
||||
b.CreateBr(join);
|
||||
b.SetInsertPoint(ok);
|
||||
b.CreateBr(join);
|
||||
b.SetInsertPoint(join);
|
||||
PHINode *phi = b.CreatePHI(ty(v.type), 2);
|
||||
phi->addIncoming(fv, badEnd);
|
||||
phi->addIncoming(v.v, ok);
|
||||
return {phi, v.type, false, owning(v.type)};
|
||||
}
|
||||
|
||||
Value_ Codegen::expr(const Expr &e) {
|
||||
switch (e.kind) {
|
||||
case ExprKind::Int: return {b.getInt64(static_cast<const IntExpr &>(e).value), FType::I64, true};
|
||||
|
|
@ -711,6 +982,9 @@ Value_ Codegen::expr(const Expr &e) {
|
|||
case ExprKind::Call: return call(static_cast<const CallExpr &>(e));
|
||||
case ExprKind::Method: return method(static_cast<const MethodExpr &>(e));
|
||||
case ExprKind::ArrayLit: return arrayLit(static_cast<const ArrayLitExpr &>(e), nullptr);
|
||||
case ExprKind::MapLit: return mapLit(static_cast<const MapLitExpr &>(e), nullptr);
|
||||
case ExprKind::Try: return tryExpr(static_cast<const TryExpr &>(e));
|
||||
case ExprKind::OrElse: return orElse(static_cast<const OrElseExpr &>(e));
|
||||
}
|
||||
return {nullptr, FType::Void};
|
||||
}
|
||||
|
|
@ -785,6 +1059,19 @@ LRef Codegen::member(const MemberExpr &m, bool forWrite) {
|
|||
t = *t.elem;
|
||||
}
|
||||
|
||||
if (t.kind == FType::Map) {
|
||||
if (m.field != "len") failAt(p.file, p.line, p.col, "a map has .len (the number of keys), not '." + m.field + "'");
|
||||
if (forWrite) failAt(p.file, p.line, p.col, ".len can't be changed directly (use m[key] = ..., remove or clear)");
|
||||
note(Pos{p.line, p.col + 1, p.file}, 3, "`.len -> int`: the number of keys");
|
||||
Value *len;
|
||||
if (obj.isPlace) {
|
||||
len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), obj.pl.addr, 1));
|
||||
} else {
|
||||
len = b.CreateExtractValue(obj.val.v, 1);
|
||||
release(obj.val);
|
||||
}
|
||||
return {false, {}, {len, FType::I64}};
|
||||
}
|
||||
if (t.kind == FType::Array || t.kind == FType::Str || t.kind == FType::Fixed) {
|
||||
if (m.field == "len" || m.field == "ptr")
|
||||
note(Pos{p.line, p.col + 1, p.file}, m.field.size(),
|
||||
|
|
@ -839,6 +1126,7 @@ LRef Codegen::index(const IndexExpr &e, bool forWrite) {
|
|||
Pos p = e.pos;
|
||||
LRef obj = ref(*e.obj, forWrite);
|
||||
FType t = obj.isPlace ? obj.pl.type : obj.val.type;
|
||||
if (t.kind == FType::Map) return mapIndex(e, obj, t, forWrite);
|
||||
Value *i = coerce(expr(*e.index), FType::I64, e.index->pos, "an index").v;
|
||||
|
||||
if (t.isPtr()) { // C-style: p[i], no bounds known
|
||||
|
|
@ -854,7 +1142,7 @@ LRef Codegen::index(const IndexExpr &e, bool forWrite) {
|
|||
return {false, {}, {b.CreateLoad(ty(*t.elem), b.CreateInBoundsGEP(ty(t), a, {b.getInt64(0), i})), *t.elem}};
|
||||
}
|
||||
if (t.kind != FType::Array && t.kind != FType::Str)
|
||||
failAt(p.file, p.line, p.col, "[ ] works on arrays, str and pointers, not on " + t.show());
|
||||
failAt(p.file, p.line, p.col, "[ ] works on arrays, maps, str and pointers, not on " + t.show());
|
||||
FType et = t.kind == FType::Str ? FType(FType::Char) : *t.elem;
|
||||
if (t.kind == FType::Str && forWrite) {
|
||||
if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary str, it can't be changed");
|
||||
|
|
@ -1139,10 +1427,12 @@ void Codegen::note(Pos at, size_t len, const std::string &hover, Pos def) {
|
|||
}
|
||||
|
||||
std::string Codegen::signature(const Fn &fn) {
|
||||
std::string s = "fn " + (fn.module.empty() ? "" : fn.module + ".") + fn.decl->name + "(";
|
||||
std::string owner = fn.recv ? fn.recv->name + "." : fn.module.empty() ? "" : fn.module + ".";
|
||||
std::string s = "fn " + owner + fn.decl->name + "(";
|
||||
for (size_t i = 0; i < fn.params.size(); i++) s += (i ? ", " : "") + fn.params[i].show() + " " + fn.decl->params[i].name;
|
||||
s += ")";
|
||||
if (fn.ret.kind != FType::Void) s += " -> " + fn.ret.show();
|
||||
if (fn.ret.kind != FType::Void) s += " -> " + fn.ret.show() + (fn.fallible ? "!" : "");
|
||||
else if (fn.fallible) s += " -> !";
|
||||
return s;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace finch {
|
|||
|
||||
bool Codegen::isBuiltin(const std::string &n) {
|
||||
static const std::set<std::string> names = {"print", "addr", "input", "new", "free", "read_file", "write_file",
|
||||
"file_exists", "delete_file", "exit", "shell"};
|
||||
"file_exists", "delete_file", "exit", "shell", "error"};
|
||||
FType t;
|
||||
return names.count(n) || typeFromName(n, t);
|
||||
}
|
||||
|
|
@ -95,6 +95,28 @@ Function *Codegen::rt(const std::string &name) {
|
|||
{"finch_shell", {I, {P}}},
|
||||
{"finch_delete_file", {I32, {P}}},
|
||||
{"finch_std_stream", {P, {I32}}},
|
||||
{"finch_str_try_int", {I32, {P, P, P}}},
|
||||
{"finch_str_try_float", {I32, {P, P, P}}},
|
||||
{"finch_try_read_file", {I32, {P, P, P}}},
|
||||
{"finch_try_write_file", {I32, {P, P, P}}},
|
||||
{"finch_try_delete_file", {I32, {P, P}}},
|
||||
{"finch_report_error", {V, {P}}},
|
||||
{"finch_map_find", {I, {P, P, I, I, I32}}},
|
||||
{"finch_map_slot", {I, {P, P, I, I, I32, P}}},
|
||||
{"finch_map_remove_at", {V, {P, I, I}}},
|
||||
{"finch_map_clear", {V, {P}}},
|
||||
{"finch_map_free", {V, {P}}},
|
||||
{"finch_map_clone_raw", {V, {P, P, I}}},
|
||||
{"finch_map_missing", {V, {P, I, P}}},
|
||||
{"finch_buf_add", {V, {P, P, I}}},
|
||||
{"finch_buf_int", {V, {P, I}}},
|
||||
{"finch_buf_uint", {V, {P, I}}},
|
||||
{"finch_buf_float", {V, {P, D}}},
|
||||
{"finch_buf_bool", {V, {P, I32}}},
|
||||
{"finch_buf_ptr", {V, {P, P}}},
|
||||
{"finch_buf_char", {V, {P, b.getInt8Ty(), I32}}},
|
||||
{"finch_buf_str", {V, {P, P, I32}}},
|
||||
{"finch_buf_print", {V, {P}}},
|
||||
};
|
||||
auto it = sigs.find(name);
|
||||
if (it == sigs.end()) {
|
||||
|
|
@ -103,11 +125,11 @@ Function *Codegen::rt(const std::string &name) {
|
|||
}
|
||||
Function *f = Function::Create(FunctionType::get(it->second.ret, it->second.params, false), Function::ExternalLinkage,
|
||||
name, mod.get());
|
||||
if (name == "finch_panic" || name == "finch_panic_index") {
|
||||
if (name == "finch_panic" || name == "finch_panic_index" || name == "finch_map_missing") {
|
||||
f->addFnAttr(Attribute::NoReturn);
|
||||
f->addFnAttr(Attribute::Cold);
|
||||
}
|
||||
if (name == "finch_str_from_char") f->addParamAttr(1, Attribute::SExt);
|
||||
if (name == "finch_str_from_char" || name == "finch_buf_char") f->addParamAttr(1, Attribute::SExt);
|
||||
return f;
|
||||
}
|
||||
|
||||
|
|
@ -185,6 +207,14 @@ Value_ Codegen::call(const CallExpr &c) {
|
|||
return *c.args[0];
|
||||
};
|
||||
|
||||
bool canFail = &c == fallibleTarget; // inside `... or ...` / `try ...`: report failures instead of stopping
|
||||
// a failure: set the flag and message that `or` / `try` look at
|
||||
auto failedIf = [&](Value *ok, Value *errSlot) {
|
||||
fallibleFailed = b.CreateICmpEQ(ok, b.getInt32(0));
|
||||
fallibleMsg = b.CreateLoad(ty(FType::Str), errSlot);
|
||||
};
|
||||
if (n == "error")
|
||||
failAt(p.file, p.line, p.col, "error(...) makes an error to return from a function that can fail: return error(\"...\")");
|
||||
if (n == "print") {
|
||||
checkArgs(c.args, c.argNames, 0, p, n, true);
|
||||
return print(c.args, p);
|
||||
|
|
@ -237,6 +267,13 @@ Value_ Codegen::call(const CallExpr &c) {
|
|||
Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name");
|
||||
Value *ss = tmpOf(ty(FType::Str));
|
||||
b.CreateCall(rt("finch_str_copy"), {ss, tmp(path.v)}); // NUL-terminated copy
|
||||
if (canFail) {
|
||||
Value *err = tmp(zero(FType::Str));
|
||||
failedIf(b.CreateCall(rt("finch_try_delete_file"), {ss, err}), err);
|
||||
dropAt(ss, FType::Str);
|
||||
release(path);
|
||||
return {nullptr, FType::Void};
|
||||
}
|
||||
Value *r = b.CreateCall(rt("finch_delete_file"), {ss});
|
||||
dropAt(ss, FType::Str);
|
||||
release(path);
|
||||
|
|
@ -248,6 +285,10 @@ Value_ Codegen::call(const CallExpr &c) {
|
|||
Value_ res;
|
||||
if (n == "file_exists") {
|
||||
res = {b.CreateICmpNE(b.CreateCall(rt("finch_file_exists"), {pp}), b.getInt32(0)), FType::Bool};
|
||||
} else if (canFail) {
|
||||
Value *out = tmp(zero(FType::Str)), *err = tmp(zero(FType::Str));
|
||||
failedIf(b.CreateCall(rt("finch_try_read_file"), {out, pp, err}), err);
|
||||
res = {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
|
||||
} else {
|
||||
Value *out = tmpOf(ty(FType::Str));
|
||||
b.CreateCall(rt("finch_read_file"), {out, pp, fileName(p), b.getInt64(p.line)});
|
||||
|
|
@ -269,6 +310,13 @@ Value_ Codegen::call(const CallExpr &c) {
|
|||
checkArgs(c.args, c.argNames, 2, p, n);
|
||||
Value_ path = coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the file name");
|
||||
Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text");
|
||||
if (canFail) {
|
||||
Value *err = tmp(zero(FType::Str));
|
||||
failedIf(b.CreateCall(rt("finch_try_write_file"), {tmp(path.v), tmp(text.v), err}), err);
|
||||
release(path);
|
||||
release(text);
|
||||
return {nullptr, FType::Void};
|
||||
}
|
||||
Value *ok = b.CreateCall(rt("finch_write_file"), {tmp(path.v), tmp(text.v)});
|
||||
release(path);
|
||||
release(text);
|
||||
|
|
@ -277,13 +325,13 @@ Value_ Codegen::call(const CallExpr &c) {
|
|||
FType target;
|
||||
if (typeFromName(n, target)) {
|
||||
checkArgs(c.args, c.argNames, 1, p, n);
|
||||
return convert(target, c.args, p, n);
|
||||
return convert(target, c.args, p, n, canFail);
|
||||
}
|
||||
|
||||
ModuleScope &m = modules[curModule];
|
||||
if (auto it = m.fns.find(n); it != m.fns.end()) {
|
||||
note(p, n.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos);
|
||||
return callFinch(it->second, c.args, c.argNames, p, n);
|
||||
return callFinch(it->second, c.args, c.argNames, p, n, &c);
|
||||
}
|
||||
if (StructInfo *s = findStruct("", n, false)) {
|
||||
note(p, n.size(), structHover(s), s->decl ? s->decl->namePos : Pos{});
|
||||
|
|
@ -303,20 +351,33 @@ Value_ Codegen::call(const CallExpr &c) {
|
|||
}
|
||||
|
||||
Value_ Codegen::callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
|
||||
const std::string &shown) {
|
||||
const std::string &shown, const Expr *site, Value *self, const std::string &selfRoot) {
|
||||
const FnDecl &d = *fn.decl;
|
||||
if (fn.module.empty() && d.name == "main") failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts");
|
||||
if (fn.module.empty() && d.name == "main" && !fn.recv) failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts");
|
||||
if (fn.fallible && site != fallibleTarget)
|
||||
failAt(p.file, p.line, p.col, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or <value>, " + shown +
|
||||
"(...) or { ... }, or try " + shown + "(...)");
|
||||
checkArgs(args, names, fn.params.size(), p, shown);
|
||||
std::vector<Value *> vals;
|
||||
std::vector<Value_> fresh;
|
||||
if (self) vals.push_back(self);
|
||||
for (size_t i = 0; i < args.size(); i++) {
|
||||
Value_ v = coerce(exprWant(*args[i], fn.params[i]), fn.params[i], args[i]->pos, "argument '" + d.params[i].name + "'");
|
||||
// a method that changes self gets its own copy of anything that lives inside self (p.add(p.items))
|
||||
if (fn.mutatesSelf && !selfRoot.empty() && owning(v.type) && !v.fresh && rootVar(*args[i]) == selfRoot)
|
||||
v = {copyValue(v.v, v.type), v.type, false, true};
|
||||
vals.push_back(v.v); // owning values are lent: the function never drops them
|
||||
if (v.fresh) fresh.push_back(v);
|
||||
}
|
||||
setLoc(p);
|
||||
Value *r = b.CreateCall(fn.llvm, vals);
|
||||
for (auto &v : fresh) release(v);
|
||||
if (fn.fallible) {
|
||||
fallibleFailed = b.CreateNot(b.CreateExtractValue(r, 0));
|
||||
fallibleMsg = b.CreateExtractValue(r, 2);
|
||||
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
|
||||
return {b.CreateExtractValue(r, 1), fn.ret, false, owning(fn.ret)};
|
||||
}
|
||||
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
|
||||
return {r, fn.ret, false, owning(fn.ret)};
|
||||
}
|
||||
|
|
@ -433,7 +494,7 @@ Value_ Codegen::addrOf(const std::vector<ExprPtr> &args, Pos p) {
|
|||
}
|
||||
|
||||
// Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), str(x), ptr(x) ...
|
||||
Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name) {
|
||||
Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name, bool canFail) {
|
||||
Value_ v = expr(*args[0]);
|
||||
const FType &from = v.type;
|
||||
Pos p = args[0]->pos;
|
||||
|
|
@ -449,12 +510,28 @@ Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos,
|
|||
else if (from.kind == FType::Array && (from.elem->kind == FType::U8 || from.elem->kind == FType::Char)) {
|
||||
b.CreateCall(rt("finch_str_from_bytes"), {out, tmp(v.v)});
|
||||
release(v);
|
||||
} else if (from.kind == FType::Array || from.kind == FType::Fixed || from.kind == FType::Map || from.kind == FType::Struct) {
|
||||
b.CreateStore(zero(FType::Str), out); // the same text print(...) shows
|
||||
emitFormat(out, v.v, from, false);
|
||||
release(v);
|
||||
} else {
|
||||
failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into str");
|
||||
}
|
||||
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
|
||||
}
|
||||
if (from == to) return v;
|
||||
if (from.kind == FType::Str && (to.isInt() || to.isFloat()) && canFail) { // int(text) or 0
|
||||
Value *sp = tmp(v.v), *err = tmp(zero(FType::Str));
|
||||
Value *out = tmp(to.isInt() ? (Value *)b.getInt64(0) : ConstantFP::get(b.getDoubleTy(), 0));
|
||||
Value *ok = b.CreateCall(rt(to.isInt() ? "finch_str_try_int" : "finch_str_try_float"), {sp, out, err});
|
||||
fallibleFailed = b.CreateICmpEQ(ok, b.getInt32(0));
|
||||
fallibleMsg = b.CreateLoad(ty(FType::Str), err);
|
||||
Value_ r;
|
||||
if (to.isInt()) r = {intCast(b.CreateLoad(b.getInt64Ty(), out), FType::I64, to), to};
|
||||
else r = {b.CreateFPCast(b.CreateLoad(b.getDoubleTy(), out), ty(to)), to};
|
||||
release(v);
|
||||
return r;
|
||||
}
|
||||
if (from.kind == FType::Str && (to.isInt() || to.isFloat())) {
|
||||
Value *sp = tmp(v.v);
|
||||
Value_ r;
|
||||
|
|
@ -494,7 +571,7 @@ Value_ Codegen::method(const MethodExpr &m) {
|
|||
Pos namePos{p.line, p.col + 1, p.file};
|
||||
if (auto it = mi->second.fns.find(m.name); it != mi->second.fns.end()) {
|
||||
note(namePos, m.name.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos);
|
||||
return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name);
|
||||
return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name, &m);
|
||||
}
|
||||
if (auto it = mi->second.structs.find(m.name); it != mi->second.structs.end()) {
|
||||
note(namePos, m.name.size(), structHover(it->second), it->second->decl->namePos);
|
||||
|
|
@ -507,8 +584,20 @@ Value_ Codegen::method(const MethodExpr &m) {
|
|||
"there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)");
|
||||
}
|
||||
static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"};
|
||||
LRef r = ref(*m.obj, changing.count(m.name) > 0);
|
||||
// writing through a map's m[key] adds the key if it's missing: groups[k].push(x)
|
||||
LRef r = ref(*m.obj, changing.count(m.name) > 0 || mutatingMethods.count(m.name) > 0);
|
||||
FType t = r.isPlace ? r.pl.type : r.val.type;
|
||||
if (t.kind == FType::Struct || (t.isPtr() && t.elem && t.elem->kind == FType::Struct)) return structMethod(m, r, t);
|
||||
if (changing.count(m.name) && (t.kind == FType::Array || t.kind == FType::Map)) {
|
||||
std::string root = rootVar(*m.obj);
|
||||
if (Var *v = root.empty() ? nullptr : lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy);
|
||||
}
|
||||
if (t.kind == FType::Map) {
|
||||
Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : Value_{tmp(r.val.v), t, false, r.val.fresh};
|
||||
Value_ out = mapMethod(m, &recv);
|
||||
if (!r.isPlace && r.val.fresh) dropAt(recv.v, t);
|
||||
return out;
|
||||
}
|
||||
if (g_index && (t.kind == FType::Array || t.kind == FType::Str)) {
|
||||
auto look = [&](const BuiltinDoc *first, const BuiltinDoc *last) {
|
||||
for (; first != last; ++first)
|
||||
|
|
@ -589,11 +678,47 @@ Value_ Codegen::method(const MethodExpr &m) {
|
|||
if (!r.isPlace) release(sv);
|
||||
return res;
|
||||
}
|
||||
if (t.kind == FType::Struct)
|
||||
failAt(p.file, p.line, p.col, "structs don't have methods; write a function and call it: " + m.name + "(value, ...)");
|
||||
failAt(p.file, p.line, p.col, t.show() + " has no methods");
|
||||
}
|
||||
|
||||
// p.move(1, 2): a method of a struct (also through a ptr[Struct]); self is passed by address
|
||||
Value_ Codegen::structMethod(const MethodExpr &m, LRef &r, const FType &t) {
|
||||
Pos p = m.pos;
|
||||
FType st = t.isPtr() ? *t.elem : t;
|
||||
auto ms = methods.find(st.info);
|
||||
const Fn *fn = nullptr;
|
||||
if (ms != methods.end())
|
||||
if (auto it = ms->second.find(m.name); it != ms->second.end()) fn = &it->second;
|
||||
if (!fn) {
|
||||
std::string names;
|
||||
if (ms != methods.end())
|
||||
for (auto &[n, f] : ms->second) names += (names.empty() ? "" : ", ") + n;
|
||||
if (names.empty())
|
||||
failAt(p.file, p.line, p.col, "the struct " + st.show() + " has no methods; add one with fn " + st.name + "." + m.name + "(...) { ... }");
|
||||
failAt(p.file, p.line, p.col, "the struct " + st.show() + " has no method '" + m.name + "' (it has: " + names + ")");
|
||||
}
|
||||
note(Pos{p.line, p.col + 1, p.file}, m.name.size(), "```finch\n" + signature(*fn) + "\n```", fn->decl->namePos);
|
||||
std::string root = rootVar(*m.obj);
|
||||
if (fn->mutatesSelf && !root.empty() && !t.isPtr())
|
||||
if (Var *v = lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy);
|
||||
|
||||
Value *self;
|
||||
bool temp = false;
|
||||
if (t.isPtr()) {
|
||||
self = r.isPlace ? b.CreateLoad(b.getPtrTy(), r.pl.addr) : r.val.v;
|
||||
panicIf(b.CreateIsNull(self), "called ." + m.name + "() on a null pointer", p);
|
||||
root.clear(); // self lives somewhere else
|
||||
} else if (r.isPlace) {
|
||||
self = r.pl.addr;
|
||||
} else {
|
||||
self = tmp(r.val.v); // a temporary struct: Point(1, 2).length()
|
||||
temp = true;
|
||||
}
|
||||
Value_ out = callFinch(*fn, m.args, m.argNames, p, st.name + "." + m.name, &m, self, root);
|
||||
if (temp && r.val.fresh && owning(st)) dropAt(self, st);
|
||||
return out;
|
||||
}
|
||||
|
||||
// An element compare for find/contains/sort: -1, 0, 1 (only numbers, chars, bools and str)
|
||||
static bool comparable(const FType &t) {
|
||||
return t.isNumber() || t.kind == FType::Char || t.kind == FType::Bool || t.kind == FType::Str;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ struct Fn {
|
|||
llvm::Function *cThunk = nullptr;
|
||||
FType ret; // resolved types
|
||||
std::vector<FType> params;
|
||||
StructInfo *recv = nullptr; // a method of this struct: the first LLVM parameter is `self`'s address
|
||||
bool mutatesSelf = false; // the method changes `self`
|
||||
bool fallible = false; // -> T!: returns { i1 ok, T value, str error }
|
||||
};
|
||||
|
||||
struct Loop {
|
||||
|
|
@ -139,6 +142,13 @@ public:
|
|||
int deferLimit = -1; // while emitting a defer: hide variables declared after it
|
||||
int inDefer = 0;
|
||||
std::map<std::string, llvm::Function *> helpers; // drop/copy/print per type
|
||||
std::map<StructInfo *, std::map<std::string, Fn>> methods;
|
||||
std::set<std::string> mutatingMethods; // names of methods that change self (for the mutation analysis)
|
||||
|
||||
// errors as values: the call that `or` / `try` is handling reports here whether it failed
|
||||
const Expr *fallibleTarget = nullptr;
|
||||
llvm::Value *fallibleFailed = nullptr; // i1
|
||||
llvm::Value *fallibleMsg = nullptr; // str
|
||||
|
||||
// debug info (-g)
|
||||
bool debug;
|
||||
|
|
@ -164,6 +174,9 @@ public:
|
|||
void forStmt(const ForStmt &s);
|
||||
void forEachStmt(const ForEachStmt &s);
|
||||
void returnStmt(const ReturnStmt &s);
|
||||
llvm::Type *retType(const Fn &f);
|
||||
void retValue(llvm::Value *v); // return from the current function (wrapped as success if it can fail)
|
||||
void retError(llvm::Value *msg); // fail the current function with the str `msg`
|
||||
void jump(bool isBreak, Pos p);
|
||||
llvm::Value *condition(const Expr &e);
|
||||
void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd);
|
||||
|
|
@ -194,6 +207,10 @@ public:
|
|||
LRef member(const MemberExpr &m, bool forWrite);
|
||||
LRef index(const IndexExpr &e, bool forWrite);
|
||||
Value_ arrayLit(const ArrayLitExpr &a, const FType *want);
|
||||
Value_ fallible(const Expr &call, llvm::Value *&failed, llvm::Value *&msg);
|
||||
Value_ tryExpr(const TryExpr &t);
|
||||
Value_ orElse(const OrElseExpr &e, bool discard = false); // discard: a statement, the value isn't used
|
||||
FType structType(StructInfo *s);
|
||||
std::string rootVar(const Expr &e);
|
||||
bool mutates(const std::string &name, const Stmt &s);
|
||||
bool mutatesExpr(const std::string &name, const Expr &e);
|
||||
|
|
@ -225,7 +242,9 @@ public:
|
|||
std::string typeKey(const FType &t);
|
||||
llvm::Value *tmp(llvm::Value *v); // a stack copy of v, to pass by address
|
||||
void emitPrint(llvm::Value *v, const FType &t, bool quoted);
|
||||
llvm::Function *printFn(const FType &t);
|
||||
void emitFormat(llvm::Value *buf, llvm::Value *v, const FType &t, bool quoted); // append v as text to the str at buf
|
||||
void bufText(llvm::Value *buf, const std::string &text);
|
||||
llvm::Function *formatFn(const FType &t);
|
||||
void printf_(const std::string &fmt, std::vector<llvm::Value *> args);
|
||||
llvm::Value *strConst(const std::string &s);
|
||||
llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C
|
||||
|
|
@ -236,13 +255,26 @@ public:
|
|||
Value_ call(const CallExpr &c);
|
||||
Value_ method(const MethodExpr &m);
|
||||
Value_ callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
|
||||
const std::string &shownName);
|
||||
const std::string &shownName, const Expr *site, llvm::Value *self = nullptr,
|
||||
const std::string &selfRoot = "");
|
||||
Value_ construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p);
|
||||
Value_ callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p);
|
||||
Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name);
|
||||
Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name, bool canFail = false);
|
||||
Value_ print(const std::vector<ExprPtr> &args, Pos p);
|
||||
Value_ addrOf(const std::vector<ExprPtr> &args, Pos p);
|
||||
Value_ arrayMethod(const MethodExpr &m, Value_ *recv);
|
||||
Value_ structMethod(const MethodExpr &m, LRef &r, const FType &t);
|
||||
|
||||
// ---------- codegen_map.cpp: maps ----------
|
||||
llvm::StructType *mapEntryTy(const FType &t);
|
||||
Value_ mapLit(const MapLitExpr &e, const FType *want);
|
||||
LRef mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite);
|
||||
Value_ mapMethod(const MethodExpr &m, Value_ *recv);
|
||||
std::vector<llvm::Value *> mapArgs(llvm::Value *map, llvm::Value *keyAddr, const FType &t);
|
||||
llvm::Value *mapSlot(llvm::Value *map, const FType &t, const Value_ &key, Pos p, bool fill);
|
||||
llvm::Value *mapEntry(llvm::Value *map, const FType &t, llvm::Value *i);
|
||||
void forEachEntry(llvm::Value *map, const FType &t, const std::function<void(llvm::Value *i, llvm::Value *entry)> &body);
|
||||
llvm::Value *keyText(const Value_ &key);
|
||||
bool isBuiltin(const std::string &n);
|
||||
std::string headerHint(const std::string &name);
|
||||
void checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,
|
||||
|
|
|
|||
242
src/codegen_map.cpp
Normal file
242
src/codegen_map.cpp
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// Code generation, part 4: maps.
|
||||
//
|
||||
// map[K]V is { entries, len, used, cap, index, icap } (see runtime/finch_rt.c). The runtime finds and
|
||||
// places entries; the compiler stores, copies and drops the keys and values in them, like it does
|
||||
// for array elements. An entry is { i64 hash, K key, V value }; hash 0 marks a removed entry.
|
||||
|
||||
#include "codegen_impl.h"
|
||||
#include "builtins_doc.h"
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
namespace finch {
|
||||
|
||||
StructType *Codegen::mapEntryTy(const FType &t) {
|
||||
return StructType::get(ctx, {b.getInt64Ty(), ty(*t.key), ty(*t.elem)});
|
||||
}
|
||||
|
||||
// The runtime's (map, key, entry size, key size, key is str) arguments.
|
||||
std::vector<Value *> Codegen::mapArgs(Value *map, Value *keyAddr, const FType &t) {
|
||||
return {map, keyAddr, b.getInt64(dl->getTypeAllocSize(mapEntryTy(t))), elemSize(*t.key),
|
||||
b.getInt32(t.key->kind == FType::Str ? 1 : 0)};
|
||||
}
|
||||
|
||||
Value *Codegen::mapEntry(Value *map, const FType &t, Value *i) {
|
||||
Value *entries = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), map, 0));
|
||||
return b.CreateInBoundsGEP(mapEntryTy(t), entries, i);
|
||||
}
|
||||
|
||||
// The entry for `key`, added if it's missing: the key is copied in, and the value is its type's
|
||||
// default when `fill` (m[k] += 1 starts from 0).
|
||||
Value *Codegen::mapSlot(Value *map, const FType &t, const Value_ &key, Pos p, bool fill) {
|
||||
StructType *ET = mapEntryTy(t);
|
||||
Value *keyAddr = tmp(key.v);
|
||||
Value *added = tmpOf(b.getInt32Ty());
|
||||
std::vector<Value *> args = mapArgs(map, keyAddr, t);
|
||||
args.push_back(added);
|
||||
Value *entry = mapEntry(map, t, b.CreateCall(rt("finch_map_slot"), args));
|
||||
BasicBlock *fresh = newBlock("map.new"), *have = newBlock("map.have");
|
||||
b.CreateCondBr(b.CreateICmpNE(b.CreateLoad(b.getInt32Ty(), added), b.getInt32(0)), fresh, have);
|
||||
b.SetInsertPoint(fresh);
|
||||
copyAt(b.CreateStructGEP(ET, entry, 1), keyAddr, *t.key);
|
||||
if (fill) b.CreateStore(own(defaultValue(*t.elem, p)), b.CreateStructGEP(ET, entry, 2));
|
||||
b.CreateBr(have);
|
||||
b.SetInsertPoint(have);
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Every live entry, in insertion order: body(entry number, entry address).
|
||||
void Codegen::forEachEntry(Value *map, const FType &t, const std::function<void(Value *, Value *)> &body) {
|
||||
Value *used = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 2));
|
||||
forN(used, [&](Value *i) {
|
||||
Value *entry = mapEntry(map, t, i);
|
||||
Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(mapEntryTy(t), entry, 0));
|
||||
BasicBlock *live = newBlock("entry.live"), *next = newBlock("entry.next");
|
||||
b.CreateCondBr(b.CreateICmpNE(hash, b.getInt64(0)), live, next);
|
||||
b.SetInsertPoint(live);
|
||||
body(i, entry);
|
||||
b.CreateBr(next);
|
||||
b.SetInsertPoint(next);
|
||||
});
|
||||
}
|
||||
|
||||
// A key as text for error messages: "bob" or 42
|
||||
Value *Codegen::keyText(const Value_ &key) {
|
||||
Value *out = tmpOf(ty(FType::Str));
|
||||
const FType &t = key.type;
|
||||
if (t.kind == FType::Str) {
|
||||
Value *q = tmp(strConst("\"")), *half = tmpOf(ty(FType::Str));
|
||||
b.CreateCall(rt("finch_str_concat"), {half, q, tmp(key.v)});
|
||||
b.CreateCall(rt("finch_str_concat"), {out, half, q});
|
||||
} else if (t.isSigned()) {
|
||||
b.CreateCall(rt("finch_str_from_int"), {out, b.CreateSExt(key.v, b.getInt64Ty())});
|
||||
} else if (t.isUnsigned()) {
|
||||
b.CreateCall(rt("finch_str_from_uint"), {out, b.CreateZExt(key.v, b.getInt64Ty())});
|
||||
} else if (t.kind == FType::Char) {
|
||||
b.CreateCall(rt("finch_str_from_char"), {out, key.v});
|
||||
} else {
|
||||
b.CreateCall(rt("finch_str_from_bool"), {out, b.CreateZExt(key.v, b.getInt32Ty())});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// m[key]: reading a missing key stops the program; writing (m[k] = v, m[k] += 1, m[k].push(x)) adds it
|
||||
LRef Codegen::mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite) {
|
||||
Pos p = e.pos;
|
||||
FType kt = *t.key, vt = *t.elem;
|
||||
Value_ key = coerce(exprWant(*e.index, kt), kt, e.index->pos, "the key");
|
||||
if (forWrite) {
|
||||
if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary map, it can't be changed");
|
||||
Value *entry = mapSlot(obj.pl.addr, t, key, p, true);
|
||||
release(key);
|
||||
return {true, {b.CreateStructGEP(mapEntryTy(t), entry, 2), vt}, {}};
|
||||
}
|
||||
Value *map = obj.isPlace ? obj.pl.addr : tmp(obj.val.v);
|
||||
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(key.v), t));
|
||||
BasicBlock *missing = newBlock("map.missing"), *found = newBlock("map.found");
|
||||
b.CreateCondBr(b.CreateICmpSLT(i, b.getInt64(0)), missing, found);
|
||||
b.SetInsertPoint(missing);
|
||||
b.CreateCall(rt("finch_map_missing"), {fileName(p), b.getInt64(p.line), keyText(key)});
|
||||
b.CreateUnreachable();
|
||||
b.SetInsertPoint(found);
|
||||
release(key);
|
||||
Value *valueAddr = b.CreateStructGEP(mapEntryTy(t), mapEntry(map, t, i), 2);
|
||||
if (obj.isPlace) return {true, {valueAddr, vt}, {}};
|
||||
Value *v = b.CreateLoad(ty(vt), valueAddr); // from a temporary map: take the value, drop the rest
|
||||
Value_ out{v, vt};
|
||||
if (owning(vt) && obj.val.fresh) out = {copyValue(v, vt), vt, false, true};
|
||||
release(obj.val);
|
||||
return {false, {}, out};
|
||||
}
|
||||
|
||||
// ["a": 1, "b": 2], or [:] where the type is known
|
||||
Value_ Codegen::mapLit(const MapLitExpr &e, const FType *want) {
|
||||
Pos p = e.pos;
|
||||
FType t;
|
||||
Value_ k0, v0; // the first entry, when it decides the types
|
||||
bool first = false;
|
||||
if (want && want->kind == FType::Map) {
|
||||
t = *want;
|
||||
} else if (e.keys.empty()) {
|
||||
failAt(p.file, p.line, p.col, "an empty [:] needs a type, like: map[str]int m = [:]");
|
||||
} else {
|
||||
// number values written in the code are all floats if any of them has a dot
|
||||
bool allNum = true, anyFloat = false;
|
||||
for (auto &x : e.values) {
|
||||
const Expr *y = x.get();
|
||||
if (y->kind == ExprKind::Unary && static_cast<const UnaryExpr *>(y)->op == '-') y = static_cast<const UnaryExpr *>(y)->operand.get();
|
||||
if (y->kind == ExprKind::Float) anyFloat = true;
|
||||
else if (y->kind != ExprKind::Int) allNum = false;
|
||||
}
|
||||
k0 = expr(*e.keys[0]);
|
||||
v0 = expr(*e.values[0]);
|
||||
Pos kp = e.keys[0]->pos, vp = e.values[0]->pos;
|
||||
if (k0.type.kind == FType::Void || k0.type.kind == FType::Null) failAt(kp.file, kp.line, kp.col, "a map key needs a value with a type");
|
||||
if (v0.type.kind == FType::Void || v0.type.kind == FType::Null) failAt(vp.file, vp.line, vp.col, "a map value needs a value with a type");
|
||||
t = resolve(FType::mapOf(k0.type, allNum && anyFloat ? FType(FType::F64) : v0.type), kp);
|
||||
first = true;
|
||||
}
|
||||
Value *map = tmp(zero(t));
|
||||
StructType *ET = mapEntryTy(t);
|
||||
for (size_t i = 0; i < e.keys.size(); i++) {
|
||||
Value_ k = i == 0 && first ? k0 : exprWant(*e.keys[i], *t.key);
|
||||
k = coerce(k, *t.key, e.keys[i]->pos, "every key of this map");
|
||||
Value_ v = i == 0 && first ? v0 : exprWant(*e.values[i], *t.elem);
|
||||
Value *vv = own(coerce(v, *t.elem, e.values[i]->pos, "every value of this map"));
|
||||
Value *valueAddr = b.CreateStructGEP(ET, mapSlot(map, t, k, p, false), 2);
|
||||
dropAt(valueAddr, *t.elem); // a key given twice: the last value wins
|
||||
b.CreateStore(vv, valueAddr);
|
||||
release(k);
|
||||
}
|
||||
return {b.CreateLoad(ty(t), map), t, false, true};
|
||||
}
|
||||
|
||||
Value_ Codegen::mapMethod(const MethodExpr &m, Value_ *recv) {
|
||||
Pos p = m.pos;
|
||||
const std::string &n = m.name;
|
||||
FType t = recv->type, kt = *t.key, vt = *t.elem;
|
||||
StructType *ET = mapEntryTy(t);
|
||||
Value *map = recv->v;
|
||||
auto need = [&](size_t k) { checkArgs(m.args, m.argNames, k, p, n); };
|
||||
auto keyArg = [&]() { return coerce(exprWant(*m.args[0], kt), kt, m.args[0]->pos, "the key"); };
|
||||
if (g_index)
|
||||
for (const BuiltinDoc &d : kMapMethods)
|
||||
if (n == d.name) note(Pos{p.line, p.col + 1, p.file}, n.size(), "```finch\n" + std::string(d.signature) + "\n```\n" + d.doc);
|
||||
|
||||
if (n == "has") {
|
||||
need(1);
|
||||
Value_ k = keyArg();
|
||||
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
|
||||
release(k);
|
||||
return {b.CreateICmpSGE(i, b.getInt64(0)), FType::Bool};
|
||||
}
|
||||
if (n == "get") { // m.get(key, default): the default is only computed when the key is missing
|
||||
need(2);
|
||||
Value_ k = keyArg();
|
||||
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
|
||||
release(k);
|
||||
BasicBlock *found = newBlock("get.found"), *missing = newBlock("get.missing"), *end = newBlock("get.end");
|
||||
b.CreateCondBr(b.CreateICmpSGE(i, b.getInt64(0)), found, missing);
|
||||
b.SetInsertPoint(found);
|
||||
Value *v = b.CreateLoad(ty(vt), b.CreateStructGEP(ET, mapEntry(map, t, i), 2));
|
||||
v = copyValue(v, vt);
|
||||
BasicBlock *foundEnd = b.GetInsertBlock();
|
||||
b.CreateBr(end);
|
||||
b.SetInsertPoint(missing);
|
||||
Value *d = own(coerce(exprWant(*m.args[1], vt), vt, m.args[1]->pos, "the default"));
|
||||
BasicBlock *missingEnd = b.GetInsertBlock();
|
||||
b.CreateBr(end);
|
||||
b.SetInsertPoint(end);
|
||||
PHINode *phi = b.CreatePHI(ty(vt), 2);
|
||||
phi->addIncoming(v, foundEnd);
|
||||
phi->addIncoming(d, missingEnd);
|
||||
return {phi, vt, false, owning(vt)};
|
||||
}
|
||||
if (n == "remove") { // true if the key was there
|
||||
need(1);
|
||||
Value_ k = keyArg();
|
||||
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
|
||||
release(k);
|
||||
BasicBlock *hit = newBlock("remove.hit"), *end = newBlock("remove.end");
|
||||
Value *was = b.CreateICmpSGE(i, b.getInt64(0));
|
||||
b.CreateCondBr(was, hit, end);
|
||||
b.SetInsertPoint(hit);
|
||||
Value *entry = mapEntry(map, t, i);
|
||||
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
|
||||
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
|
||||
b.CreateCall(rt("finch_map_remove_at"), {map, i, b.getInt64(dl->getTypeAllocSize(ET))});
|
||||
b.CreateBr(end);
|
||||
b.SetInsertPoint(end);
|
||||
return {was, FType::Bool};
|
||||
}
|
||||
if (n == "clear") {
|
||||
need(0);
|
||||
if (owning(kt) || owning(vt))
|
||||
forEachEntry(map, t, [&](Value *, Value *entry) {
|
||||
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
|
||||
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
|
||||
});
|
||||
b.CreateCall(rt("finch_map_clear"), {map});
|
||||
return {nullptr, FType::Void};
|
||||
}
|
||||
if (n == "keys" || n == "values") { // a new array, in insertion order
|
||||
need(0);
|
||||
FType et = n == "keys" ? kt : vt;
|
||||
unsigned field = n == "keys" ? 1 : 2;
|
||||
FType at = FType::arrayOf(et);
|
||||
Value *out = tmpOf(ty(at));
|
||||
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 1));
|
||||
b.CreateCall(rt("finch_arr_make"), {out, len, elemSize(et)});
|
||||
Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(at), out, 0));
|
||||
Value *j = tmp(b.getInt64(0));
|
||||
forEachEntry(map, t, [&](Value *, Value *entry) {
|
||||
Value *jv = b.CreateLoad(b.getInt64Ty(), j);
|
||||
copyAt(b.CreateInBoundsGEP(ty(et), data, jv), b.CreateStructGEP(ET, entry, field), et);
|
||||
b.CreateStore(b.CreateAdd(jv, b.getInt64(1)), j);
|
||||
});
|
||||
return {b.CreateLoad(ty(at), out), at, false, true};
|
||||
}
|
||||
failAt(p.file, p.line, p.col, "a map has no method '" + n + "' (it has: has, get, remove, clear, keys, values; and m[key], .len)");
|
||||
}
|
||||
|
||||
} // namespace finch
|
||||
|
|
@ -25,6 +25,10 @@ llvm::Type *Codegen::ty(const FType &t) {
|
|||
case FType::F64: return b.getDoubleTy();
|
||||
case FType::Str:
|
||||
case FType::Array: return StructType::get(ctx, {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty()});
|
||||
case FType::Map: {
|
||||
llvm::Type *P = b.getPtrTy(), *I = b.getInt64Ty();
|
||||
return StructType::get(ctx, {P, I, I, I, P, I}); // entries, len, used, cap, index, icap
|
||||
}
|
||||
case FType::Ptr:
|
||||
case FType::Null: return b.getPtrTy();
|
||||
case FType::Fixed: return ArrayType::get(ty(*t.elem), t.count);
|
||||
|
|
@ -60,6 +64,14 @@ FType Codegen::resolveT(const FType &t, Pos p, bool deep) {
|
|||
if (!t.elem) return t;
|
||||
return FType::ptrTo(resolveT(*t.elem, p, false));
|
||||
case FType::Array: return FType::arrayOf(resolveT(*t.elem, p, false));
|
||||
case FType::Map: {
|
||||
FType k = resolveT(*t.key, p, true);
|
||||
if (!k.isInt() && k.kind != FType::Char && k.kind != FType::Bool && k.kind != FType::Str)
|
||||
failAt(p.file, p.line, p.col, "a map key must be a whole number, a char, a bool or a str, not " + k.show());
|
||||
FType v = resolveT(*t.elem, p, false);
|
||||
if (v.kind == FType::Void) failAt(p.file, p.line, p.col, "a map value can't have the type 'nothing'");
|
||||
return FType::mapOf(k, v);
|
||||
}
|
||||
case FType::Fixed: {
|
||||
FType f = t;
|
||||
f.elem = std::make_shared<FType>(resolveT(*t.elem, p, deep));
|
||||
|
|
@ -136,7 +148,8 @@ void Codegen::resolveStruct(StructInfo *s) {
|
|||
bool Codegen::owning(const FType &t) {
|
||||
switch (t.kind) {
|
||||
case FType::Str:
|
||||
case FType::Array: return true;
|
||||
case FType::Array:
|
||||
case FType::Map: return true;
|
||||
case FType::Struct:
|
||||
resolveStruct(t.info);
|
||||
return t.info->owning;
|
||||
|
|
@ -279,6 +292,7 @@ void Codegen::forN(Value *n, const std::function<void(Value *)> &body) {
|
|||
std::string Codegen::typeKey(const FType &t) {
|
||||
switch (t.kind) {
|
||||
case FType::Array: return "arr_" + typeKey(*t.elem);
|
||||
case FType::Map: return "map_" + typeKey(*t.key) + "_" + typeKey(*t.elem);
|
||||
case FType::Fixed: return "fix" + std::to_string(t.count) + "_" + typeKey(*t.elem);
|
||||
case FType::Ptr: return "ptr";
|
||||
case FType::Struct: return "S_" + (t.info->module.empty() ? std::string("main") : t.info->module) + "_" + t.info->name;
|
||||
|
|
@ -315,6 +329,14 @@ Function *Codegen::dropFn(const FType &t) {
|
|||
forN(len, [&](Value *i) { dropAt(b.CreateInBoundsGEP(ty(et), data, i), et); });
|
||||
}
|
||||
b.CreateCall(rt("finch_arr_free"), {a});
|
||||
} else if (t.kind == FType::Map) {
|
||||
FType kt = *t.key, vt = *t.elem;
|
||||
if (owning(kt) || owning(vt))
|
||||
forEachEntry(a, t, [&](Value *, Value *entry) {
|
||||
dropAt(b.CreateStructGEP(mapEntryTy(t), entry, 1), kt);
|
||||
dropAt(b.CreateStructGEP(mapEntryTy(t), entry, 2), vt);
|
||||
});
|
||||
b.CreateCall(rt("finch_map_free"), {a});
|
||||
} else if (t.kind == FType::Struct) {
|
||||
for (auto &fl : t.info->fields)
|
||||
if (owning(fl.type)) dropAt(b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex), fl.type);
|
||||
|
|
@ -342,6 +364,16 @@ Function *Codegen::copyFn(const FType &t) {
|
|||
copyAt(b.CreateInBoundsGEP(ty(et), d, i), b.CreateInBoundsGEP(ty(et), s, i), et);
|
||||
});
|
||||
}
|
||||
} else if (t.kind == FType::Map) {
|
||||
StructType *ET = mapEntryTy(t);
|
||||
FType kt = *t.key, vt = *t.elem;
|
||||
b.CreateCall(rt("finch_map_clone_raw"), {dst, src, b.getInt64(dl->getTypeAllocSize(ET))});
|
||||
if (owning(kt) || owning(vt)) // the slots were copied byte for byte: now give the copy its own keys and values
|
||||
forEachEntry(src, t, [&](Value *i, Value *from) {
|
||||
Value *to = mapEntry(dst, t, i);
|
||||
copyAt(b.CreateStructGEP(ET, to, 1), b.CreateStructGEP(ET, from, 1), kt);
|
||||
copyAt(b.CreateStructGEP(ET, to, 2), b.CreateStructGEP(ET, from, 2), vt);
|
||||
});
|
||||
} else if (t.kind == FType::Struct) {
|
||||
b.CreateStore(b.CreateLoad(ty(t), src), dst);
|
||||
for (auto &fl : t.info->fields)
|
||||
|
|
@ -388,30 +420,37 @@ void Codegen::emitPrint(Value *v, const FType &t, bool quoted) {
|
|||
else if (t.kind == FType::Bool) printf_("%s", {b.CreateSelect(v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))});
|
||||
else if (t.kind == FType::Str)
|
||||
printf_(quoted ? "\"%.*s\"" : "%.*s", {b.CreateTrunc(b.CreateExtractValue(v, 1), b.getInt32Ty()), cstr(v)});
|
||||
else if (t.isPtr()) { // the same on every system: null or 0x...
|
||||
Value *isNull = b.CreateIsNull(v);
|
||||
BasicBlock *nul = newBlock("ptr.null"), *addr = newBlock("ptr.addr"), *done = newBlock("ptr.done");
|
||||
b.CreateCondBr(isNull, nul, addr);
|
||||
b.SetInsertPoint(nul);
|
||||
printf_("null", {});
|
||||
b.CreateBr(done);
|
||||
b.SetInsertPoint(addr);
|
||||
printf_("0x%llx", {b.CreatePtrToInt(v, b.getInt64Ty())});
|
||||
b.CreateBr(done);
|
||||
b.SetInsertPoint(done);
|
||||
else { // pointers, arrays, maps, structs: built as text first (the same text str(x) gives)
|
||||
Value *buf = tmp(zero(FType::Str));
|
||||
emitFormat(buf, v, t, quoted);
|
||||
b.CreateCall(rt("finch_buf_print"), {buf});
|
||||
}
|
||||
else if (t.kind == FType::Null) printf_("null", {});
|
||||
else b.CreateCall(printFn(t), {tmp(v)});
|
||||
}
|
||||
|
||||
Function *Codegen::printFn(const FType &t) {
|
||||
std::string key = "print." + typeKey(t);
|
||||
void Codegen::bufText(Value *buf, const std::string &text) {
|
||||
b.CreateCall(rt("finch_buf_add"), {buf, b.CreateGlobalString(text, "txt"), b.getInt64(text.size())});
|
||||
}
|
||||
|
||||
void Codegen::emitFormat(Value *buf, Value *v, const FType &t, bool quoted) {
|
||||
if (t.isSigned()) b.CreateCall(rt("finch_buf_int"), {buf, b.CreateSExt(v, b.getInt64Ty())});
|
||||
else if (t.isUnsigned()) b.CreateCall(rt("finch_buf_uint"), {buf, b.CreateZExt(v, b.getInt64Ty())});
|
||||
else if (t.isFloat()) b.CreateCall(rt("finch_buf_float"), {buf, b.CreateFPExt(v, b.getDoubleTy())});
|
||||
else if (t.kind == FType::Char) b.CreateCall(rt("finch_buf_char"), {buf, v, b.getInt32(quoted)});
|
||||
else if (t.kind == FType::Bool) b.CreateCall(rt("finch_buf_bool"), {buf, b.CreateZExt(v, b.getInt32Ty())});
|
||||
else if (t.kind == FType::Str) b.CreateCall(rt("finch_buf_str"), {buf, tmp(v), b.getInt32(quoted)});
|
||||
else if (t.isPtr() || t.kind == FType::Null) b.CreateCall(rt("finch_buf_ptr"), {buf, v}); // the same everywhere: null or 0x...
|
||||
else b.CreateCall(formatFn(t), {buf, tmp(v)});
|
||||
}
|
||||
|
||||
// One function per array / map / struct type: (str *buf, T *value)
|
||||
Function *Codegen::formatFn(const FType &t) {
|
||||
std::string key = "format." + typeKey(t);
|
||||
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
|
||||
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage,
|
||||
"finch." + key, mod.get());
|
||||
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getPtrTy()}, false),
|
||||
Function::InternalLinkage, "finch." + key, mod.get());
|
||||
helpers[key] = f;
|
||||
HelperScope hs(*this, f);
|
||||
Value *a = f->getArg(0);
|
||||
Value *buf = f->getArg(0), *a = f->getArg(1);
|
||||
if (t.kind == FType::Array || t.kind == FType::Fixed) {
|
||||
FType et = *t.elem;
|
||||
Value *data, *len;
|
||||
|
|
@ -422,26 +461,44 @@ Function *Codegen::printFn(const FType &t) {
|
|||
data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0));
|
||||
len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1));
|
||||
}
|
||||
printf_("[", {});
|
||||
bufText(buf, "[");
|
||||
forN(len, [&](Value *i) {
|
||||
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
|
||||
b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item);
|
||||
b.SetInsertPoint(sep);
|
||||
printf_(", ", {});
|
||||
bufText(buf, ", ");
|
||||
b.CreateBr(item);
|
||||
b.SetInsertPoint(item);
|
||||
emitPrint(b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true);
|
||||
emitFormat(buf, b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true);
|
||||
});
|
||||
printf_("]", {});
|
||||
bufText(buf, "]");
|
||||
} else if (t.kind == FType::Map) { // {"a": 1, "b": 2}
|
||||
StructType *ET = mapEntryTy(t);
|
||||
Value *firstItem = tmp(b.getInt1(true));
|
||||
bufText(buf, "{");
|
||||
forEachEntry(a, t, [&](Value *, Value *entry) {
|
||||
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
|
||||
b.CreateCondBr(b.CreateLoad(b.getInt1Ty(), firstItem), item, sep);
|
||||
b.SetInsertPoint(sep);
|
||||
bufText(buf, ", ");
|
||||
b.CreateBr(item);
|
||||
b.SetInsertPoint(item);
|
||||
b.CreateStore(b.getInt1(false), firstItem);
|
||||
emitFormat(buf, b.CreateLoad(ty(*t.key), b.CreateStructGEP(ET, entry, 1)), *t.key, true);
|
||||
bufText(buf, ": ");
|
||||
emitFormat(buf, b.CreateLoad(ty(*t.elem), b.CreateStructGEP(ET, entry, 2)), *t.elem, true);
|
||||
});
|
||||
bufText(buf, "}");
|
||||
} else if (t.kind == FType::Struct) {
|
||||
printf_(t.info->name + "(", {});
|
||||
bufText(buf, t.info->name + "(");
|
||||
bool first = true;
|
||||
for (auto &fl : t.info->fields) {
|
||||
printf_((first ? "" : ", ") + fl.name + ": ", {});
|
||||
if (fl.hidden) continue;
|
||||
bufText(buf, (first ? "" : ", ") + fl.name + ": ");
|
||||
first = false;
|
||||
emitPrint(b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true);
|
||||
emitFormat(buf, b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true);
|
||||
}
|
||||
printf_(")", {});
|
||||
bufText(buf, ")");
|
||||
}
|
||||
b.CreateRetVoid();
|
||||
return f;
|
||||
|
|
@ -486,6 +543,16 @@ DIType *Codegen::diType(const FType &t) {
|
|||
d = di->createStructType(diUnit, t.show(), f, 0, 192, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els));
|
||||
break;
|
||||
}
|
||||
case FType::Map: { // shown by its parts; the entries are behind `entries`
|
||||
DIType *i64 = diType(FType::I64), *pt = di->createPointerType(nullptr, 64);
|
||||
DIFile *f = diFiles[0];
|
||||
const char *names[] = {"entries", "len", "used", "cap", "index", "icap"};
|
||||
std::vector<Metadata *> els;
|
||||
for (unsigned k = 0; k < 6; k++)
|
||||
els.push_back(di->createMemberType(diUnit, names[k], f, 0, 64, 64, 64 * k, DINode::FlagZero, k == 0 || k == 4 ? pt : i64));
|
||||
d = di->createStructType(diUnit, t.show(), f, 0, 384, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els));
|
||||
break;
|
||||
}
|
||||
case FType::Struct: {
|
||||
StructInfo *s = t.info;
|
||||
DIFile *f = s->isC ? diFiles[0] : diFiles[s->pos.file];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ static const std::unordered_map<std::string, Tok> keywords = {
|
|||
{"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue},
|
||||
{"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null},
|
||||
{"import", Tok::Import}, {"link", Tok::Link}, {"struct", Tok::Struct},
|
||||
{"defer", Tok::Defer},
|
||||
{"defer", Tok::Defer}, {"or", Tok::OrElse}, {"try", Tok::Try},
|
||||
};
|
||||
|
||||
std::vector<Token> lex(int file) {
|
||||
|
|
@ -169,6 +169,8 @@ const char *tokName(Tok t) {
|
|||
case Tok::Link: return "'link'";
|
||||
case Tok::Struct: return "'struct'";
|
||||
case Tok::Defer: return "'defer'";
|
||||
case Tok::OrElse: return "'or'";
|
||||
case Tok::Try: return "'try'";
|
||||
case Tok::Colon: return "':'";
|
||||
case Tok::LParen: return "'('";
|
||||
case Tok::RParen: return "')'";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ enum class Tok {
|
|||
|
||||
// keywords
|
||||
Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer,
|
||||
OrElse, Try, // or, try
|
||||
|
||||
// punctuation
|
||||
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow,
|
||||
|
|
|
|||
25
src/lsp.cpp
25
src/lsp.cpp
|
|
@ -412,17 +412,34 @@ json::Value Server::completion(const std::string &path, int line, int col) {
|
|||
if (v.name == name && v.line <= line && line <= v.fnEnd + 1) type = v.type;
|
||||
if (element && type.rfind("[]", 0) == 0) type = type.substr(2);
|
||||
if (element && type == "str") type = "char";
|
||||
if (element && type.rfind("map[", 0) == 0) { // m[key]. -> the value type
|
||||
int depth = 0;
|
||||
for (size_t k = 3; k < type.size(); k++) {
|
||||
if (type[k] == '[') depth++;
|
||||
else if (type[k] == ']' && --depth == 0) {
|
||||
type = type.substr(k + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type.rfind("ptr[", 0) == 0) type = type.substr(4, type.size() - 5); // fields through a pointer
|
||||
if (type == "str") {
|
||||
for (const BuiltinDoc &d : kStrMethods) items.push_back(item(d.name, d.name[0] == 'l' || d.name[0] == 'p' ? KField : KMethod, d.signature, d.doc));
|
||||
} else if (type.rfind("[]", 0) == 0) {
|
||||
for (const BuiltinDoc &d : kArrayMethods) items.push_back(item(d.name, std::string(d.name) == "len" || std::string(d.name) == "ptr" ? KField : KMethod, d.signature, d.doc));
|
||||
} else if (type.rfind("map[", 0) == 0) {
|
||||
for (const BuiltinDoc &d : kMapMethods) items.push_back(item(d.name, std::string(d.name) == "len" ? KField : KMethod, d.signature, d.doc));
|
||||
} else if (!type.empty()) {
|
||||
std::string mod, sname = type;
|
||||
if (size_t dot = type.find('.'); dot != std::string::npos) mod = type.substr(0, dot), sname = type.substr(dot + 1);
|
||||
for (const StructIndex &s2 : a->index.structs)
|
||||
if (s2.name == sname && (mod.empty() || s2.module == mod))
|
||||
for (auto &[fname, ftype] : s2.fields) items.push_back(item(fname, KField, ftype + " " + fname));
|
||||
std::set<std::string> seen;
|
||||
for (const FnInfo &f : a->index.fns) // methods: indexed as Struct.name
|
||||
if (f.name.size() > sname.size() + 1 && f.name.compare(0, sname.size() + 1, sname + ".") == 0 &&
|
||||
seen.insert(f.name).second)
|
||||
items.push_back(item(f.name.substr(sname.size() + 1), KMethod, f.signature));
|
||||
if (type.rfind("ptr", 0) == 0 || items.empty()) items.push_back(item("value", KField, "the value the pointer points to"));
|
||||
}
|
||||
return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
|
||||
|
|
@ -511,9 +528,12 @@ json::Value Server::signatureHelp(const std::string &path, int line, int col) {
|
|||
std::string label, doc;
|
||||
std::vector<std::string> params;
|
||||
if (a) {
|
||||
for (const FnInfo &f : a->index.fns)
|
||||
if (f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod)))
|
||||
for (const FnInfo &f : a->index.fns) {
|
||||
bool method = f.name.size() > name.size() && f.name.compare(f.name.size() - name.size() - 1, name.size() + 1, "." + name) == 0;
|
||||
if ((f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod))) ||
|
||||
(method && !mod.empty() && !a->modules.count(mod))) // p.move( -> Point.move
|
||||
label = f.signature, params = f.params;
|
||||
}
|
||||
if (label.empty())
|
||||
for (const StructIndex &s : a->index.structs)
|
||||
if (s.name == name) {
|
||||
|
|
@ -544,6 +564,7 @@ json::Value Server::signatureHelp(const std::string &path, int line, int col) {
|
|||
if (!mod.empty()) {
|
||||
look(std::begin(kArrayMethods), std::end(kArrayMethods));
|
||||
look(std::begin(kStrMethods), std::end(kStrMethods));
|
||||
look(std::begin(kMapMethods), std::end(kMapMethods));
|
||||
}
|
||||
}
|
||||
if (label.empty()) return nullptr;
|
||||
|
|
|
|||
|
|
@ -39,10 +39,11 @@ static void usage() {
|
|||
" finch build <file.fch> [-o name] compile to a program\n"
|
||||
" finch ir <file.fch> show the generated LLVM IR\n"
|
||||
" finch version show the version\n"
|
||||
" finch lsp the language server, for editors (VS Code)\n"
|
||||
" finch lsp the language server, for editors (VS Code, Kate, ...)\n"
|
||||
"\n"
|
||||
" -l <lib> link a C library, same as link \"lib\" in the file\n"
|
||||
" --target <name> build for another system: windows, linux, or an LLVM triple\n"
|
||||
" --target <name> build for another system: windows, linux, arm64 (Linux), macos,\n"
|
||||
" or an LLVM triple\n"
|
||||
" -g add debug info (gdb / lldb / Visual Studio)\n"
|
||||
" -O0 skip optimizations (to read the raw IR)\n");
|
||||
std::exit(1);
|
||||
|
|
|
|||
|
|
@ -79,13 +79,20 @@ private:
|
|||
// A token can continue an expression only if it is on the same line (or we're inside brackets).
|
||||
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
|
||||
|
||||
// int, u8, ptr, ptr[int], []str, Point, math.Vec ...
|
||||
// int, u8, ptr, ptr[int], []str, map[str]int, Point, math.Vec ...
|
||||
Type type() {
|
||||
if (accept(Tok::LBracket)) {
|
||||
expect(Tok::RBracket, "']' (an array type looks like []int)");
|
||||
return Type::arrayOf(type());
|
||||
}
|
||||
if (!at(Tok::Ident)) unexpected("(expected a type like int, str, []int, ptr[int] or a struct name)");
|
||||
if (cur().text == "map" && peekTok().kind == Tok::LBracket) {
|
||||
next();
|
||||
next();
|
||||
Type k = type();
|
||||
expect(Tok::RBracket, "']' (a map type looks like map[str]int)");
|
||||
return Type::mapOf(k, type());
|
||||
}
|
||||
std::string name = next().text;
|
||||
Type t;
|
||||
if (typeFromName(name, t)) {
|
||||
|
|
@ -111,7 +118,7 @@ private:
|
|||
if (!at(Tok::Ident)) return false;
|
||||
Tok n = peekTok().kind;
|
||||
if (n == Tok::Ident) return !peekTok().newlineBefore;
|
||||
if (cur().text == "ptr" && n == Tok::LBracket) return true;
|
||||
if ((cur().text == "ptr" || cur().text == "map") && n == Tok::LBracket) return true;
|
||||
return n == Tok::Dot && peekTok(2).kind == Tok::Ident && peekTok(3).kind == Tok::Ident &&
|
||||
!peekTok(3).newlineBefore;
|
||||
}
|
||||
|
|
@ -179,6 +186,12 @@ private:
|
|||
|
||||
fn.namePos = pos();
|
||||
fn.name = expect(Tok::Ident, "a function name").text;
|
||||
if (accept(Tok::Dot)) { // fn Point.move(...): a method
|
||||
fn.recv = fn.name;
|
||||
fn.recvPos = fn.namePos;
|
||||
fn.namePos = pos();
|
||||
fn.name = expect(Tok::Ident, "a method name (fn Point.name(...))").text;
|
||||
}
|
||||
expect(Tok::LParen);
|
||||
parenDepth++;
|
||||
if (!at(Tok::RParen)) {
|
||||
|
|
@ -193,7 +206,12 @@ private:
|
|||
}
|
||||
parenDepth--;
|
||||
expect(Tok::RParen);
|
||||
fn.ret = accept(Tok::Arrow) ? type() : Type(Type::Void);
|
||||
fn.ret = Type(Type::Void);
|
||||
if (accept(Tok::Arrow)) {
|
||||
if (!at(Tok::Not)) fn.ret = type();
|
||||
fn.fallible = accept(Tok::Not); // -> int! or -> !
|
||||
if (!fn.fallible && fn.ret.kind == Type::Void) unexpected("(after '->' comes a type, or ! for a function that can fail)");
|
||||
}
|
||||
fn.body = block();
|
||||
return fn;
|
||||
}
|
||||
|
|
@ -236,8 +254,15 @@ private:
|
|||
next();
|
||||
Pos varPos = pos();
|
||||
std::string var = expect(Tok::Ident, "a loop variable name").text;
|
||||
std::string var2;
|
||||
Pos var2Pos;
|
||||
if (accept(Tok::Comma)) { // for i, x in list / for key, value in map
|
||||
var2Pos = pos();
|
||||
var2 = expect(Tok::Ident, "a second loop variable name").text;
|
||||
}
|
||||
expect(Tok::In);
|
||||
ExprPtr first = expr();
|
||||
if (!var2.empty() && at(Tok::DotDot)) fail(var2Pos.line, var2Pos.col, "a range (for i in 0..n) has one loop variable");
|
||||
if (accept(Tok::DotDot)) { // for i in 0..10
|
||||
auto s = std::make_unique<ForStmt>(p);
|
||||
s->var = var;
|
||||
|
|
@ -250,6 +275,8 @@ private:
|
|||
auto s = std::make_unique<ForEachStmt>(p); // for x in list
|
||||
s->var = var;
|
||||
s->varPos = varPos;
|
||||
s->var2 = var2;
|
||||
s->var2Pos = var2Pos;
|
||||
s->list = std::move(first);
|
||||
s->body = block();
|
||||
return s;
|
||||
|
|
@ -323,7 +350,7 @@ private:
|
|||
return s;
|
||||
}
|
||||
|
||||
if (e->kind != ExprKind::Call && e->kind != ExprKind::Method)
|
||||
if (e->kind != ExprKind::Call && e->kind != ExprKind::Method && e->kind != ExprKind::Try && e->kind != ExprKind::OrElse)
|
||||
fail(p.line, p.col, "this value is computed but never used");
|
||||
return std::make_unique<ExprStmt>(p, std::move(e));
|
||||
}
|
||||
|
|
@ -342,7 +369,17 @@ private:
|
|||
|
||||
// ---------- expressions (lowest to highest precedence) ----------
|
||||
|
||||
ExprPtr expr() { return orExpr(); }
|
||||
// f(x) or fallback / f(x) or { ... }: the lowest precedence, right to left
|
||||
ExprPtr expr() {
|
||||
ExprPtr l = orExpr();
|
||||
if (!sameLine() || !at(Tok::OrElse)) return l;
|
||||
Pos p = pos();
|
||||
next();
|
||||
auto e = std::make_unique<OrElseExpr>(p, std::move(l));
|
||||
if (at(Tok::LBrace)) e->block = block();
|
||||
else e->fallback = expr();
|
||||
return e;
|
||||
}
|
||||
|
||||
ExprPtr orExpr() {
|
||||
ExprPtr l = andExpr();
|
||||
|
|
@ -429,6 +466,7 @@ private:
|
|||
if (accept(Tok::Minus)) return std::make_unique<UnaryExpr>(p, '-', unary());
|
||||
if (accept(Tok::Not)) return std::make_unique<UnaryExpr>(p, '!', unary());
|
||||
if (accept(Tok::Tilde)) return std::make_unique<UnaryExpr>(p, '~', unary());
|
||||
if (accept(Tok::Try)) return std::make_unique<TryExpr>(p, postfix());
|
||||
return postfix();
|
||||
}
|
||||
|
||||
|
|
@ -506,14 +544,37 @@ private:
|
|||
expect(Tok::RParen);
|
||||
return e;
|
||||
}
|
||||
case Tok::LBracket: { // [1, 2, 3]
|
||||
case Tok::LBracket: { // [1, 2, 3] or a map: ["a": 1, "b": 2], [:]
|
||||
next();
|
||||
auto a = std::make_unique<ArrayLitExpr>(p);
|
||||
parenDepth++;
|
||||
while (!at(Tok::RBracket)) {
|
||||
a->elems.push_back(expr());
|
||||
if (!accept(Tok::Comma)) break; // a trailing comma is fine
|
||||
if (accept(Tok::Colon)) {
|
||||
parenDepth--;
|
||||
expect(Tok::RBracket, "']' (an empty map is [:])");
|
||||
return std::make_unique<MapLitExpr>(p);
|
||||
}
|
||||
if (!at(Tok::RBracket)) {
|
||||
ExprPtr first = expr();
|
||||
if (accept(Tok::Colon)) {
|
||||
auto m = std::make_unique<MapLitExpr>(p);
|
||||
m->keys.push_back(std::move(first));
|
||||
m->values.push_back(expr());
|
||||
while (accept(Tok::Comma) && !at(Tok::RBracket)) { // a trailing comma is fine
|
||||
m->keys.push_back(expr());
|
||||
expect(Tok::Colon, "':' (every map entry is key: value)");
|
||||
m->values.push_back(expr());
|
||||
}
|
||||
parenDepth--;
|
||||
expect(Tok::RBracket);
|
||||
return m;
|
||||
}
|
||||
auto a = std::make_unique<ArrayLitExpr>(p);
|
||||
a->elems.push_back(std::move(first));
|
||||
while (accept(Tok::Comma) && !at(Tok::RBracket)) a->elems.push_back(expr());
|
||||
parenDepth--;
|
||||
expect(Tok::RBracket);
|
||||
return a;
|
||||
}
|
||||
auto a = std::make_unique<ArrayLitExpr>(p);
|
||||
parenDepth--;
|
||||
expect(Tok::RBracket);
|
||||
return a;
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ fi
|
|||
pass=0; failed=0; skipped=0
|
||||
for f in tests/run/*.fch; do
|
||||
grep -q "^fn main" "$f" || continue
|
||||
# the bootstrap compiler doesn't do C imports, sized numbers or defer
|
||||
# the bootstrap compiler doesn't do C imports, sized numbers, defer, maps, methods or errors as values
|
||||
# (checked in the test and in the modules it imports)
|
||||
files="$f"
|
||||
for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done
|
||||
if grep -qE '^import "|^link |defer |0x' $files || grep -qwE 'u8|u16|u32|u64|i8|i16|i32|f32|f64' $files; then
|
||||
if grep -qE '^import "|^link |defer |0x| or |try |map\[|!\s*\{|^fn [A-Z][A-Za-z0-9_]*\.|for [a-z_]+, ' $files ||
|
||||
grep -qwE 'u8|u16|u32|u64|i8|i16|i32|f32|f64' $files; then
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
|
|
|||
9
tests/fail/error_not_fallible.fch
Normal file
9
tests/fail/error_not_fallible.fch
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// expect: 'check' can't fail, so it can't return an error (make it fallible: -> !)
|
||||
fn check(int n) {
|
||||
if n < 0 {
|
||||
return error("negative")
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
check(1)
|
||||
}
|
||||
4
tests/fail/error_outside_return.fch
Normal file
4
tests/fail/error_outside_return.fch
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// expect: error(...) makes an error to return from a function that can fail
|
||||
fn main() {
|
||||
e := error("oops")
|
||||
}
|
||||
4
tests/fail/map_empty_literal.fch
Normal file
4
tests/fail/map_empty_literal.fch
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// expect: an empty [:] needs a type, like: map[str]int m = [:]
|
||||
fn main() {
|
||||
m := [:]
|
||||
}
|
||||
5
tests/fail/map_float_key.fch
Normal file
5
tests/fail/map_float_key.fch
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// expect: a map key must be a whole number, a char, a bool or a str, not float
|
||||
fn main() {
|
||||
map[float]int m
|
||||
print(m)
|
||||
}
|
||||
5
tests/fail/map_missing_key.fch
Normal file
5
tests/fail/map_missing_key.fch
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// expect: the key "zoe" is not in the map
|
||||
fn main() {
|
||||
ages := ["anna": 31]
|
||||
print(ages["zoe"])
|
||||
}
|
||||
9
tests/fail/method_field_clash.fch
Normal file
9
tests/fail/method_field_clash.fch
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// expect: the struct Point already has a field named 'x'
|
||||
struct Point {
|
||||
int x
|
||||
}
|
||||
fn Point.x() -> int {
|
||||
return 1
|
||||
}
|
||||
fn main() {
|
||||
}
|
||||
12
tests/fail/method_loop_readonly.fch
Normal file
12
tests/fail/method_loop_readonly.fch
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// expect: the loop variable 'c' can't be changed
|
||||
struct Counter {
|
||||
int n
|
||||
}
|
||||
fn Counter.bump() {
|
||||
self.n += 1
|
||||
}
|
||||
fn main() {
|
||||
for c in [Counter(1), Counter(2)] {
|
||||
c.bump()
|
||||
}
|
||||
}
|
||||
8
tests/fail/method_missing.fch
Normal file
8
tests/fail/method_missing.fch
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// expect: the struct Point has no methods; add one with fn Point.norm(...) { ... }
|
||||
struct Point {
|
||||
int x
|
||||
}
|
||||
fn main() {
|
||||
p := Point(1)
|
||||
print(p.norm())
|
||||
}
|
||||
5
tests/fail/or_block_value.fch
Normal file
5
tests/fail/or_block_value.fch
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// expect: this block must leave with return, break, continue or exit
|
||||
fn main() {
|
||||
n := int("x") or { print("not a number") }
|
||||
print(n)
|
||||
}
|
||||
4
tests/fail/or_cannot_fail.fch
Normal file
4
tests/fail/or_cannot_fail.fch
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// expect: this call can't fail, so it needs no 'or' or 'try'
|
||||
fn main() {
|
||||
print(int(3.5) or 0)
|
||||
}
|
||||
10
tests/fail/try_outside_fallible.fch
Normal file
10
tests/fail/try_outside_fallible.fch
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// expect: try passes the error on, so 'twice' must be able to fail too (-> int!)
|
||||
fn parse(str s) -> int! {
|
||||
return int(s) or { return error("bad") }
|
||||
}
|
||||
fn twice(str s) -> int {
|
||||
return try parse(s) * 2
|
||||
}
|
||||
fn main() {
|
||||
print(twice("2"))
|
||||
}
|
||||
7
tests/fail/unhandled_error.fch
Normal file
7
tests/fail/unhandled_error.fch
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// expect: 'parse' can fail, so say what happens then
|
||||
fn parse(str s) -> int! {
|
||||
return int(s) or { return error("bad") }
|
||||
}
|
||||
fn main() {
|
||||
print(parse("1"))
|
||||
}
|
||||
|
|
@ -131,6 +131,61 @@ check("error shown while typing", diags and "must be int, but this is str" in di
|
|||
c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 7}, "contentChanges": [{"text": src}]})
|
||||
check("error cleared after the fix", c.diagnostics(uri) == [])
|
||||
|
||||
# methods, maps, errors as values
|
||||
src2 = """struct Counter {
|
||||
int count
|
||||
}
|
||||
|
||||
fn Counter.add(int n) {
|
||||
self.count += n
|
||||
}
|
||||
|
||||
fn parse(str s) -> int! {
|
||||
return int(s) or { return error("bad") }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
c := Counter(0)
|
||||
c.add(2)
|
||||
map[str]int m
|
||||
n := parse("4") or 0
|
||||
print(c, m, n)
|
||||
}
|
||||
"""
|
||||
lines2 = src2.split("\n")
|
||||
path2 = os.path.join(d, "more.fch")
|
||||
open(path2, "w").write(src2)
|
||||
uri2 = pathlib.Path(path2).resolve().as_uri()
|
||||
td2 = {"uri": uri2}
|
||||
c.notify("textDocument/didOpen", {"textDocument": {"uri": uri2, "languageId": "finch", "version": 1, "text": src2}})
|
||||
check("methods, maps and errors compile", c.diagnostics(uri2) == [])
|
||||
h = c.request("textDocument/hover", {"textDocument": td2, "position": {"line": 14, "character": lines2[14].index("add")}})
|
||||
check("hover: method signature", h and "fn Counter.add(int n)" in h["contents"]["value"], h)
|
||||
h = c.request("textDocument/hover", {"textDocument": td2, "position": {"line": 16, "character": lines2[16].index("parse")}})
|
||||
check("hover: fallible signature", h and "-> int!" in h["contents"]["value"], h)
|
||||
defn = c.request("textDocument/definition", {"textDocument": td2, "position": {"line": 14, "character": lines2[14].index("add")}})
|
||||
check("definition: method", defn and defn["range"]["start"]["line"] == 4, defn)
|
||||
|
||||
for text, line, col, want, what in [
|
||||
(" c.\n", 18, 6, ["add", "count"], "completion: methods and fields after c."),
|
||||
(" m.\n", 18, 6, ["has", "get", "keys", "remove"], "completion: map methods"),
|
||||
]:
|
||||
e2 = src2.replace(" print(c, m, n)\n", " print(c, m, n)\n" + text)
|
||||
c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 2}, "contentChanges": [{"text": e2}]})
|
||||
comp = c.request("textDocument/completion", {"textDocument": td2, "position": {"line": line, "character": col}})
|
||||
labels = [i["label"] for i in comp["items"]]
|
||||
check(what, all(x in labels for x in want), labels)
|
||||
|
||||
e2 = src2.replace(" print(c, m, n)\n", " print(c, m, n)\n c.add(\n")
|
||||
c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 3}, "contentChanges": [{"text": e2}]})
|
||||
sig = c.request("textDocument/signatureHelp", {"textDocument": td2, "position": {"line": 18, "character": 10}})
|
||||
check("signature help: method", sig and sig["signatures"][0]["label"].startswith("fn Counter.add"), sig)
|
||||
|
||||
e2 = src2.replace('n := parse("4") or 0', 'n := parse("4")')
|
||||
c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 4}, "contentChanges": [{"text": e2}]})
|
||||
diags = c.diagnostics(uri2)
|
||||
check("error: unhandled failure", diags and "can fail" in diags[0]["message"], diags)
|
||||
|
||||
c.request("shutdown", None)
|
||||
c.notify("exit", None)
|
||||
c.p.wait(timeout=5)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ for f in run/*.fch; do
|
|||
if [ "${MEMCHECK:-0}" = 1 ]; then
|
||||
exe="$tmp/$(basename "${f%.fch}")"
|
||||
"$FINCH" build "$f" -o "$exe" >/dev/null 2>&1
|
||||
if ! valgrind -q --leak-check=full --errors-for-leak-kinds=all --error-exitcode=99 "$exe" < "$input" >/dev/null 2>"$tmp/vg"; then
|
||||
valgrind -q --leak-check=full --errors-for-leak-kinds=all --error-exitcode=99 "$exe" < "$input" >/dev/null 2>"$tmp/vg"
|
||||
if [ $? -eq 99 ]; then # (other exit codes are the program's own)
|
||||
failed=$((failed + 1))
|
||||
echo "MEMCHECK FAIL $f"
|
||||
head -20 "$tmp/vg"
|
||||
|
|
|
|||
86
tests/run/errors.fch
Normal file
86
tests/run/errors.fch
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
struct Config {
|
||||
str name
|
||||
int port = 8080
|
||||
}
|
||||
|
||||
fn parse_port(str text) -> int! {
|
||||
n := int(text) or { return error("the port '" + text + "' is not a number") }
|
||||
if n < 1 || n > 65535 {
|
||||
return error("the port " + str(n) + " is out of range")
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn parse_config(str line) -> Config! {
|
||||
parts := line.split("=")
|
||||
if parts.len != 2 {
|
||||
return error("expected name=port, got '" + line + "'")
|
||||
}
|
||||
port := try parse_port(parts[1].trim()) // a failure here fails parse_config too
|
||||
return Config(parts[0].trim(), port)
|
||||
}
|
||||
|
||||
fn save(str path, str text) -> ! {
|
||||
if path.len == 0 {
|
||||
return error("no file name")
|
||||
}
|
||||
try write_file(path, text)
|
||||
}
|
||||
|
||||
struct Account {
|
||||
int balance
|
||||
}
|
||||
|
||||
fn Account.withdraw(int amount) -> ! {
|
||||
if amount > self.balance {
|
||||
return error("not enough money: " + str(self.balance) + " < " + str(amount))
|
||||
}
|
||||
self.balance -= amount
|
||||
}
|
||||
|
||||
fn first_number([]str words) -> int! {
|
||||
for w in words {
|
||||
n := int(w) or { continue }
|
||||
return n
|
||||
}
|
||||
return error("no numbers")
|
||||
}
|
||||
|
||||
fn main() -> ! {
|
||||
print(parse_port("80") or 0)
|
||||
print(parse_port("abc") or -1)
|
||||
print(parse_port("99999") or -1)
|
||||
|
||||
for line in ["web = 80", "db=5432", "bad", "x = y", "big = 70000"] {
|
||||
c := parse_config(line) or {
|
||||
print("skipped:", err)
|
||||
continue
|
||||
}
|
||||
print(c)
|
||||
}
|
||||
|
||||
print(int("12") or 0, int(" 7 ") or 0, int("seven") or 0, float("2.5") or 0, float("") or -1.0)
|
||||
|
||||
text := read_file("does/not/exist.txt") or "default text"
|
||||
print(text)
|
||||
read_file("does/not/exist.txt") or { print("read failed:", err.starts_with("can't read the file")) }
|
||||
|
||||
save("", "x") or { print("save failed:", err) }
|
||||
try save("errors_tmp.txt", "saved")
|
||||
print(try read_file("errors_tmp.txt"))
|
||||
try delete_file("errors_tmp.txt")
|
||||
delete_file("errors_tmp.txt") or { print("second delete failed") }
|
||||
|
||||
a := Account(100)
|
||||
a.withdraw(30) or { print("no") }
|
||||
a.withdraw(500) or { print(err) }
|
||||
print(a.balance)
|
||||
|
||||
print(first_number(["a", "b", "42", "7"]) or 0, first_number(["x"]) or -1)
|
||||
|
||||
// nested: the fallback can be another call that may fail
|
||||
print(parse_port("x") or parse_port("443") or 0)
|
||||
|
||||
try parse_port("not a port") // main fails: prints the error, exit code 1
|
||||
print("never printed")
|
||||
}
|
||||
19
tests/run/errors.out
Normal file
19
tests/run/errors.out
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
80
|
||||
-1
|
||||
-1
|
||||
Config(name: "web", port: 80)
|
||||
Config(name: "db", port: 5432)
|
||||
skipped: expected name=port, got 'bad'
|
||||
skipped: the port 'y' is not a number
|
||||
skipped: the port 70000 is out of range
|
||||
12 7 0 2.5 -1
|
||||
default text
|
||||
read failed: true
|
||||
save failed: no file name
|
||||
saved
|
||||
second delete failed
|
||||
not enough money: 70 < 500
|
||||
70
|
||||
42 -1
|
||||
443
|
||||
error: the port 'not a port' is not a number
|
||||
|
|
@ -16,3 +16,8 @@ fn add(Vec a, Vec b) -> Vec {
|
|||
fn describe(Vec v) -> str {
|
||||
return "(" + str(v.x) + ", " + str(v.y) + ") of length " + str(length(v))
|
||||
}
|
||||
|
||||
// a method in a module: callable as v.scaled(2) wherever Vec is used
|
||||
fn Vec.scaled(float k) -> Vec {
|
||||
return Vec(self.x * k, self.y * k)
|
||||
}
|
||||
|
|
|
|||
101
tests/run/maps.fch
Normal file
101
tests/run/maps.fch
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
struct Stats {
|
||||
int count
|
||||
[]str words
|
||||
}
|
||||
|
||||
fn count_words(str text) -> map[str]int {
|
||||
map[str]int counts
|
||||
for w in text.split(" ") {
|
||||
if w.len > 0 {
|
||||
counts[w.lower()] += 1 // a missing key starts at 0
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
fn main() {
|
||||
ages := ["anna": 31, "bob": 25]
|
||||
ages["cyril"] = 40
|
||||
ages["bob"] = 26
|
||||
print(ages, ages.len)
|
||||
print(ages["anna"], ages.has("bob"), ages.has("zoe"), ages.get("zoe", -1))
|
||||
|
||||
for name, age in ages {
|
||||
print(name, "is", age)
|
||||
}
|
||||
for name in ages {
|
||||
print(name)
|
||||
}
|
||||
|
||||
print(ages.remove("bob"), ages.remove("bob"), ages)
|
||||
ages["dave"] = 19 // goes to the end; insertion order is kept
|
||||
print(ages.keys(), ages.values())
|
||||
|
||||
counts := count_words("the cat and The dog and THE bird")
|
||||
print(counts)
|
||||
print(str(counts))
|
||||
|
||||
// values that own memory
|
||||
map[str][]int groups
|
||||
for i in 0..10 {
|
||||
key := "odd"
|
||||
if i % 2 == 0 {
|
||||
key = "even"
|
||||
}
|
||||
groups[key].push(i) // m[k].push(x) adds the key with an empty array first
|
||||
}
|
||||
print(groups)
|
||||
|
||||
copy := groups
|
||||
copy["odd"].clear()
|
||||
print(groups["odd"].len, copy["odd"].len)
|
||||
|
||||
map[int]Stats by_len
|
||||
for w in ["a", "bb", "cc", "ddd", "e"] {
|
||||
by_len[w.len].count += 1
|
||||
by_len[w.len].words.push(w)
|
||||
}
|
||||
for n, s in by_len {
|
||||
print(n, s.count, s.words)
|
||||
}
|
||||
|
||||
// many keys: growing, removing and adding again
|
||||
map[int]int squares
|
||||
for i in 0..1000 {
|
||||
squares[i] = i * i
|
||||
}
|
||||
for i in 0..1000 {
|
||||
if i % 3 != 0 {
|
||||
squares.remove(i)
|
||||
}
|
||||
}
|
||||
for i in 2000..2010 {
|
||||
squares[i] = 1
|
||||
}
|
||||
total := 0
|
||||
for k, v in squares {
|
||||
total += v
|
||||
}
|
||||
print(squares.len, total, squares[999], squares.has(998))
|
||||
|
||||
map[char]bool seen
|
||||
for c in "hello" {
|
||||
seen[c] = true
|
||||
}
|
||||
print(seen.keys())
|
||||
|
||||
empty := ages
|
||||
empty.clear()
|
||||
print(empty, empty.len, ages.len)
|
||||
|
||||
map[str]str m = [:]
|
||||
m["x"] = "1"
|
||||
print(m)
|
||||
|
||||
for i, w in ["zero", "one", "two"] {
|
||||
print(i, w)
|
||||
}
|
||||
for i, c in "hey" {
|
||||
print(i, c)
|
||||
}
|
||||
}
|
||||
27
tests/run/maps.out
Normal file
27
tests/run/maps.out
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{"anna": 31, "bob": 26, "cyril": 40} 3
|
||||
31 true false -1
|
||||
anna is 31
|
||||
bob is 26
|
||||
cyril is 40
|
||||
anna
|
||||
bob
|
||||
cyril
|
||||
true false {"anna": 31, "cyril": 40}
|
||||
["anna", "cyril", "dave"] [31, 40, 19]
|
||||
{"the": 3, "cat": 1, "and": 2, "dog": 1, "bird": 1}
|
||||
{"the": 3, "cat": 1, "and": 2, "dog": 1, "bird": 1}
|
||||
{"even": [0, 2, 4, 6, 8], "odd": [1, 3, 5, 7, 9]}
|
||||
5 0
|
||||
1 2 ["a", "e"]
|
||||
2 2 ["bb", "cc"]
|
||||
3 1 ["ddd"]
|
||||
344 111277621 998001 false
|
||||
['h', 'e', 'l', 'o']
|
||||
{} 0 3
|
||||
{"x": "1"}
|
||||
0 zero
|
||||
1 one
|
||||
2 two
|
||||
0 h
|
||||
1 e
|
||||
2 y
|
||||
101
tests/run/methods.fch
Normal file
101
tests/run/methods.fch
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import geometry
|
||||
import "abi.h"
|
||||
link "abi.c"
|
||||
|
||||
struct Counter {
|
||||
str name
|
||||
int count
|
||||
[]int history
|
||||
}
|
||||
|
||||
fn Counter.add(int n) {
|
||||
self.count += n
|
||||
self.history.push(n)
|
||||
}
|
||||
|
||||
fn Counter.add_twice(int n) {
|
||||
self.add(n) // a method calling another method on self
|
||||
self.add(n)
|
||||
}
|
||||
|
||||
fn Counter.total() -> int {
|
||||
sum := 0
|
||||
for x in self.history {
|
||||
sum += x
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
fn Counter.describe() -> str {
|
||||
return self.name + ": " + str(self.count) + " " + str(self.history)
|
||||
}
|
||||
|
||||
fn Counter.copy_named(str name) -> Counter {
|
||||
c := self // a copy of the struct, not the original
|
||||
c.name = name
|
||||
return c
|
||||
}
|
||||
|
||||
fn Counter.merge(Counter other) {
|
||||
for x in other.history {
|
||||
self.add(x)
|
||||
}
|
||||
}
|
||||
|
||||
struct Node {
|
||||
int value
|
||||
ptr[Node] next
|
||||
}
|
||||
|
||||
fn Node.sum() -> int {
|
||||
total := self.value
|
||||
if self.next != null {
|
||||
total += self.next.sum() // through a pointer
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// a method on a struct from C
|
||||
fn Vec2.length2() -> f32 {
|
||||
return self.x * self.x + self.y * self.y
|
||||
}
|
||||
|
||||
fn main() {
|
||||
c := Counter(name: "clicks")
|
||||
c.add(3)
|
||||
c.add_twice(2)
|
||||
print(c.describe(), c.total())
|
||||
|
||||
d := c.copy_named("copy")
|
||||
d.add(10)
|
||||
print(c.describe())
|
||||
print(d.describe())
|
||||
|
||||
c.merge(c) // the argument lives inside self: the method gets its own copy
|
||||
print(c.describe())
|
||||
|
||||
[]Counter all = [Counter(name: "a"), Counter(name: "b")]
|
||||
all[1].add(7)
|
||||
for i in 0..all.len {
|
||||
all[i].add(i)
|
||||
}
|
||||
print(all[0].describe(), "|", all[1].describe())
|
||||
|
||||
print(Counter(name: "temp", count: 5).describe())
|
||||
|
||||
last := new(Node(3, null))
|
||||
first := new(Node(1, new(Node(2, last))))
|
||||
print(first.sum())
|
||||
p := first.next
|
||||
p.value = 20
|
||||
print(first.sum(), p.sum())
|
||||
free(last)
|
||||
free(first.next)
|
||||
free(first)
|
||||
|
||||
v := geometry.Vec(3, 4)
|
||||
print(geometry.length(v.scaled(2)))
|
||||
|
||||
w := Vec2(3, 4)
|
||||
print(w.length2())
|
||||
}
|
||||
10
tests/run/methods.out
Normal file
10
tests/run/methods.out
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
clicks: 7 [3, 2, 2] 7
|
||||
clicks: 7 [3, 2, 2]
|
||||
copy: 17 [3, 2, 2, 10]
|
||||
clicks: 14 [3, 2, 2, 3, 2, 2]
|
||||
a: 0 [0] | b: 8 [7, 1]
|
||||
temp: 5 []
|
||||
6
|
||||
24 23
|
||||
10
|
||||
25
|
||||
Loading…
Reference in a new issue