diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c85d92..eba3e44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,14 +89,6 @@ jobs: build/finch version - name: Tests run: tests/run.sh - - name: Package - run: | - mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r runtime std examples dist/finch/ - tar -czf finch-macos-arm64.tar.gz -C dist finch - - uses: actions/upload-artifact@v4 - with: - name: finch-macos-arm64 - path: finch-macos-arm64.tar.gz windows: runs-on: windows-2022 @@ -124,16 +116,6 @@ jobs: export PATH="/c/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH" export FINCH_LDFLAGS="-LC:/llvm/bin" FINCH=../build/finch.exe tests/run.sh - - name: Package - run: | - mkdir -p dist/finch - cp build/finch.exe build/libclang.dll README.md LICENSE dist/finch/ - cp -r runtime std examples dist/finch/ - cd dist && 7z a -tzip finch-windows-x64.zip finch > /dev/null - - uses: actions/upload-artifact@v4 - with: - name: finch-windows-x64 - path: dist/finch-windows-x64.zip vscode: runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index 7f62239..db0e813 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Pick the guide that fits you: | | | |---|---| -| Variables | `x := 5` (inferred) or `int x = 5`; `const W = 640` | +| Variables | `x := 5` (inferred) or `int x = 5` | | 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)` | @@ -98,17 +98,15 @@ Pick the guide that fits you: | 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 | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | -| Control | `if` / `else if` / `else` (also as a value), `while`, `for i in 0..10`, `break`, `continue`, `defer` | +| 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 | | Enums, match | `enum Color { Red, Green }`, `match c { Red -> ... else -> ... }` (also as a value) | | Generics | `fn max[T](T a, T b) -> T`, `struct Stack[T]`, `Stack[int]()` | -| Modules | `import shapes` → `shapes.area(b)`, `import ui/button as btn`; the standard library: `json`, `path`, `time`, `proc`, `fs`, `thread` | -| Threads | `thread.spawn(addr(work), addr(data))`, `t.join()`, `thread.mutex()`, `atomic_add(addr(n), 1)` | -| Tests | `test "name" { assert(x == 3) }` next to the code, run with `finch test` | -| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"` / `"glue.cpp"`, unions, `#define`s (also `(Color){…}`), `addr(fn)` for callbacks | +| Modules | `import shapes` → `shapes.area(b)`; the standard library: `json`, `path`, `time`, `proc` | +| 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/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors | +| Tools | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` for gdb/lldb, `-O0` | ## Examples @@ -148,17 +146,14 @@ Pick the guide that fits you: - [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`) - [x] Generics (`fn max[T]`, `struct Stack[T]`), `match`, enums -- [x] A standard library in Finch: `json`, `path`, `time`, `proc`, `fs` -- [x] `const`, `if` as a value, `"\(x)"` in text, `"""` text, C unions and `#define` expressions -- [x] `test` blocks and `finch test`, `finch fmt`, `finch.toml`, a build cache, the chain of calls on runtime errors -- [x] Threads (`import thread`) and atomics; C++ files; `--target android` (compiles; NDK linking and devices untried) +- [x] A standard library in Finch: `json`, `path`, `time`, `proc` ## Layout ``` src/ the compiler, in Finch: lexer, parser, AST, code generation (to LLVM IR as text), C import (libclang) and the C calling conventions, the language server, the driver -std/ the standard library, in Finch: json, path, time, proc, fs +std/ the standard library, in Finch: json, path, time, proc runtime/ the small C runtime linked into every program bootstrap/ build.sh and the seeds: the compiler's own LLVM IR, one per system examples/ example programs diff --git a/README.pl.md b/README.pl.md index 52b438d..7931879 100644 --- a/README.pl.md +++ b/README.pl.md @@ -90,7 +90,7 @@ Wybierz przewodnik dla siebie: | | | |---|---| -| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5`; `const W = 640` | +| 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`, 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)` | @@ -98,17 +98,15 @@ Wybierz przewodnik dla siebie: | Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` | | Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cy")`, `ile[s] += 1`, `for k, v in wiek { }` | | Błędy | `fn parsuj(str s) -> int! { … return error("zły") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` | -| Tekst | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | -| Sterowanie | `if` / `else if` / `else` (też jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` | +| 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 | | Enumy, match | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (także jako wartość) | | Generyki | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` | -| Moduły | `import ksztalty` → `ksztalty.pole(p)`, `import ui/button as btn`; biblioteka standardowa: `json`, `path`, `time`, `proc`, `fs`, `thread` | -| Wątki | `thread.spawn(addr(praca), addr(dane))`, `t.join()`, `thread.mutex()`, `atomic_add(addr(n), 1)` | -| Testy | `test "nazwa" { assert(x == 3) }` obok kodu, uruchamiane przez `finch test` | -| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"` / `"glue.cpp"`, unie, `#define` (też `(Color){…}`), `addr(fn)` dla callbacków | +| Moduły | `import ksztalty` → `ksztalty.pole(p)`; biblioteka standardowa: `json`, `path`, `time`, `proc` | +| 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/test/fmt/ir/lsp`, `finch.toml` dla projektu, cache budowania, `--target windows/arm64/macos/android`, `-g` dla gdb/lldb, `-O0`, łańcuch wywołań przy błędach w czasie działania | +| Narzędzia | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` dla gdb/lldb, `-O0` | ## Przykłady @@ -148,17 +146,14 @@ Wybierz przewodnik dla siebie: - [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate - [x] Metody w strukturach, mapy, błędy jako wartości (`!`, `or`, `try`) - [x] Typy generyczne (`fn max[T]`, `struct Stos[T]`), `match`, enumy -- [x] Biblioteka standardowa w Finchu: `json`, `path`, `time`, `proc`, `fs` -- [x] `const`, `if` jako wartość, `"\(x)"` w tekście, tekst `"""`, unie C i wyrażenia `#define` -- [x] Bloki `test` i `finch test`, `finch fmt`, `finch.toml`, cache budowania, łańcuch wywołań przy błędach -- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android` (kompiluje się; linkowanie z NDK i urządzenia niesprawdzone) +- [x] Biblioteka standardowa w Finchu: `json`, `path`, `time`, `proc` ## Struktura ``` src/ kompilator, w Finchu: lekser, parser, AST, generowanie kodu (do LLVM IR jako tekst), import C (libclang) i konwencje wywołań C, serwer języka, sterownik -std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs +std/ biblioteka standardowa, w Finchu: json, path, time, proc runtime/ mały runtime w C dołączany do każdego programu bootstrap/ build.sh i ziarna: własny LLVM IR kompilatora, po jednym na system examples/ przykładowe programy diff --git a/bootstrap/build.sh b/bootstrap/build.sh index 2381b90..9c66d1b 100755 --- a/bootstrap/build.sh +++ b/bootstrap/build.sh @@ -27,20 +27,8 @@ case $seed in windows-*) EXE=.exe ;; esac CLANG=${FINCH_CLANG:-clang} LIBS=${FINCH_LIBCLANG:--lclang} INC= -# where clang-c/Index.h is, if clang does not find it itself (Debian and Ubuntu keep it in /usr/lib/llvm-N/include) -if [ -z "${FINCH_LLVM_INCLUDE:-}" ] && command -v llvm-config > /dev/null 2>&1 \ - && [ -f "$(llvm-config --includedir)/clang-c/Index.h" ]; then - FINCH_LLVM_INCLUDE=$(llvm-config --includedir) -fi -[ -z "${FINCH_LLVM_INCLUDE:-}" ] || INC="-I $FINCH_LLVM_INCLUDE" +[ -z "${FINCH_LLVM_INCLUDE:-}" ] || INC="-I $FINCH_LLVM_INCLUDE" # where clang-c/Index.h is, if clang does not find it itself export FINCH_RT=$PWD/runtime/finch_rt.c -# the -L folders of FINCH_LIBCLANG are where the compiler's own link "clang" is found too -if [ -z "${FINCH_LDFLAGS:-}" ]; then - for w in $LIBS; do - case $w in -L*) FINCH_LDFLAGS="${FINCH_LDFLAGS:-} $w" ;; esac - done - [ -z "${FINCH_LDFLAGS:-}" ] || export FINCH_LDFLAGS -fi # Windows: libclang.dll has to sit next to the programs that use it (FINCH_LIBCLANG is then the path of that dll) if [ -n "$EXE" ]; then cp "$LIBS" build/; fi diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index d27262c..4bc41ca 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -13,36 +13,15 @@ Everything in Finch on one page. For explanations, see the [guides](../../README | `finch run file.fch [args…]` | compile and run (args go to the program) | | `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) | | `finch build file.fch --shared [-I dir]` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) | -| `finch test file.fch` | run its `test "…" { }` blocks, each on its own (and those of the modules it imports) | -| `finch fmt [--check] [files/folders]` | indent by `{ }` with 4 spaces, no spaces at line ends, at most one empty line (`--check`: only list what it would change) | -| `finch run` / `build` / `test` with no file | in a folder with a `finch.toml`: its `entry` | | `finch ir file.fch` | print the LLVM IR | | `finch lsp` | the language server (editors use it) | | `finch version` | version | | `-l lib` | link a C library (like `link "lib"`) | -| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | build for another system (`android`: the NDK's clang from `ANDROID_NDK_HOME`, usually with `--shared`) | +| `--target windows` / `linux` / `arm64` / `macos` / triple | build for another system | | `-g` | debug info (gdb, lldb, Visual Studio) | | `-O0` | no optimizations | -| `--trace` / `--no-trace` | runtime errors show the chain of calls; on for `run`, `test`, `-g`, `-O0`, off for a plain `build` (it costs speed) | -Environment: `FINCH_PATH` (folders with modules), `FINCH_CC` (the C compiler that links), `FINCH_CXX` (for `.cpp` files). -A program's compiled code and its `link "x.c"` files are kept in the cache (`~/.cache/finch`) until they or their headers change. - -`finch.toml` (next to the code; `finch run` there needs no file and no flags): - -```toml -name = "game" # what finch build makes -entry = "src/main.fch" -paths = ["src"] # folders with modules -include = ["vendor/SDL3/include"] # C headers -links = ["SDL3", "native/glue.cpp"] # libraries and your own .c / .cpp / .o / .a -libdirs = ["vendor/SDL3/lib"] -cflags = ["-DFAST"] # for your .c / .cpp files -ldflags = ["-Wl,-rpath,."] - -[target.windows] # only when building for that system (also linux, macos, android, linux-arm64) -links = ["ws2_32"] -``` +Environment: `FINCH_PATH` (folders with modules), `FINCH_CC` / `CC` (the C compiler that links). --- @@ -74,10 +53,9 @@ One statement per line, no semicolons, `{` on the same line, no brackets around |---|---| | `x := 5` | new variable, type inferred | | `int x = 5` | new variable, type written | -| `int x` | zero / `""` / `[]` / `null` / the struct's defaults (also of the structs inside it) | +| `int x` | zero / `""` / `[]` / `null` / the struct's defaults | | `x = 7` | assign | | `x += 1` `-=` `*=` `/=` `%=` | update | -| `const W = 640`, `const f32 G = 9.8` | a constant (top level of a file; `shapes.W` from a module) | --- @@ -86,10 +64,10 @@ One statement per line, no semicolons, `{` on the same line, no brackets around | Type | Example | |---|---| | `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` | -| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` | +| `float` (= `f64`) | `3.14` | | `bool` | `true`, `false` | | `char` | `'A'`, `'\n'` | -| `str` | `"text\t\"quoted\""`, `"hp \(hp), speed \(v, 2)"` (code in the text; `, 2` = two digits after the point), `"""` several lines `"""` (the closing quotes' indentation is removed) | +| `str` | `"text\t\"quoted\""` | | `i8 i16 i32 i64` | signed, sized | | `u8 u16 u32 u64` | unsigned, sized | | `f32 f64` | floats | @@ -121,7 +99,6 @@ Escapes: `\n \t \r \0 \\ \" \'`. ```c if a > b { … } else if a == b { … } else { … } -x := if a > b { a } else { b } // as a value: one value per branch, else needed while x > 0 { … } for i in 0..10 { … } // 0 … 9 for item in list { … } // every element (or char of a str) @@ -247,7 +224,6 @@ Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_fil | `s.len` | bytes | | `s[i]`, `s[i] = 'x'` | char | | `a + b` | joined text | -| `"x = \(x)"` | the value of any code in the text (`\(x, 2)`: a float with 2 digits) | | `s.sub(a, b)` | part a … b-1 | | `s.find(t)` | position or -1 | | `s.contains(t)` | bool | @@ -258,7 +234,7 @@ Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_fil | `s.replace(a, b)` | every a → b | | `s.repeat(n)` | n times | | `s.bytes()` | `[]u8` | -| `s.ptr` | `ptr[char]` (for C; a `str` also goes to a `const char *` parameter as it is) | +| `s.ptr` | `ptr[char]` (for C) | | `==` `!=` `<` `>` | compare | --- @@ -274,8 +250,7 @@ Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_fil | `u8(300)` | `44` (keeps the low bits) | | `char(66)` | `'B'` | | `bool(0)` | `false` | -| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text (a float: the shortest text that is the same number) | -| `str(3.14159, 2)` | `"3.14"` (that many digits after the point) | +| `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 | @@ -298,12 +273,9 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa | `file_exists(path)` | bool | | `delete_file(path)` | bool | | `shell(command)` | run a command, its exit code | -| `exit(code)` | stop the program now (also ends an `or { }` block) | -| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | for any numbers (your own function of that name wins) | +| `exit(code)` | stop the program now | | `error(message)` | `return error("…")` in a `-> T!` function | | `addr(x)` | pointer to x; `addr(fn)` = C callback | -| `assert(cond)`, `assert(cond, "why")` | stop with a runtime error when cond is false (in a `test` block: that test fails) | -| `atomic_add(p, n)`, `atomic_sub`, `atomic_load(p)`, `atomic_store(p, n)`, `atomic_swap(p, n)`, `atomic_cas(p, old, new)` | a whole number other threads change too, through its `ptr` (`addr(count)`) | | `new(value)` | value on the heap → `ptr[T]` | | `free(p)` | give it back | @@ -314,7 +286,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa | Finch | C | |---|---| | `ptr[int] p` | `int *p` | -| `addr(x)` | `&x` (a struct variable given where `ptr[ThatStruct]` goes: `push(body)` = `push(addr(body))`) | +| `addr(x)` | `&x` | | `p.value` | `*p` | | `p.field` | `p->field` | | `p[i]` | `p[i]` | @@ -327,7 +299,6 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa - Arrays, text and structs free themselves at the end of their block. No `free` needed. - `b := a` copies; a call's result moves; `return x` moves. -- Passing a struct: `fn f(Body b)` works on its own copy (the caller's stays as it was), `fn f(ptr[Body] b)` changes the caller's (`f(body)` or `f(addr(body))`), a method gets `self` by address, never a copy. A struct with 256+ bytes of numbers passed by value or in `for x in list` gets a warning. - `new(...)` + `free(...)` (often with `defer`) for linked lists, trees, sharing. - C memory: C's own functions (`malloc` / `free`, `fclose`, …). @@ -340,8 +311,6 @@ import shapes // shapes.fch next to this file (or in FINCH_PATH) shapes.area(b) // its functions shapes.Box(2, 3) // its structs shapes.Box other // in declarations -import ui/button // ui/button.fch: button.draw() -import ui/button as btn // another name: btn.draw() ``` --- @@ -352,7 +321,7 @@ import ui/button as btn // another name: btn.draw() |---|---| | `import "x.h"` | functions, structs, enums, `#define` numbers, globals | | `link "name"` | libname.so / name.lib (pkg-config first) | -| `link "file.c"`, `link "glue.cpp"` | compile your own C or C++ file (C++: functions in `extern "C"`) | +| `link "file.c"` | compile your own C file | | `addr(myFn)` | pass a Finch function as a callback | | C type | Finch | @@ -365,14 +334,6 @@ import ui/button as btn // another name: btn.draw() | `T *` | `ptr[T]` | | `void *`, handles | `ptr` | | `T x[N]` (in a struct) | `[N]T` | -| `union U` | `U`: `U u`, `u.field`, `U(field: v)`, by pointer to C (not by value) | -| `#define N 4`, `"text"`, `(1u << 3)`, `((Uint64)2)` | constants (not function-like macros) | -| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | a `Color` | - -**`char *` is `str`**: a C function that takes or returns `char *` takes or gives a Finch `str` (no `.ptr`, no conversion); only a `char *` field or a `char **` stays `ptr[char]`. -C++ and Rust: through a C header. C++ with `extern "C"` (and `link "x.cpp"`), Rust with `#[no_mangle] pub extern "C" fn` built as a `staticlib` (`link "target/release/libgame.a"`) and a header (cbindgen writes one). - -A C function's `char *` result is a `str` already; structs by value work with the System V, Windows x64 and ARM64 Linux calling conventions (tested in emulators; macOS by CI). --- @@ -387,26 +348,6 @@ A C function's `char *` result is a `str` already; structs by value work with th | `can't turn "x" into int` | `int("x")` | | `can't read the file "…"` | `read_file` (without `or`) | | `the key "x" is not in the map` | `m["x"]` | -| `assert failed: why` | `assert(cond, "why")` | - -Below the message, the calls that led there (`finch run`, `finch test`, `-g`, `-O0`, `--trace`): - -``` -game.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (game.fch:6) - in step (game.fch:10) - in main (game.fch:15) -``` - -## Tests - -```c -test "jump height" { // next to the code; finch test file.fch runs them, finch run/build leave them out - p := Player() - p.jump() - assert(p.y > 0.0, "y is \(p.y)") -} -``` --- @@ -456,12 +397,9 @@ s := Stack[int]() Stack[int] t Pair[str, int]("age", 31) // ne | `path` | `join(a, b)` `base(p)` `dir(p)` `ext(p)` `stem(p)` `parts(p)` `clean(p)` `is_abs(p)` | | `time` | `now()` `millis()` `sleep(ms)` `date(t)` `local(t)` `format(t, fmt)` `days_in_month(y, m)` | | `proc` | `run(cmd)` `output(cmd)` (fails with its output) `env(name, fallback)` `pid()` `cwd()` | -| `thread` | `spawn(addr(work), addr(data))` → `t.join()`; `mutex()` → `m.lock()` `m.unlock()` `m.close()`; `cpus()` (a thread's function: `fn work(ptr arg)`) | -| `fs` | `read_bytes(p)` `write_bytes(p, data)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(dir)` `remove_dir(p)` `data_dir(app)` | | `json` | `parse(text)` `stringify(v)` `pretty(v)` `new_text/number/bool/null/list/object` `v.get(k)` `v.at(i)` `v.set(k, x)` `v.push(x)` `v.has(k)` `v.str_or(d)` `v.int_or(d)` `v.float_or(d)` `v.bool_or(d)` | ## Keywords -`fn return if else while for in break continue true false null import link struct enum match defer or try const export` -(`test` and `as` only where they mean something: `test "name" { }`, `import x as y`) +`fn return if else while for in break continue true false null import link struct enum match defer or try` (and `self` inside methods) diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md index fd3bedc..60f32b4 100644 --- a/docs/en/for-beginners.md +++ b/docs/en/for-beginners.md @@ -76,7 +76,7 @@ To check it works, type: ./build/finch version ``` -You should see something like `Finch 2.5.0`. +You should see something like `Finch 2.4.0`. --- diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index 56ce0a9..a1b08b8 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -1,6 +1,6 @@ # Finch for Engineers -**The internals of Finch 2.5, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*, +**The internals of Finch 2.4, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*, which `clang` compiles and links with a small C runtime; how C headers are read with libclang from Finch itself and how the C calling conventions are reproduced by hand; the type and ownership rules; how the compiler builds itself from a seed; and the language server. @@ -69,20 +69,18 @@ Design decisions that shape the rest: | File | Lines | Responsibility | |---|---|---| -| `src/lexer.fch` | 470 | Tokens, positions, `"""` text, `"\(x)"` turned into `("…" + str(x) + "…")` tokens | +| `src/lexer.fch` | 220 | Tokens, positions, indentation-free layout | | `src/ast.fch` | 120 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) | -| `src/parser.fch` | 910 | Recursive descent: declarations, statements, expressions, generics (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` | -| `src/gen.fch` | 6450 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, `-g` debug metadata, the `index` mode | -| `src/cimport.fch` | 790 | libclang: functions, structs and unions (fields, offsets, padding), enums, constants, globals, macros (also struct-valued) | +| `src/parser.fch` | 780 | Recursive descent: declarations, statements, expressions, generics (`[T]`), `enum`, `match`, `export`, `link` | +| `src/gen.fch` | 5200 | Type check + IR: structs, functions, methods, scopes and drops, statements, expressions, built-ins, maps, errors as values, generics (monomorphisation), enums and `match`, `-g` debug metadata, the `index` mode | +| `src/cimport.fch` | 560 | libclang: functions, structs (fields, offsets, padding), enums, constants, globals, macros | | `src/platform.fch` | 90 | `Target`: triple, data layout, C compiler, signedness of `char` per system | -| `src/linker.fch` | 430 | The cache (runtime, program object, C/C++ files), libraries (`link`, pkg-config), link-error advice | -| `src/main.fch` | 515 | Driver: CLI, loader with module lookup and overlays, `build/run/test/fmt/ir/index/lsp` | -| `src/manifest.fch` | 240 | `finch.toml`: a small TOML reader, per-target lists | -| `src/fmt.fch` | 225 | `finch fmt` and LSP formatting | -| `src/lsp.fch` | 910 | The language server (§11) | +| `src/linker.fch` | 300 | The cache, runtime object, libraries (`link`, pkg-config), link-error advice | +| `src/main.fch` | 310 | Driver: CLI, loader with module lookup and overlays, `build/run/ir/index/lsp` | +| `src/lsp.fch` | 875 | The language server (§11) | | `src/docs.fch` | 110 | One description of the built-ins, shared by hover and completion | -| `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` | -| `runtime/finch_rt.c` | 1150 | The C runtime: text, arrays, maps, files, float printing, fs, threads, the test runner, the call chain | +| `std/*.fch` | 900 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`) | +| `runtime/finch_rt.c` | 760 | The C runtime | | `bootstrap/` | | `build.sh`, `install.sh`, the seeds (§10) | --- @@ -100,18 +98,6 @@ its `endLine` (used by the language server). The old `fn name(...) type` order i `name.fch` there, the folders of `FINCH_PATH`, then the standard library `std/`. Each module is parsed once; its names are used as `module.name`. `import "x.h"` goes to the C importer instead. -A module is identified by its **file**: the key is its path from the main file's folder (or from a `FINCH_PATH` folder or -`std/`) without `.fch` and without an `_` suffix, e.g. `ui/button`. `Program.imports` holds the name each file uses -(`button`, or `btn` for `import ui/button as btn`) and `Program.modules` the key; `modFor()` maps one to the other. -Symbols use the key with `/` turned into `$` (`@finch.ui$button.make`). - -**Constants.** `const NAME = expr` is a top-level node; `constValue()` evaluates the expression at each use, in the -defining module with no local scopes, so it folds to a literal (integer literal operators are folded in `arith`). -`checkConst()` allows only literals, names, operators, members and numeric conversions; `constBusy` catches cycles. - -**Lexer extras.** `1e9` / `2.5e-3` are float tokens (the text is normalised to have a `.` for LLVM); `"""` strings are -dedented by the closing line's indentation in `longString()`. - --- ## 4. Code generation @@ -152,14 +138,6 @@ LLVM IR does not do the C calling convention for you, so `gen.fch` reproduces it The same code is what lets the compiler call libclang, whose API passes `CXCursor`, `CXType` and `CXString` by value. -**Macros.** A `#define` that is one number or a string literal is read from its tokens. One whose tokens could be a -constant expression (no braces, semicolons or statement keywords) is evaluated by clang: a second parse of the header -adds `static const __auto_type __finch_m_NAME = (NAME);` for each, and `clang_Cursor_Evaluate` gives an int, a double -or a string. Those that are not constants only produce errors in that second parse and are dropped. - -**Unions** are a `CStruct` with `isUnion`: the LLVM type is `{ [n x iA] }` (size and alignment from clang), every field has -offset 0, and a field is read through memory. They can't be passed by value to C (`unsupported` says why). - --- ## 6. Driver, linking and targets @@ -168,7 +146,7 @@ offset 0, and a field is read through memory. They can't be passed by value to C `ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the `export fn`s. -`--target windows|linux|arm64|macos|android|` changes the triple and data layout, the C `char` signedness, the ABI rules +`--target windows|linux|arm64|macos|` changes the triple and data layout, the C `char` signedness, the ABI rules above, the OS variant of modules (`name_windows.fch`), and the C compiler: `FINCH_CC` or `clang --target=…`. Linking asks `pkg-config` for flags for `link "lib"`; when the linker fails the output is analysed and the likely missing `link "…"` is suggested. The runtime object is keyed by a hash of its source and the target. @@ -178,38 +156,6 @@ compiled by `clang`, never by `CC`), `FINCH_LIBCLANG` (for `bootstrap/build.sh`) The compiler finds `runtime/` and `std/` next to itself (`../`, `./`) or under `../share/finch/` (`bootstrap/install.sh`). -**The build cache** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`): - -- `ir/.o` with a `.key` holding a hash of clang, the flags and the whole IR. One slot - per program, so the cache does not grow with every edit; the same IR gives the same object without running clang. -- `c/.o` for `link "x.c"` / `.cpp`, built with `-MMD`; the `.key` is a hash of - every file the `.d` lists (the source and its headers). Any change in one of them rebuilds it. -- Objects are written to a `.part` file and renamed, so two builds at once don't see half a file. - Hashes are two independent 32-bit ones (FNV-1a and djb2), 64 bits in all. - -**`finch.toml`** (`src/manifest.fch`) is searched from the main file's folder (or the working folder) upwards. It is read -into `values["section.key"]` and `lists["section.key"]`; `list(key, targets)` concatenates the top-level list and those of -`[target.]` and `[target.-]`. Paths are resolved against the manifest's folder. Entries feed the same -places as the command line: `paths` → the loader, `include` → `-I` (libclang and C files), `links` → `link`, `libdirs` -and `ldflags` → the link command, `cflags` → C/C++ files. - -**C++ files**: a `.cpp`/`.cc`/`.cxx` in `links` is compiled by `cxxFor(cc)` (`FINCH_CXX`, or `clang` → `clang++`, -`…gcc` → `…g++`, `cc` → `c++`), and then that driver links, which brings in the C++ standard library. - -**Android**: `--target android` is `aarch64-linux-android24`; the C compiler is the NDK's -`toolchains/llvm/prebuilt//bin/aarch64-linux-android24-clang` (from `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`), the link -adds `-llog` and no `-lpthread` (bionic has it in libc). The OS variant of modules is `linux`. - -**`finch test`** compiles like `run`, with `Options.test`: every function is still defined, but `@main` calls -`finch_test_run(name, fn)` for each `test` block of the user's modules (`__test_`, a `void` function made by the -parser), then `finch_test_summary()`. The runtime runs each test under `setjmp`; `fatal()` (every runtime error and a -failed `assert`) `longjmp`s back through `fatal_hook`, so one test failing doesn't end the run. Memory the failed test -owned at that point is not freed. - -**`finch fmt`** is line-based: it tracks `{ }`, `( )`/`[ ]`, strings (with `\( )`), `'…'`, `"""` and `/* */` per line. -A line that starts at bracket depth 0 and doesn't continue a line ending in an operator or `,` is re-indented to -4 × brace depth; other lines (continuations, `"""` bodies) are shifted by the same amount as the line that started the -statement, so their shape (and the `"""` dedent) is kept. --- ## 7. Type system @@ -255,9 +201,8 @@ from the slot they flow into (`exprWant()`), so `[]f32 xs = [1, 2.5]` works. ### Binary operands: `unify(l, r)` -A literal side adapts to the other (also a float literal next to an `f32`: `x * 2.0` stays `f32`); otherwise the side -that widens grows; `i32` + `u32` is an error; int + float → float; f32 + f64 → f64. There is no C integer promotion: -`u8 + u8` stays `u8`. Two integer literals fold to a literal, so `FLAG_A | FLAG_B` still fits a `u64` or `u32`. +A literal side adapts to the other; otherwise the side that widens grows; `i32` + `u32` is an error; +int + float → float; f32 + f64 → f64. There is no C integer promotion: `u8 + u8` stays `u8`. ### Explicit conversions: `convert()` @@ -474,48 +419,6 @@ test (22 checks). --- -### The chain of calls on runtime errors - -With `Options.trace` (default for `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; never with `--shared`) every Finch -function gets a frame on its stack: - -```llvm -%frame = alloca { ptr, ptr, ptr, i64 } ; caller's frame, name, file, line -store ptr (load @finch_trace), ...field 0 -store ptr @"name", ...field 1 ; ptr @"file", field 2 ; i64 , field 3 -store ptr %frame, ptr @finch_trace -``` - -`@finch_trace` is a `thread_local` global in the program's module (initial-exec model unless shared or Windows); -each statement stores its line into field 3. After every call to a Finch function the caller stores its own frame back -into `@finch_trace`, which also undoes frames left by a callee that returned early, so no epilogue is needed on the many -return paths. C-callable wrappers (`addr(fn)` thunks, `export fn`) save and restore `@finch_trace` around their call. -`main` calls `finch_trace_init(top, set)` with two tiny functions that read and write it, which is how the runtime -(`print_trace()` in `fatal()`, and the test runner) gets at a thread-local it cannot name. Cost: about 3x on `fib(40)`, -which is nothing but calls; that is why a plain `finch build` leaves it out. - -### Threads and atomics - -`std/thread.fch` is a thin wrapper over the runtime (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads, or -`CreateThread` and `CRITICAL_SECTION` on Windows). The thread function is any `fn f(ptr)`, passed with `addr(f)`, which -makes the C-callable thunk. `atomic_add/sub/swap` lower to `atomicrmw add/sub/xchg … seq_cst`, `atomic_load/store` to -`load/store atomic … seq_cst, align `, and `atomic_cas` to `cmpxchg … seq_cst seq_cst` returning the `i1`. They -take a `ptr[T]` to any integer type. Finch's arrays, text and maps are not synchronised; sharing one between threads -needs a mutex. - -### Warnings - -`warn()` prints `file:line:col: warning: …` to stderr (so `finch ir` stays pure IR) or, with `index`, an `@W` record the -language server publishes with severity 2. One per source position. Two are given: a struct parameter, and the element of -`for x in list`, whose plain data (`byteSize()`: numbers, nested structs, C `[N]T`; `str`/arrays/maps count 0, as only -their handle is copied) is 256 bytes or more. - -### Struct arguments by address - -`argWant()` (call arguments only, not assignments): when a parameter is `ptr[S]` and the argument is a plain variable -of type `S`, the argument is the variable's address, as if `addr(x)` had been written. Only variables, so nothing is -evaluated twice and temporaries are never pointed to. - ## 13. Known limitations - The compiler runs on Linux x86-64 and ARM64 only (no seed for macOS or Windows yet). Programs can be built for all three. @@ -525,6 +428,4 @@ evaluated twice and temporaries are never pointed to. - The driver uses `sh` utilities (`rm`, `mkdir`), so it needs a POSIX shell. - Generics are monomorphised (code size grows with instantiations); there are no trait bounds, a wrong use is an error inside the instantiated body. -- A failed test's owned memory is not freed (the test runner `longjmp`s out of it). -- `--target android` is checked to the object file only; linking needs the NDK and has not been run on a device. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index 9e41283..bdc368c 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -37,10 +37,6 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md) 24. [Generics](#24-generics) 25. [The standard library](#25-the-standard-library) 26. [Current limits](#26-current-limits) -27. [Tests: test blocks and finch test](#27-tests-test-blocks-and-finch-test) -28. [A project: finch.toml](#28-a-project-finchtoml) -29. [Threads](#29-threads) -30. [C++, Rust and Android](#30-c-rust-and-android) --- @@ -141,31 +137,17 @@ kept in `~/.cache/finch`. `FINCH_CC` names a different compiler to link with; `F ``` finch run [args...] compile and run right away finch build [-o name] compile into a program (default name: the file's name) -finch test run its test "..." { } blocks (section 27) -finch fmt [--check] [files/dirs] tidy the indentation of .fch files (all under here by default) finch ir print the LLVM IR (for the curious) finch lsp the language server, for editors finch version show the version options: -l link a C library, same as link "lib" in the file - -I look for C headers there too - --target build for another system: windows, linux, arm64, macos, android, or an LLVM triple + --target build for another system: windows, linux, arm64, macos, or an LLVM triple -g add debug info (for gdb / lldb / Visual Studio) -O0 skip optimizations - --trace runtime errors show the chain of calls (on anyway for run, test, -g and -O0) - --no-trace they don't (a plain finch build doesn't: keeping the chain makes calls slower) ``` -In a folder with a `finch.toml` (section 28), `finch run`, `finch build` and `finch test` need no file name. - -The compiled program and the C files it builds (`link "x.c"`) are kept in a cache (`~/.cache/finch`, or -`%LOCALAPPDATA%` on Windows). The second build of the same code, or of code whose C files did not change, skips that work. - -`finch fmt` changes only the layout: four spaces per `{ }` level, no spaces at line ends, at most one empty line in a row. -Lines that continue an expression (inside `( )` or after a `+`) and `"""` text keep their shape. `finch fmt --check` -changes nothing and fails if a file would change (for CI). Editors with the language server format on request. - Examples: ```sh @@ -232,19 +214,7 @@ x += 1 // also -= *= /= %= - Names: ASCII letters, digits and `_`, not starting with a digit. Non-ASCII letters are allowed only inside strings. Type names (`int`, `u8`, …), built-in function names and keywords are reserved. -**Keywords:** `fn return if else while for in break continue true false null import link struct enum match defer or try const export` - -### Constants - -```c -const WIDTH = 640 -const HEIGHT = WIDTH / 2 // from other constants -const f32 GRAVITY = 9.8 // with a type -const TITLE = "My game" -``` - -A constant is written at the top level of a file, next to the functions, and can't be changed. It is made of -numbers, text, `true`/`false`, chars, other constants and operators; a module's constant is `shapes.SIDES`. +**Keywords:** `fn return if else while for in break continue true false null import link struct defer` --- @@ -346,13 +316,6 @@ Notes: ## 8. Control flow -`if` can also give a value; each branch holds one value, and the `else` is needed: - -```c -label := if score > 100 { "high" } else if score > 10 { "mid" } else { "low" } -f32 speed = if running { 2.5 } else { 1.0 } -``` - ```c if x > 10 { ... @@ -521,29 +484,8 @@ s[0] = 'J' // change a character | `s.bytes()` | `[]u8` with the bytes | | `s.ptr` | a `ptr[char]` for C (valid while `s` lives) | -Values go into text with `\(...)`: any code, and `, 2` after a number for two digits after the point. -A `\` before the `(` you want to keep: `"\\(not code)"`. - -```c -print("hp \(hp) of \(max_hp), speed \(v, 2), name \(p.name.upper())") -``` - `str` keeps its length, so `.len` is instant, and it is always NUL-terminated, so passing it -to C as `char*` works. The other way round, a C function that gives back a `char *` gives you a `str` -(a copy is made when you keep it), and `str(p)` reads a `ptr[char]` you got some other way. - -Text over several lines goes between `"""`. The line break after the opening quotes is dropped, and so is the -indentation of the closing quotes, from every line, so the text can be indented with your code: - -```c -shader := """ - #version 330 - void main() { gl_Position = vec4(0.0); } - """ -``` - -Numbers as text: `str(x)` is the shortest text that reads back as the same number (`0.1`, `0.30000000000000004`, -`1e+20`); `str(x, 2)` always has two digits after the point (`3.14`). +to C as `char*` works. --- @@ -700,8 +642,6 @@ fn main() { - Everything in a module is used with the module's name in front: `shapes.area`, `shapes.Box`. - A module doesn't have `main`. Modules can import other modules. -- Modules in folders: `import ui/button` loads `ui/button.fch` and is used as `button.draw()`. -- Another name: `import ui/button as btn` → `btn.draw()`. Two imports can't use the same name. --- @@ -802,10 +742,8 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete ### What does not work yet -- C **unions** and structs with **bit fields** passed to or returned from a C function **by value** (pointers to them - work, and so do unions as variables: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`). -- **Function-like macros** (`#define MAX(a, b) ...`). Macros that are a number, a text, an expression of them - (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) or a struct of numbers (raylib's `RAYWHITE`, `(Color){ 245, 245, 245, 255 }`) work. +- C **unions** by value, and structs with **bit fields** by value (pointers to them work). +- **Function-like macros** and macros that are not a single number. - `long double`, 128-bit integers. --- @@ -885,19 +823,6 @@ Instead of undefined behavior (as in C), Finch stops with a message and exit cod | `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]` | -| `runtime error: assert failed: …` | `assert(cond, "…")` with a false `cond` | - -Under the message comes the chain of calls that led there, the innermost first: - -``` -game.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (game.fch:6) - in step (game.fch:10) - in main (game.fch:15) -``` - -`finch run`, `finch test`, `-g` and `-O0` builds keep it; a plain `finch build` leaves it out because keeping it costs -time in every call (about 3x in a program that is nothing but tiny calls, much less in a real one). `--trace` puts it in. --- @@ -977,14 +902,13 @@ Visual Studio debugger and WinDbg read. ``` Finch/ ├── src/ the compiler, in Finch (lexer, parser, gen: LLVM IR as text, cimport: libclang, lsp, main ...) -├── std/ the standard library, in Finch: json, path, time, proc, fs, thread +├── std/ the standard library, in Finch: json, path, time, proc ├── runtime/ finch_rt.c: the small runtime (text, arrays, maps, input, files) ├── bootstrap/ build.sh, install.sh and the seeds (the compiler's own IR, one per system) ├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm ├── tests/ │ ├── run/ new/ programs + the exact output they must print (.out), input (.in) │ ├── fail/ new/fail/ programs that must fail, with the expected error in line 1 -│ ├── tool/ finch test, finch fmt, the chain of calls, a finch.toml project │ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind) │ ├── cross.sh every test built for Windows / ARM64 and run under Wine / QEMU │ └── lsp_test.py the language server, talked to like an editor does @@ -1111,11 +1035,6 @@ library's second, so your own file of the same name wins. | `time` | `now()`, `millis()` (a clock for measuring), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` | | `proc` | `run(cmd)` (the exit code), `output(cmd)` (what it printed; an error if it failed), `env(name, fallback)`, `pid()`, `cwd()` | | `json` | `parse(text)`, `stringify(v)`, `pretty(v)`, and the value type `Json` with `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... | -| `fs` | `read_bytes(p)` / `write_bytes(p, data)` (`[]u8`, any bytes), `exists`, `is_dir`, `mkdir` (with the folders above), `list` (sorted names), `remove_dir`, `data_dir(app)` (a folder for the user's saves) | -| `thread` | `spawn(addr(work), addr(data))`, `t.join()`, `mutex()` with `lock`, `unlock`, `close`, `cpus()` (section 29) | - -`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` and `clamp(x, lo, hi)` work on any numbers without an import (a function of -your own with the same name wins). ``` import json @@ -1133,165 +1052,7 @@ Not there yet: - Interfaces. - Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII. -- Checks that stop two threads from changing the same array at once (use a mutex). -- Windows on ARM64, 32-bit systems, WebAssembly. `--target android` makes code for Android, but linking with the NDK and running on a device are untried. - -### Warnings about big copies - -A struct with 256 bytes or more of plain data (numbers, structs of numbers, C's `[N]T`) is copied byte by byte when it -is a parameter (`fn f(Body b)`) and in `for x in bodies`. Finch says so, and still builds the program: - -``` -game.fch:27:11: warning: 'b' is a Body (264 bytes), copied on every call to weight; take ptr[Body] to share it instead (callers can still pass the variable as it is) -game.fch:45:5: warning: each Body (264 bytes) is copied into 'x'; for i in 0..bodies.len with bodies[i] works on it in place -``` - -Text, arrays and maps inside a struct don't count: passing the struct copies only their handles, not what they hold. -`b := a` is not warned about: it says "copy" in the code. A method never copies `self`. Editors show the warnings in yellow. +- Threads. +- Windows on ARM64, 32-bit systems, WebAssembly. See the [roadmap in the README](../../README.md#roadmap). - -## 27. Tests: test blocks and finch test - -Tests go next to the code they test: - -```c -fn damage(int hp, int hit) -> int { - return max(hp - hit, 0) -} - -test "damage never goes below zero" { - assert(damage(5, 10) == 0) - assert(damage(5, 2) == 3, "got \(damage(5, 2))") -} -``` - -`finch test game.fch` runs every `test` block of the file and of the modules it imports (not of the standard library), -each on its own: a failed `assert` or any runtime error ends only that test, and the next one runs. - -``` -ok damage never goes below zero -FAIL loading a level - level.fch:31: runtime error: can't read the file "levels/1.txt" - -1 passed, 1 failed -``` - -The exit code is 1 when a test failed, so CI can use it. `finch run` and `finch build` leave test blocks out. -`assert(cond)` also works outside tests: it stops the program with `assert failed` when `cond` is false. - -## 28. A project: finch.toml - -A `finch.toml` next to the code says once what would otherwise go on every command line: - -```toml -name = "game" # the program finch build makes (from the entry) -entry = "src/main.fch" # used when finch run / build / test get no file -paths = ["src", "lib"] # folders with Finch modules -include = ["vendor/SDL3/include"] # folders with C headers (also for your .c / .cpp files) -links = ["SDL3", "native/glue.cpp"] # libraries, and your own .c / .cpp / .o / .a files -libdirs = ["vendor/SDL3/lib"] # folders with libraries -cflags = ["-DFAST"] # for your .c / .cpp files -ldflags = ["-Wl,-rpath,$ORIGIN"] # for the linker - -[target.windows] # added only when building for Windows -links = ["ws2_32"] -libdirs = ["vendor/SDL3/lib/x64"] - -[target.linux-arm64] # system and CPU: windows, linux, macos, android, and -x86_64 / -arm64 -libdirs = ["vendor/SDL3/lib/arm64"] -``` - -Folders are relative to `finch.toml`. Finch looks for it in the file's folder and the folders above, so -`finch run src/tools/editor.fch` inside the project uses it too. Only this much of TOML is read: `[sections]`, -`key = "text"`, `key = ["a", "b"]` (over several lines too), numbers, `true`/`false` and `#` comments. - -## 29. Threads - -```c -import thread - -struct Chunk { - []float samples - float peak -} - -fn analyse(ptr arg) { // a thread's function takes one ptr - ptr[Chunk] c = arg - for s in c.samples { - c.peak = max(c.peak, abs(s)) - } -} - -fn main() { - []Chunk chunks = load_chunks() - []thread.Thread running - for i in 0..chunks.len { - running.push(thread.spawn(addr(analyse), addr(chunks[i])) or { - print(err) - exit(1) - }) - } - for i in 0..running.len { - running[i].join() // wait for all of them - } -} -``` - -What a thread gets is shared, not copied, so: - -- it must live until `join()` (here `chunks` lives to the end of `main`); -- don't change the array the threads point into (no `push`) until they are joined; -- what more than one thread changes goes under a lock, or is a number changed with `atomic_...`. - -```c -m := thread.mutex() -defer m.close() -m.lock() -log.push("done") // one thread at a time between lock and unlock -m.unlock() - -atomic_add(addr(done), 1) // done is an int shared by the threads -n := atomic_load(addr(done)) -``` - -`atomic_add`, `atomic_sub`, `atomic_swap` give back the old value; `atomic_cas(p, old, new)` puts `new` only if `old` -is there and says whether it did. `thread.cpus()` says how many threads the machine runs at once. - -## 30. C++, Rust and Android - -Finch talks to other languages through C, the way every language does. - -**C++**: put what Finch calls behind a C header, with `extern "C"` on the C++ side: - -```cpp -// glue.h -#ifdef __cplusplus -extern "C" { -#endif -int physics_step(float dt); -#ifdef __cplusplus -} -#endif -``` - -and `import "glue.h"` with `link "glue.cpp"` (or `links = ["glue.cpp"]` in `finch.toml`). The `.cpp` file is built with -the C++ compiler (`clang++` or `g++`, or `FINCH_CXX`), which then also links, so the C++ library comes along. -A C++ library that has a C API (Box2D 3, Jolt's JoltC, PhysX's C wrapper) is imported like any C library. - -**Rust**: make a `staticlib` and export C functions: - -```rust -// Cargo.toml: [lib] crate-type = ["staticlib"] -#[no_mangle] -pub extern "C" fn path_len(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() } -``` - -Write the header (`float path_len(float x, float y);`, or let `cbindgen` write it), then `import "pathfind.h"` and -`link "target/release/libpathfind.a"`. On Linux a Rust static library also needs `links = ["dl", "pthread"]`. - -**Android**: `finch build game.fch --target android --shared` makes `libgame.so` for 64-bit ARM Android (API 24 and -up) with the NDK's clang, found through `ANDROID_NDK_HOME` (or `FINCH_CC`). Load it from an app, for example through -SDL3's Android project (the library's `SDL_main` is an `export fn`). This is new and only partly checked: Finch's -output compiles for Android (thread-local data goes through the emulated TLS older Android needs), but linking with the -NDK and running on a device have not been tried yet. diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index c948e86..6378e6d 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -1,6 +1,6 @@ # Finch dla inżynierów -**Wnętrze Fincha 2.5, którego kompilator jest napisany w Finchu.** Jak kod źródłowy staje się tokenami, drzewem AST i +**Wnętrze Fincha 2.4, którego kompilator jest napisany w Finchu.** Jak kod źródłowy staje się tokenami, drzewem AST i LLVM IR *jako tekstem*, który `clang` kompiluje i linkuje z małym runtime'em w C; jak nagłówki C czyta libclang wywoływany z samego Fincha i jak ręcznie odtwarzane są konwencje wywołań C; reguły typów i własności; jak kompilator buduje sam siebie z ziarna; oraz serwer języka. @@ -68,20 +68,18 @@ Decyzje projektowe: | Plik | Linie | Zadanie | |---|---|---| -| `src/lexer.fch` | 470 | Tokeny i pozycje, tekst `"""`, `"\(x)"` zamienione na tokeny `("…" + str(x) + "…")` | +| `src/lexer.fch` | 220 | Tokeny i pozycje | | `src/ast.fch` | 120 | Jedna struktura `Node` z napisem `kind`, dziećmi, nazwami typów, pozycjami (`line`, `endLine`) | -| `src/parser.fch` | 910 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, generyki (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` | -| `src/gen.fch` | 6450 | Kontrola typów + IR: struktury, funkcje, metody, zakresy i zwolnienia, instrukcje, wyrażenia, wbudowane, mapy, błędy jako wartości, generyki (monomorfizacja), enumy i `match`, metadane `-g`, tryb `index` | -| `src/cimport.fch` | 790 | libclang: funkcje, struktury i unie (pola, offsety, wypełnienie), enumy, stałe, globalne, makra (także będące strukturą) | +| `src/parser.fch` | 780 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, generyki (`[T]`), `enum`, `match`, `export`, `link` | +| `src/gen.fch` | 5200 | Kontrola typów + IR: struktury, funkcje, metody, zakresy i zwolnienia, instrukcje, wyrażenia, wbudowane, mapy, błędy jako wartości, generyki (monomorfizacja), enumy i `match`, metadane `-g`, tryb `index` | +| `src/cimport.fch` | 560 | libclang: funkcje, struktury (pola, offsety, wypełnienie), enumy, stałe, globalne, makra | | `src/platform.fch` | 90 | `Target`: triple, układ danych, kompilator C, znak `char` zależnie od systemu | -| `src/linker.fch` | 430 | Pamięć podręczna (runtime, obiekt programu, pliki C/C++), biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera | -| `src/main.fch` | 515 | Sterownik: CLI, loader z wyszukiwaniem modułów i nakładkami, `build/run/test/fmt/ir/index/lsp` | -| `src/manifest.fch` | 240 | `finch.toml`: mały czytnik TOML, listy zależne od celu | -| `src/fmt.fch` | 225 | `finch fmt` i formatowanie w LSP | -| `src/lsp.fch` | 910 | Serwer języka (§11) | +| `src/linker.fch` | 300 | Pamięć podręczna, obiekt runtime'u, biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera | +| `src/main.fch` | 310 | Sterownik: CLI, loader z wyszukiwaniem modułów i nakładkami, `build/run/ir/index/lsp` | +| `src/lsp.fch` | 875 | Serwer języka (§11) | | `src/docs.fch` | 110 | Jeden opis wbudowanych funkcji dla podpowiedzi i uzupełniania | -| `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` | -| `runtime/finch_rt.c` | 1150 | Runtime w C: tekst, tablice, mapy, pliki, wypisywanie liczb, fs, wątki, uruchamianie testów, łańcuch wywołań | +| `std/*.fch` | 900 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`) | +| `runtime/finch_rt.c` | 760 | Runtime w C | | `bootstrap/` | | `build.sh`, `install.sh`, ziarna (§10) | --- @@ -99,18 +97,6 @@ kolejność `fn nazwa(...) typ` to błąd składni z podpowiedzią. katalogów z `FINCH_PATH`, potem biblioteki standardowej `std/`. Każdy moduł jest parsowany raz; jego nazwy są używane jako `modul.nazwa`. `import "x.h"` trafia do importera C. -Moduł rozpoznaje się po **pliku**: kluczem jest jego ścieżka od folderu głównego pliku (albo od folderu z `FINCH_PATH` -lub `std/`) bez `.fch` i bez końcówki `_`, np. `ui/przycisk`. `Program.imports` trzyma nazwę używaną w danym -pliku (`przycisk`, albo `p` przy `import ui/przycisk as p`), a `Program.modules` klucz; `modFor()` zamienia jedno na drugie. -Symbole używają klucza z `/` zamienionym na `$` (`@finch.ui$przycisk.rysuj`). - -**Stałe.** `const NAZWA = wyrażenie` to węzeł najwyższego poziomu; `constValue()` liczy wyrażenie przy każdym użyciu, w -module, w którym je zdefiniowano, bez zmiennych lokalnych, więc wychodzi literał (operatory na literałach całkowitych -składa `arith`). `checkConst()` dopuszcza tylko literały, nazwy, operatory, pola i konwersje liczb; `constBusy` łapie pętle. - -**Dodatki leksera.** `1e9` / `2.5e-3` to tokeny float (tekst dostaje `.` dla LLVM); teksty `"""` tracą wcięcie -linii zamykającej w `longString()`. - --- ## 4. Generowanie kodu @@ -144,14 +130,6 @@ LLVM IR nie realizuje za ciebie konwencji wywołań C, więc `gen.fch` ją odtwa - reguły Microsoft x64 (struktury 1, 2, 4, 8 bajtów w rejestrze, reszta przez wskaźnik); - `makePlan` zamienia klasyfikację w plan wywołania; `cThunk` robi z `addr(fn)` coś, co C może wywołać (callbacki). -**Makra.** `#define` będący jedną liczbą albo tekstem czyta się z tokenów. Taki, którego tokeny mogą być wyrażeniem -stałym (bez klamer, średników i słów instrukcji), liczy clang: drugie parsowanie nagłówka dodaje -`static const __auto_type __finch_m_NAZWA = (NAZWA);` dla każdego, a `clang_Cursor_Evaluate` daje liczbę całkowitą, -double albo tekst. Te, które nie są stałymi, dają tylko błędy w tym drugim parsowaniu i są pomijane. - -**Unie** to `CStruct` z `isUnion`: typ LLVM to `{ [n x iA] }` (rozmiar i wyrównanie od clanga), każde pole ma -przesunięcie 0, a pole czyta się przez pamięć. Nie da się ich przekazać do C przez wartość (`unsupported` mówi dlaczego). - Ten sam kod pozwala kompilatorowi wołać libclang, którego API przekazuje `CXCursor`, `CXType` i `CXString` przez wartość. --- @@ -161,7 +139,7 @@ Ten sam kod pozwala kompilatorowi wołać libclang, którego API przekazuje `CXC `finch build|run|ir|index plik.fch [-o wyjście] [-O0] [-g] [-l biblioteka] [-I katalog] [--target cel] [--shared] [--overlay ścieżka=plik]`. `ir` wypisuje IR; `index` wypisuje rekordy dla serwera języka (§11); `--shared` robi bibliotekę współdzieloną z `export fn`. -`--target windows|linux|arm64|macos|android|` zmienia triple i układ danych, znak `char`, reguły ABI powyżej, wariant OS +`--target windows|linux|arm64|macos|` zmienia triple i układ danych, znak `char`, reguły ABI powyżej, wariant OS modułów (`nazwa_windows.fch`) i kompilator C: `FINCH_CC` albo `clang --target=…`. Przy linkowaniu `pkg-config` podaje flagi dla `link "biblioteka"`; gdy linker zawiedzie, wynik jest analizowany i podpowiadane jest brakujące `link "…"`. Obiekt runtime'u jest kluczowany skrótem jego źródła i celu. @@ -171,38 +149,6 @@ zawsze kompiluje `clang`, nigdy `CC`), `FINCH_LIBCLANG` (dla `bootstrap/build.sh Kompilator znajduje `runtime/` i `std/` obok siebie (`../`, `./`) albo w `../share/finch/` (`bootstrap/install.sh`). -**Pamięć podręczna budowania** (`host.cacheDir()`: `$XDG_CACHE_HOME/finch`, `~/.cache/finch`, `%LOCALAPPDATA%\finch`): - -- `ir/.o` z plikiem `.key`, w którym jest skrót clanga, flag i całego IR. Jedno miejsce - na program, więc cache nie rośnie z każdą zmianą; to samo IR daje ten sam obiekt bez uruchamiania clanga. -- `c/.o` dla `link "x.c"` / `.cpp`, budowane z `-MMD`; `.key` to skrót każdego - pliku z listy `.d` (źródła i jego nagłówków). Zmiana któregokolwiek przebudowuje obiekt. -- Obiekty są zapisywane do pliku `.part` i przemianowywane, więc dwa budowania naraz nie widzą pół pliku. - Skróty to dwa niezależne 32-bitowe (FNV-1a i djb2), razem 64 bity. - -**`finch.toml`** (`src/manifest.fch`) jest szukany od folderu pliku głównego (albo bieżącego) w górę. Trafia do -`values["sekcja.klucz"]` i `lists["sekcja.klucz"]`; `list(klucz, cele)` skleja listę z najwyższego poziomu i te z -`[target.]` oraz `[target.-]`. Ścieżki liczą się od folderu manifestu. Wpisy trafiają tam, gdzie opcje -z linii poleceń: `paths` → loader, `include` → `-I` (libclang i pliki C), `links` → `link`, `libdirs` i `ldflags` → -polecenie linkowania, `cflags` → pliki C/C++. - -**Pliki C++**: `.cpp`/`.cc`/`.cxx` w `links` kompiluje `cxxFor(cc)` (`FINCH_CXX` albo `clang` → `clang++`, -`…gcc` → `…g++`, `cc` → `c++`), a potem ten sam sterownik linkuje, co dokłada standardową bibliotekę C++. - -**Android**: `--target android` to `aarch64-linux-android24`; kompilator C to clang z NDK, -`toolchains/llvm/prebuilt//bin/aarch64-linux-android24-clang` (z `ANDROID_NDK_HOME`/`ANDROID_NDK_ROOT`), linkowanie -dodaje `-llog` i nie dodaje `-lpthread` (bionic ma go w libc). Wariant OS modułów to `linux`. - -**`finch test`** kompiluje jak `run`, z `Options.test`: wszystkie funkcje są dalej definiowane, ale `@main` wywołuje -`finch_test_run(nazwa, fn)` dla każdego bloku `test` z modułów użytkownika (`__test_`, funkcja `void` zrobiona przez -parser), a na końcu `finch_test_summary()`. Runtime uruchamia każdy test pod `setjmp`; `fatal()` (każdy błąd w czasie -działania i nieudany `assert`) wraca przez `longjmp` z `fatal_hook`, więc jeden nieudany test nie kończy całości. -Pamięć, którą test miał w tym momencie, nie jest zwalniana. - -**`finch fmt`** działa na liniach: śledzi `{ }`, `( )`/`[ ]`, teksty (z `\( )`), `'…'`, `"""` i `/* */`. Linia, która -zaczyna się na głębokości nawiasów 0 i nie ciągnie linii zakończonej operatorem albo `,`, dostaje wcięcie 4 × głębokość -klamer; pozostałe (ciągi dalsze, wnętrza `"""`) przesuwa się o tyle samo co linię, która zaczęła instrukcję, więc ich -kształt (i ucinanie wcięcia `"""`) zostaje. --- ## 7. System typów @@ -248,10 +194,9 @@ Literały tablic biorą typ elementu z miejsca, do którego trafiają (`exprWant ### Argumenty operatorów: `unify(l, r)` -Strona-literał dopasowuje się do drugiej (także literał float obok `f32`: `x * 2.0` zostaje `f32`); w przeciwnym razie -rośnie strona, która się rozszerza; `i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa; -f32 + f64 → f64. Nie ma promocji całkowitych z C: `u8 + u8` zostaje `u8`. Dwa literały całkowite składają się w literał, -więc `FLAG_A | FLAG_B` dalej mieści się w `u64` albo `u32`. +Strona-literał dopasowuje się do drugiej; w przeciwnym razie rośnie strona, która się rozszerza; +`i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa; f32 + f64 → f64. +Nie ma promocji całkowitych z C: `u8 + u8` zostaje `u8`. ### Konwersje jawne: `convert()` @@ -467,48 +412,6 @@ serwer podpowiada dalej z ostatniej wersji, która się skompilowała. Test akce --- -### Łańcuch wywołań przy błędach w czasie działania - -Z `Options.trace` (domyślnie dla `run`, `test`, `-g`, `-O0`; `--trace`/`--no-trace`; nigdy z `--shared`) każda funkcja -Fincha ma ramkę na swoim stosie: - -```llvm -%frame = alloca { ptr, ptr, ptr, i64 } ; ramka wywołującego, nazwa, plik, linia -store ptr (load @finch_trace), ...pole 0 -store ptr @"nazwa", ...pole 1 ; ptr @"plik", pole 2 ; i64 , pole 3 -store ptr %frame, ptr @finch_trace -``` - -`@finch_trace` to globalna `thread_local` w module programu (model initial-exec, chyba że biblioteka albo Windows); -każda instrukcja zapisuje swoją linię do pola 3. Po każdym wywołaniu funkcji Fincha wywołujący wpisuje z powrotem swoją -ramkę do `@finch_trace`, co sprząta też ramki funkcji, która wróciła wcześniej, więc wiele ścieżek `return` nie -potrzebuje epilogu. Opakowania wołane z C (thunki `addr(fn)`, `export fn`) zapamiętują i przywracają `@finch_trace`. -`main` woła `finch_trace_init(top, set)` z dwiema małymi funkcjami do czytania i zapisu, i tak runtime (`print_trace()` -w `fatal()` oraz uruchamianie testów) dostaje się do zmiennej wątku, której nie zna z nazwy. Koszt: ok. 3 razy na -`fib(40)`, który jest samymi wywołaniami; dlatego zwykły `finch build` go pomija. - -### Wątki i operacje atomowe - -`std/thread.fch` to cienka warstwa nad runtime'em (`finch_thread_spawn/join`, `finch_mutex_*`: pthreads albo -`CreateThread` i `CRITICAL_SECTION` na Windowsie). Funkcja wątku to dowolna `fn f(ptr)` podana przez `addr(f)`, co robi -thunk wołany z C. `atomic_add/sub/swap` to `atomicrmw add/sub/xchg … seq_cst`, `atomic_load/store` to -`load/store atomic … seq_cst, align `, a `atomic_cas` to `cmpxchg … seq_cst seq_cst` zwracające `i1`. Biorą -`ptr[T]` do dowolnej liczby całkowitej. Tablice, teksty i mapy Fincha nie są synchronizowane; dzielenie ich między -wątkami wymaga mutexu. - -### Ostrzeżenia - -`warn()` wypisuje `plik:linia:kolumna: warning: …` na stderr (więc `finch ir` daje samo IR) albo, w trybie `index`, -rekord `@W`, który serwer języka publikuje z ważnością 2. Jedno na miejsce w kodzie. Są dwa: parametr-struktura oraz -element `for x in lista`, gdy ich zwykłe dane (`byteSize()`: liczby, zagnieżdżone struktury, `[N]T` z C; `str`, tablice -i mapy liczą się jako 0, bo kopiowany jest tylko ich uchwyt) mają 256 bajtów lub więcej. - -### Struktura jako argument przez adres - -`argWant()` (tylko argumenty wywołań, nie przypisania): gdy parametr to `ptr[S]`, a argument to zwykła zmienna typu `S`, -argumentem jest adres zmiennej, jakby napisano `addr(x)`. Tylko zmienne, więc nic nie jest liczone dwa razy, a na -wartości tymczasowe nic nie wskazuje. - ## 13. Znane ograniczenia - Kompilator działa tylko na Linuksie x86-64 i ARM64 (brak ziarna dla macOS i Windowsa). Programy można budować na wszystkie trzy. @@ -518,6 +421,4 @@ wartości tymczasowe nic nie wskazuje. - Sterownik używa narzędzi `sh` (`rm`, `mkdir`), więc wymaga powłoki POSIX. - Generyki są monomorfizowane (kod rośnie z liczbą instancji); nie ma ograniczeń na typy, błędne użycie to błąd wewnątrz wygenerowanego ciała. -- Pamięć nieudanego testu nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`). -- `--target android` jest sprawdzany do pliku obiektowego; linkowanie wymaga NDK i nie było uruchamiane na urządzeniu. diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md index 5b48cf2..a0d5355 100644 --- a/docs/pl/dla-poczatkujacych.md +++ b/docs/pl/dla-poczatkujacych.md @@ -76,7 +76,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.5.0`. +Powinieneś zobaczyć coś w stylu `finch 2.4.0`. --- diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md index 4ebceba..506792d 100644 --- a/docs/pl/dla-technikow.md +++ b/docs/pl/dla-technikow.md @@ -37,10 +37,6 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla- 24. [Typy generyczne](#24-typy-generyczne) 25. [Biblioteka standardowa](#25-biblioteka-standardowa) 26. [Obecne ograniczenia](#26-obecne-ograniczenia) -27. [Testy: bloki test i finch test](#27-testy-bloki-test-i-finch-test) -28. [Projekt: finch.toml](#28-projekt-finchtoml) -29. [Wątki](#29-wątki) -30. [C++, Rust i Android](#30-c-rust-i-android) --- @@ -142,31 +138,17 @@ katalogi, w których szukać modułów. ``` finch run [argumenty...] skompiluj i od razu uruchom finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) -finch test uruchom jego bloki test "..." { } (rozdział 27) -finch fmt [--check] [pliki/foldery] uporządkuj wcięcia plików .fch (domyślnie wszystkich stąd w dół) finch ir pokaż LLVM IR (dla ciekawskich) finch lsp serwer języka, dla edytorów finch version pokaż wersję opcje: -l dolinkuj bibliotekę C, to samo co link "lib" w pliku - -I szukaj nagłówków C także tam - --target zbuduj na inny system: windows, linux, arm64, macos, android albo triple LLVM + --target zbuduj na inny system: windows, linux, arm64, macos albo triple LLVM -g dodaj informacje dla debuggera (gdb / lldb / Visual Studio) -O0 bez optymalizacji - --trace błędy w czasie działania pokazują łańcuch wywołań (i tak włączone dla run, test, -g i -O0) - --no-trace nie pokazują (zwykły finch build go nie ma: pilnowanie łańcucha spowalnia wywołania) ``` -W folderze z plikiem `finch.toml` (rozdział 28) `finch run`, `finch build` i `finch test` nie potrzebują nazwy pliku. - -Skompilowany program i pliki C, które buduje (`link "x.c"`), zostają w cache (`~/.cache/finch`, na Windowsie -`%LOCALAPPDATA%`). Drugie budowanie tego samego kodu albo kodu, którego pliki C się nie zmieniły, pomija tę pracę. - -`finch fmt` zmienia tylko układ: cztery spacje na poziom `{ }`, bez spacji na końcach linii, najwyżej jedna pusta linia -z rzędu. Linie, które ciągną wyrażenie (w `( )` albo po `+`), i tekst `"""` zachowują swój kształt. `finch fmt --check` -niczego nie zmienia i kończy się błędem, gdy jakiś plik by się zmienił (dla CI). Edytory z serwerem języka formatują na żądanie. - Przykłady: ```sh @@ -232,19 +214,7 @@ x += 1 // także -= *= /= %= - Nazwy: litery ASCII, cyfry i `_`, bez cyfry na początku. Polskie litery tylko w tekstach w cudzysłowie. Nazwy typów (`int`, `u8`, …), funkcji wbudowanych i słowa kluczowe są zarezerwowane. -**Słowa kluczowe:** `fn return if else while for in break continue true false null import link struct enum match defer or try const export` - -### Stałe - -```c -const SZEROKOSC = 640 -const WYSOKOSC = SZEROKOSC / 2 // z innych stałych -const f32 GRAWITACJA = 9.8 // z typem -const TYTUL = "Moja gra" -``` - -Stałą piszesz na najwyższym poziomie pliku, obok funkcji, i nie da się jej zmienić. Składa się z liczb, tekstu, -`true`/`false`, znaków, innych stałych i operatorów; stała z modułu to `ksztalty.BOKI`. +**Słowa kluczowe:** `fn return if else while for in break continue true false null import link struct defer` --- @@ -346,13 +316,6 @@ Uwagi: ## 8. Sterowanie przebiegiem -`if` może też dawać wartość; każda gałąź to jedna wartość, a `else` jest wymagane: - -```c -opis := if wynik > 100 { "dużo" } else if wynik > 10 { "średnio" } else { "mało" } -f32 predkosc = if biegnie { 2.5 } else { 1.0 } -``` - ```c if x > 10 { ... @@ -523,29 +486,8 @@ s[0] = 'D' // zmiana znaku | `s.bytes()` | `[]u8` z bajtami | | `s.ptr` | `ptr[char]` dla C (ważny, dopóki żyje `s`) | -Wartości wstawia się do tekstu przez `\(...)`: dowolny kod, a `, 2` po liczbie daje dwie cyfry po przecinku. -`\` przed `(`, które ma zostać zwykłym znakiem: `"\\(to nie kod)"`. - -```c -print("hp \(hp) z \(max_hp), prędkość \(v, 2), imię \(p.imie.upper())") -``` - `str` pamięta swoją długość, więc `.len` działa natychmiast. Zawsze kończy się zerem, więc -przekazanie go do C jako `char*` działa. W drugą stronę: funkcja C, która zwraca `char *`, daje ci `str` -(kopia powstaje, gdy go zachowujesz), a `str(p)` czyta `ptr[char]` zdobyty inaczej. - -Tekst w kilku liniach piszesz między `"""`. Znak nowej linii zaraz po otwarciu znika, a z każdej linii znika -wcięcie zamykających cudzysłowów, więc tekst może być wcięty razem z kodem: - -```c -shader := """ - #version 330 - void main() { gl_Position = vec4(0.0); } - """ -``` - -Liczby jako tekst: `str(x)` to najkrótszy tekst, który czyta się z powrotem jako ta sama liczba (`0.1`, -`0.30000000000000004`, `1e+20`); `str(x, 2)` ma zawsze dwie cyfry po kropce (`3.14`). +przekazanie go do C jako `char*` działa. --- @@ -702,8 +644,6 @@ fn main() { - Wszystkiego z modułu używasz z jego nazwą z przodu: `ksztalty.pole`, `ksztalty.Pudlo`. - Moduł nie ma `main`. Moduły mogą importować inne moduły. -- Moduły w folderach: `import ui/przycisk` wczytuje `ui/przycisk.fch`, a używasz go jako `przycisk.rysuj()`. -- Inna nazwa: `import ui/przycisk as p` → `p.rysuj()`. Dwa importy nie mogą mieć tej samej nazwy. --- @@ -804,10 +744,8 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k ### Co jeszcze nie działa -- **Unie** C i struktury z **polami bitowymi** przekazywane do funkcji C albo zwracane z niej **przez wartość** - (wskaźniki do nich działają, unie jako zmienne też: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`). -- **Makra-funkcje** (`#define MAX(a, b) ...`). Makra będące liczbą, tekstem, wyrażeniem z nich - (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) albo strukturą z liczb (`RAYWHITE` z raylib, `(Color){ 245, 245, 245, 255 }`) działają. +- **Unie** C przez wartość i struktury z **polami bitowymi** przez wartość (wskaźniki do nich działają). +- **Makra-funkcje** i makra, które nie są pojedynczą liczbą. - `long double`, liczby 128-bitowe. --- @@ -888,19 +826,6 @@ Zamiast niezdefiniowanego zachowania (jak w C) Finch zatrzymuje się z komunikat | `runtime error: can't read the file "…"` | `read_file` na brakującym albo nieczytelnym pliku (obsłuż to przez `or`) | | `runtime error: the key "x" is not in the map …` | `m["x"]`, gdy mapa nie ma takiego klucza (użyj `.has` albo `.get`) | | `runtime error: called .f() on a null pointer` | metoda wywołana przez `null` w `ptr[T]` | -| `runtime error: assert failed: …` | `assert(warunek, "…")` z fałszywym warunkiem | - -Pod komunikatem jest łańcuch wywołań, które do niego doprowadziły, od najgłębszego: - -``` -gra.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (gra.fch:6) - in step (gra.fch:10) - in main (gra.fch:15) -``` - -`finch run`, `finch test` oraz budowanie z `-g` i `-O0` go mają; zwykły `finch build` go pomija, bo pilnowanie go kosztuje -czas w każdym wywołaniu (ok. 3 razy w programie złożonym z samych drobnych wywołań, dużo mniej w prawdziwym). `--trace` go dodaje. --- @@ -981,14 +906,13 @@ CodeView, który czyta debugger Visual Studio i WinDbg. ``` Finch/ ├── src/ kompilator, w Finchu (lekser, parser, gen: LLVM IR jako tekst, cimport: libclang, lsp, main ...) -├── std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs, thread +├── std/ biblioteka standardowa, w Finchu: json, path, time, proc ├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, mapy, wejście, pliki) ├── bootstrap/ build.sh, install.sh i ziarna (własny IR kompilatora, po jednym na system) ├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm ├── tests/ │ ├── run/ new/ programy + dokładny wynik (.out) i wejście (.in) │ ├── fail/ new/fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce -│ ├── tool/ finch test, finch fmt, łańcuch wywołań, projekt z finch.toml │ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem) │ ├── cross.sh każdy test zbudowany na Windows / ARM64 i uruchomiony w Wine / QEMU │ └── lsp_test.py serwer języka, obsługiwany jak przez edytor @@ -1115,11 +1039,6 @@ bibliotece — więc Twój własny plik o tej nazwie ma pierwszeństwo. | `time` | `now()`, `millis()` (zegar do mierzenia), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` | | `proc` | `run(cmd)` (kod wyjścia), `output(cmd)` (co wypisało; błąd, jeśli się nie udało), `env(nazwa, domyślna)`, `pid()`, `cwd()` | | `json` | `parse(tekst)`, `stringify(v)`, `pretty(v)` oraz typ wartości `Json` z `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... | -| `fs` | `read_bytes(p)` / `write_bytes(p, dane)` (`[]u8`, dowolne bajty), `exists`, `is_dir`, `mkdir` (z folderami wyżej), `list` (posortowane nazwy), `remove_dir`, `data_dir(app)` (folder na zapisy użytkownika) | -| `thread` | `spawn(addr(praca), addr(dane))`, `t.join()`, `mutex()` z `lock`, `unlock`, `close`, `cpus()` (rozdział 29) | - -`min(a, b, ...)`, `max(a, b, ...)`, `abs(x)` i `clamp(x, od, do)` działają na dowolnych liczbach bez importu (twoja -własna funkcja o tej samej nazwie wygrywa). ``` import json @@ -1137,167 +1056,7 @@ Jeszcze nie ma: - Interfejsów. - Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII. -- Pilnowania, żeby dwa wątki nie zmieniały tej samej tablicy naraz (do tego mutex). -- Windowsa na ARM64, systemów 32-bitowych, WebAssembly. `--target android` robi kod dla Androida, ale linkowanie - z NDK i uruchomienie na urządzeniu nie były jeszcze próbowane. - -### Ostrzeżenia o dużych kopiach - -Struktura z 256 lub więcej bajtami zwykłych danych (liczby, struktury z liczb, `[N]T` z C) jest kopiowana bajt po bajcie, -gdy jest parametrem (`fn f(Cialo c)`) i w `for x in ciala`. Finch o tym mówi, a program i tak buduje: - -``` -gra.fch:27:11: warning: 'c' is a Cialo (264 bytes), copied on every call to waga; take ptr[Cialo] to share it instead (callers can still pass the variable as it is) -gra.fch:45:5: warning: each Cialo (264 bytes) is copied into 'x'; for i in 0..ciala.len with ciala[i] works on it in place -``` - -Teksty, tablice i mapy w strukturze się nie liczą: przekazanie struktury kopiuje tylko ich uchwyty, nie zawartość. -`b := a` nie dostaje ostrzeżenia: w kodzie widać, że to kopia. Metoda nigdy nie kopiuje `self`. Edytory pokazują -ostrzeżenia na żółto. +- Wątków. +- Windowsa na ARM64, systemów 32-bitowych, WebAssembly. Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju). - -## 27. Testy: bloki test i finch test - -Testy stoją obok kodu, który sprawdzają: - -```c -fn obrazenia(int hp, int cios) -> int { - return max(hp - cios, 0) -} - -test "obrażenia nigdy nie schodzą poniżej zera" { - assert(obrazenia(5, 10) == 0) - assert(obrazenia(5, 2) == 3, "wyszło \(obrazenia(5, 2))") -} -``` - -`finch test gra.fch` uruchamia każdy blok `test` z pliku i z modułów, które importuje (bez biblioteki standardowej), -każdy osobno: nieudany `assert` albo dowolny błąd w czasie działania kończy tylko ten test, a następny rusza dalej. - -``` -ok obrażenia nigdy nie schodzą poniżej zera -FAIL wczytanie poziomu - poziom.fch:31: runtime error: can't read the file "levels/1.txt" - -1 passed, 1 failed -``` - -Kod wyjścia to 1, gdy jakiś test nie przeszedł, więc CI może z tego korzystać. `finch run` i `finch build` pomijają bloki test. -`assert(warunek)` działa też poza testami: zatrzymuje program z `assert failed`, gdy warunek jest fałszywy. - -## 28. Projekt: finch.toml - -`finch.toml` obok kodu mówi raz to, co inaczej trzeba by podawać w każdym poleceniu: - -```toml -name = "gra" # program, który robi finch build (z entry) -entry = "src/main.fch" # gdy finch run / build / test nie dostaną pliku -paths = ["src", "lib"] # foldery z modułami Fincha -include = ["vendor/SDL3/include"] # foldery z nagłówkami C (też dla własnych .c / .cpp) -links = ["SDL3", "native/glue.cpp"] # biblioteki i własne pliki .c / .cpp / .o / .a -libdirs = ["vendor/SDL3/lib"] # foldery z bibliotekami -cflags = ["-DFAST"] # dla własnych plików .c / .cpp -ldflags = ["-Wl,-rpath,$ORIGIN"] # dla linkera - -[target.windows] # dodawane tylko przy budowaniu na Windowsa -links = ["ws2_32"] -libdirs = ["vendor/SDL3/lib/x64"] - -[target.linux-arm64] # system i procesor: windows, linux, macos, android oraz -x86_64 / -arm64 -libdirs = ["vendor/SDL3/lib/arm64"] -``` - -Foldery liczą się od `finch.toml`. Finch szuka go w folderze pliku i w folderach wyżej, więc -`finch run src/narzedzia/edytor.fch` wewnątrz projektu też z niego korzysta. Czytana jest tylko taka część TOML: -`[sekcje]`, `klucz = "tekst"`, `klucz = ["a", "b"]` (także w kilku liniach), liczby, `true`/`false` i komentarze `#`. - -## 29. Wątki - -```c -import thread - -struct Kawalek { - []float probki - float szczyt -} - -fn analizuj(ptr arg) { // funkcja wątku bierze jeden ptr - ptr[Kawalek] k = arg - for s in k.probki { - k.szczyt = max(k.szczyt, abs(s)) - } -} - -fn main() { - []Kawalek kawalki = wczytaj_kawalki() - []thread.Thread dzialajace - for i in 0..kawalki.len { - dzialajace.push(thread.spawn(addr(analizuj), addr(kawalki[i])) or { - print(err) - exit(1) - }) - } - for i in 0..dzialajace.len { - dzialajace[i].join() // poczekaj na wszystkie - } -} -``` - -To, co dostaje wątek, jest współdzielone, nie kopiowane, więc: - -- musi żyć do `join()` (tu `kawalki` żyje do końca `main`); -- nie zmieniaj tablicy, w którą wskazują wątki (bez `push`), dopóki się nie skończą; -- to, co zmienia więcej niż jeden wątek, idzie pod blokadę albo jest liczbą zmienianą przez `atomic_...`. - -```c -m := thread.mutex() -defer m.close() -m.lock() -dziennik.push("gotowe") // jeden wątek naraz między lock a unlock -m.unlock() - -atomic_add(addr(gotowe), 1) // gotowe to int wspólny dla wątków -n := atomic_load(addr(gotowe)) -``` - -`atomic_add`, `atomic_sub`, `atomic_swap` zwracają starą wartość; `atomic_cas(p, stara, nowa)` wstawia `nowa` tylko -wtedy, gdy jest tam `stara`, i mówi, czy to zrobił. `thread.cpus()` mówi, ile wątków komputer wykonuje naraz. - -## 30. C++, Rust i Android - -Finch rozmawia z innymi językami przez C, jak każdy język. - -**C++**: to, co woła Finch, wystaw przez nagłówek C, z `extern "C"` po stronie C++: - -```cpp -// glue.h -#ifdef __cplusplus -extern "C" { -#endif -int physics_step(float dt); -#ifdef __cplusplus -} -#endif -``` - -i `import "glue.h"` z `link "glue.cpp"` (albo `links = ["glue.cpp"]` w `finch.toml`). Plik `.cpp` buduje kompilator C++ -(`clang++` albo `g++`, albo `FINCH_CXX`), który wtedy też linkuje, więc biblioteka C++ dochodzi sama. -Bibliotekę C++ z API w C (Box2D 3, JoltC dla Jolt, wrapper C dla PhysX) importuje się jak każdą bibliotekę C. - -**Rust**: zrób `staticlib` i wystaw funkcje C: - -```rust -// Cargo.toml: [lib] crate-type = ["staticlib"] -#[no_mangle] -pub extern "C" fn path_len(x: f32, y: f32) -> f32 { (x * x + y * y).sqrt() } -``` - -Napisz nagłówek (`float path_len(float x, float y);` albo niech napisze go `cbindgen`), potem `import "pathfind.h"` i -`link "target/release/libpathfind.a"`. Na Linuksie statyczna biblioteka Rusta potrzebuje też `links = ["dl", "pthread"]`. - -**Android**: `finch build gra.fch --target android --shared` robi `libgra.so` dla 64-bitowego Androida na ARM (API 24 -i wyżej) clangiem z NDK, znalezionym przez `ANDROID_NDK_HOME` (albo `FINCH_CC`). Wczytaj go z aplikacji, na przykład -przez projekt Androida z SDL3 (`SDL_main` biblioteki to `export fn`). To nowość, sprawdzona tylko częściowo: kod Fincha -kompiluje się dla Androida (dane wątków idą przez emulowane TLS, którego wymaga starszy Android), ale linkowanie z NDK -i uruchomienie na urządzeniu nie były jeszcze próbowane. diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index cb4e7e7..3ef9bba 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -12,37 +12,15 @@ Cały Finch na jednej stronie. Wyjaśnienia są w [przewodnikach](../../README.p |---|---| | `finch run plik.fch [argumenty…]` | skompiluj i uruchom (argumenty trafiają do programu) | | `finch build plik.fch [-o nazwa]` | zrób program (`nazwa.exe` na Windowsie) | -| `finch build plik.fch --shared` | zrób bibliotekę (`libplik.so`, `plik.dll`) dla programu w C; jej `export fn` to jej funkcje C | -| `finch test plik.fch` | uruchom jego bloki `test "…" { }`, każdy osobno (także z importowanych modułów) | -| `finch fmt [--check] [pliki/foldery]` | wcięcia wg `{ }` po 4 spacje, bez spacji na końcach linii, najwyżej jedna pusta linia (`--check`: tylko wypisz, co by zmienił) | -| `finch run` / `build` / `test` bez pliku | w folderze z `finch.toml`: jego `entry` | | `finch ir plik.fch` | pokaż LLVM IR | | `finch lsp` | serwer języka (korzystają z niego edytory) | | `finch version` | wersja | | `-l lib` | dolinkuj bibliotekę C (jak `link "lib"`) | -| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | zbuduj dla innego systemu (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) | +| `--target windows` / `linux` / `arm64` / `macos` / triple | zbuduj dla innego systemu | | `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) | | `-O0` | bez optymalizacji | -| `--trace` / `--no-trace` | błędy w czasie działania pokazują łańcuch wywołań; włączone dla `run`, `test`, `-g`, `-O0`, wyłączone dla zwykłego `build` (kosztuje szybkość) | -Zmienne środowiskowe: `FINCH_PATH` (foldery z modułami), `FINCH_CC` (kompilator C, który linkuje), `FINCH_CXX` (dla plików `.cpp`). -Skompilowany kod programu i jego pliki `link "x.c"` zostają w cache (`~/.cache/finch`), dopóki one albo ich nagłówki się nie zmienią. - -`finch.toml` (obok kodu; `finch run` w tym folderze nie potrzebuje pliku ani flag): - -```toml -name = "gra" # co robi finch build -entry = "src/main.fch" -paths = ["src"] # foldery z modułami -include = ["vendor/SDL3/include"] # nagłówki C -links = ["SDL3", "native/glue.cpp"] # biblioteki i własne .c / .cpp / .o / .a -libdirs = ["vendor/SDL3/lib"] -cflags = ["-DFAST"] # dla własnych plików .c / .cpp -ldflags = ["-Wl,-rpath,."] - -[target.windows] # tylko przy budowaniu dla tego systemu (też linux, macos, android, linux-arm64) -links = ["ws2_32"] -``` +Zmienne środowiskowe: `FINCH_PATH` (foldery z modułami), `FINCH_CC` / `CC` (kompilator C, który linkuje). --- @@ -74,10 +52,9 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi |---|---| | `x := 5` | nowa zmienna, typ zgadnięty | | `int x = 5` | nowa zmienna, typ podany | -| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (też struktur w niej) | +| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury | | `x = 7` | przypisanie | | `x += 1` `-=` `*=` `/=` `%=` | zmiana | -| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; `ksztalty.W` z modułu) | --- @@ -86,10 +63,10 @@ Jedna instrukcja na linijkę, bez średników, `{` w tej samej linijce, bez nawi | Typ | Przykład | |---|---| | `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` | -| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` | +| `float` (= `f64`) | `3.14` | | `bool` | `true` (prawda), `false` (fałsz) | | `char` | `'A'`, `'\n'` | -| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykających cudzysłowów znika) | +| `str` | `"tekst\t\"w cudzysłowie\""` | | `i8 i16 i32 i64` | ze znakiem, z rozmiarem | | `u8 u16 u32 u64` | bez znaku, z rozmiarem | | `f32 f64` | zmiennoprzecinkowe | @@ -121,7 +98,6 @@ Znaki specjalne: `\n \t \r \0 \\ \" \'`. ```c if a > b { … } else if a == b { … } else { … } -x := if a > b { a } else { b } // jako wartość: jedna wartość na gałąź, else wymagane while x > 0 { … } for i in 0..10 { … } // 0 … 9 for el in lista { … } // każdy element (albo znak str) @@ -247,7 +223,6 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file | `s.len` | liczba bajtów (ą, ż… to 2) | | `s[i]`, `s[i] = 'x'` | znak | | `a + b` | sklejony tekst | -| `"x = \(x)"` | wartość dowolnego kodu w tekście (`\(x, 2)`: liczba z 2 cyframi po przecinku) | | `s.sub(a, b)` | fragment a … b-1 | | `s.find(t)` | pozycja albo -1 | | `s.contains(t)` | czy zawiera | @@ -258,7 +233,7 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file | `s.replace(a, b)` | każde a → b | | `s.repeat(n)` | n razy | | `s.bytes()` | `[]u8` | -| `s.ptr` | `ptr[char]` (dla C; `str` trafia też do parametru `const char *` bez zmian) | +| `s.ptr` | `ptr[char]` (dla C) | | `==` `!=` `<` `>` | porównanie | --- @@ -274,8 +249,7 @@ Funkcje wbudowane, które mogą się nie udać: `int(s)`, `float(s)`, `read_file | `u8(300)` | `44` (zostawia najniższe bity) | | `char(66)` | `'B'` | | `bool(0)` | `false` | -| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (float: najkrótszy tekst, który jest tą samą liczbą) | -| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po kropce) | +| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst | | `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 | @@ -298,12 +272,9 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo | `file_exists(sciezka)` | czy plik istnieje | | `delete_file(sciezka)` | usuń plik | | `shell(polecenie)` | uruchom polecenie, jego kod wyjścia | -| `exit(kod)` | zakończ program od razu (kończy też blok `or { }`) | -| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, od, do)` | dla dowolnych liczb (twoja funkcja o tej nazwie wygrywa) | +| `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 | -| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymaj się z błędem, gdy warunek jest fałszywy (w bloku `test`: ten test nie przechodzi) | -| `atomic_add(p, n)`, `atomic_sub`, `atomic_load(p)`, `atomic_store(p, n)`, `atomic_swap(p, n)`, `atomic_cas(p, stara, nowa)` | liczba całkowita zmieniana też przez inne wątki, przez jej `ptr` (`addr(licznik)`) | | `new(wartosc)` | wartość na stercie → `ptr[T]` | | `free(p)` | oddaj ją | @@ -314,7 +285,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo | Finch | C | |---|---| | `ptr[int] p` | `int *p` | -| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie idzie `ptr[TaStruktura]`: `pchnij(cialo)` = `pchnij(addr(cialo))`) | +| `addr(x)` | `&x` | | `p.value` | `*p` | | `p.pole` | `p->pole` | | `p[i]` | `p[i]` | @@ -327,7 +298,6 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo - Tablice, teksty i struktury zwalniają się same na końcu swojego bloku. Bez `free`. - `b := a` kopiuje; wynik wywołania jest przenoszony; `return x` przenosi. -- Przekazywanie struktury: `fn f(Cialo c)` pracuje na własnej kopii (ta u wywołującego się nie zmienia), `fn f(ptr[Cialo] c)` zmienia tę u wywołującego (`f(cialo)` albo `f(addr(cialo))`), metoda dostaje `self` przez adres, nigdy kopię. Struktura z 256+ bajtami liczb przekazana przez wartość albo w `for x in lista` dostaje ostrzeżenie. - `new(...)` + `free(...)` (często z `defer`) do list wiązanych, drzew, współdzielenia. - Pamięć z C: funkcjami z C (`malloc` / `free`, `fclose`, …). @@ -340,8 +310,6 @@ import ksztalty // ksztalty.fch obok tego pliku (albo w FINCH_PATH) ksztalty.pole(b) // jego funkcje ksztalty.Pudlo(2, 3) // jego struktury ksztalty.Pudlo inne // w deklaracjach -import ui/przycisk // ui/przycisk.fch: przycisk.rysuj() -import ui/przycisk as p // inna nazwa: p.rysuj() ``` --- @@ -352,7 +320,7 @@ import ui/przycisk as p // inna nazwa: p.rysuj() |---|---| | `import "x.h"` | funkcje, struktury, enumy, liczbowe `#define`, zmienne globalne | | `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pkg-config) | -| `link "plik.c"`, `link "glue.cpp"` | skompiluj własny plik C albo C++ (C++: funkcje w `extern "C"`) | +| `link "plik.c"` | skompiluj własny plik C | | `addr(mojaFn)` | funkcja Fincha jako callback | | Typ C | Finch | @@ -365,14 +333,6 @@ import ui/przycisk as p // inna nazwa: p.rysuj() | `T *` | `ptr[T]` | | `void *`, uchwyty | `ptr` | | `T x[N]` (w strukturze) | `[N]T` | -| `union U` | `U`: `U u`, `u.pole`, `U(pole: v)`, przez wskaźnik do C (nie przez wartość) | -| `#define N 4`, `"tekst"`, `(1u << 3)`, `((Uint64)2)` | stałe (bez makr-funkcji) | -| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | `Color` | - -**`char *` to `str`**: funkcja C, która bierze albo zwraca `char *`, bierze albo daje `str` Fincha (bez `.ptr`, bez zamiany); tylko pole `char *` i `char **` zostają `ptr[char]`. -C++ i Rust: przez nagłówek C. C++ z `extern "C"` (i `link "x.cpp"`), Rust z `#[no_mangle] pub extern "C" fn` zbudowany jako `staticlib` (`link "target/release/libgra.a"`) i nagłówek (napisze go cbindgen). - -Wynik `char *` z funkcji C to od razu `str`; struktury przez wartość działają w konwencjach System V, Windows x64 i ARM64 Linux (sprawdzone w emulatorach; macOS przez CI). --- @@ -387,26 +347,6 @@ Wynik `char *` z funkcji C to od razu `str`; struktury przez wartość działaj | `can't turn "x" into int` | `int("x")` | | `can't read the file "…"` | `read_file` (bez `or`) | | `the key "x" is not in the map` | `m["x"]` | -| `assert failed: dlaczego` | `assert(warunek, "dlaczego")` | - -Pod komunikatem wywołania, które do niego doprowadziły (`finch run`, `finch test`, `-g`, `-O0`, `--trace`): - -``` -gra.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (gra.fch:6) - in step (gra.fch:10) - in main (gra.fch:15) -``` - -## Testy - -```c -test "wysokość skoku" { // obok kodu; finch test plik.fch je uruchamia, finch run/build je pomijają - g := Gracz() - g.skocz() - assert(g.y > 0.0, "y to \(g.y)") -} -``` --- @@ -456,8 +396,6 @@ s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // ni | `path` | `join(a, b)` `base(p)` `dir(p)` `ext(p)` `stem(p)` `parts(p)` `clean(p)` `is_abs(p)` | | `time` | `now()` `millis()` `sleep(ms)` `date(t)` `local(t)` `format(t, fmt)` `days_in_month(r, m)` | | `proc` | `run(cmd)` `output(cmd)` (błąd z jego wyjściem) `env(nazwa, domyślna)` `pid()` `cwd()` | -| `thread` | `spawn(addr(praca), addr(dane))` → `t.join()`; `mutex()` → `m.lock()` `m.unlock()` `m.close()`; `cpus()` (funkcja wątku: `fn praca(ptr arg)`) | -| `fs` | `read_bytes(p)` `write_bytes(p, dane)` `exists(p)` `is_dir(p)` `mkdir(p)` `list(folder)` `remove_dir(p)` `data_dir(app)` | | `json` | `parse(tekst)` `stringify(v)` `pretty(v)` `new_text/number/bool/null/list/object` `v.get(k)` `v.at(i)` `v.set(k, x)` `v.push(x)` `v.has(k)` `v.str_or(d)` `v.int_or(d)` `v.float_or(d)` `v.bool_or(d)` | ## Słowa kluczowe @@ -478,9 +416,5 @@ s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // ni | `defer` | odłóż (na koniec bloku) | | `or` | albo (co zrobić przy porażce) | | `try` | spróbuj (porażkę przekaż dalej) | -| `const` | stała | -| `export` | wystaw (funkcja dla programu w C) | | `self` | sam (w metodzie: struktura, na której ją wywołano) | -| `test` | test (tylko jako `test "nazwa" { }`) | -| `as` | jako (tylko w `import x as y`) | | `map` | mapa | diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml index 883a6e0..5651fad 100644 --- a/editors/kate/finch.xml +++ b/editors/kate/finch.xml @@ -5,7 +5,7 @@ Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml (or run editors/kate/install.sh), then restart Kate. --> - @@ -13,7 +13,6 @@ struct enum export - const if @@ -71,17 +70,6 @@ new free error - min - max - abs - clamp - assert - atomic_add - atomic_sub - atomic_load - atomic_store - atomic_swap - atomic_cas @@ -91,13 +79,10 @@ - - - @@ -106,8 +91,7 @@ - - + @@ -147,30 +131,11 @@ - - - - - - - - - - - - - - - - - - - diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 195374b..168a8cd 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -1,12 +1,5 @@ # Changelog -## 2.5.0 - -- Highlighting for code inside text (`"hp \(hp)"`), `test "…" { }` blocks, `const`, `"""` text, `1e9`. -- Built-ins `min`, `max`, `abs`, `clamp`, `assert` and `atomic_…` highlighted and described on hover. -- Format Document (the language server runs `finch fmt` on the open file). -- Warnings in yellow: a big struct copied on every call or in `for x in list`. - ## 2.4.0 - Highlighting for methods (`fn Point.move`), maps (`map[K]V`), `or`, `try`, `self` and `error(...)`. diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 40c7715..2d01781 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "finch-lang", "displayName": "Finch", "description": "The Finch programming language: highlighting, errors as you type, completion, hover, go to definition, and a Run button.", - "version": "2.5.0", + "version": "2.4.0", "publisher": "nexoniarz", "license": "Apache-2.0", "icon": "images/icon.png", diff --git a/editors/vscode/syntaxes/finch.tmLanguage.json b/editors/vscode/syntaxes/finch.tmLanguage.json index 9154a4e..d377758 100644 --- a/editors/vscode/syntaxes/finch.tmLanguage.json +++ b/editors/vscode/syntaxes/finch.tmLanguage.json @@ -103,16 +103,9 @@ }, "statements": { "patterns": [ - { - "name": "keyword.other.test.finch", - "match": "\\btest(?=\\s*\")" - }, { "include": "#comments" }, - { - "include": "#longstrings" - }, { "include": "#strings" }, @@ -132,7 +125,7 @@ }, { "name": "storage.type.struct.finch", - "match": "\\b(struct|enum|const)\\b" + "match": "\\b(struct|enum)\\b" }, { "name": "storage.modifier.finch", @@ -151,7 +144,7 @@ "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64)\\b" }, { - "match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas)\\s*(?=\\()", + "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" @@ -221,9 +214,6 @@ "begin": "\"", "end": "\"|$", "patterns": [ - { - "include": "#interpolation" - }, { "name": "constant.character.escape.finch", "match": "\\\\[ntr0\\\\\"']" @@ -253,62 +243,13 @@ }, { "name": "constant.numeric.float.finch", - "match": "\\b\\d[\\d_]*(\\.\\d[\\d_]*([eE][+-]?\\d+)?|[eE][+-]?\\d+)\\b" + "match": "\\b\\d[\\d_]*\\.\\d[\\d_]*\\b" }, { "name": "constant.numeric.integer.finch", "match": "\\b\\d[\\d_]*\\b" } ] - }, - "longstrings": { - "name": "string.quoted.triple.finch", - "begin": "\"\"\"", - "end": "\"\"\"", - "patterns": [ - { - "include": "#interpolation" - }, - { - "name": "constant.character.escape.finch", - "match": "\\\\[ntr0\\\\\"']" - } - ] - }, - "interpolation": { - "name": "meta.embedded.line.finch", - "begin": "\\\\\\(", - "end": "\\)", - "beginCaptures": { - "0": { - "name": "punctuation.section.embedded.begin.finch" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.section.embedded.end.finch" - } - }, - "patterns": [ - { - "include": "#interpolation-parens" - }, - { - "include": "$self" - } - ] - }, - "interpolation-parens": { - "begin": "\\(", - "end": "\\)", - "patterns": [ - { - "include": "#interpolation-parens" - }, - { - "include": "$self" - } - ] } } } diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js index bf2aae7..3d5ba77 100644 --- a/editors/vscode/test/grammar.js +++ b/editors/vscode/test/grammar.js @@ -26,9 +26,6 @@ const code = [ ' self.x += dx', ' return error("no")', '}', - 'test "lives \\(hp + f(1)) left" {', - ' assert(clamp(x, 0, 9) == 3)', - '}', ]; const expect = [ // [line, token text, scope that must be present] [0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'], @@ -46,9 +43,6 @@ const expect = [ // [line, token text, scope that must be present] [13, 'try', 'keyword.control'], [13, 'or', 'keyword.control'], [14, 'self', 'variable.language.self'], [15, 'error', 'support.function.builtin'], - [17, 'test', 'keyword.other.test'], [17, '\\(', 'punctuation.section.embedded.begin'], [17, 'hp', 'meta.embedded.line'], - [17, 'f', 'meta.embedded.line'], [17, 'left', 'string.quoted.double'], - [18, 'assert', 'support.function.builtin'], [18, 'clamp', 'support.function.builtin'], ]; registry.loadGrammar('source.finch').then((g) => { let state = vsctm.INITIAL, toks = []; diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index ab806a3..d825f64 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -12,24 +12,12 @@ #include #include -#include -#include #include #include #include #include #include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#include -#include -#else -#include -#include -#include -#include +#ifndef _WIN32 #include #endif @@ -55,38 +43,10 @@ static char empty[1] = ""; static void (*fatal_hook)(const char *msg); void finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; } -// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output): -// each frame is { the caller's frame, the function's name, its file, the line it is at }. -typedef struct FFrame { - struct FFrame *prev; - const char *name; - const char *file; - int64_t line; -} FFrame; -static void *(*trace_top)(void); -static void (*trace_set)(void *); -void finch_trace_init(void *(*top)(void), void (*set)(void *)) { - trace_top = top; - trace_set = set; -} - -static void print_trace(void) { - if (!trace_top) return; - FFrame *f = trace_top(); - for (int n = 0; f; n++, f = f->prev) { - if (n == 30) { - fprintf(stderr, " ...\n"); - break; - } - fprintf(stderr, " in %s (%s:%lld)\n", f->name, f->file, (long long)f->line); - } -} - static __attribute__((noreturn)) void fatal(const char *msg) { if (fatal_hook) fatal_hook(msg); fflush(stdout); fprintf(stderr, "%s\n", msg); - print_trace(); exit(1); } @@ -114,50 +74,6 @@ static void *must(void *p) { void *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); } void *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); } -// ---------- assert and finch test ---------- - -__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) { - char msg[600]; - if (why->len) snprintf(msg, sizeof msg, "assert failed: %.*s", (int)(why->len > 500 ? 500 : why->len), why->ptr); - else snprintf(msg, sizeof msg, "assert failed"); - finch_panic(file, line, msg); -} - -// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs. -static jmp_buf test_jump; -static char test_message[1100]; -static int64_t tests_passed, tests_failed; - -static void test_fatal(const char *msg) { - snprintf(test_message, sizeof test_message, "%s", msg); - longjmp(test_jump, 1); -} - -void finch_test_run(const char *name, void (*fn)(void)) { - void (*saved)(const char *) = fatal_hook; - void *chain = trace_top ? trace_top() : NULL; - test_message[0] = 0; - fatal_hook = test_fatal; - int failed = setjmp(test_jump); - if (!failed) fn(); - fatal_hook = saved; - if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain - fflush(stdout); - if (failed) { - tests_failed++; - printf("FAIL %s\n %s\n", name, test_message); - } else { - tests_passed++; - printf("ok %s\n", name); - } - fflush(stdout); -} - -int finch_test_summary(void) { - printf("\n%lld passed, %lld failed\n", (long long)tests_passed, (long long)tests_failed); - return tests_failed ? 1 : 0; -} - // ---------- strings ---------- static void str_heap(FStr *out, const char *p, int64_t len) { @@ -237,50 +153,12 @@ void finch_str_from_uint(FStr *out, uint64_t v) { str_heap(out, buf, n); } -// The shortest text that reads back as the same number (as an f32 when `single`): -// 0.1 -> "0.1", 1.0/3 -> "0.3333333333333333", 1e8 -> "100000000", 1e20 -> "1e+20". -static int fmt_float(char *buf, size_t size, double v, int single) { - if (isnan(v)) return snprintf(buf, size, "nan"); - if (isinf(v)) return snprintf(buf, size, v < 0 ? "-inf" : "inf"); - int p = 1; - for (; p < 17; p++) { - snprintf(buf, size, "%.*e", p - 1, v); - double back = strtod(buf, NULL); - if (single ? (float)back == (float)v : back == v) break; - } - int exp = atoi(strchr(buf, 'e') + 1); - if (exp < -5 || exp >= 17) return snprintf(buf, size, "%.*g", p, v); - int decimals = p - 1 - exp; - return snprintf(buf, size, "%.*f", decimals > 0 ? decimals : 0, v); -} - void finch_str_from_float(FStr *out, double v) { char buf[64]; - int n = fmt_float(buf, sizeof buf, v, 0); + int n = snprintf(buf, sizeof buf, "%g", v); str_heap(out, buf, n); } -void finch_str_from_f32(FStr *out, double v) { - char buf[64]; - int n = fmt_float(buf, sizeof buf, v, 1); - str_heap(out, buf, n); -} - -// str(x, 2): a fixed number of digits after the point -void finch_str_fixed(FStr *out, double v, int64_t digits) { - char buf[400]; - if (digits < 0) digits = 0; - if (digits > 60) digits = 60; - int n = snprintf(buf, sizeof buf, "%.*f", (int)digits, v); - str_heap(out, buf, n); -} - -void finch_print_float(double v, int32_t single) { - char buf[64]; - fmt_float(buf, sizeof buf, v, single); - fputs(buf, stdout); -} - 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"); } @@ -364,7 +242,6 @@ static void file_error(FStr *err, const char *what, const FStr *path) { void finch_report_error(const FStr *msg) { fflush(stdout); fprintf(stderr, "error: %.*s\n", (int)msg->len, msg->ptr ? msg->ptr : ""); - finch_str_drop((FStr *)msg); } void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) { @@ -522,14 +399,7 @@ static void buf_fmt(FStr *b, const char *fmt, ...) { 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) { - char t[64]; - finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0)); -} -void finch_buf_f32(FStr *b, double v) { - char t[64]; - finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1)); -} +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); @@ -628,12 +498,6 @@ void finch_args(FArr *out, int argc, char **argv) { for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i])); } -// after main: the arguments go -void finch_args_free(FArr *a) { - for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i); - finch_arr_free(a); -} - // input("Name? ") prints the prompt and reads one line, without the newline. void finch_input(FStr *out, const FStr *prompt) { if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout); @@ -744,255 +608,6 @@ int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) { return 0; } -// ---------- folders (std/fs.fch, through std/finch_fs.h) ---------- - -int finch_fs_is_dir(const char *path) { -#ifdef _WIN32 - struct _finddata_t d; - intptr_t h = _findfirst(path, &d); - if (h == -1) return 0; - _findclose(h); - return (d.attrib & _A_SUBDIR) != 0; -#else - struct stat st; - return stat(path, &st) == 0 && S_ISDIR(st.st_mode); -#endif -} - -int finch_fs_exists(const char *path) { -#ifdef _WIN32 - return _access(path, 0) == 0; -#else - struct stat st; - return stat(path, &st) == 0; -#endif -} - -static int make_one_dir(const char *p) { -#ifdef _WIN32 - return _mkdir(p) == 0 || errno == EEXIST; -#else - return mkdir(p, 0777) == 0 || errno == EEXIST; -#endif -} - -// mkdir -p: the parents too; 1 if the folder is there afterwards -int finch_fs_mkdir(const char *path) { - size_t n = strlen(path); - char *p = finch_alloc((int64_t)n + 1); - memcpy(p, path, n + 1); - for (size_t i = 1; i < n; i++) { - if (p[i] == '/' || p[i] == '\\') { - if (p[i - 1] == ':') continue; /* C: is a drive */ - char c = p[i]; - p[i] = 0; - make_one_dir(p); - p[i] = c; - } - } - int ok = make_one_dir(p) && finch_fs_is_dir(p); - free(p); - return ok; -} - -int finch_fs_remove_dir(const char *path) { -#ifdef _WIN32 - return _rmdir(path) == 0; -#else - return rmdir(path) == 0; -#endif -} - -// the names in a folder, one per call ("" at the end), without . and .. -typedef struct { -#ifdef _WIN32 - intptr_t h; - struct _finddata_t d; - int first; -#else - DIR *d; -#endif -} FsList; - -void *finch_fs_list_open(const char *path) { - FsList *l = finch_zalloc((int64_t)sizeof(FsList)); -#ifdef _WIN32 - size_t n = strlen(path); - char *pat = finch_alloc((int64_t)n + 3); - memcpy(pat, path, n); - memcpy(pat + n, "\\*", 3); - l->h = _findfirst(pat, &l->d); - free(pat); - if (l->h == -1) { - free(l); - return NULL; - } - l->first = 1; -#else - l->d = opendir(path); - if (!l->d) { - free(l); - return NULL; - } -#endif - return l; -} - -const char *finch_fs_list_next(void *handle) { - FsList *l = handle; - for (;;) { -#ifdef _WIN32 - if (!l->first && _findnext(l->h, &l->d) != 0) return ""; - l->first = 0; - const char *name = l->d.name; -#else - struct dirent *e = readdir(l->d); - if (!e) return ""; - const char *name = e->d_name; -#endif - if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0) return name; - } -} - -void finch_fs_list_close(void *handle) { - FsList *l = handle; -#ifdef _WIN32 - _findclose(l->h); -#else - closedir(l->d); -#endif - free(l); -} - -// where a program keeps its own files for this user (saves, settings); "" if unknown -// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA% -const char *finch_fs_data_home(void) { - static char buf[4096]; - const char *v; -#if defined(_WIN32) - v = getenv("APPDATA"); - snprintf(buf, sizeof buf, "%s", v ? v : ""); -#elif defined(__APPLE__) - v = getenv("HOME"); - if (v) snprintf(buf, sizeof buf, "%s/Library/Application Support", v); - else buf[0] = 0; -#else - v = getenv("XDG_DATA_HOME"); - if (v && *v) snprintf(buf, sizeof buf, "%s", v); - else if ((v = getenv("HOME"))) snprintf(buf, sizeof buf, "%s/.local/share", v); - else buf[0] = 0; -#endif - return buf; -} - -// ---------- threads (std/thread.fch, through std/finch_thread.h) ---------- - -typedef struct { - void (*fn)(void *); - void *arg; -} ThreadStart; - -#ifdef _WIN32 -static DWORD WINAPI thread_main(LPVOID p) { - ThreadStart st = *(ThreadStart *)p; - free(p); - st.fn(st.arg); - return 0; -} -#else -static void *thread_main(void *p) { - ThreadStart st = *(ThreadStart *)p; - free(p); - st.fn(st.arg); - return NULL; -} -#endif - -// run fn(arg) on a new thread; NULL if it couldn't start -void *finch_thread_spawn(void (*fn)(void *), void *arg) { - ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart)); - st->fn = fn; - st->arg = arg; -#ifdef _WIN32 - HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL); - if (!h) { - free(st); - return NULL; - } - return h; -#else - pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t)); - if (pthread_create(t, NULL, thread_main, st) != 0) { - free(st); - free(t); - return NULL; - } - return t; -#endif -} - -// wait for the thread to end; 1 if that worked -int finch_thread_join(void *t) { - if (!t) return 0; -#ifdef _WIN32 - int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0; - CloseHandle((HANDLE)t); - return ok; -#else - int ok = pthread_join(*(pthread_t *)t, NULL) == 0; - free(t); - return ok; -#endif -} - -void *finch_mutex_new(void) { -#ifdef _WIN32 - CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION)); - InitializeCriticalSection(m); -#else - pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t)); - pthread_mutex_init(m, NULL); -#endif - return m; -} - -void finch_mutex_lock(void *m) { -#ifdef _WIN32 - EnterCriticalSection((CRITICAL_SECTION *)m); -#else - pthread_mutex_lock((pthread_mutex_t *)m); -#endif -} - -void finch_mutex_unlock(void *m) { -#ifdef _WIN32 - LeaveCriticalSection((CRITICAL_SECTION *)m); -#else - pthread_mutex_unlock((pthread_mutex_t *)m); -#endif -} - -void finch_mutex_free(void *m) { - if (!m) return; -#ifdef _WIN32 - DeleteCriticalSection((CRITICAL_SECTION *)m); -#else - pthread_mutex_destroy((pthread_mutex_t *)m); -#endif - free(m); -} - -int finch_cpu_count(void) { -#ifdef _WIN32 - SYSTEM_INFO si; - GetSystemInfo(&si); - return (int)si.dwNumberOfProcessors; -#else - long n = sysconf(_SC_NPROCESSORS_ONLN); - return n > 0 ? (int)n : 1; -#endif -} - // ---------- maps ---------- // map[K]V = { entries; len; used; cap; index; icap; adds } // entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order. diff --git a/src/ast.fch b/src/ast.fch index 0870037..28bb900 100644 --- a/src/ast.fch +++ b/src/ast.fch @@ -114,14 +114,11 @@ struct Node { struct Program { str module str path - []str imports // Finch modules, by the name this file uses: import ui/button -> "button" - []str modules // which module each is: "ui/button" (import path as p: "path") + []str imports // Finch modules []str cimports // C headers: import "stdio.h" []str cwhere // where each was written, for error messages []str links // link "glfw", link "mine.c" []Node structs []Node enums []Node fns - []Node consts // const NAME = value - []str tests // the functions made from test "..." { } blocks, in order } diff --git a/src/cimport.fch b/src/cimport.fch index 8d1701f..09d4031 100644 --- a/src/cimport.fch +++ b/src/cimport.fch @@ -18,29 +18,14 @@ struct CFunc { ast.Type ret []ast.Type params bool variadic - bool inline // static inline in the header: called through a pointer a small C file keeps (finch_inl_name) str unsupported // non-empty: why Finch can't call it (yet) } -struct CConst { // enum values and #define constants +struct CConst { // enum values and simple #define numbers bool isFloat int i float f str text // a float as written, so no digits are lost: 3.14159265358979323846 - bool isStr // #define NAME "text" - str s - str structName // #define RAYWHITE (Color){ 245, 245, 245, 255 }: the struct, and its fields in order - []str fields // each as a Finch literal: 245, 0.5 - []bool fieldFloat -} - -// what visitMacroVar looks for inside (Color){ ... } -struct InitFind { - bool found - CXCursor list - bool ok - []str fields - []bool floats } struct CField { @@ -59,7 +44,6 @@ struct CStruct { int align str unsupported // why it can't be used by value (pointers to it still work) bool hasHidden - bool isUnion // every field starts at byte 0 } struct CImports { @@ -91,7 +75,6 @@ struct Importer { map[str]str recordNames // record USR -> Finch name (a typedef name wins) map[str]bool defining // USR -> true while its fields are being mapped int anon - []str exprMacros // #defines that are expressions: clang works out their values } fn text(CXString s) -> str { @@ -186,7 +169,6 @@ fn defineRecord(ptr[Importer] imp, CXType type) -> str { s := CStruct(name: name, size: size, align: int(clang_Type_getAlignOf(t))) if clang_getCursorKind(decl) == CXCursor_UnionDecl { s.unsupported = "is a C union" - s.isUnion = true } // the fields are collected by a callback that gets the importer and the struct through one pointer ctx := FieldCtx(imp: imp, s: addr(s)) @@ -212,10 +194,9 @@ fn visitField(CXCursor field, ptr data) -> i32 { if clang_Cursor_isBitField(field) != 0 { c.s.unsupported = "has bit fields" f.hidden = true + } else if ft.kind == CXType_Record && clang_getCursorKind(clang_getTypeDeclaration(ft)) == CXCursor_UnionDecl { + f.hidden = true // unions are kept as opaque bytes } else { - if ft.kind == CXType_Record && clang_getCursorKind(clang_getTypeDeclaration(ft)) == CXCursor_UnionDecl { - c.s.unsupported = "has a union inside" // its fields can be used, but not passed by value - } m := mapType(c.imp, ft, false, false) if m.ok { f.ty = m.ty @@ -312,6 +293,9 @@ fn mapType(ptr[Importer] imp, CXType type, bool signature, bool param) -> Mapped return ok(ast.ptrTo(inner.ty)) } if k == CXType_Record { + if clang_getCursorKind(clang_getTypeDeclaration(t)) == CXCursor_UnionDecl { + return no("uses a C union by value") + } name := defineRecord(imp, t) if name.len == 0 { return no("uses an incomplete C struct by value") @@ -382,93 +366,19 @@ fn parseInt(str lit) -> Number { return Number(c: CConst(i: n), ok: true) } -// The tokens after a #define's name; `safe` is false if any of them could not be part of a -// constant expression (braces, semicolons, statements), so it is never handed to clang. -struct MacroTokens { - []str parts - bool safe -} - -fn macroTokens(ptr[Importer] imp, CXCursor c) -> MacroTokens { +// Simple #defines: a single number, maybe negative, maybe in parentheses. +fn macroValue(ptr[Importer] imp, CXCursor c) -> Number { ptr[CXToken] tokens = null u32 n = 0 clang_tokenize(imp.tu, clang_getCursorExtent(c), addr(tokens), addr(n)) - out := MacroTokens(safe: true) - allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ",", "{", "}"] - words := ["sizeof", "unsigned", "signed", "int", "long", "short", "char", "float", "double", "const", "_Bool"] - depth := 0 + []str parts for k in 1..int(n) { // token 0 is the macro's name - kind := clang_getTokenKind(tokens[k]) - if kind == CXToken_Comment { + if clang_getTokenKind(tokens[k]) == CXToken_Comment { continue } - t := text(clang_getTokenSpelling(imp.tu, tokens[k])) - out.parts.push(t) - if kind == CXToken_Punctuation { - if !allowed.contains(t) { - out.safe = false - } - if t == "(" { - depth += 1 - } else if t == ")" { - depth -= 1 - } - } else if kind == CXToken_Keyword && !words.contains(t) { - out.safe = false - } + parts.push(text(clang_getTokenSpelling(imp.tu, tokens[k]))) } clang_disposeTokens(imp.tu, tokens, n) - if depth != 0 { - out.safe = false - } - return out -} - -// "text" as C writes it -> the text: \n \t \" \\ \0 and \xNN; "a" "b" are joined -fn cString([]str parts) -> Number { - out := "" - for p in parts { - if p.len < 2 || p[0] != '"' || p[p.len - 1] != '"' { - return Number() - } - i := 1 - while i < p.len - 1 { - c := p[i] - if c == '\\' && i + 1 < p.len - 1 { - i += 1 - e := p[i] - if e == 'n' { - c = '\n' - } else if e == 't' { - c = '\t' - } else if e == 'r' { - c = '\r' - } else if e == '0' { - c = char(0) - } else if e == 'x' { - v := 0 - while i + 1 < p.len - 1 && "0123456789abcdefABCDEF".contains(str(p[i + 1])) { - i += 1 - v = v * 16 + parseInt("0x" + str(p[i])).c.i - } - c = char(v & 255) - } else { - c = e - } - } - out = out + str(c) - i += 1 - } - } - return Number(c: CConst(isStr: true, s: out), ok: true) -} - -// Simple #defines: a single number, maybe negative, maybe in parentheses; or a "text". -fn macroValue(ptr[Importer] imp, CXCursor c) -> Number { - parts := macroTokens(imp, c).parts - if parts.len > 0 && parts[0].starts_with("\"") { - return cString(parts) - } while parts.len >= 2 && parts[0] == "(" && parts[parts.len - 1] == ")" { parts.remove(0) @@ -528,7 +438,6 @@ fn visit(ptr[Importer] imp, CXCursor c) { if !used && !imp.out.structs.has(name) { imp.recordNames[usr] = name } - defineRecord(imp, u) // usable even if no function mentions it: Motion m } } else if kind == CXCursor_FunctionDecl { if imp.out.fns.has(name) { @@ -536,7 +445,9 @@ fn visit(ptr[Importer] imp, CXCursor c) { } f := CFunc(name: name, header: imp.header) if clang_Cursor_getStorageClass(c) == CX_SC_Static { - f.inline = true // mingw's printf and time, header-only helpers + f.unsupported = "is a static inline C function (it has no code to link against)" + imp.out.fns[name] = f + return } ft := clang_getCanonicalType(clang_getCursorType(c)) if ft.kind == CXType_FunctionNoProto { @@ -572,7 +483,7 @@ fn visit(ptr[Importer] imp, CXCursor c) { if g.ok { imp.out.globals[name] = g.ty } - } else if kind == CXCursor_StructDecl || kind == CXCursor_UnionDecl { + } else if kind == CXCursor_StructDecl { if clang_isCursorDefinition(c) != 0 && name.len > 0 && !name.contains(" ") { usr := text(clang_getCursorUSR(c)) if !imp.recordNames.has(usr) { @@ -589,17 +500,9 @@ fn visit(ptr[Importer] imp, CXCursor c) { if clang_Cursor_isMacroBuiltin(c) != 0 || clang_Cursor_isMacroFunctionLike(c) != 0 { return } - if imp.out.consts.has(name) { - return - } v := macroValue(imp, c) - if v.ok { + if v.ok && !imp.out.consts.has(name) { imp.out.consts[name] = v.c - return - } - mt := macroTokens(imp, c) - if mt.safe && mt.parts.len > 0 { - imp.exprMacros.push(name) // (1 << 3), (FLAG_A | FLAG_B), ((Uint64)1): asked of clang later } } } @@ -660,134 +563,7 @@ fn importHeaders([]Header headers, []str dirs, str target, bool windows, str ccF imp.header = h.path clang_visitChildren(clang_getTranslationUnitCursor(tu), addr(visitTop), addr(imp)) clang_disposeTranslationUnit(tu) - evalMacros(addr(imp), index, h, argv) } clang_disposeIndex(index) return imp.out } - -// #defines that are expressions: each becomes `static const __auto_type __finch_m_NAME = (NAME);` -// in a second look at the header, and clang works out the value. One that isn't a constant -// (a type, a function call) just gives clang an error there, and is left out. -fn evalMacros(ptr[Importer] imp, ptr index, Header h, []ptr[char] argv) { - if imp.exprMacros.len == 0 { - return - } - src := "#include \"" + h.path + "\"\n" - for m in imp.exprMacros { - src = src + "static const __auto_type __finch_m_" + m + " = (" + m + ");\n" - } - []str none - imp.exprMacros = none - file := host.unsavedFile("finch_macros.c", src) - ptr tu = null - err := clang_parseTranslationUnit2(index, "finch_macros.c", argv.ptr, i32(argv.len), addr(file), 1, u32(CXTranslationUnit_SkipFunctionBodies), addr(tu)) - if err != CXError_Success || tu == null { - return - } - clang_visitChildren(clang_getTranslationUnitCursor(tu), addr(visitMacroVar), imp) - clang_disposeTranslationUnit(tu) -} - -// a float as a Finch literal: 2.0, not 2 -fn floatText(float v) -> str { - t := str(v) - if !t.contains(".") && !t.contains("e") { - t = t + ".0" - } - return t -} - -// (Color){ 245, 245, 245, 255 }: a struct whose fields are all numbers, in order -fn structMacro(ptr[Importer] imp, str name, CXCursor c, CXType t) { - sname := recordName(imp, t) - if !imp.out.structs.has(sname) { - return - } - find := InitFind(ok: true) - clang_visitChildren(c, addr(visitFindInit), addr(find)) - if !find.found { - return - } - clang_visitChildren(find.list, addr(visitInitField), addr(find)) - if !find.ok || find.fields.len == 0 || find.fields.len > imp.out.structs[sname].fields.len { - return - } - imp.out.consts[name] = CConst(structName: sname, fields: find.fields, fieldFloat: find.floats) -} - -fn visitFindInit(CXCursor c, CXCursor parent, ptr data) -> i32 { - ptr[InitFind] f = data - if clang_getCursorKind(c) == CXCursor_InitListExpr { - f.found = true - f.list = c - return CXChildVisit_Break - } - return CXChildVisit_Recurse -} - -fn visitInitField(CXCursor c, CXCursor parent, ptr data) -> i32 { - ptr[InitFind] f = data - r := clang_Cursor_Evaluate(c) - if r == null { - f.ok = false - return CXChildVisit_Break - } - k := clang_EvalResult_getKind(r) - if k == CXEval_Int { - f.fields.push(str(int(clang_EvalResult_getAsLongLong(r)))) - f.floats.push(false) - } else if k == CXEval_Float { - v := clang_EvalResult_getAsDouble(r) - if v == v && v - v == 0.0 { - f.fields.push(floatText(v)) - f.floats.push(true) - } else { - f.ok = false - } - } else { - f.ok = false // a struct in the struct, a pointer, a text: not a simple constant - } - clang_EvalResult_dispose(r) - if !f.ok { - return CXChildVisit_Break - } - return CXChildVisit_Continue -} - -fn visitMacroVar(CXCursor c, CXCursor parent, ptr data) -> i32 { - ptr[Importer] imp = data - if clang_getCursorKind(c) != CXCursor_VarDecl { - return CXChildVisit_Continue - } - name := text(clang_getCursorSpelling(c)) - if !name.starts_with("__finch_m_") { - return CXChildVisit_Continue - } - name = name.sub(10, name.len) - if imp.out.consts.has(name) { - return CXChildVisit_Continue - } - t := clang_getCanonicalType(clang_getCursorType(c)) - if t.kind == CXType_Record { - structMacro(imp, name, c, t) - return CXChildVisit_Continue - } - r := clang_Cursor_Evaluate(c) - if r == null { - return CXChildVisit_Continue - } - k := clang_EvalResult_getKind(r) - if k == CXEval_Int { - imp.out.consts[name] = CConst(i: int(clang_EvalResult_getAsLongLong(r))) - } else if k == CXEval_Float { - v := clang_EvalResult_getAsDouble(r) - if v == v && v - v == 0.0 { // not nan or inf - imp.out.consts[name] = CConst(isFloat: true, f: v, text: floatText(v)) - } - } else if k == CXEval_StrLiteral { - imp.out.consts[name] = CConst(isStr: true, s: clang_EvalResult_getAsStr(r)) - } - clang_EvalResult_dispose(r) - return CXChildVisit_Continue -} diff --git a/src/docs.fch b/src/docs.fch index 977f0de..2c75a0e 100644 --- a/src/docs.fch +++ b/src/docs.fch @@ -21,21 +21,11 @@ fn builtins() -> []Doc { doc("delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."), doc("error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."), doc("shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."), - doc("exit", "exit(int code)", "Ends the program right away with this exit code (it also ends an or { } block)."), - doc("min", "min(a, b, ...) -> number", "The smallest of the numbers, in the type they all fit."), - doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."), - doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."), - doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."), - doc("atomic_add", "atomic_add(ptr[int] p, n) -> int", "Adds n to the number at p in one step other threads can't split; gives back what was there before."), - doc("atomic_sub", "atomic_sub(ptr[int] p, n) -> int", "Takes n from the number at p in one step; gives back what was there before."), - doc("atomic_load", "atomic_load(ptr[int] p) -> int", "Reads the number at p, as another thread last wrote it."), - doc("atomic_store", "atomic_store(ptr[int] p, n)", "Writes n to p so other threads see it whole."), - doc("atomic_swap", "atomic_swap(ptr[int] p, n) -> int", "Puts n at p and gives back what was there, in one step."), - doc("atomic_cas", "atomic_cas(ptr[int] p, old, new) -> bool", "Puts new at p only if old is there (compare and swap); true if it did."), + doc("exit", "exit(int code)", "Ends the program right away with this exit code."), doc("addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."), doc("new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."), doc("free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."), - doc("str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text. str(x, 2): a number with 2 digits after the point."), + doc("str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."), doc("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)."), doc("float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."), doc("char", "char(int n) -> char", "The character with this code."), @@ -96,7 +86,7 @@ fn mapMethods() -> []Doc { fn keywords() -> []str { return ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", "null", "import", - "link", "struct", "enum", "match", "export", "defer", "or", "try", "self", "const"] + "link", "struct", "enum", "match", "export", "defer", "or", "try", "self"] } fn typeNames() -> []str { diff --git a/src/embedded.fch b/src/embedded.fch index cfef97d..15ef247 100644 --- a/src/embedded.fch +++ b/src/embedded.fch @@ -4,33 +4,25 @@ fn names() -> []str { []str list list.push("runtime/finch_rt.c") - list.push("std/fs.fch") list.push("std/json.fch") list.push("std/path.fch") list.push("std/proc.fch") list.push("std/proc_windows.fch") - list.push("std/thread.fch") list.push("std/time.fch") list.push("std/time_windows.fch") - list.push("std/finch_fs.h") - list.push("std/finch_thread.h") list.push("std/finch_win.h") return list } fn texts() -> []str { []str list - list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/codegen.cpp):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include \n#include \n#include \n#else\n#include \n#include \n#include \n#include \n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\n// The chain of Finch calls, kept by the program itself (see @finch_trace in the compiler's output):\n// each frame is { the caller's frame, the function's name, its file, the line it is at }.\ntypedef struct FFrame {\n struct FFrame *prev;\n const char *name;\n const char *file;\n int64_t line;\n} FFrame;\nstatic void *(*trace_top)(void);\nstatic void (*trace_set)(void *);\nvoid finch_trace_init(void *(*top)(void), void (*set)(void *)) {\n trace_top = top;\n trace_set = set;\n}\n\nstatic void print_trace(void) {\n if (!trace_top) return;\n FFrame *f = trace_top();\n for (int n = 0; f; n++, f = f->prev) {\n if (n == 30) {\n fprintf(stderr, \" ...\\n\");\n break;\n }\n fprintf(stderr, \" in %s (%s:%lld)\\n\", f->name, f->file, (long long)f->line);\n }\n}\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n print_trace();\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- assert and finch test ----------\n\n__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {\n char msg[600];\n if (why->len) snprintf(msg, sizeof msg, \"assert failed: %.*s\", (int)(why->len > 500 ? 500 : why->len), why->ptr);\n else snprintf(msg, sizeof msg, \"assert failed\");\n finch_panic(file, line, msg);\n}\n\n// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.\nstatic jmp_buf test_jump;\nstatic char test_message[1100];\nstatic int64_t tests_passed, tests_failed;\n\nstatic void test_fatal(const char *msg) {\n snprintf(test_message, sizeof test_message, \"%s\", msg);\n longjmp(test_jump, 1);\n}\n\nvoid finch_test_run(const char *name, void (*fn)(void)) {\n void (*saved)(const char *) = fatal_hook;\n void *chain = trace_top ? trace_top() : NULL;\n test_message[0] = 0;\n fatal_hook = test_fatal;\n int failed = setjmp(test_jump);\n if (!failed) fn();\n fatal_hook = saved;\n if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain\n fflush(stdout);\n if (failed) {\n tests_failed++;\n printf(\"FAIL %s\\n %s\\n\", name, test_message);\n } else {\n tests_passed++;\n printf(\"ok %s\\n\", name);\n }\n fflush(stdout);\n}\n\nint finch_test_summary(void) {\n printf(\"\\n%lld passed, %lld failed\\n\", (long long)tests_passed, (long long)tests_failed);\n return tests_failed ? 1 : 0;\n}\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\n// The shortest text that reads back as the same number (as an f32 when `single`):\n// 0.1 -> \"0.1\", 1.0/3 -> \"0.3333333333333333\", 1e8 -> \"100000000\", 1e20 -> \"1e+20\".\nstatic int fmt_float(char *buf, size_t size, double v, int single) {\n if (isnan(v)) return snprintf(buf, size, \"nan\");\n if (isinf(v)) return snprintf(buf, size, v < 0 ? \"-inf\" : \"inf\");\n int p = 1;\n for (; p < 17; p++) {\n snprintf(buf, size, \"%.*e\", p - 1, v);\n double back = strtod(buf, NULL);\n if (single ? (float)back == (float)v : back == v) break;\n }\n int exp = atoi(strchr(buf, 'e') + 1);\n if (exp < -5 || exp >= 17) return snprintf(buf, size, \"%.*g\", p, v);\n int decimals = p - 1 - exp;\n return snprintf(buf, size, \"%.*f\", decimals > 0 ? decimals : 0, v);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 0);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_f32(FStr *out, double v) {\n char buf[64];\n int n = fmt_float(buf, sizeof buf, v, 1);\n str_heap(out, buf, n);\n}\n\n// str(x, 2): a fixed number of digits after the point\nvoid finch_str_fixed(FStr *out, double v, int64_t digits) {\n char buf[400];\n if (digits < 0) digits = 0;\n if (digits > 60) digits = 60;\n int n = snprintf(buf, sizeof buf, \"%.*f\", (int)digits, v);\n str_heap(out, buf, n);\n}\n\nvoid finch_print_float(double v, int32_t single) {\n char buf[64];\n fmt_float(buf, sizeof buf, v, single);\n fputs(buf, stdout);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n finch_str_drop((FStr *)msg);\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 0));\n}\nvoid finch_buf_f32(FStr *b, double v) {\n char t[64];\n finch_buf_add(b, t, fmt_float(t, sizeof t, v, 1));\n}\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// after main: the arguments go\nvoid finch_args_free(FArr *a) {\n for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);\n finch_arr_free(a);\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- folders (std/fs.fch, through std/finch_fs.h) ----------\n\nint finch_fs_is_dir(const char *path) {\n#ifdef _WIN32\n struct _finddata_t d;\n intptr_t h = _findfirst(path, &d);\n if (h == -1) return 0;\n _findclose(h);\n return (d.attrib & _A_SUBDIR) != 0;\n#else\n struct stat st;\n return stat(path, &st) == 0 && S_ISDIR(st.st_mode);\n#endif\n}\n\nint finch_fs_exists(const char *path) {\n#ifdef _WIN32\n return _access(path, 0) == 0;\n#else\n struct stat st;\n return stat(path, &st) == 0;\n#endif\n}\n\nstatic int make_one_dir(const char *p) {\n#ifdef _WIN32\n return _mkdir(p) == 0 || errno == EEXIST;\n#else\n return mkdir(p, 0777) == 0 || errno == EEXIST;\n#endif\n}\n\n// mkdir -p: the parents too; 1 if the folder is there afterwards\nint finch_fs_mkdir(const char *path) {\n size_t n = strlen(path);\n char *p = finch_alloc((int64_t)n + 1);\n memcpy(p, path, n + 1);\n for (size_t i = 1; i < n; i++) {\n if (p[i] == '/' || p[i] == '\\\\') {\n if (p[i - 1] == ':') continue; /* C: is a drive */\n char c = p[i];\n p[i] = 0;\n make_one_dir(p);\n p[i] = c;\n }\n }\n int ok = make_one_dir(p) && finch_fs_is_dir(p);\n free(p);\n return ok;\n}\n\nint finch_fs_remove_dir(const char *path) {\n#ifdef _WIN32\n return _rmdir(path) == 0;\n#else\n return rmdir(path) == 0;\n#endif\n}\n\n// the names in a folder, one per call (\"\" at the end), without . and ..\ntypedef struct {\n#ifdef _WIN32\n intptr_t h;\n struct _finddata_t d;\n int first;\n#else\n DIR *d;\n#endif\n} FsList;\n\nvoid *finch_fs_list_open(const char *path) {\n FsList *l = finch_zalloc((int64_t)sizeof(FsList));\n#ifdef _WIN32\n size_t n = strlen(path);\n char *pat = finch_alloc((int64_t)n + 3);\n memcpy(pat, path, n);\n memcpy(pat + n, \"\\\\*\", 3);\n l->h = _findfirst(pat, &l->d);\n free(pat);\n if (l->h == -1) {\n free(l);\n return NULL;\n }\n l->first = 1;\n#else\n l->d = opendir(path);\n if (!l->d) {\n free(l);\n return NULL;\n }\n#endif\n return l;\n}\n\nconst char *finch_fs_list_next(void *handle) {\n FsList *l = handle;\n for (;;) {\n#ifdef _WIN32\n if (!l->first && _findnext(l->h, &l->d) != 0) return \"\";\n l->first = 0;\n const char *name = l->d.name;\n#else\n struct dirent *e = readdir(l->d);\n if (!e) return \"\";\n const char *name = e->d_name;\n#endif\n if (strcmp(name, \".\") != 0 && strcmp(name, \"..\") != 0) return name;\n }\n}\n\nvoid finch_fs_list_close(void *handle) {\n FsList *l = handle;\n#ifdef _WIN32\n _findclose(l->h);\n#else\n closedir(l->d);\n#endif\n free(l);\n}\n\n// where a program keeps its own files for this user (saves, settings); \"\" if unknown\n// Linux: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%\nconst char *finch_fs_data_home(void) {\n static char buf[4096];\n const char *v;\n#if defined(_WIN32)\n v = getenv(\"APPDATA\");\n snprintf(buf, sizeof buf, \"%s\", v ? v : \"\");\n#elif defined(__APPLE__)\n v = getenv(\"HOME\");\n if (v) snprintf(buf, sizeof buf, \"%s/Library/Application Support\", v);\n else buf[0] = 0;\n#else\n v = getenv(\"XDG_DATA_HOME\");\n if (v && *v) snprintf(buf, sizeof buf, \"%s\", v);\n else if ((v = getenv(\"HOME\"))) snprintf(buf, sizeof buf, \"%s/.local/share\", v);\n else buf[0] = 0;\n#endif\n return buf;\n}\n\n// ---------- threads (std/thread.fch, through std/finch_thread.h) ----------\n\ntypedef struct {\n void (*fn)(void *);\n void *arg;\n} ThreadStart;\n\n#ifdef _WIN32\nstatic DWORD WINAPI thread_main(LPVOID p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return 0;\n}\n#else\nstatic void *thread_main(void *p) {\n ThreadStart st = *(ThreadStart *)p;\n free(p);\n st.fn(st.arg);\n return NULL;\n}\n#endif\n\n// run fn(arg) on a new thread; NULL if it couldn't start\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg) {\n ThreadStart *st = finch_alloc((int64_t)sizeof(ThreadStart));\n st->fn = fn;\n st->arg = arg;\n#ifdef _WIN32\n HANDLE h = CreateThread(NULL, 0, thread_main, st, 0, NULL);\n if (!h) {\n free(st);\n return NULL;\n }\n return h;\n#else\n pthread_t *t = finch_alloc((int64_t)sizeof(pthread_t));\n if (pthread_create(t, NULL, thread_main, st) != 0) {\n free(st);\n free(t);\n return NULL;\n }\n return t;\n#endif\n}\n\n// wait for the thread to end; 1 if that worked\nint finch_thread_join(void *t) {\n if (!t) return 0;\n#ifdef _WIN32\n int ok = WaitForSingleObject((HANDLE)t, INFINITE) == WAIT_OBJECT_0;\n CloseHandle((HANDLE)t);\n return ok;\n#else\n int ok = pthread_join(*(pthread_t *)t, NULL) == 0;\n free(t);\n return ok;\n#endif\n}\n\nvoid *finch_mutex_new(void) {\n#ifdef _WIN32\n CRITICAL_SECTION *m = finch_alloc((int64_t)sizeof(CRITICAL_SECTION));\n InitializeCriticalSection(m);\n#else\n pthread_mutex_t *m = finch_alloc((int64_t)sizeof(pthread_mutex_t));\n pthread_mutex_init(m, NULL);\n#endif\n return m;\n}\n\nvoid finch_mutex_lock(void *m) {\n#ifdef _WIN32\n EnterCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_lock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_unlock(void *m) {\n#ifdef _WIN32\n LeaveCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_unlock((pthread_mutex_t *)m);\n#endif\n}\n\nvoid finch_mutex_free(void *m) {\n if (!m) return;\n#ifdef _WIN32\n DeleteCriticalSection((CRITICAL_SECTION *)m);\n#else\n pthread_mutex_destroy((pthread_mutex_t *)m);\n#endif\n free(m);\n}\n\nint finch_cpu_count(void) {\n#ifdef _WIN32\n SYSTEM_INFO si;\n GetSystemInfo(&si);\n return (int)si.dwNumberOfProcessors;\n#else\n long n = sysconf(_SC_NPROCESSORS_ONLN);\n return n > 0 ? (int)n : 1;\n#endif\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n") - list.push("// Files and folders: bytes, folders, listing, and a place for a program's own files.\n//\n// import fs\n// data := try fs.read_bytes(\"atlas.png\") the whole file as []u8 (any bytes, NUL included)\n// try fs.write_bytes(\"out.bin\", data)\n// try fs.mkdir(\"saves/slot1\") with the folders above it\n// names := try fs.list(\"saves\") the names in a folder, sorted\n// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p)\n// dir := try fs.data_dir(\"MyGame\") a folder for this user's saves, made if needed\nimport \"finch_fs.h\"\n\nfn read_bytes(str path) -> []u8! {\n text := try read_file(path)\n return text.bytes()\n}\n\nfn write_bytes(str path, []u8 data) -> ! {\n try write_file(path, str(data))\n}\n\nfn exists(str path) -> bool {\n return finch_fs_exists(path) != 0\n}\n\nfn is_dir(str path) -> bool {\n return finch_fs_is_dir(path) != 0\n}\n\nfn mkdir(str path) -> ! {\n if finch_fs_mkdir(path) == 0 {\n return error(\"can't make the folder \\\"\" + path + \"\\\"\")\n }\n}\n\n// only an empty folder\nfn remove_dir(str path) -> ! {\n if finch_fs_remove_dir(path) == 0 {\n return error(\"can't remove the folder \\\"\" + path + \"\\\" (is it empty?)\")\n }\n}\n\nfn list(str path) -> []str! {\n l := finch_fs_list_open(path)\n if l == null {\n return error(\"can't list the folder \\\"\" + path + \"\\\"\")\n }\n []str names\n while true {\n name := finch_fs_list_next(l)\n if name.len == 0 {\n break\n }\n names.push(name)\n }\n finch_fs_list_close(l)\n names.sort()\n return names\n}\n\n// The folder where a program keeps this user's files (saves, settings), made if it isn't there:\n// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\\ on Windows.\nfn data_dir(str app) -> str! {\n home := finch_fs_data_home()\n if home.len == 0 {\n return error(\"this system doesn't say where programs keep their files\")\n }\n dir := home + \"/\" + app\n try mkdir(dir)\n return dir\n}\n") + list.push("// The Finch runtime: string and array helpers, input, files and runtime errors.\n// It is embedded in the compiler, compiled once into a cache folder, and linked\n// statically into every Finch program. Everything here is plain C with no state.\n//\n// Layout shared with the compiler (src/codegen.cpp):\n// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated\n// cap > 0 : heap memory owned by the value (cap bytes allocated)\n// cap == 0 : a string literal, valid forever, never written to\n// cap == -1: borrowed from C, must be copied before it is kept\n// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0\n// map[K]V: see \"maps\" below\n\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#ifndef _WIN32\n#include \n#endif\n\ntypedef struct {\n char *ptr;\n int64_t len;\n int64_t cap;\n} FStr;\n\ntypedef struct {\n void *ptr;\n int64_t len;\n int64_t cap;\n} FArr;\n\nstatic char empty[1] = \"\";\n\n// ---------- runtime errors ----------\n\n// A program that loads Finch libraries (finch build --shared) can take runtime errors over\n// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler\n// gets the message and must not return (longjmp out, or exit itself).\nstatic void (*fatal_hook)(const char *msg);\nvoid finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; }\n\nstatic __attribute__((noreturn)) void fatal(const char *msg) {\n if (fatal_hook) fatal_hook(msg);\n fflush(stdout);\n fprintf(stderr, \"%s\\n\", msg);\n exit(1);\n}\n\n__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {\n char full[1024];\n snprintf(full, sizeof full, \"%s:%lld: runtime error: %s\", file, (long long)line, msg);\n fflush(stdout);\n fatal(full);\n}\n\n__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {\n char msg[128];\n snprintf(msg, sizeof msg, \"index %lld is out of range (the length is %lld)\", (long long)i, (long long)len);\n finch_panic(file, line, msg);\n}\n\nstatic void *must(void *p) {\n if (!p) {\n fflush(stdout);\n fatal(\"runtime error: out of memory\");\n }\n return p;\n}\n\nvoid *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }\nvoid *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }\n\n// ---------- strings ----------\n\nstatic void str_heap(FStr *out, const char *p, int64_t len) {\n char *m = finch_alloc(len + 1);\n if (len) memcpy(m, p, (size_t)len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void str_static(FStr *out, const char *lit) {\n out->ptr = (char *)lit;\n out->len = (int64_t)strlen(lit);\n out->cap = 0;\n}\n\n// A copy that can be kept: literals are shared, everything else is duplicated.\nvoid finch_str_copy(FStr *out, const FStr *s) {\n if (s->cap == 0) *out = *s;\n else str_heap(out, s->ptr, s->len);\n}\n\n// Make `s` writable (before s[i] = c).\nvoid finch_str_own(FStr *s) {\n if (s->cap > 0) return;\n FStr t;\n str_heap(&t, s->ptr, s->len);\n *s = t;\n}\n\nvoid finch_str_drop(FStr *s) {\n if (s->cap > 0) free(s->ptr);\n}\n\nvoid finch_str_from_c(FStr *out, const char *p) {\n if (!p) {\n str_static(out, empty);\n return;\n }\n out->ptr = (char *)p;\n out->len = (int64_t)strlen(p);\n out->cap = -1;\n}\n\nvoid finch_str_concat(FStr *out, const FStr *a, const FStr *b) {\n int64_t len = a->len + b->len;\n char *m = finch_alloc(len + 1);\n memcpy(m, a->ptr, (size_t)a->len);\n memcpy(m + a->len, b->ptr, (size_t)b->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nint finch_str_eq(const FStr *a, const FStr *b) {\n return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;\n}\n\nint64_t finch_str_cmp(const FStr *a, const FStr *b) {\n int64_t n = a->len < b->len ? a->len : b->len;\n int c = memcmp(a->ptr, b->ptr, (size_t)n);\n if (c) return c < 0 ? -1 : 1;\n return a->len < b->len ? -1 : a->len > b->len;\n}\n\nvoid finch_str_from_int(FStr *out, int64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%lld\", (long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_uint(FStr *out, uint64_t v) {\n char buf[32];\n int n = snprintf(buf, sizeof buf, \"%llu\", (unsigned long long)v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_float(FStr *out, double v) {\n char buf[64];\n int n = snprintf(buf, sizeof buf, \"%g\", v);\n str_heap(out, buf, n);\n}\n\nvoid finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }\n\nvoid finch_str_from_bool(FStr *out, int b) { str_static(out, b ? \"true\" : \"false\"); }\n\nstatic void number_error(char *msg, size_t size, const FStr *s, const char *kind) {\n snprintf(msg, size, \"can't turn \\\"%.*s\\\" into %s\", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : \"\", kind);\n}\n\nstatic int parse_int(const FStr *s, int64_t *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n errno = 0;\n long long v = strtoll(p, &end, 10);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end || errno == ERANGE);\n}\n\nstatic int parse_float(const FStr *s, double *out) {\n const char *p = s->ptr ? s->ptr : \"\";\n while (isspace((unsigned char)*p)) p++;\n char *end;\n double v = strtod(p, &end);\n while (isspace((unsigned char)*end)) end++;\n *out = v;\n return !(end == p || *end);\n}\n\nint64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {\n int64_t v;\n if (!parse_int(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\ndouble finch_str_to_float(const FStr *s, const char *file, int64_t line) {\n double v;\n if (!parse_float(s, &v)) {\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n finch_panic(file, line, msg);\n }\n return v;\n}\n\n// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------\n// Each gives back 1 on success; on failure 0, with the message in *err.\n\nstatic void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }\n\nint finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {\n if (parse_int(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"int\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nint finch_str_try_float(const FStr *s, double *out, FStr *err) {\n if (parse_float(s, out)) return 1;\n char msg[200];\n number_error(msg, sizeof msg, s, \"float\");\n set_error(err, msg);\n *out = 0;\n return 0;\n}\n\nstatic void file_error(FStr *err, const char *what, const FStr *path) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't %s the file \\\"%.*s\\\": %s\", what, (int)(path->len > 200 ? 200 : path->len),\n path->ptr ? path->ptr : \"\", strerror(errno));\n set_error(err, msg);\n}\n\n// when `fn main() -> !` fails\nvoid finch_report_error(const FStr *msg) {\n fflush(stdout);\n fprintf(stderr, \"error: %.*s\\n\", (int)msg->len, msg->ptr ? msg->ptr : \"\");\n}\n\nvoid finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {\n if (start < 0 || end > s->len || start > end) {\n char msg[160];\n snprintf(msg, sizeof msg, \"sub(%lld, %lld) is out of range (the length is %lld)\", (long long)start,\n (long long)end, (long long)s->len);\n finch_panic(file, line, msg);\n }\n str_heap(out, s->ptr + start, end - start);\n}\n\nint64_t finch_str_find(const FStr *s, const FStr *t) {\n if (t->len == 0) return 0;\n for (int64_t i = 0; i + t->len <= s->len; i++)\n if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;\n return -1;\n}\n\nint finch_str_starts(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;\n}\n\nint finch_str_ends(const FStr *s, const FStr *t) {\n return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;\n}\n\nvoid finch_str_trim(FStr *out, const FStr *s) {\n int64_t a = 0, b = s->len;\n while (a < b && isspace((unsigned char)s->ptr[a])) a++;\n while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;\n str_heap(out, s->ptr + a, b - a);\n}\n\nvoid finch_str_upper(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_lower(FStr *out, const FStr *s) {\n str_heap(out, s->ptr, s->len);\n for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);\n}\n\nvoid finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {\n if (from->len == 0) {\n str_heap(out, s->ptr, s->len);\n return;\n }\n int64_t count = 0;\n for (int64_t i = 0; i + from->len <= s->len;) {\n if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n count++;\n i += from->len;\n } else {\n i++;\n }\n }\n int64_t len = s->len + count * (to->len - from->len);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < s->len;) {\n if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {\n memcpy(w, to->ptr, (size_t)to->len);\n w += to->len;\n i += from->len;\n } else {\n *w++ = s->ptr[i++];\n }\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len);\n\n// \"a,b,,c\".split(\",\") -> [\"a\", \"b\", \"\", \"c\"]; split(\"\") gives single characters.\nvoid finch_str_split(FArr *out, const FStr *s, const FStr *sep) {\n out->ptr = NULL;\n out->len = out->cap = 0;\n if (sep->len == 0) {\n for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);\n return;\n }\n int64_t start = 0;\n for (int64_t i = 0; i + sep->len <= s->len;) {\n if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {\n arr_push_str(out, s->ptr + start, i - start);\n i += sep->len;\n start = i;\n } else {\n i++;\n }\n }\n arr_push_str(out, s->ptr + start, s->len - start);\n}\n\nvoid finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {\n const FStr *p = parts->ptr;\n int64_t len = 0;\n for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);\n char *m = finch_alloc(len + 1), *w = m;\n for (int64_t i = 0; i < parts->len; i++) {\n if (i) {\n memcpy(w, sep->ptr, (size_t)sep->len);\n w += sep->len;\n }\n memcpy(w, p[i].ptr, (size_t)p[i].len);\n w += p[i].len;\n }\n *w = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_repeat(FStr *out, const FStr *s, int64_t n) {\n if (n < 0) n = 0;\n int64_t len = s->len * n;\n char *m = finch_alloc(len + 1);\n for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);\n m[len] = 0;\n out->ptr = m;\n out->len = len;\n out->cap = len + 1;\n}\n\nvoid finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }\n\n// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------\n// A buffer starts as an empty str and grows on the heap.\n\nvoid finch_buf_add(FStr *b, const char *p, int64_t n) {\n if (b->len + n + 1 > b->cap) {\n int64_t cap = b->cap > 0 ? b->cap * 2 : 32;\n while (cap < b->len + n + 1) cap *= 2;\n b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));\n b->cap = cap;\n }\n if (n) memcpy(b->ptr + b->len, p, (size_t)n);\n b->len += n;\n b->ptr[b->len] = 0;\n}\n\nstatic void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));\nstatic void buf_fmt(FStr *b, const char *fmt, ...) {\n char t[64];\n va_list ap;\n va_start(ap, fmt);\n int n = vsnprintf(t, sizeof t, fmt, ap);\n va_end(ap);\n finch_buf_add(b, t, n);\n}\n\nvoid finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, \"%lld\", (long long)v); }\nvoid finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, \"%llu\", (unsigned long long)v); }\nvoid finch_buf_float(FStr *b, double v) { buf_fmt(b, \"%g\", v); }\nvoid finch_buf_bool(FStr *b, int v) { finch_buf_add(b, v ? \"true\" : \"false\", v ? 4 : 5); }\nvoid finch_buf_ptr(FStr *b, const void *p) {\n if (p) buf_fmt(b, \"0x%llx\", (unsigned long long)(uintptr_t)p);\n else finch_buf_add(b, \"null\", 4);\n}\n\nvoid finch_buf_char(FStr *b, char c, int quoted) {\n if (quoted) finch_buf_add(b, \"'\", 1);\n finch_buf_add(b, &c, 1);\n if (quoted) finch_buf_add(b, \"'\", 1);\n}\n\nvoid finch_buf_str(FStr *b, const FStr *s, int quoted) {\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n finch_buf_add(b, s->ptr, s->len);\n if (quoted) finch_buf_add(b, \"\\\"\", 1);\n}\n\n// print the buffer and free it\nvoid finch_buf_print(FStr *b) {\n if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);\n if (b->cap > 0) free(b->ptr);\n}\n\n// ---------- arrays ----------\n\nvoid finch_arr_reserve(FArr *a, int64_t need, int64_t es) {\n if (need <= a->cap) return;\n int64_t cap = a->cap * 2;\n if (cap < 4) cap = 4;\n if (cap < need) cap = need;\n a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));\n a->cap = cap;\n}\n\nvoid finch_arr_make(FArr *out, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n out->ptr = len ? finch_zalloc(len * es) : NULL;\n out->len = len;\n out->cap = len;\n}\n\n// grow or shrink to `len`; new elements are zero\nvoid finch_arr_resize(FArr *a, int64_t len, int64_t es) {\n if (len < 0) len = 0;\n finch_arr_reserve(a, len, es);\n if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));\n a->len = len;\n}\n\n// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)\nvoid finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {\n out->len = a->len;\n out->cap = a->len;\n out->ptr = a->len ? finch_alloc(a->len * es) : NULL;\n if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));\n}\n\nvoid finch_arr_free(FArr *a) {\n if (a->cap > 0) free(a->ptr);\n}\n\n// open a hole at i (0 <= i <= len), len grows by one\nvoid finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {\n if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);\n finch_arr_reserve(a, a->len + 1, es);\n char *p = a->ptr;\n memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));\n a->len++;\n}\n\n// close the hole at i (the element was already moved out), len shrinks by one\nvoid finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {\n char *p = a->ptr;\n memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));\n a->len--;\n}\n\nstatic void arr_push_str(FArr *a, const char *p, int64_t len) {\n finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));\n str_heap((FStr *)a->ptr + a->len, p, len);\n a->len++;\n}\n\nvoid finch_bytes_from_str(FArr *out, const FStr *s) {\n out->len = out->cap = s->len;\n out->ptr = s->len ? finch_alloc(s->len) : NULL;\n if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);\n}\n\n// ---------- program arguments, input and files ----------\n\nvoid finch_args(FArr *out, int argc, char **argv) {\n out->len = out->cap = argc;\n out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));\n for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));\n}\n\n// input(\"Name? \") prints the prompt and reads one line, without the newline.\nvoid finch_input(FStr *out, const FStr *prompt) {\n if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);\n fflush(stdout);\n // read one line of any length (getline isn't available everywhere)\n size_t size = 128, n = 0;\n char *line = finch_alloc((int64_t)size);\n int c, got = 0;\n while ((c = fgetc(stdin)) != EOF) {\n got = 1;\n if (c == '\\n') break;\n if (n + 1 >= size) line = must(realloc(line, size *= 2));\n line[n++] = (char)c;\n }\n if (!got) {\n free(line);\n str_static(out, empty);\n return;\n }\n while (n > 0 && line[n - 1] == '\\r') n--;\n line[n] = 0;\n out->ptr = line;\n out->len = (int64_t)n;\n out->cap = (int64_t)size;\n}\n\nint finch_file_exists(const FStr *path) {\n FILE *f = fopen(path->ptr, \"rb\");\n if (f) fclose(f);\n return f != NULL;\n}\n\nint finch_try_read_file(FStr *out, const FStr *path, FStr *err) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n file_error(err, \"read\", path);\n return 0;\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n return 1;\n}\n\nvoid finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"rb\");\n if (!f) {\n char msg[300];\n snprintf(msg, sizeof msg, \"can't read the file \\\"%.*s\\\": %s\", (int)(path->len > 200 ? 200 : path->len),\n path->ptr, strerror(errno));\n finch_panic(file, line, msg);\n }\n fseek(f, 0, SEEK_END);\n long size = ftell(f);\n fseek(f, 0, SEEK_SET);\n char *m = finch_alloc(size + 1);\n size_t got = fread(m, 1, (size_t)size, f);\n fclose(f);\n m[got] = 0;\n out->ptr = m;\n out->len = (int64_t)got;\n out->cap = size + 1;\n}\n\n// shell(\"ls -l\"): run a command, give back its exit code\nint64_t finch_shell(const FStr *cmd) {\n fflush(stdout);\n int status = system(cmd->ptr ? cmd->ptr : \"\");\n#ifdef _WIN32\n return status; // Windows gives the exit code directly\n#else\n if (status == -1) return -1;\n if (WIFEXITED(status)) return WEXITSTATUS(status);\n return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);\n#endif\n}\n\nint finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : \"\") == 0; }\n\nint finch_try_delete_file(const FStr *path, FStr *err) {\n if (remove(path->ptr ? path->ptr : \"\") == 0) return 1;\n file_error(err, \"delete\", path);\n return 0;\n}\n\n// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here\nvoid *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }\n\nint finch_write_file(const FStr *path, const FStr *text) {\n FILE *f = fopen(path->ptr ? path->ptr : \"\", \"wb\");\n if (!f) return 0;\n size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0;\n return fclose(f) == 0 && put == (size_t)text->len;\n}\n\nint finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {\n errno = 0;\n if (finch_write_file(path, text)) return 1;\n if (!errno) errno = EIO;\n file_error(err, \"write\", path);\n return 0;\n}\n\n// ---------- maps ----------\n// map[K]V = { entries; len; used; cap; index; icap; adds }\n// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.\n// A removed entry keeps its slot (hash 0) until the next resize packs the slots.\n// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.\n// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.\n// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.\n// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).\n\ntypedef struct {\n char *entries;\n int64_t len, used, cap;\n int64_t *index;\n int64_t icap;\n int64_t adds;\n} FMap;\n\nstatic uint64_t map_hash(const void *key, int64_t ks, int kstr) {\n const unsigned char *p;\n size_t n;\n if (kstr) {\n const FStr *s = key;\n p = (const unsigned char *)s->ptr;\n n = (size_t)s->len;\n } else {\n p = key;\n n = (size_t)ks;\n }\n uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well\n for (size_t i = 0; i < n; i++) {\n h ^= p[i];\n h *= 1099511628211ULL;\n }\n h ^= h >> 31;\n h *= 0xbf58476d1ce4e5b9ULL;\n h ^= h >> 29;\n return h | 1; // 0 marks a removed entry\n}\n\nstatic int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {\n if (!kstr) return memcmp(a, b, (size_t)ks) == 0;\n const FStr *x = (const FStr *)a, *y = b;\n return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);\n}\n\nstatic void map_reindex(FMap *m, int64_t es) {\n if (m->len != m->used) { // pack the slots of removed entries away\n int64_t j = 0;\n for (int64_t i = 0; i < m->used; i++) {\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == 0) continue;\n if (i != j) memmove(m->entries + j * es, e, (size_t)es);\n j++;\n }\n m->used = j;\n }\n int64_t icap = 16;\n while (icap < m->cap * 2) icap *= 2;\n free(m->index);\n m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));\n memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));\n m->icap = icap;\n for (int64_t i = 0; i < m->used; i++) {\n uint64_t h = *(uint64_t *)(m->entries + i * es);\n int64_t s = (int64_t)(h & (uint64_t)(icap - 1));\n while (m->index[s] != -1) s = (s + 1) & (icap - 1);\n m->index[s] = i;\n }\n}\n\n// the entry number of `key`, or -1\nint64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {\n if (m->len == 0) return -1;\n uint64_t h = map_hash(key, ks, kstr);\n for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {\n int64_t i = m->index[s];\n if (i == -1) return -1;\n if (i < 0) continue;\n char *e = m->entries + i * es;\n if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;\n }\n}\n\n// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.\nint64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {\n int64_t i = finch_map_find(m, key, es, ks, kstr);\n *added = i < 0;\n if (i >= 0) return i;\n if (m->used == m->cap) {\n if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing\n map_reindex(m, es);\n } else {\n m->cap = m->cap ? m->cap * 2 : 8;\n m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));\n map_reindex(m, es);\n }\n }\n uint64_t h = map_hash(key, ks, kstr);\n i = m->used++;\n char *e = m->entries + i * es;\n memset(e, 0, (size_t)es);\n *(uint64_t *)e = h;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);\n m->index[s] = i;\n m->len++;\n m->adds++;\n return i;\n}\n\n// Forget entry i (its key and value were already dropped by the compiler).\nvoid finch_map_remove_at(FMap *m, int64_t i, int64_t es) {\n char *e = m->entries + i * es;\n uint64_t h = *(uint64_t *)e;\n int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));\n while (m->index[s] != i) s = (s + 1) & (m->icap - 1);\n m->index[s] = -2;\n *(uint64_t *)e = 0;\n if (--m->len == 0) {\n m->used = 0;\n memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n }\n}\n\nvoid finch_map_clear(FMap *m) {\n m->len = m->used = 0;\n if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));\n}\n\nvoid finch_map_free(FMap *m) {\n free(m->entries);\n free(m->index);\n}\n\n// A copy of the slots and the index; the compiler then copies the keys and values that own memory.\nvoid finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {\n *out = *m;\n if (!m->cap) return;\n out->entries = finch_alloc(m->cap * es);\n memcpy(out->entries, m->entries, (size_t)(m->used * es));\n out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));\n memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));\n}\n\n__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {\n finch_panic(file, line, \"a key was added to the map while looping over it (collect the new keys, and add them after the loop)\");\n}\n\n__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {\n char msg[200];\n snprintf(msg, sizeof msg, \"the key %.*s is not in the map (check with .has(key), or use .get(key, default))\",\n (int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : \"\");\n finch_panic(file, line, msg);\n}\n") list.push("// JSON: read it, build it, write it.\n//\n// import json\n//\n// doc := try json.parse(\"{\\\"name\\\": \\\"Finch\\\", \\\"tags\\\": [\\\"fast\\\", \\\"small\\\"], \\\"stars\\\": 5}\")\n// print(doc.get(\"name\").str_or(\"\")) Finch\n// print(doc.get(\"tags\").at(1).str_or(\"\")) small\n// print(doc.get(\"stars\").int_or(0)) 5\n// print(doc.get(\"missing\").is_null()) true (a missing key is null, so chains never fail)\n//\n// out := json.new_object()\n// out.set(\"ok\", json.new_bool(true))\n// list := json.new_list()\n// list.push(json.new_number(1.5))\n// out.set(\"values\", list)\n// print(json.stringify(out)) {\"ok\":true,\"values\":[1.5]}\n// print(json.pretty(out)) the same, over several lines\n\nenum Kind { Null, Bool, Number, Text, List, Object }\n\n// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,\n// text for Text, items for List, and items with keys (the same length, in the same order) for Object.\nstruct Json {\n Kind kind\n bool flag\n float num\n str text\n []Json items\n []str keys\n}\n\nfn new_null() -> Json {\n return Json(kind: Kind.Null)\n}\n\nfn new_bool(bool b) -> Json {\n return Json(kind: Kind.Bool, flag: b)\n}\n\nfn new_number(float n) -> Json {\n return Json(kind: Kind.Number, num: n)\n}\n\nfn new_text(str s) -> Json {\n return Json(kind: Kind.Text, text: s)\n}\n\nfn new_list() -> Json {\n return Json(kind: Kind.List)\n}\n\nfn new_object() -> Json {\n return Json(kind: Kind.Object)\n}\n\n// ---------- looking inside ----------\n\nfn Json.is_null() -> bool {\n return self.kind == Kind.Null\n}\n\n// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest\nfn Json.len() -> int {\n return match self.kind {\n List, Object -> self.items.len\n Text -> self.text.len\n else -> 0\n }\n}\n\nfn Json.has(str key) -> bool {\n return self.kind == Kind.Object && self.keys.contains(key)\n}\n\n// the value of a key of an object; null if there is none (or this is not an object)\nfn Json.get(str key) -> Json {\n if self.kind != Kind.Object {\n return new_null()\n }\n i := self.keys.find(key)\n if i < 0 {\n return new_null()\n }\n return self.items[i]\n}\n\n// the element of a list; null if there is none\nfn Json.at(int i) -> Json {\n if self.kind != Kind.List || i < 0 || i >= self.items.len {\n return new_null()\n }\n return self.items[i]\n}\n\n// the value if it is of that kind, else what was given\nfn Json.str_or(str fallback) -> str {\n if self.kind == Kind.Text {\n return self.text\n }\n return fallback\n}\n\nfn Json.float_or(float fallback) -> float {\n if self.kind == Kind.Number {\n return self.num\n }\n return fallback\n}\n\nfn Json.int_or(int fallback) -> int {\n if self.kind == Kind.Number {\n return int(self.num)\n }\n return fallback\n}\n\nfn Json.bool_or(bool fallback) -> bool {\n if self.kind == Kind.Bool {\n return self.flag\n }\n return fallback\n}\n\n// the keys of an object, in the order they were written\nfn Json.names() -> []str {\n return self.keys\n}\n\n// ---------- building ----------\n\n// add to the end of a list\nfn Json.push(Json v) {\n self.items.push(v)\n}\n\n// give a key a value in an object; a key that is already there is replaced\nfn Json.set(str key, Json v) {\n i := self.keys.find(key)\n if i >= 0 {\n self.items[i] = v\n return\n }\n self.keys.push(key)\n self.items.push(v)\n}\n\n// ---------- writing ----------\n\nfn quote(str s) -> str {\n []str parts = [\"\\\"\"]\n for c in s {\n n := int(c)\n if c == '\"' {\n parts.push(\"\\\\\\\"\")\n } else if c == '\\\\' {\n parts.push(\"\\\\\\\\\")\n } else if c == '\\n' {\n parts.push(\"\\\\n\")\n } else if c == '\\r' {\n parts.push(\"\\\\r\")\n } else if c == '\\t' {\n parts.push(\"\\\\t\")\n } else if n < 32 {\n parts.push(\"\\\\u00\" + hexdigit(n / 16) + hexdigit(n % 16))\n } else {\n parts.push(str(c))\n }\n }\n parts.push(\"\\\"\")\n return parts.join(\"\")\n}\n\nfn hexdigit(int n) -> str {\n digits := \"0123456789abcdef\"\n return str(digits[n])\n}\n\nfn number_text(float n) -> str {\n if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {\n return str(int(n))\n }\n return str(n)\n}\n\nstruct Writer {\n []str out\n}\n\nfn write(Json j, str indent, str pad, ptr[Writer] w) {\n nl := \"\"\n inner := \"\"\n sep := \",\"\n colon := \":\"\n if indent.len > 0 {\n nl = \"\\n\"\n inner = pad + indent\n colon = \": \"\n }\n match j.kind {\n Null -> w.out.push(\"null\")\n Bool -> {\n if j.flag {\n w.out.push(\"true\")\n } else {\n w.out.push(\"false\")\n }\n }\n Number -> w.out.push(number_text(j.num))\n Text -> w.out.push(quote(j.text))\n List -> {\n if j.items.len == 0 {\n w.out.push(\"[]\")\n return\n }\n w.out.push(\"[\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"]\")\n }\n Object -> {\n if j.items.len == 0 {\n w.out.push(\"{}\")\n return\n }\n w.out.push(\"{\" + nl)\n for i, item in j.items {\n if i > 0 {\n w.out.push(sep + nl)\n }\n w.out.push(inner + quote(j.keys[i]) + colon)\n write(item, indent, inner, w)\n }\n w.out.push(nl + pad + \"}\")\n }\n }\n}\n\n// the value as text on one line\nfn stringify(Json j) -> str {\n w := Writer()\n write(j, \"\", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// the value as text over several lines, indented by two spaces\nfn pretty(Json j) -> str {\n w := Writer()\n write(j, \" \", \"\", addr(w))\n return w.out.join(\"\")\n}\n\n// ---------- reading ----------\n\nstruct Reader {\n str src\n int i\n}\n\n// the line and column of a position, for a message\nfn where(ptr[Reader] r) -> str {\n line := 1\n col := 1\n for k in 0..r.i {\n if k < r.src.len && r.src[k] == '\\n' {\n line += 1\n col = 1\n } else {\n col += 1\n }\n }\n return \"line \" + str(line) + \", column \" + str(col)\n}\n\nfn peek(ptr[Reader] r) -> char {\n if r.i < r.src.len {\n return r.src[r.i]\n }\n return char(0)\n}\n\nfn skip_space(ptr[Reader] r) {\n while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\\n' || r.src[r.i] == '\\t' || r.src[r.i] == '\\r') {\n r.i += 1\n }\n}\n\nfn unexpected(ptr[Reader] r, str wanted) -> str {\n if r.i >= r.src.len {\n return \"json: the text ends where \" + wanted + \" should be (\" + where(r) + \")\"\n }\n return \"json: found '\" + str(r.src[r.i]) + \"' where \" + wanted + \" should be (\" + where(r) + \")\"\n}\n\n// does `word` come next?\nfn comes(ptr[Reader] r, str word) -> bool {\n return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word\n}\n\nfn expect_word(ptr[Reader] r, str word) -> ! {\n if !comes(r, word) {\n return error(unexpected(r, \"'\" + word + \"'\"))\n }\n r.i += word.len\n}\n\nfn hex_value(char c) -> int {\n if c >= '0' && c <= '9' {\n return int(c) - int('0')\n }\n if c >= 'a' && c <= 'f' {\n return int(c) - int('a') + 10\n }\n if c >= 'A' && c <= 'F' {\n return int(c) - int('A') + 10\n }\n return -1\n}\n\n// a code point as UTF-8\nfn utf8(int cp) -> str {\n []u8 b\n if cp < 128 {\n b.push(u8(cp))\n } else if cp < 2048 {\n b.push(u8(192 + cp / 64))\n b.push(u8(128 + cp % 64))\n } else if cp < 65536 {\n b.push(u8(224 + cp / 4096))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n } else {\n b.push(u8(240 + cp / 262144))\n b.push(u8(128 + (cp / 4096) % 64))\n b.push(u8(128 + (cp / 64) % 64))\n b.push(u8(128 + cp % 64))\n }\n return str(b)\n}\n\nfn read_hex4(ptr[Reader] r) -> int! {\n n := 0\n for k in 0..4 {\n if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {\n return error(unexpected(r, \"a hexadecimal digit\"))\n }\n n = n * 16 + hex_value(r.src[r.i])\n r.i += 1\n }\n return n\n}\n\nfn read_string(ptr[Reader] r) -> str! {\n r.i += 1 // the opening quote\n []str parts\n start := r.i\n while true {\n if r.i >= r.src.len {\n return error(\"json: a text is never closed (\" + where(r) + \")\")\n }\n c := r.src[r.i]\n if c == '\"' {\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n return parts.join(\"\")\n }\n if int(c) < 32 {\n return error(\"json: a line break or control character inside a text (\" + where(r) + \")\")\n }\n if c != '\\\\' {\n r.i += 1\n continue\n }\n parts.push(r.src.sub(start, r.i))\n r.i += 1\n e := peek(r)\n r.i += 1\n if e == 'n' {\n parts.push(\"\\n\")\n } else if e == 't' {\n parts.push(\"\\t\")\n } else if e == 'r' {\n parts.push(\"\\r\")\n } else if e == 'b' {\n parts.push(utf8(8))\n } else if e == 'f' {\n parts.push(utf8(12))\n } else if e == '\"' || e == '\\\\' || e == '/' {\n parts.push(str(e))\n } else if e == 'u' {\n cp := try read_hex4(r)\n // a pair of 😀 stands for one character above U+FFFF\n if cp >= 55296 && cp < 56320 && comes(r, \"\\\\u\") {\n r.i += 2\n low := try read_hex4(r)\n cp = 65536 + (cp - 55296) * 1024 + (low - 56320)\n }\n parts.push(utf8(cp))\n } else {\n return error(\"json: unknown escape \\\\\" + str(e) + \" in a text (\" + where(r) + \")\")\n }\n start = r.i\n }\n}\n\nfn read_number(ptr[Reader] r) -> Json! {\n start := r.i\n if peek(r) == '-' {\n r.i += 1\n }\n while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {\n r.i += 1\n }\n n := float(r.src.sub(start, r.i)) or {\n r.i = start\n return error(unexpected(r, \"a number\"))\n }\n return new_number(n)\n}\n\nfn read_value(ptr[Reader] r) -> Json! {\n skip_space(r)\n c := peek(r)\n if r.i >= r.src.len {\n return error(unexpected(r, \"a value\"))\n }\n if c == '{' {\n obj := new_object()\n r.i += 1\n skip_space(r)\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n while true {\n skip_space(r)\n if peek(r) != '\"' {\n return error(unexpected(r, \"a key in quotes\"))\n }\n key := try read_string(r)\n skip_space(r)\n if peek(r) != ':' {\n return error(unexpected(r, \"':'\"))\n }\n r.i += 1\n v := try read_value(r)\n obj.set(key, v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == '}' {\n r.i += 1\n return obj\n }\n return error(unexpected(r, \"',' or '}'\"))\n }\n }\n if c == '[' {\n list := new_list()\n r.i += 1\n skip_space(r)\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n while true {\n v := try read_value(r)\n list.push(v)\n skip_space(r)\n if peek(r) == ',' {\n r.i += 1\n continue\n }\n if peek(r) == ']' {\n r.i += 1\n return list\n }\n return error(unexpected(r, \"',' or ']'\"))\n }\n }\n if c == '\"' {\n s := try read_string(r)\n return new_text(s)\n }\n if c == 't' {\n try expect_word(r, \"true\")\n return new_bool(true)\n }\n if c == 'f' {\n try expect_word(r, \"false\")\n return new_bool(false)\n }\n if c == 'n' {\n try expect_word(r, \"null\")\n return new_null()\n }\n if c == '-' || (c >= '0' && c <= '9') {\n return try read_number(r)\n }\n return error(unexpected(r, \"a value\"))\n}\n\n// the JSON in `text`; an error with the line and column where it went wrong\nfn parse(str text) -> Json! {\n r := Reader(src: text)\n v := try read_value(addr(r))\n skip_space(addr(r))\n if r.i < r.src.len {\n return error(unexpected(addr(r), \"the end of the text\"))\n }\n return v\n}\n") list.push("// Paths, as text. Elements are separated by \"/\".\n//\n// import path\n// path.join(\"a\", \"b.txt\") a/b.txt\n// path.base(\"/tmp/x/a.tar.gz\") a.tar.gz path.stem(...) a.tar path.ext(...) .gz\n// path.dir(\"/tmp/x/a.txt\") /tmp/x\n// path.clean(\"a//b/../c/./d\") a/c/d\n\n// a and b with one separator between; an absolute b replaces a\nfn join(str a, str b) -> str {\n if a.len == 0 {\n return b\n }\n if b.len == 0 {\n return a\n }\n if b[0] == '/' {\n return b\n }\n if a[a.len - 1] == '/' {\n return a + b\n }\n return a + \"/\" + b\n}\n\nfn is_abs(str p) -> bool {\n return p.len > 0 && p[0] == '/'\n}\n\n// the last element: \"a/b.txt\" -> \"b.txt\", \"a/b/\" -> \"b\", \"\" -> \".\", \"/\" -> \"/\"\nfn base(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n if end == 1 && p[0] == '/' {\n return \"/\"\n }\n start := end\n while start > 0 && p[start - 1] != '/' {\n start -= 1\n }\n return p.sub(start, end)\n}\n\n// everything before the last element: \"a/b.txt\" -> \"a\", \"b.txt\" -> \".\", \"/b\" -> \"/\"\nfn dir(str p) -> str {\n end := p.len\n while end > 1 && p[end - 1] == '/' {\n end -= 1\n }\n i := end\n while i > 0 && p[i - 1] != '/' {\n i -= 1\n }\n if i == 0 {\n return \".\"\n }\n while i > 1 && p[i - 1] == '/' {\n i -= 1\n }\n return p.sub(0, i)\n}\n\n// the part of the last element from its last dot: \"a.tar.gz\" -> \".gz\"; \"\" when there is none\nfn ext(str p) -> str {\n b := base(p)\n i := b.len\n while i > 0 && b[i - 1] != '.' {\n i -= 1\n }\n if i <= 1 { // no dot, or a name that only starts with one (.bashrc)\n return \"\"\n }\n return b.sub(i - 1, b.len)\n}\n\n// the last element without its extension\nfn stem(str p) -> str {\n b := base(p)\n e := ext(p)\n return b.sub(0, b.len - e.len)\n}\n\n// the elements of a path: \"/a/b/c\" -> [\"a\", \"b\", \"c\"]\nfn parts(str p) -> []str {\n []str out\n for piece in p.split(\"/\") {\n if piece.len > 0 {\n out.push(piece)\n }\n }\n return out\n}\n\n// the shortest path with the same meaning: no \"//\", no \".\", and \"..\" resolved where it can be\nfn clean(str p) -> str {\n if p.len == 0 {\n return \".\"\n }\n abs := is_abs(p)\n []str out\n for piece in parts(p) {\n if piece == \".\" {\n continue\n }\n if piece == \"..\" {\n if out.len > 0 && out[out.len - 1] != \"..\" {\n out.pop()\n } else if !abs {\n out.push(\"..\")\n }\n continue\n }\n out.push(piece)\n }\n joined := out.join(\"/\")\n if abs {\n return \"/\" + joined\n }\n if joined.len == 0 {\n return \".\"\n }\n return joined\n}\n") list.push("// Programs: run a command, read what it printed, look at the environment.\n//\n// import proc\n// code := proc.run(\"make\") the exit code; what the command prints goes to the screen\n// text := try proc.output(\"git describe\") what the command printed; an error if it failed\n// proc.env(\"HOME\", \"/\") an environment variable, or the second value if it is not set\nimport \"stdlib.h\"\nimport \"unistd.h\"\nimport \"time.h\"\n\n// run a command; its output goes to the screen. Gives its exit code.\nfn run(str command) -> int {\n return shell(command)\n}\n\n// run a command and give what it printed. Fails, with its output as the message, if it exits with an error.\nfn output(str command) -> str! {\n tmp := \"/tmp/finch_out_\" + str(getpid()) + \"_\" + str(int(time(null)))\n code := shell(\"(\" + command + \") > '\" + tmp + \"' 2>&1\")\n text := read_file(tmp) or \"\"\n delete_file(tmp)\n if code != 0 {\n return error(\"'\" + command + \"' exited with code \" + str(code) + \": \" + text.trim())\n }\n return text\n}\n\nfn env(str name, str fallback) -> str {\n v := getenv(name)\n if v.len == 0 {\n return fallback\n }\n return v\n}\n\nfn pid() -> int {\n return int(getpid())\n}\n\n// the folder the program is running in\nfn cwd() -> str {\n []char buf\n for i in 0..4096 {\n buf.push(char(0))\n }\n getcwd(buf.ptr, 4096)\n return str(buf.ptr)\n}\n") list.push("// Programs, for Windows. The same functions as proc.fch (that file is for Linux and macOS).\nimport \"stdlib.h\"\nimport \"process.h\"\nimport \"direct.h\"\nimport \"time.h\"\n\nfn run(str command) -> int {\n return shell(command)\n}\n\nfn output(str command) -> str! {\n tmp := proc_env(\"TEMP\", \".\") + \"\\\\finch_out_\" + str(getpid()) + \"_\" + str(int(time(null)))\n code := shell(command + \" > \\\"\" + tmp + \"\\\" 2>&1\")\n text := read_file(tmp) or \"\"\n delete_file(tmp)\n if code != 0 {\n return error(\"'\" + command + \"' exited with code \" + str(code) + \": \" + text.trim())\n }\n return text\n}\n\nfn proc_env(str name, str fallback) -> str {\n v := getenv(name)\n if v.len == 0 {\n return fallback\n }\n return v\n}\n\nfn env(str name, str fallback) -> str {\n return proc_env(name, fallback)\n}\n\nfn pid() -> int {\n return int(getpid())\n}\n\nfn cwd() -> str {\n []char buf\n for i in 0..4096 {\n buf.push(char(0))\n }\n getcwd(buf.ptr, 4096)\n return str(buf.ptr)\n}\n") - list.push("// Threads: run a function next to the rest of the program, and a lock for what they share.\n//\n// import thread\n// fn work(ptr arg) { a thread's function takes one ptr (to whatever it needs)\n// ptr[Job] job = arg\n// ...\n// }\n// t := try thread.spawn(addr(work), addr(job))\n// t.join() wait for it; `job` must live until then\n//\n// m := thread.mutex()\n// defer m.close()\n// m.lock() ... m.unlock() one thread at a time in between\n// atomic_add(addr(count), 1) (built in) numbers changed by many threads at once\n//\n// What a thread is given is shared, not copied: don't let it outlive the block it comes from, and\n// change it only under a lock (or with atomic_...).\nimport \"finch_thread.h\"\n\nstruct Thread {\n ptr handle\n}\n\nstruct Mutex {\n ptr handle\n}\n\n// start fn(arg) on a new thread; fn is addr(f) of a fn f(ptr arg)\nfn spawn(ptr fn_, ptr arg) -> Thread! {\n h := finch_thread_spawn(fn_, arg)\n if h == null {\n return error(\"can't start a thread\")\n }\n return Thread(handle: h)\n}\n\n// wait until the thread has finished\nfn Thread.join() {\n if self.handle != null {\n finch_thread_join(self.handle)\n self.handle = null\n }\n}\n\nfn mutex() -> Mutex {\n return Mutex(handle: finch_mutex_new())\n}\n\nfn Mutex.lock() {\n finch_mutex_lock(self.handle)\n}\n\nfn Mutex.unlock() {\n finch_mutex_unlock(self.handle)\n}\n\nfn Mutex.close() {\n finch_mutex_free(self.handle)\n self.handle = null\n}\n\n// how many threads the computer runs at the same time\nfn cpus() -> int {\n return int(finch_cpu_count())\n}\n") list.push("// Time: the clock, sleeping, and dates.\n//\n// import time\n// start := time.millis() milliseconds from a clock that only goes forward (to measure with)\n// time.sleep(250) wait a quarter of a second\n// time.now() seconds since 1970-01-01 00:00 UTC\n// d := time.date(time.now()) the parts: d.year, d.month, d.day, d.hour, d.minute, d.second\n// time.format(time.now(), \"%Y-%m-%d %H:%M\") as text, in the computer's time zone\nimport \"time.h\"\n\nstruct Date {\n int year\n int month // 1 to 12\n int day // 1 to 31\n int hour\n int minute\n int second\n int weekday // 0 is Sunday\n int yearday // 1 to 366\n}\n\n// seconds since 1970-01-01 00:00 UTC\nfn now() -> int {\n return int(time(null))\n}\n\n// milliseconds on a clock that only goes forward: for measuring how long something took\nfn millis() -> int {\n timespec ts\n clock_gettime(CLOCK_MONOTONIC, addr(ts))\n return int(ts.tv_sec) * 1000 + int(ts.tv_nsec) / 1000000\n}\n\nfn sleep(int ms) {\n timespec ts\n ts.tv_sec = ms / 1000\n ts.tv_nsec = (ms % 1000) * 1000000\n nanosleep(addr(ts), null)\n}\n\nfn parts(tm t) -> Date {\n return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour),\n minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1)\n}\n\n// the date and time of a moment, in UTC\nfn date(int unix) -> Date {\n tm t\n i64 stamp = unix\n gmtime_r(addr(stamp), addr(t))\n return parts(t)\n}\n\n// the date and time of a moment, in the computer's time zone\nfn local(int unix) -> Date {\n tm t\n i64 stamp = unix\n localtime_r(addr(stamp), addr(t))\n return parts(t)\n}\n\n// a moment as text, in the computer's time zone: %Y year, %m month, %d day, %H hour, %M minute, %S second\n// (the rest of C's strftime works too: %A weekday name, %B month name, ...)\nfn format(int unix, str pattern) -> str {\n tm t\n i64 stamp = unix\n localtime_r(addr(stamp), addr(t))\n []char buf\n for i in 0..256 {\n buf.push(char(0))\n }\n strftime(buf.ptr, 256, pattern, addr(t))\n return str(buf.ptr)\n}\n\n// the number of days in a month (month is 1 to 12)\nfn days_in_month(int year, int month) -> int {\n if month == 2 {\n if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {\n return 29\n }\n return 28\n }\n if month == 4 || month == 6 || month == 9 || month == 11 {\n return 30\n }\n return 31\n}\n") list.push("// Time, for Windows. The same functions as time.fch (that file is for Linux and macOS).\nimport \"time.h\"\nimport \"finch_win.h\"\n\nstruct Date {\n int year\n int month // 1 to 12\n int day // 1 to 31\n int hour\n int minute\n int second\n int weekday // 0 is Sunday\n int yearday // 1 to 366\n}\n\nfn now() -> int {\n return int(time(null))\n}\n\n// milliseconds on a clock that only goes forward (the performance counter: GetTickCount64 ticks every 15 ms)\nfn millis() -> int {\n i64 count = 0\n i64 freq = 1\n QueryPerformanceFrequency(addr(freq))\n QueryPerformanceCounter(addr(count))\n return int(count / freq) * 1000 + int(count % freq) * 1000 / int(freq)\n}\n\nfn sleep(int ms) {\n Sleep(u32(ms))\n}\n\nfn parts(tm t) -> Date {\n return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour),\n minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1)\n}\n\nfn date(int unix) -> Date {\n tm t\n i64 stamp = unix\n _gmtime64_s(addr(t), addr(stamp))\n return parts(t)\n}\n\nfn local(int unix) -> Date {\n tm t\n i64 stamp = unix\n _localtime64_s(addr(t), addr(stamp))\n return parts(t)\n}\n\nfn format(int unix, str pattern) -> str {\n tm t\n i64 stamp = unix\n _localtime64_s(addr(t), addr(stamp))\n []char buf\n for i in 0..256 {\n buf.push(char(0))\n }\n strftime(buf.ptr, 256, pattern, addr(t))\n return str(buf.ptr)\n}\n\nfn days_in_month(int year, int month) -> int {\n if month == 2 {\n if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {\n return 29\n }\n return 28\n }\n if month == 4 || month == 6 || month == 9 || month == 11 {\n return 30\n }\n return 31\n}\n") - list.push("/* The runtime's folder functions, for std/fs.fch (they are in runtime/finch_rt.c, linked into every program). */\nint finch_fs_is_dir(const char *path);\nint finch_fs_exists(const char *path);\nint finch_fs_mkdir(const char *path);\nint finch_fs_remove_dir(const char *path);\nvoid *finch_fs_list_open(const char *path);\nconst char *finch_fs_list_next(void *list);\nvoid finch_fs_list_close(void *list);\nconst char *finch_fs_data_home(void);\n") - list.push("/* The runtime's thread functions, for std/thread.fch (they are in runtime/finch_rt.c, linked into every program). */\nvoid *finch_thread_spawn(void (*fn)(void *), void *arg);\nint finch_thread_join(void *thread);\nvoid *finch_mutex_new(void);\nvoid finch_mutex_lock(void *mutex);\nvoid finch_mutex_unlock(void *mutex);\nvoid finch_mutex_free(void *mutex);\nint finch_cpu_count(void);\n") list.push("/* The Windows functions std/time_windows.fch needs. Declared here, not by including :\n libclang can't parse that header with MinGW's intrinsics. Both are in kernel32. */\nunsigned long long GetTickCount64(void);\nvoid Sleep(unsigned long milliseconds);\nint QueryPerformanceCounter(long long *count);\nint QueryPerformanceFrequency(long long *frequency);\n") return list } diff --git a/src/fmt.fch b/src/fmt.fch deleted file mode 100644 index 29324cb..0000000 --- a/src/fmt.fch +++ /dev/null @@ -1,224 +0,0 @@ -// finch fmt: the indentation by { }, 4 spaces a level; no spaces at line ends; at most one empty line -// in a row; one line end at the end of the file. Nothing else is moved: lines that go on inside ( or [, -// """ text and /* */ comments keep their shape, shifted along with the line they belong to. - -struct Scan { - int braces // { } open at the start of the line - int parens // ( [ open - bool inLong // inside """ text - bool inBlock // inside /* */ - char last // the last thing of code on the line ('"' for text): + or , means the next line goes on -} - -// how deep a line's leading spaces go (a tab is 4) -fn indentOf(str line) -> int { - n := 0 - for c in line { - if c == ' ' { - n += 1 - } else if c == '\t' { - n += 4 - } else { - break - } - } - return n -} - -// how many spaces and tabs a line starts with -fn leading(str line) -> int { - i := 0 - while i < line.len && (line[i] == ' ' || line[i] == '\t') { - i += 1 - } - return i -} - -fn trimEnd(str line) -> str { - n := line.len - while n > 0 && (line[n - 1] == ' ' || line[n - 1] == '\t' || line[n - 1] == '\r') { - n -= 1 - } - return line.sub(0, n) -} - -fn spaces(int n) -> str { - []str out - for i in 0..n { - out.push(" ") - } - return out.join("") -} - -fn startsWith(str line, int i, str what) -> bool { - return i + what.len <= line.len && line.sub(i, i + what.len) == what -} - -// past one line: what is open at its end -fn scanLine(str line, ptr[Scan] s) { - s.last = ' ' - i := 0 - while i < line.len { - c := line[i] - if s.inLong { - if startsWith(line, i, "\"\"\"") { - s.inLong = false - i += 3 - } else { - i += 1 - } - continue - } - if s.inBlock { - if startsWith(line, i, "*/") { - s.inBlock = false - i += 2 - } else { - i += 1 - } - continue - } - if startsWith(line, i, "//") { - return - } - if startsWith(line, i, "/*") { - s.inBlock = true - i += 2 - continue - } - if startsWith(line, i, "\"\"\"") { - s.inLong = true - i += 3 - continue - } - if c == '"' { - i = pastString(line, i + 1) - s.last = '"' - continue - } - if c == '\'' { - i += 1 - while i < line.len && line[i] != '\'' { - if line[i] == '\\' { - i += 1 - } - i += 1 - } - i += 1 - s.last = '"' - continue - } - if c != ' ' && c != '\t' && c != '\r' { - s.last = c - } - if c == '{' { - s.braces += 1 - } else if c == '}' { - s.braces -= 1 - } else if c == '(' || c == '[' { - s.parens += 1 - } else if c == ')' || c == ']' { - s.parens -= 1 - } - i += 1 - } -} - -// past "text" (i is just after the opening quote), with \(code) that may have its own "text" in it -fn pastString(str line, int start) -> int { - i := start - while i < line.len { - c := line[i] - if c == '"' { - return i + 1 - } - if c == '\\' && i + 1 < line.len && line[i + 1] == '(' { - depth := 1 - i += 2 - while i < line.len && depth > 0 { - if line[i] == '"' { - i = pastString(line, i + 1) - continue - } - if line[i] == '(' { - depth += 1 - } else if line[i] == ')' { - depth -= 1 - } - i += 1 - } - continue - } - if c == '\\' { - i += 1 - } - i += 1 - } - return i -} - -fn format(str text) -> str { - lines := text.replace("\r\n", "\n").split("\n") - []str out - s := Scan() - stmtWas := 0 // where the line that started the statement was, and where it is now - stmtNow := 0 - blank := 0 - for raw in lines { - goesOn := s.last == '+' || s.last == '-' || s.last == '*' || s.last == '/' || s.last == ',' || s.last == '=' || s.last == '|' || s.last == '&' || s.last == '%' - own := !s.inLong && !s.inBlock && s.parens <= 0 && !goesOn // the line starts something of its own - if s.inLong { - // """ text as it is, moved with its statement (its closing line says how much is cut off) - out.push(shifted(raw, stmtNow - stmtWas)) - scanLine(raw, addr(s)) - continue - } - line := trimEnd(raw) - if line.len == 0 { - if s.inBlock { - out.push("") - } else { - blank += 1 - } - continue - } - if blank > 0 && out.len > 0 { - out.push("") - } - blank = 0 - body := line.sub(leading(line), line.len) - if own { - depth := s.braces - if body.starts_with("}") { - depth -= 1 - } - if depth < 0 { - depth = 0 - } - stmtWas = indentOf(line) - stmtNow = depth * 4 - out.push(spaces(stmtNow) + body) - } else { - out.push(shifted(line, stmtNow - stmtWas)) - } - scanLine(line, addr(s)) - } - while out.len > 0 && out[out.len - 1].len == 0 { - out.pop() - } - return out.join("\n") + "\n" -} - -// the line moved right (or left) by `by`, keeping what it has after its indentation -fn shifted(str line, int by) -> str { - n := indentOf(line) - body := line.sub(leading(line), line.len) - if body.len == 0 { - return line - } - to := n + by - if to < 0 { - to = 0 - } - return spaces(to) + body -} diff --git a/src/gen.fch b/src/gen.fch index 4768440..a375015 100644 --- a/src/gen.fch +++ b/src/gen.fch @@ -7,7 +7,6 @@ import ast import docs import cimport import platform -import "stdio.h" struct Var { str name @@ -31,9 +30,6 @@ struct Options { bool debug // -g str cwd // the folder the compiler was started in (for the debug info) platform.Target target - bool trace // runtime errors show the chain of calls (off for --shared and --no-trace) - bool test // finch test: main runs the test "..." blocks instead - []str testModules // the modules whose tests run ("" is the main file); not the standard library's } // A generic struct or function, kept as written until something asks for a version of it. @@ -144,7 +140,6 @@ struct StructDef { int index int state // 0 new, 1 resolving, 2 done bool owning - bool isUnion // C: a union, every field at byte 0 } struct Val { @@ -167,11 +162,6 @@ struct Ref { struct Gen { []ast.Program progs - []str warned // file:line:col of the warnings given - bool trace // each function keeps a frame { prev, name, file, line } in @finch_trace - str frame // the current function's frame ("" in helpers) - str frameLine // where its line goes - []str constBusy // the constants being worked out: a loop between them is an error []str files []str head []str out @@ -287,8 +277,8 @@ fn noteCall(ptr[Gen] g, ast.Node c, FnSig sig) { // what is known about every function, struct and enum, for completion and the outline fn indexAll(ptr[Gen] g, []str mainImports) { - for i in 0..mainImports.len { - g.idx.push("@I\t" + mainImports[i] + "\t" + g.progs[0].modules[i]) // the name used here, the module + for im in mainImports { + g.idx.push("@I\t" + im) } for sg in g.sigs { f := g.progs[sg.prog].fns[sg.index] @@ -339,58 +329,6 @@ fn fail(ptr[Gen] g, ast.Node n, str msg) { exit(1) } -// A warning: the program is still built. On stderr (finch ir's output stays the IR); an @W record for editors. -fn warn(ptr[Gen] g, ast.Node n, str msg) { - where := g.files[n.file] + ":" + str(n.line) + ":" + str(n.col) - if g.warned.contains(where) { - return // once per place (a generic function is built more than once) - } - g.warned.push(where) - if g.index { - g.idx.push("@W\t" + g.files[n.file] + "\t" + str(n.line) + "\t" + str(n.col) + "\t" + msg) - return - } - fputs(where + ": warning: " + msg + "\n", stderr) -} - -// About how many bytes of plain data (numbers, structs of them, C's [N]T) a value has, for the copy warnings -fn byteSize(ptr[Gen] g, ast.Type t) -> int { - k := t.kind - if k == "struct" { - sd := g.structs[findStruct(g, t.module, t.name)] - if sd.isC && sd.csize > 0 { - return sd.csize - } - n := 0 - for ft in sd.ftypes { - n += byteSize(g, ft) - } - return n - } - if k == "fixed" && t.elem.len > 0 { - return t.count * byteSize(g, t.elem[0]) - } - if k == "str" || k == "array" || k == "map" { - return 0 // only its { ptr, len, cap } is copied when it is passed: not counted - } - if k == "ptr" || k == "enum" || k == "int" || k == "float" { - return 8 - } - return bitsOf(t) / 8 + int(bitsOf(t) == 1) -} - -// a struct this big passed or looped over by value is copied byte by byte each time -fn bigCopy(ptr[Gen] g, ast.Type t) -> int { - if t.kind != "struct" { - return 0 - } - n := byteSize(g, t) - if n >= 256 { - return n - } - return 0 -} - fn emit(ptr[Gen] g, str line) { // with -g every call in a function that has debug info has to say where it is if g.dbgScope >= 0 && line.contains("call ") { @@ -581,17 +519,7 @@ fn progOf(ptr[Gen] g, str module) -> int { } fn imports(ptr[Gen] g, str module) -> bool { - return modFor(g, module).len > 0 -} - -// the module a name stands for in this file: button -> "ui/button", p -> "path"; "" if none -fn modFor(ptr[Gen] g, str name) -> str { - pr := g.progs[progOf(g, g.module)] - k := pr.imports.find(name) - if k < 0 { - return "" - } - return pr.modules[k] + return g.progs[progOf(g, g.module)].imports.contains(module) } fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type { @@ -638,7 +566,7 @@ fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type { if !imports(g, t.module) { fail(g, at, "'" + t.module + "' is not imported here (add: import " + t.module + ")") } - gm = modFor(g, t.module) + gm = t.module } return instStruct(g, gm, t.name, args, at) } @@ -653,7 +581,7 @@ fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type { if !imports(g, t.module) { fail(g, at, "'" + t.module + "' is not imported here (add: import " + t.module + ")") } - module = modFor(g, t.module) + module = t.module } if findEnum(g, module, t.name) >= 0 { return ast.Type(kind: "enum", name: t.name, module: module) @@ -677,7 +605,7 @@ fn modName(str m) -> str { if m.len == 0 { return "main" } - return m.replace("/", "$") // ui/button -> ui$button in symbol names + return m } fn llty(ptr[Gen] g, ast.Type t) -> str { @@ -842,8 +770,6 @@ fn boundsCheck(ptr[Gen] g, str i, str len, ast.Node at) { // ---------- helper functions (generated once per type) ---------- struct Saved { - str frame - str frameLine []str body []str allocas int tmp @@ -852,10 +778,8 @@ struct Saved { } fn beginHelper(ptr[Gen] g) -> Saved { - s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term, dbgScope: g.dbgScope, frame: g.frame, frameLine: g.frameLine) + s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term, dbgScope: g.dbgScope) g.dbgScope = -1 // a helper made on the side has no source lines - g.frame = "" - g.frameLine = "" []str empty g.body = empty g.allocas = empty @@ -871,8 +795,6 @@ fn endHelper(ptr[Gen] g, Saved s, str header) { g.tmp = s.tmp g.term = s.term g.dbgScope = s.dbgScope - g.frame = s.frame - g.frameLine = s.frameLine } // for i in 0..n, inside the current function @@ -1098,21 +1020,6 @@ fn own(ptr[Gen] g, Val v) -> str { // ---------- printing ---------- -// f32 prints the shortest text that is the same f32 (0.1, not 0.100000001) -fn floatName(ast.Type t) -> str { - if t.kind == "f32" { - return "f32" - } - return "float" -} - -fn single(ast.Type t) -> str { - if t.kind == "f32" { - return "1" - } - return "0" -} - fn printf(ptr[Gen] g, str fmt, str args) { f := strGlobal(g, fmt) t := tmpName(g) @@ -1137,7 +1044,7 @@ fn emitPrint(ptr[Gen] g, str v, ast.Type t, bool quoted) { } else if isUnsigned(t) { printf(g, "%llu", "i64 " + intCast(g, v, t, T("u64"))) } else if isFloat(t) { - emit(g, "call void @finch_print_float(double " + floatCast(g, v, t, T("float")) + ", i32 " + single(t) + ")") + printf(g, "%g", "double " + floatCast(g, v, t, T("float"))) } else if k == "char" { z := op(g, "sext i8 " + v + " to i32") if quoted { @@ -1199,7 +1106,7 @@ fn emitFormat(ptr[Gen] g, str buf, str v, ast.Type t, bool quoted) { } else if isUnsigned(t) { emit(g, "call void @finch_buf_uint(ptr " + buf + ", i64 " + intCast(g, v, t, T("u64")) + ")") } else if isFloat(t) { - emit(g, "call void @finch_buf_" + floatName(t) + "(ptr " + buf + ", double " + floatCast(g, v, t, T("float")) + ")") + emit(g, "call void @finch_buf_float(ptr " + buf + ", double " + floatCast(g, v, t, T("float")) + ")") } else if k == "char" { emit(g, "call void @finch_buf_char(ptr " + buf + ", i8 " + v + ", i32 " + q + ")") } else if k == "bool" { @@ -1935,10 +1842,6 @@ fn genericCall(ptr[Gen] g, ast.Node c, int ti, int first, str shown) -> Val { fn resolveCStruct(ptr[Gen] g, int i) { cs := g.ci.structs[g.structs[i].name] g.structs[i].state = 1 - if cs.isUnion { - resolveCUnion(g, i, cs) - return - } cur := 0 []str elems for f in cs.fields { @@ -1972,37 +1875,6 @@ fn resolveCStruct(ptr[Gen] g, int i) { g.head.push("%S.C." + cs.name + " = type { " + elems.join(", ") + " }") } -// A C union: room for its biggest field, aligned like C aligns it; every field is at byte 0. -fn resolveCUnion(ptr[Gen] g, int i, cimport.CStruct cs) { - for f in cs.fields { - if f.hidden { - continue - } - t := resolve(g, f.ty, ast.Node()) - if t.kind == "struct" { - resolveStruct(g, findStruct(g, t.module, t.name)) - } - g.structs[i].fnames.push(f.name) - g.structs[i].ftypes.push(t) - g.structs[i].lidx.push(0) - g.structs[i].offs.push(0) - } - a := cs.align - if a > 8 { - a = 8 - } - if a < 1 { - a = 1 - } - n := (cs.size + a - 1) / a - g.structs[i].unsupported = cs.unsupported - g.structs[i].isUnion = true - g.structs[i].csize = cs.size - g.structs[i].calign = cs.align - g.structs[i].state = 2 - g.head.push("%S.C." + cs.name + " = type { [" + str(n) + " x i" + str(a * 8) + "] }") -} - fn resolveStruct(ptr[Gen] g, int i) { if g.structs[i].state == 2 { return @@ -2324,25 +2196,6 @@ fn define(ptr[Gen] g, int si) { []Scope none g.scopes = none pushScope(g) - g.frame = "" - g.frameLine = "" - if g.trace { - // this call on the chain a runtime error shows: in move (game.fch:12) - shownName := g.cur.name - if g.cur.recv.len > 0 { - shownName = g.cur.recv + "." + g.cur.name - } - if f.text.len > 0 && g.cur.name.starts_with("__test_") { - shownName = "test \"" + f.text + "\"" - } - g.frame = alloca(g, "{ ptr, ptr, ptr, i64 }") - emit(g, "store ptr " + load(g, "ptr", "@finch_trace") + ", ptr " + g.frame) - emit(g, "store ptr " + strGlobal(g, shownName) + ", ptr " + op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 1")) - emit(g, "store ptr " + strGlobal(g, g.files[f.file]) + ", ptr " + op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 2")) - g.frameLine = op(g, "getelementptr inbounds { ptr, ptr, ptr, i64 }, ptr " + g.frame + ", i32 0, i32 3") - emit(g, "store i64 " + str(f.line) + ", ptr " + g.frameLine) - emit(g, "store ptr " + g.frame + ", ptr @finch_trace") - } []str params if g.cur.recv.len > 0 { // self is the caller's struct itself, reached through its address @@ -2360,10 +2213,6 @@ fn define(ptr[Gen] g, int si) { } for k in 0..g.cur.params.len { pt := g.cur.params[k] - big := bigCopy(g, pt) - if big > 0 { - warn(g, f.kids[k], "'" + g.cur.pnames[k] + "' is a " + ast.show(pt) + " (" + str(big) + " bytes), copied on every call to " + f.name + "; take ptr[" + ast.show(pt) + "] to share it instead (callers can still pass the variable as it is)") - } params.push(llty(g, pt) + " %p" + str(k)) v := "%p" + str(k) if owning(g, pt) && !g.cur.borrow[k] { @@ -2405,7 +2254,7 @@ fn block(ptr[Gen] g, ast.Node b, bool scope) { } for s in b.kids { if g.term { - fail(g, s, "this code can never run (it comes after return, break, continue or exit)") + fail(g, s, "this code can never run (it comes after return, break or continue)") } stmt(g, s) } @@ -2417,9 +2266,6 @@ fn block(ptr[Gen] g, ast.Node b, bool scope) { fn stmt(ptr[Gen] g, ast.Node s) { g.dbgLine = s.line g.dbgCol = s.col - if g.frameLine.len > 0 && !g.term { - emit(g, "store i64 " + str(s.line) + ", ptr " + g.frameLine) - } k := s.kind if k == "block" { block(g, s, true) @@ -2511,12 +2357,6 @@ fn hasDefaults(ptr[Gen] g, ast.Type t) -> bool { return true } } - // a field that is itself a struct with defaults gets them too - for ft in sd.ftypes { - if ft.kind == "struct" && hasDefaults(g, ft) { - return true - } - } return false } @@ -2526,9 +2366,6 @@ fn assignStmt(ptr[Gen] g, ast.Node s) { if root.len > 0 { id := lookup(g, root) if id < 0 && target.kind == "var" { - if findConst(g, g.module, root) >= 0 { - fail(g, s, "'" + root + "' is a constant; it can't be changed") - } fail(g, s, "there is no variable named '" + root + "' (create it with " + root + " := ...)") } if id >= 0 && varAt(g, id).readonly { @@ -2772,14 +2609,6 @@ fn eachStmt(ptr[Gen] g, ast.Node s) { et = lt.elem[0] } copyElems := r.isPlace && owning(g, et) && mutates(g, rootVar(s.kids[0]), s.kids[1]) - big := bigCopy(g, et) - if big > 0 && s.text.len == 0 { - list := "list" - if s.kids[0].kind == "var" { - list = s.kids[0].name - } - warn(g, s, "each " + ast.show(et) + " (" + str(big) + " bytes) is copied into '" + s.name + "'; for i in 0.." + list + ".len with " + list + "[i] works on it in place") - } iv := alloca(g, "i64") store(g, "i64", "0", iv) cond := newLabel(g, "each.cond") @@ -3380,38 +3209,6 @@ fn matchGen(ptr[Gen] g, ast.Node e, bool asValue) -> Val { return Val(ty: T("void")) } -// x := if c { a } else { b }: both branches give the same type -fn ifValue(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { - c := coerce(g, expr(g, e.kids[0]), T("bool"), e.kids[0], "the condition") - yes := newLabel(g, "ifv.yes") - no := newLabel(g, "ifv.no") - end := newLabel(g, "ifv.end") - condBr(g, c.v, yes, no) - label(g, yes) - a := exprWant(g, e.kids[1], want) - if a.ty.kind == "void" || a.ty.kind == "null" { - fail(g, e.kids[1], "this branch has to give a value") - } - if want.kind != "void" { - a = coerce(g, a, want, e.kids[1], "this branch") - } - t := a.ty - av := own(g, a) - fromYes := newLabel(g, "ifv.from") - br(g, fromYes) - label(g, fromYes) - br(g, end) - label(g, no) - b := coerce(g, exprWant(g, e.kids[2], t), t, e.kids[2], "this branch (both branches of the if have to give the same type)") - bv := own(g, b) - fromNo := newLabel(g, "ifv.from") - br(g, fromNo) - label(g, fromNo) - br(g, end) - label(g, end) - return Val(v: op(g, "phi " + llty(g, t) + " " + phiList([av, bv], [fromYes, fromNo])), ty: t, fresh: owning(g, t)) -} - fn phiList([]str vals, []str labels) -> str { []str parts for i in 0..vals.len { @@ -3524,19 +3321,6 @@ fn orElse(ptr[Gen] g, ast.Node e, bool discard) -> Val { return Val(v: op(g, "phi " + llty(g, v.ty) + " [ " + fv + ", %" + badEnd + " ], [ " + v.v + ", %" + ok + " ]"), ty: v.ty, fresh: owning(g, v.ty)) } -// An argument: a struct variable given where a ptr to that struct goes is passed by its address, -// as if it were addr(x) (worldDef for b2CreateWorld(const b2WorldDef*), body for move(ptr[Body] b)) -fn argWant(ptr[Gen] g, ast.Node a, ast.Type want) -> Val { - if a.kind == "var" && want.kind == "ptr" && want.elem.len == 1 && want.elem[0].kind == "struct" { - id := lookup(g, a.name) - if id >= 0 && ast.same(varAt(g, id).ty, want.elem[0]) { - pl := place(g, a, true) - return Val(v: pl.addr, ty: ast.ptrTo(pl.ty)) - } - } - return exprWant(g, a, want) -} - fn exprWant(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { if e.kind == "array" { return arrayLit(g, e, want) @@ -3544,9 +3328,6 @@ fn exprWant(ptr[Gen] g, ast.Node e, ast.Type want) -> Val { if e.kind == "map" { return mapLit(g, e, want) } - if e.kind == "ifvalue" { - return ifValue(g, e, want) // f32 x = if c { 1.0 } else { 0.5 } - } return expr(g, e) } @@ -3595,9 +3376,6 @@ fn expr(ptr[Gen] g, ast.Node e) -> Val { if k == "match" { return matchGen(g, e, true) } - if k == "ifvalue" { - return ifValue(g, e, T("void")) - } if k == "try" { return tryExpr(g, e) } @@ -3714,15 +3492,6 @@ fn unify(ptr[Gen] g, ptr[Val] l, ptr[Val] r) -> bool { return true } if isFloat(l.ty) && isFloat(r.ty) { - // a number written in the code adapts to an f32 on the other side: x * 2.0 stays f32 - if l.ty.kind == "f32" && r.lit { - r.value = Val(v: floatCast(g, r.v, r.ty, l.ty), ty: l.ty) - return true - } - if r.ty.kind == "f32" && l.lit { - l.value = Val(v: floatCast(g, l.v, l.ty, r.ty), ty: r.ty) - return true - } if l.ty.kind == "f32" { l.value = Val(v: floatCast(g, l.v, l.ty, T("float")), ty: T("float")) } @@ -3768,14 +3537,6 @@ fn arith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { panicIf(g, op(g, "icmp eq " + lt + " " + r.v + ", 0"), "division by zero", at) } } - if l.lit && r.lit && l.ty.kind == "int" { - // numbers written in the code stay such a number: FLAG_A | FLAG_B fits a u64 or a u32 - a := int(l.v) or 0 - b := int(r.v) or 0 - if (o != "<<" && o != ">>") || (b >= 0 && b < 63) { - return Val(v: str(foldInt(o, a, b)), ty: l.ty, lit: true) - } - } return Val(v: op(g, insts[k] + " " + lt + " " + l.v + ", " + r.v), ty: l.ty) } if isFloat(l.ty) { @@ -3791,29 +3552,6 @@ fn arith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { return l } -fn foldInt(str o, int a, int b) -> int { - if o == "+" { - return a + b - } else if o == "-" { - return a - b - } else if o == "*" { - return a * b - } else if o == "/" { - return a / b - } else if o == "%" { - return a % b - } else if o == "&" { - return a & b - } else if o == "|" { - return a | b - } else if o == "^" { - return a ^ b - } else if o == "<<" { - return a << b - } - return a >> b -} - fn compare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { eq := o == "==" || o == "!=" if l.ty.kind == "ptr" || r.ty.kind == "ptr" || l.ty.kind == "null" || r.ty.kind == "null" { @@ -3979,24 +3717,6 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref { id := lookup(g, e.name) if id < 0 && g.ci.consts.has(e.name) { c := g.ci.consts[e.name] - if c.structName.len > 0 { - // RAYWHITE: Color(245, 245, 245, 255) - si := findStruct(g, "C", c.structName) - []ast.Node args - []str names - for i in 0..c.fields.len { - kind := "int" - if c.fieldFloat[i] { - kind = "float" - } - args.push(ast.Node(kind: kind, text: c.fields[i], line: e.line, col: e.col, file: e.file)) - names.push(g.structs[si].fnames[i]) // by name: C leaves the fields not given at 0 - } - return Ref(isPlace: false, val: construct(g, si, args, names, e)) - } - if c.isStr { - return Ref(isPlace: false, val: strConst(g, c.s)) - } if c.isFloat { return Ref(isPlace: false, val: Val(v: llFloat(c.text), ty: T("float"), lit: true)) } @@ -4020,12 +3740,6 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref { } return Ref(isPlace: true, pl: Place(addr: "@" + e.name, ty: gt)) } - if id < 0 && findConst(g, g.module, e.name) >= 0 { - if write { - fail(g, e, "'" + e.name + "' is a constant; it can't be changed") - } - return Ref(isPlace: false, val: constValue(g, g.module, e.name, e)) - } if id < 0 { if imports(g, e.name) { fail(g, e, "'" + e.name + "' is a module; use something from it, like " + e.name + ".name(...)") @@ -4064,76 +3778,13 @@ fn llFloat(str text) -> str { return s } -// ---------- constants ---------- - -fn findConst(ptr[Gen] g, str module, str name) -> int { - pi := progOf(g, module) - if pi < 0 { - return -1 - } - for i in 0..g.progs[pi].consts.len { - if g.progs[pi].consts[i].name == name { - return i - } - } - return -1 -} - -// A constant is worked out where it is used, from the code it was written with: numbers, -// text, true/false, chars, other constants, enum variants and operators between them. -fn constValue(ptr[Gen] g, str module, str name, ast.Node at) -> Val { - pi := progOf(g, module) - c := g.progs[pi].consts[findConst(g, module, name)] - key := module + "." + name - if g.constBusy.contains(key) { - fail(g, at, "the constant '" + name + "' is worked out from itself") - } - checkConst(g, c.kids[0]) - g.constBusy.push(key) - savedScopes := g.scopes - savedModule := g.module - []Scope none - g.scopes = none - pushScope(g) - g.module = module - v := Val() - if c.flag { - t := resolve(g, c.ty, c) - v = coerce(g, exprWant(g, c.kids[0], t), t, c.kids[0], "the constant '" + name + "'") - } else { - v = expr(g, c.kids[0]) - } - g.module = savedModule - g.scopes = savedScopes - g.constBusy.pop() - noteRef(g, at, at.col, name.len, "```finch\nconst " + ast.show(v.ty) + " " + name + "\n```", g.files[c.file], c.line, c.col) - return v -} - -fn checkConst(ptr[Gen] g, ast.Node e) { - k := e.kind - if ["int", "float", "char", "str", "bool", "var", "unary", "binary", "member"].contains(k) { - for kid in e.kids { - checkConst(g, kid) - } - return - } - if k == "call" && numType(e.name).len > 0 { - for kid in e.kids { - checkConst(g, kid) - } - return // f32(0.5), u8(255) - } - fail(g, e, "a constant is made of numbers, text, true/false, chars and other constants (not calls or variables)") -} - // Which enum does `m.kids[0]` name, if it names one? Color, or shapes.Color. -1 if neither. fn enumNamed(ptr[Gen] g, ast.Node n) -> int { if n.kind == "var" && lookup(g, n.name) < 0 { return findEnum(g, g.module, n.name) } if n.kind == "member" && n.kids[0].kind == "var" && lookup(g, n.kids[0].name) < 0 && imports(g, n.kids[0].name) { - return findEnum(g, modFor(g, n.kids[0].name), n.name) + return findEnum(g, n.kids[0].name, n.name) } return -1 } @@ -4144,7 +3795,7 @@ fn sumNamed(ptr[Gen] g, ast.Node n) -> int { return findSum(g, g.module, n.name) } if n.kind == "member" && n.kids[0].kind == "var" && lookup(g, n.kids[0].name) < 0 && imports(g, n.kids[0].name) { - return findSum(g, modFor(g, n.kids[0].name), n.name) + return findSum(g, n.kids[0].name, n.name) } return -1 } @@ -4246,13 +3897,6 @@ fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref { } return Ref(isPlace: false, val: enumValue(g, m, ei)) } - mod := m.kids[0] - if mod.kind == "var" && lookup(g, mod.name) < 0 && imports(g, mod.name) && findConst(g, modFor(g, mod.name), m.name) >= 0 { - if write { - fail(g, m, "'" + mod.name + "." + m.name + "' is a constant; it can't be changed") - } - return Ref(isPlace: false, val: constValue(g, modFor(g, mod.name), m.name, m)) // shapes.SIDES - } obj := ref(g, m.kids[0], write) t := obj.val.ty if obj.isPlace { @@ -4343,10 +3987,6 @@ fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref { fp := op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + obj.pl.addr + ", i32 0, i32 " + str(sd.lidx[k])) return Ref(isPlace: true, pl: Place(addr: fp, ty: ft)) } - if sd.isUnion { - u := spill(g, obj.val) - return Ref(isPlace: false, val: Val(v: load(g, llty(g, ft), u), ty: ft)) - } fv := op(g, "extractvalue " + llty(g, t) + " " + obj.val.v + ", " + str(sd.lidx[k])) out := Val(v: fv, ty: ft) if owning(g, ft) && obj.val.fresh { @@ -4859,20 +4499,19 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str { } } text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" - savedTrace := traceSave(g) retLL := "void" retExt := "" if sig.ret.kind == "void" { - tracedEmit(g, text, savedTrace) + emit(g, text) emit(g, "ret void") } else if sig.ret.kind == "bool" { retLL = "i8" retExt = " zeroext" - emit(g, "ret i8 " + op(g, "zext i1 " + tracedCall(g, text, savedTrace) + " to i8")) + emit(g, "ret i8 " + op(g, "zext i1 " + op(g, text) + " to i8")) } else { retLL = llty(g, sig.ret) retExt = cExt(g, sig.ret) - emit(g, "ret " + retLL + " " + tracedCall(g, text, savedTrace)) + emit(g, "ret " + retLL + " " + op(g, text)) } dll := "" if g.tg.windows { @@ -4882,29 +4521,6 @@ fn exportWrapper(ptr[Gen] g, FnSig sig) -> str { return sig.name } -// A call from C into Finch (a callback, an export fn) leaves the chain of calls as it found it -fn traceSave(ptr[Gen] g) -> str { - if !g.trace { - return "" - } - return load(g, "ptr", "@finch_trace") -} - -fn tracedCall(ptr[Gen] g, str text, str saved) -> str { - r := op(g, text) - if saved.len > 0 { - emit(g, "store ptr " + saved + ", ptr @finch_trace") - } - return r -} - -fn tracedEmit(ptr[Gen] g, str text, str saved) { - emit(g, text) - if saved.len > 0 { - emit(g, "store ptr " + saved + ", ptr @finch_trace") - } -} - // A C-callable door into a Finch function, for addr(fn): callbacks like qsort's or libclang's. fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { sig := g.sigs[fi] @@ -4961,21 +4577,20 @@ fn cThunk(ptr[Gen] g, int fi, ast.Node at) -> str { } } text := "call " + llty(g, sig.ret) + " " + sig.ll + "(" + args.join(", ") + ")" - savedTrace := traceSave(g) retExt := "" if sig.ret.kind == "void" { - tracedEmit(g, text, savedTrace) + emit(g, text) emit(g, "ret void") } else if pl.sret { - store(g, llty(g, sig.ret), tracedCall(g, text, savedTrace), sretName) + store(g, llty(g, sig.ret), op(g, text), sretName) emit(g, "ret void") } else if pl.ret.kind == "expand" { slot := alloca(g, "[32 x i8]") - store(g, llty(g, sig.ret), tracedCall(g, text, savedTrace), slot) + store(g, llty(g, sig.ret), op(g, text), slot) emit(g, "ret " + pl.ret.coerced + " " + load(g, pl.ret.coerced, slot)) } else { retExt = cExt(g, sig.ret) - emit(g, "ret " + cLL(g, sig.ret) + " " + tracedCall(g, text, savedTrace)) + emit(g, "ret " + cLL(g, sig.ret) + " " + op(g, text)) } endHelper(g, s, "define internal" + retExt + " " + pl.retLL + " " + name + "(" + hdr.join(", ") + ")") return name @@ -5080,7 +4695,7 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { if i < f.params.len { pt := ptypes[i] a := pl.params[i] - v := coerce(g, argWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name) + v := coerce(g, exprWant(g, c.kids[i], pt), pt, c.kids[i], "argument " + str(i + 1) + " of " + f.name) temps.push(v) hold(g, v) if a.kind == "memory" { @@ -5152,26 +4767,17 @@ fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val { tail = "..." } } - callee := "@" + f.name - sig := "" - if f.variadic { - sig = " (" + ptys.join(", ") + tail + ")" - } - if f.inline { - // a static inline function has no symbol: the linker builds a C file with void *finch_inl_name = &name; - if !g.cdone.contains("i:" + f.name) { - g.cdone.push("i:" + f.name) - g.head.push("@finch_inl_" + f.name + " = external global ptr") - } - callee = load(g, "ptr", "@finch_inl_" + f.name) - sig = " (" + ptys.join(", ") + tail + ")" - } else if !g.cdone.contains("f:" + f.name) { + if !g.cdone.contains("f:" + f.name) { g.cdone.push("f:" + f.name) if !runtimeDeclares(f.name) { g.head.push("declare" + retExt + " " + retLL + " @" + f.name + "(" + ptys.join(", ") + tail + ")") } } - text := "call" + retExt + " " + retLL + sig + " " + callee + "(" + args.join(", ") + ")" + sig := "" + if f.variadic { + sig = " (" + ptys.join(", ") + tail + ")" + } + text := "call" + retExt + " " + retLL + sig + " @" + f.name + "(" + args.join(", ") + ")" if ret.kind == "void" { emit(g, text) releaseAll(g, temps, mark) @@ -5266,19 +4872,6 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { return Val(v: pl.addr, ty: ast.ptrTo(pl.ty)) } if n == "str" { - if c.kids.len == 2 { - // str(x, 2): a float with that many digits after the point - checkArgs(g, c, 0, 2, n) - x := expr(g, c.kids[0]) - if !isNumber(x.ty) { - fail(g, c.kids[0], "str(x, digits) is for numbers, but this is " + ast.show(x.ty)) - } - x = coerce(g, x, T("float"), c.kids[0], "the number") - d := coerce(g, expr(g, c.kids[1]), T("int"), c.kids[1], "the number of digits") - fixed := strOut(g) - emit(g, "call void @finch_str_fixed(ptr " + fixed + ", double " + x.v + ", i64 " + d.v + ")") - return Val(v: load(g, "{ ptr, i64, i64 }", fixed), ty: T("str"), fresh: true) - } checkArgs(g, c, 0, 1, n) v := expr(g, c.kids[0]) out := strOut(g) @@ -5291,7 +4884,7 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { } else if isUnsigned(v.ty) { emit(g, "call void @finch_str_from_uint(ptr " + out + ", i64 " + intCast(g, v.v, v.ty, T("u64")) + ")") } else if isFloat(v.ty) { - emit(g, "call void @finch_str_from_" + floatName(v.ty) + "(ptr " + out + ", double " + floatCast(g, v.v, v.ty, T("float")) + ")") + emit(g, "call void @finch_str_from_float(ptr " + out + ", double " + floatCast(g, v.v, v.ty, T("float")) + ")") } else if k == "char" { emit(g, "call void @finch_str_from_char(ptr " + out + ", i8 signext " + v.v + ")") } else if k == "bool" { @@ -5405,8 +4998,6 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { t := op(g, "trunc i64 " + code.v + " to i32") f := op(g, "call i32 @fflush(ptr null)") emit(g, "call void @exit(i32 " + t + ")") - emit(g, "unreachable") - g.term = true // like return: an or-block may end with it return Val(ty: T("void")) } if n == "input" { @@ -5481,22 +5072,13 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { if ei >= 0 { return enumFrom(g, c, ei) } - if g.ci.fns.has(n) && !["min", "max", "abs", "clamp"].contains(n) { - return callC(g, c, g.ci.fns[n]) // (C's abs only takes an int; Finch's takes any number) + if g.ci.fns.has(n) { + return callC(g, c, g.ci.fns[n]) } si = findStruct(g, "C", n) if si >= 0 { return construct(g, si, c.kids, c.names, c) } - if ["min", "max", "abs", "clamp"].contains(n) { - return mathCall(g, c) // only when nothing of yours (or C's abs) has the name - } - if n == "assert" { - return assertCall(g, c) - } - if ["atomic_add", "atomic_sub", "atomic_load", "atomic_store", "atomic_swap", "atomic_cas"].contains(n) { - return atomicCall(g, c) - } if lookup(g, n) >= 0 { fail(g, c, "'" + n + "' is a variable, not a function") } @@ -5504,179 +5086,6 @@ fn call(ptr[Gen] g, ast.Node c) -> Val { return Val() } -// min(a, b, ...), max(a, b, ...), abs(x), clamp(x, lo, hi) for any numbers -// assert(cond) and assert(cond, "why"): stops the program (in finch test: fails the test) when cond is false -fn assertCall(ptr[Gen] g, ast.Node c) -> Val { - if c.kids.len != 1 && c.kids.len != 2 { - fail(g, c, "assert takes a condition, and maybe a message: assert(x > 0, \"x must be positive\")") - } - cond := coerce(g, expr(g, c.kids[0]), T("bool"), c.kids[0], "the condition of assert") - bad := newLabel(g, "assert.failed") - ok := newLabel(g, "assert.ok") - condBr(g, cond.v, ok, bad) - label(g, bad) - file := strGlobal(g, g.files[c.file]) - msg := strConst(g, "") - if c.kids.len == 2 { - msg = strArg(g, c.kids[1], "the message") - } - emit(g, "call void @finch_assert_failed(ptr " + file + ", i64 " + str(c.line) + ", ptr " + spill(g, msg) + ")") - emit(g, "unreachable") - g.term = true - label(g, ok) - return Val(ty: T("void")) -} - -fn traceInit() -> str { - return " call void @finch_trace_init(ptr @finch_trace_top, ptr @finch_trace_set)\n" -} - -// finch test: a main that runs every test "..." block of these modules, each on its own -fn testMain(ptr[Gen] g, []str modules) { - body := "" - n := 0 - for pr in g.progs { - if !modules.contains(pr.module) { - continue - } - for i in 0..pr.tests.len { - fi := findFn(g, pr.module, pr.tests[i]) - name := g.progs[g.sigs[fi].prog].fns[g.sigs[fi].index].text - shown := name - if pr.module.len > 0 { - shown = pr.module + ": " + name - } - body = body + " call void @finch_test_run(ptr " + strGlobal(g, shown) + ", ptr " + g.sigs[fi].ll + ")\n" - n += 1 - } - } - if n == 0 { - print(g.files[0] + ":1:1: error: there are no tests here (write them as test \"what it checks\" { ... })") - exit(1) - } - g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + body + " %r = call i32 @finch_test_summary()\n ret i32 %r\n}\n") -} - -fn mathCall(ptr[Gen] g, ast.Node c) -> Val { - n := c.name - if g.index { - noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) - } - for nm in c.names { - if nm.len > 0 { - fail(g, c, "named arguments (" + nm + ": ...) are only for creating structs") - } - } - []Val vals - for a in c.kids { - v := expr(g, a) - if !isNumber(v.ty) { - fail(g, a, n + "(...) is for numbers, but this is " + ast.show(v.ty)) - } - vals.push(v) - } - if n == "abs" { - checkArgs(g, c, 0, 1, n) - x := vals[0] - if isUnsigned(x.ty) { - return x - } - zero := "0" - if isFloat(x.ty) { - zero = "0.0" - } - neg := arith(g, "-", Val(v: zero, ty: x.ty, lit: true), x, c) - below := compare(g, "<", x, Val(v: zero, ty: x.ty, lit: true), c) - return Val(v: op(g, "select i1 " + below.v + ", " + llty(g, x.ty) + " " + neg.v + ", " + llty(g, x.ty) + " " + x.v), ty: x.ty) - } - if n == "clamp" { - checkArgs(g, c, 0, 3, n) - low := pick(g, "<", vals[0], vals[2], c) // min(x, hi) - return pick(g, ">", low, vals[1], c) // max(that, lo) - } - if vals.len < 2 { - fail(g, c, "'" + n + "' takes two or more numbers") - } - o := "<" - if n == "max" { - o = ">" - } - out := vals[0] - for i in 1..vals.len { - out = pick(g, o, out, vals[i], c) - } - return out -} - -fn atomicAlign(ptr[Gen] g, ast.Type t) -> str { - return str(bitsOf(t) / 8) -} - -// atomic_add(p, n) and the rest: one step on a whole number that other threads see all at once. -// p is a ptr[int] (or any whole number type) to it; add, sub and swap give back what was there before, -// cas(p, old, new) puts new there only if old is there, and says whether it did. -fn atomicCall(ptr[Gen] g, ast.Node c) -> Val { - n := c.name - if g.index { - noteRef(g, c, c.col, n.len, docs.hover(docs.find(docs.builtins(), n)), "", 0, 0) - } - want := 2 - if n == "atomic_load" { - want = 1 - } else if n == "atomic_cas" { - want = 3 - } - checkArgs(g, c, 0, want, n) - p := expr(g, c.kids[0]) - if p.ty.kind != "ptr" || p.ty.elem.len == 0 || !isInt(p.ty.elem[0]) { - fail(g, c.kids[0], n + "(...) works on a whole number through its address, like " + n + "(addr(count), ...) -- this is " + ast.show(p.ty)) - } - t := p.ty.elem[0] - lt := llty(g, t) - []str vs - for i in 1..c.kids.len { - vs.push(coerce(g, exprWant(g, c.kids[i], t), t, c.kids[i], "the value").v) - } - if n == "atomic_load" { - return Val(v: op(g, "load atomic " + lt + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)), ty: t) - } - if n == "atomic_store" { - emit(g, "store atomic " + lt + " " + vs[0] + ", ptr " + p.v + " seq_cst, align " + atomicAlign(g, t)) - return Val(ty: T("void")) - } - if n == "atomic_cas" { - r := op(g, "cmpxchg ptr " + p.v + ", " + lt + " " + vs[0] + ", " + lt + " " + vs[1] + " seq_cst seq_cst") - return Val(v: op(g, "extractvalue { " + lt + ", i1 } " + r + ", 1"), ty: T("bool")) - } - how := "add" - if n == "atomic_sub" { - how = "sub" - } else if n == "atomic_swap" { - how = "xchg" - } - return Val(v: op(g, "atomicrmw " + how + " ptr " + p.v + ", " + lt + " " + vs[0] + " seq_cst"), ty: t) -} - -// the smaller (o "<") or bigger (o ">") of two numbers, in the type both fit -fn pick(ptr[Gen] g, str o, Val a, Val b, ast.Node at) -> Val { - l := a - r := b - if !unify(g, addr(l), addr(r)) { - badOperands(g, o, a, b, at) - } - if l.lit && r.lit && l.ty.kind == "int" { - x := int(l.v) or 0 - y := int(r.v) or 0 - if (o == "<") == (x < y) { - return l - } - return r - } - first := compare(g, o, l, r, at) - t := llty(g, l.ty) - return Val(v: op(g, "select i1 " + first.v + ", " + t + " " + l.v + ", " + t + " " + r.v), ty: l.ty) -} - fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, str selfRoot) -> Val { sig := g.sigs[fi] if sig.module.len == 0 && sig.name == "main" { @@ -5692,7 +5101,7 @@ fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, st mark := g.pending.len for k in 0..sig.params.len { a := c.kids[first + k] - v := coerce(g, argWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'") + v := coerce(g, exprWant(g, a, sig.params[k]), sig.params[k], a, "argument '" + sig.pnames[k] + "'") // the argument lives inside the receiver (c.merge(c)): the method gets its own copy if owning(g, sig.params[k]) && !v.fresh && selfRoot.len > 0 && rootVar(a) == selfRoot { v = Val(v: copyValue(g, v.v, v.ty), ty: v.ty, fresh: true) @@ -5717,30 +5126,6 @@ fn emitCall(ptr[Gen] g, FnSig sig, ast.Node c, []str args, str shown) -> Val { fail(g, c, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or , " + shown + "(...) or { ... }, or try " + shown + "(...)") } text := "call " + retTy(g, sig) + " " + sig.ll + "(" + args.join(", ") + ")" - if g.frame.len > 0 { - // the callee put its frame on the chain; it is gone once the call is back - out := Val(ty: sig.ret) - r := "" - if sig.ret.kind == "void" && !sig.fallible { - emit(g, text) - } else { - r = op(g, text) - } - emit(g, "store ptr " + g.frame + ", ptr @finch_trace") - if sig.fallible { - ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0") - g.ffailed = op(g, "xor i1 " + ok + ", true") - g.fmsg = op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 2") - if sig.ret.kind == "void" { - return out - } - return Val(v: op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 1"), ty: sig.ret, fresh: owning(g, sig.ret)) - } - if sig.ret.kind == "void" { - return out - } - return Val(v: r, ty: sig.ret, fresh: owning(g, sig.ret)) - } if sig.fallible { r := op(g, text) ok := op(g, "extractvalue " + retTy(g, sig) + " " + r + ", 0") @@ -5767,21 +5152,6 @@ fn construct(ptr[Gen] g, int si, []ast.Node args, []str names, ast.Node at) -> V t := ast.Type(kind: "struct", name: sd.name, module: sd.module) st := llty(g, t) named := names.len > 0 && names[0].len > 0 - if sd.isUnion && args.len > 0 { - // a union holds one field at a time: Ev(key: k) - if args.len != 1 || !named { - fail(g, at, sd.name + " is a C union: give one field by name, like " + sd.name + "(" + sd.fnames[0] + ": ...), or none") - } - k := sd.fnames.find(names[0]) - if k < 0 { - fail(g, args[0], sd.name + " has no field '" + names[0] + "'") - } - u := alloca(g, st) - store(g, st, "zeroinitializer", u) - fv := coerce(g, exprWant(g, args[0], sd.ftypes[k]), sd.ftypes[k], args[0], "the field '" + names[0] + "'") - store(g, llty(g, sd.ftypes[k]), fv.v, u) - return Val(v: load(g, st, u), ty: t) - } []int given for k in 0..sd.fnames.len { given.push(-1) @@ -5830,8 +5200,6 @@ fn construct(ptr[Gen] g, int si, []ast.Node args, []str names, ast.Node at) -> V g.tnames = savedTN g.targs = savedTA g.scopes = savedScopes - } else if ft.kind == "struct" && hasDefaults(g, ft) { - fv = Val(v: defaultVal(g, ft, at), ty: ft, fresh: owning(g, ft)) } else { continue } @@ -5871,15 +5239,13 @@ fn method(ptr[Gen] g, ast.Node m) -> Val { if !imports(g, mn) { fail(g, objNode, "there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)") } - shown := mn - mn = modFor(g, mn) fi := findFn(g, mn, m.name) if fi >= 0 { - return callFn(g, fi, m, 1, shown + "." + m.name, "", "") + return callFn(g, fi, m, 1, mn + "." + m.name, "", "") } mti := findTemplate(g, mn, m.name, "", false) if mti >= 0 { - return genericCall(g, m, mti, 1, shown + "." + m.name) + return genericCall(g, m, mti, 1, mn + "." + m.name) } mei := findEnum(g, mn, m.name) if mei >= 0 { @@ -5893,7 +5259,7 @@ fn method(ptr[Gen] g, ast.Node m) -> Val { } return construct(g, si, args, m.names, m) } - fail(g, m, "the module '" + shown + "' has no function or struct named '" + m.name + "'") + fail(g, m, "the module '" + mn + "' has no function or struct named '" + m.name + "'") } changing := ["push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"] r := ref(g, objNode, changing.contains(m.name)) @@ -6350,10 +5716,6 @@ fn runtimeDecls() -> str { "declare void @finch_str_from_int(ptr, i64)", "declare void @finch_str_from_uint(ptr, i64)", "declare void @finch_str_from_float(ptr, double)", - "declare void @finch_str_from_f32(ptr, double)", - "declare void @finch_str_fixed(ptr, double, i64)", - "declare void @finch_print_float(double, i32)", - "declare void @finch_buf_f32(ptr, double)", "declare void @finch_str_from_char(ptr, i8 signext)", "declare void @finch_str_from_bool(ptr, i32)", "declare i64 @finch_str_to_int(ptr, ptr, i64)", @@ -6385,11 +5747,6 @@ fn runtimeDecls() -> str { "declare void @finch_arr_remove_gap(ptr, i64, i64)", "declare void @qsort(ptr, i64, i64, ptr)", "declare void @finch_args(ptr, i32, ptr)", - "declare void @finch_args_free(ptr)", - "declare void @finch_assert_failed(ptr, i64, ptr)", - "declare void @finch_test_run(ptr, ptr)", - "declare void @finch_trace_init(ptr, ptr)", - "declare i32 @finch_test_summary()", "declare void @finch_input(ptr, ptr)", "declare i32 @finch_file_exists(ptr)", "declare i32 @finch_str_try_int(ptr, ptr, ptr)", @@ -6420,14 +5777,6 @@ fn assemble(ptr[Gen] g) -> str { text = text + "target triple = \"" + g.tg.triple + "\"\n\n" } text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" - // the chain of calls a runtime error shows; the runtime reads it through these two - tls := "thread_local(initialexec)" - if g.shared || g.tg.windows { - tls = "thread_local" - } - text = text + "@finch_trace = internal " + tls + " global ptr null\n" - text = text + "define hidden ptr @finch_trace_top() {\nentry:\n %t = load ptr, ptr @finch_trace\n ret ptr %t\n}\n" - text = text + "define hidden void @finch_trace_set(ptr %t) {\nentry:\n store ptr %t, ptr @finch_trace\n ret void\n}\n\n" if g.debug { text = text + "declare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n" } @@ -6442,7 +5791,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) shared := opt.shared gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1) g := addr(gen) - g.trace = opt.trace && !opt.shared if g.debug { // !0 the compile unit, !1 and !2 the module flags, then the source files g.md.push("") @@ -6455,15 +5803,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) } declare(g) main := findFn(g, "", "main") - if opt.test { - di := 0 - while di < g.sigs.len { - define(g, di) - di += 1 - } - testMain(g, opt.testModules) - return assemble(g) - } if shared { if main >= 0 { fail(g, g.progs[g.sigs[main].prog].fns[g.sigs[main].index], "a library (--shared) has no main: the program that loads it calls its 'export fn's") @@ -6507,10 +5846,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) } callText = callText + ")" ret := " ret i32 0\n" - after := "" - if m.params.len == 1 { - after = " call void @finch_args_free(ptr %args)\n" - } if m.fallible { // fn main() -> !: a failure is printed, and the exit code is 1 callText = "%r = " + callText @@ -6519,7 +5854,7 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) rt := retTy(g, m) pre = pre + " %ok = extractvalue " + rt + " %r, 0\n br i1 %ok, label %main.ok, label %main.bad\nmain.bad:\n" pre = pre + " %m = extractvalue " + rt + " %r, 2\n %mp = alloca { ptr, i64, i64 }\n store { ptr, i64, i64 } %m, ptr %mp\n" - pre = pre + " call void @finch_report_error(ptr %mp)\n" + after + " ret i32 1\nmain.ok:\n" + pre = pre + " call void @finch_report_error(ptr %mp)\n ret i32 1\nmain.ok:\n" if m.ret.kind == "int" { ret = " %v = extractvalue " + rt + " %r, 1\n %code = trunc i64 %v to i32\n ret i32 %code\n" } @@ -6527,6 +5862,6 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) callText = "%r = " + callText ret = " %code = trunc i64 %r to i32\n ret i32 %code\n" } - g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + traceInit() + pre + " " + callText + "\n" + after + ret + "}\n") + g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + pre + " " + callText + "\n" + ret + "}\n") return assemble(g) } diff --git a/src/host.fch b/src/host.fch index 3a13551..1b1b8bd 100644 --- a/src/host.fch +++ b/src/host.fch @@ -81,18 +81,3 @@ fn removeTree(str path) { fn move(str from, str to) { shell("mv " + quote(from) + " " + quote(to)) } - -// the files under dir (and its folders) whose names end with ext, like ".fch" -fn listFiles(str dir, str ext) -> []str { - tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt" - run("find " + quote(dir) + " -type f -name " + quote("*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice()) - text := read_file(tmp) or "" - remove(tmp) - []str out - for line in text.replace("\r", "").split("\n") { - if line.ends_with(ext) { - out.push(line) - } - } - return out -} diff --git a/src/host_windows.fch b/src/host_windows.fch index 93a2ad9..9801e39 100644 --- a/src/host_windows.fch +++ b/src/host_windows.fch @@ -102,18 +102,3 @@ fn move(str from, str to) { delete_file(to) rename(from, to) } - -// the files under dir (and its folders) whose names end with ext, like ".fch" -fn listFiles(str dir, str ext) -> []str { - tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt" - run("dir /s /b " + quote(dir.replace("/", "\\") + "\\*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice()) - text := read_file(tmp) or "" - remove(tmp) - []str out - for line in text.replace("\r", "").split("\n") { - if line.ends_with(ext) { - out.push(line) - } - } - return out -} diff --git a/src/lexer.fch b/src/lexer.fch index ed4337b..5fbb2ee 100644 --- a/src/lexer.fch +++ b/src/lexer.fch @@ -24,7 +24,7 @@ fn isHex(char c) -> bool { fn isKeyword(str w) -> bool { keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", - "null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export", "const"] + "null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export"] return keywords.contains(w) } @@ -78,216 +78,6 @@ fn operator(ptr[Lexer] lx) -> str { return "" } -// """ text over several lines """: the line break after the opening quotes is dropped, and so is -// the indentation of the closing quotes, from every line (the text can be indented with the code). -// Gives the text as written (escapes and \(...) still in it); cook() finishes it. -fn longString(ptr[Lexer] lx, Token t) -> str { - for k in 0..3 { - advance(lx) - } - []str parts - while !(peek(lx, 0) == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"') { - if lx.i >= lx.src.len { - fail(lx, t.line, t.col, "text is never closed (missing \"\"\")") - } - ch := peek(lx, 0) - if ch == '\\' && peek(lx, 1) == '(' { - start := lx.i - advance(lx) - advance(lx) - skipEmbedded(lx, t) - parts.push(lx.src.sub(start, lx.i)) - continue - } - advance(lx) - if ch == '\r' { - continue - } - parts.push(str(ch)) - if ch == '\\' && lx.i < lx.src.len { - parts.push(str(peek(lx, 0))) // \" doesn't end the text - advance(lx) - } - } - for k in 0..3 { - advance(lx) - } - lx.nl = false // the line breaks inside were text, not the end of a statement - body := parts.join("") - if body.starts_with("\n") { - body = body.sub(1, body.len) - } - last := body.split("\n") - indent := last[last.len - 1] - if last.len > 1 && indent.trim().len == 0 { - // the closing quotes are on their own line: their indentation goes from every line - []str lines - for i in 0..last.len - 1 { - line := last[i] - if line.starts_with(indent) { - line = line.sub(indent.len, line.len) - } else if line.trim().len > 0 { - fail(lx, t.line, t.col, "a line of this text is indented less than the closing \"\"\"") - } else { - line = "" - } - lines.push(line) - } - return lines.join("\n") - } - return body -} - -// "text" as written, up to the closing quote (which is skipped); escapes and \(...) are still in it -fn rawString(ptr[Lexer] lx, Token t) -> str { - advance(lx) - start := lx.i - while peek(lx, 0) != '"' { - if lx.i >= lx.src.len || peek(lx, 0) == '\n' { - fail(lx, t.line, t.col, "string is never closed (missing \")") - } - if peek(lx, 0) == '\\' && peek(lx, 1) == '(' { - advance(lx) - advance(lx) - skipEmbedded(lx, t) - continue - } - if peek(lx, 0) == '\\' { - advance(lx) // the escaped character can't end the string - } - advance(lx) - } - raw := lx.src.sub(start, lx.i) - advance(lx) - return raw -} - -// past the code of a \( ... ), up to and including its ) -fn skipEmbedded(ptr[Lexer] lx, Token t) { - depth := 1 - while depth > 0 { - c := peek(lx, 0) - if lx.i >= lx.src.len || c == '\n' { - fail(lx, t.line, t.col, "a \\( in this text is never closed (missing ')')") - } - if c == '(' { - depth += 1 - } else if c == ')' { - depth -= 1 - } else if c == '"' || c == '\'' { - advance(lx) - while peek(lx, 0) != c { - if lx.i >= lx.src.len || peek(lx, 0) == '\n' { - fail(lx, t.line, t.col, "a text inside \\( ... ) is never closed") - } - if peek(lx, 0) == '\\' { - advance(lx) - } - advance(lx) - } - } - advance(lx) - } -} - -fn punct(str k, Token at) -> Token { - return Token(kind: k, text: k, line: at.line, col: at.col, nl: false) -} - -// The tokens of a text: one string, or for "x is \(x)" the tokens of ("x is " + str(x) + ""). -// \(v, 2) is str(v, 2): a number with two digits after the point. -fn cook(ptr[Lexer] lx, str raw, Token t) -> []Token { - []Token toks - []str parts - pieces := 0 - i := 0 - while i < raw.len { - ch := raw[i] - if ch == '\\' && i + 1 < raw.len && raw[i + 1] == '(' { - j := i + 2 - depth := 1 - while depth > 0 { - c := raw[j] - if c == '(' { - depth += 1 - } else if c == ')' { - depth -= 1 - } else if c == '"' || c == '\'' { - j += 1 - while raw[j] != c { - if raw[j] == '\\' { - j += 1 - } - j += 1 - } - } - j += 1 - } - code := raw.sub(i + 2, j - 1) - if code.trim().len == 0 { - fail(lx, t.line, t.col, "\\( ) needs a value inside, like \\(x)") - } - if pieces == 0 { - open := punct("(", t) - open.nl = t.nl - toks.push(open) - } - toks.push(Token(kind: "string", text: parts.join(""), line: t.line, col: t.col, nl: false)) - []str none - parts = none - toks.push(punct("+", t)) - toks.push(Token(kind: "ident", text: "str", line: t.line, col: t.col, nl: false)) - toks.push(punct("(", t)) - inner := lexFrom(code, lx.path, t.line, t.col + 3 + i) - for k in 0..inner.len - 1 { - tk := inner[k] - tk.nl = false - toks.push(tk) - } - toks.push(punct(")", t)) - toks.push(punct("+", t)) - pieces += 1 - i = j - continue - } - if ch == '\\' { - i += 1 - ch = escapeAt(lx, raw[i], t.line, t.col) - } - parts.push(str(ch)) - i += 1 - } - last := Token(kind: "string", text: parts.join(""), line: t.line, col: t.col, nl: false) - if pieces == 0 { - last.nl = t.nl - toks.push(last) - return toks - } - toks.push(last) - toks.push(punct(")", t)) - return toks -} - -fn escapeAt(ptr[Lexer] lx, char e, int line, int col) -> char { - if e == 'n' { - return '\n' - } - if e == 't' { - return '\t' - } - if e == 'r' { - return '\r' - } - if e == '0' { - return char(0) - } - if e == '\\' || e == '"' || e == '\'' { - return e - } - fail(lx, line, col, "unknown escape \\" + str(e) + " in this text (\\n \\t \\r \\0 \\\\ \\\" \\' and \\(value))") - return e -} - fn escape(ptr[Lexer] lx, char e) -> char { if e == 'n' { return '\n' @@ -309,12 +99,7 @@ fn escape(ptr[Lexer] lx, char e) -> char { } fn lex(str src, str path) -> []Token { - return lexFrom(src, path, 1, 1) -} - -// `src` starts at this line and column of the file (the code inside a "\(...)") -fn lexFrom(str src, str path, int firstLine, int firstCol) -> []Token { - lexer := Lexer(src: src, path: path, i: 0, line: firstLine, col: firstCol, nl: true) + lexer := Lexer(src: src, path: path, i: 0, line: 1, col: 1, nl: true) lx := addr(lexer) []Token out while lx.i < lx.src.len { @@ -359,8 +144,6 @@ fn lexFrom(str src, str path, int firstLine, int firstCol) -> []Token { } } else if isDigit(c) { t.kind = "int" - mant := "" - expStart := 0 if c == '0' && (peek(lx, 1) == 'x' || peek(lx, 1) == 'X') { advance(lx) advance(lx) @@ -378,38 +161,8 @@ fn lexFrom(str src, str path, int firstLine, int firstCol) -> []Token { advance(lx) } } - // an exponent: 1e9, 2.5e-3 - e := peek(lx, 0) - sign := peek(lx, 1) == '+' || peek(lx, 1) == '-' - if (e == 'e' || e == 'E') && (isDigit(peek(lx, 1)) || (sign && isDigit(peek(lx, 2)))) { - if t.kind == "int" { - t.kind = "float" - mant = lx.src.sub(start, lx.i) + ".0" - } - expStart = lx.i - advance(lx) - if sign { - advance(lx) - } - while lx.i < lx.src.len && isDigit(lx.src[lx.i]) { - advance(lx) - } - } } t.text = lx.src.sub(start, lx.i).replace("_", "") - if mant.len > 0 { - t.text = (mant + lx.src.sub(expStart, lx.i)).replace("_", "") // LLVM wants the '.' - } - } else if c == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"' { - for tk in cook(lx, longString(lx, t), t) { - out.push(tk) - } - continue - } else if c == '"' { - for tk in cook(lx, rawString(lx, t), t) { - out.push(tk) - } - continue } else if c == '"' || c == '\'' { quote := c t.kind = "string" diff --git a/src/linker.fch b/src/linker.fch index e55addc..ba5592f 100644 --- a/src/linker.fch +++ b/src/linker.fch @@ -18,169 +18,10 @@ fn fingerprint(str text) -> str { return parts.join("") } -// two different 32-bit hashes: names a cached object after everything it was built from -fn fingerprint2(str text) -> str { - u32 h = 2166136261 - u32 k = 5381 - for c in text { - h = (h ^ u32(c)) * 16777619 - k = k * 33 + u32(c) - } - digits := "0123456789abcdef" - []str parts - for i in 0..8 { - parts.push(str(digits[int((h >> u32(28 - 4 * i)) & 15)])) - } - for i in 0..8 { - parts.push(str(digits[int((k >> u32(28 - 4 * i)) & 15)])) - } - return parts.join("") -} - fn quote(str a) -> str { return host.quote(a) } -// a file of the program's own (next to the .fch file), not a library's name -fn isOwnCode(str l) -> bool { - return l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") || isCpp(l) -} - -fn isCpp(str l) -> bool { - return l.ends_with(".cpp") || l.ends_with(".cc") || l.ends_with(".cxx") -} - -// the C++ compiler that goes with a C compiler: clang -> clang++, x86_64-w64-mingw32-gcc -> ...-g++, cc -> c++ -fn cxxFor(str cc) -> str { - if getenv("FINCH_CXX").len > 0 { - return getenv("FINCH_CXX") - } - words := cc.split(" ") - first := words[0] - if first.ends_with("clang") { - first = first + "++" - } else if first.ends_with("gcc") { - first = first.sub(0, first.len - 3) + "g++" - } else if first.ends_with("cc") { - first = first.sub(0, first.len - 2) + "c++" - } - words[0] = first - return words.join(" ") -} - -// The files a C file was built from, as the compiler's -MMD wrote them: "x.o: x.c x.h \\n y.h" -fn depFiles(str dep) -> []str { - colon := dep.find(": ") - []str out - if colon < 0 { - return out - } - text := dep.sub(colon + 2, dep.len).replace("\\\r\n", " ").replace("\\\n", " ").replace("\\ ", str(char(1))) - for w in text.replace("\n", " ").replace("\t", " ").split(" ") { - if w.trim().len > 0 { - out.push(w.trim().replace(str(char(1)), " ")) - } - } - return out -} - -// what the files say now, as one fingerprint ("" if one is gone) -fn depsKey([]str files, str flags) -> str { - []str all - all.push(flags) - for f in files { - text := read_file(f) or { - return "" - } - all.push(f) - all.push(text) - } - return fingerprint2(all.join("\n")) -} - -// "@finch_inl_name = external global ptr" in the IR -> a C file (in the cache) that defines them; "" if none -fn inlineFile(str ir, cimport.CImports ci) -> str { - []str headers - []str lines - for line in ir.split("\n") { - if line.starts_with("@finch_inl_") && line.ends_with(" = external global ptr") { - name := line.sub(11, line.find(" = ")) - h := ci.fns[name].header - if !headers.contains(h) { - headers.push(h) - } - lines.push("void *finch_inl_" + name + " = (void *)&" + name + ";") - } - } - if lines.len == 0 { - return "" - } - []str src - for h in headers { - src.push("#include \"" + h + "\"") - } - for l in lines { - src.push(l) - } - text := src.join("\n") + "\n" - dir := host.cacheDir() + "/c" - host.makeDirs(dir) - path := dir + "/inline_" + fingerprint2(text) + ".c" - if !file_exists(path) { - write_file(path, text) - } - return path -} - -// A C file of the program (link "x.c"), built once and kept until it or a header it includes changes. -fn cObject(str src, str cc, str flags, platform.Target tg, str log) -> str! { - dir := host.cacheDir() + "/c" - host.makeDirs(dir) - slot := dir + "/" + fingerprint2(src + "\n" + cc + "\n" + flags + "\n" + tg.triple) - obj := slot + ".o" - key := cc + flags + tg.triple - if file_exists(obj) && file_exists(slot + ".d") && file_exists(slot + ".key") { - was := read_file(slot + ".key") or "" - if was.len > 0 && was == depsKey(depFiles(read_file(slot + ".d") or ""), key) { - return obj - } - } - part := slot + ".part" + str(host.pid()) - code := host.run(cc + flags + " -MMD -MF " + quote(slot + ".d") + " -c " + quote(src) + " -o " + quote(part) + " > " + quote(log) + " 2>&1") - if code != 0 { - host.remove(part) - return error((read_file(log) or "") + "error: couldn't compile " + src) - } - host.move(part, obj) - write_file(slot + ".key", depsKey(depFiles(read_file(slot + ".d") or ""), key)) - return obj -} - -// The program's IR, compiled once: the same IR with the same flags gives the same object. -// One slot per program (`name`), so the cache does not grow with every change. -fn irObject(str clang, str flags, str ir, str name, str log) -> str! { - dir := host.cacheDir() + "/ir" - host.makeDirs(dir) - slot := dir + "/" + fingerprint2(name + "\n" + flags) - obj := slot + ".o" - key := fingerprint2(clang + flags + "\n" + ir) - if file_exists(obj) && (read_file(slot + ".key") or "") == key { - return obj - } - ll := slot + ".part" + str(host.pid()) + ".ll" - part := slot + ".part" + str(host.pid()) + ".o" - write_file(ll, ir) - code := host.run(clang + flags + " -w -c " + quote(ll) + " -o " + quote(part) + " > " + quote(log) + " 2>&1") - host.remove(ll) - if code != 0 { - host.remove(part) - return error((read_file(log) or "") + "\nerror: clang couldn't compile the program's IR") - } - host.move(part, obj) - write_file(slot + ".key", key) - return obj -} - // "-lname", or whatever pkg-config says the library needs fn libFlags(str name, platform.Target tg) -> str { if name.ends_with(".framework") { @@ -259,8 +100,6 @@ fn linkFailed(str output, cimport.CImports ci, platform.Target tg) -> int { missing.add(between(l, "unresolved external symbol ", " \n\r")) } else if l.contains("cannot find -l") { libs.add(between(l, "cannot find -l", ": \n\r")) - } else if l.contains("unable to find library -l") { - libs.add(between(l, "unable to find library -l", ": \n\r")) // lld (MinGW) } else if l.contains("library not found for -l") { libs.add(between(l, "library not found for -l", ": \n\r")) } else if l.contains("ld: library '") { @@ -364,9 +203,6 @@ struct Build { str extra // -g, -fPIC ... bool shared []str links // what the program asked to link: a library name, a .c file (with its folder), a .o or .a - str name // the main file (names its slot in the cache) - str cflags // for the program's own C files: -I dirs, finch.toml's cflags - str ldflags // more for the linker: finch.toml's ldflags, -L dirs } // IR -> program. 0 if it worked. @@ -386,77 +222,57 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { print(err) return 1 } + ll := b.out + ".ll" + obj := b.out + ".o" + write_file(ll, b.ir) target := "" if tg.cross || tg.windows { target = " --target=" + tg.triple } log := b.out + ".log" - obj := irObject(clang, target + " " + b.opt + b.extra, b.ir, b.name + "\n" + tg.triple, log) or { - print(err) + code := host.run(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1") + host.remove(ll) + if code != 0 { + print(read_file(log) or "") + print("error: clang couldn't compile the program's IR") + host.remove(obj) host.remove(log) return 1 } - // with C++ files the C++ compiler links, so its library comes along - linkWith := cc - for l in b.links { - if isCpp(l) { - linkWith = cxxFor(cc) - } - } - cmd := linkWith + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out) - // static inline C functions the program calls: a C file that keeps a pointer to each - inl := inlineFile(b.ir, ci) - if inl.len > 0 { - pic := " -fPIC" - if tg.windows { - pic = "" - } - o := cObject(inl, cc, " -O2" + pic + b.cflags, tg, log) or { - print(err) - host.remove(log) - return 1 - } - cmd = cmd + " " + quote(o) - } + cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out) if !tg.msvc { cmd = cmd + " -lm" } - if tg.android { - cmd = cmd + " -llog" // bionic has threads built in - } else if !tg.windows && !tg.darwin { - cmd = cmd + " -lpthread" // std/thread (in glibc since 2.34, needed before) - } - if b.ldflags.len > 0 { - cmd = cmd + b.ldflags // finch.toml: libdirs and ldflags - } if getenv("FINCH_LDFLAGS").len > 0 { cmd = cmd + " " + getenv("FINCH_LDFLAGS") // more flags for the linker, like -L/where/libclang/is } if b.shared { if tg.darwin { - cmd = cmd + " -dynamiclib -undefined dynamic_lookup" // like a .so: the host's functions are found when it is loaded + cmd = cmd + " -dynamiclib" } else { cmd = cmd + " -shared" } } + []str temps for l in b.links { - if l.ends_with(".c") || isCpp(l) { - // the program's own C or C++ file, built along with it (and kept in the cache until it changes) + if l.ends_with(".c") { + // the program's own C file, built along with it + o := b.out + "." + str(temps.len) + ".o" pic := " -fPIC" if tg.windows { pic = "" } - compiler := cc - if isCpp(l) { - compiler = cxxFor(cc) - } - o := cObject(l, compiler, " -O2" + pic + b.cflags, tg, log) or { - print(err) + c := host.run(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1") + if c != 0 { + print((read_file(log) or "") + "error: couldn't compile " + l) + host.remove(obj) host.remove(log) + host.remove(o) return 1 } + temps.push(o) cmd = cmd + " " + quote(o) - } else if l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") { + } else if l.ends_with(".o") || l.ends_with(".a") { cmd = cmd + " " + quote(l) } else { cmd = cmd + " " + libFlags(l, tg) @@ -464,7 +280,11 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int { } status := host.run(cmd + " > " + quote(log) + " 2>&1") output := read_file(log) or "" + host.remove(obj) host.remove(log) + for t in temps { + host.remove(t) + } if status == 127 { print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)") return 1 diff --git a/src/lsp.fch b/src/lsp.fch index 0bc0fcc..6cb075f 100644 --- a/src/lsp.fch +++ b/src/lsp.fch @@ -7,7 +7,6 @@ import json import host import docs -import fmt import "stdio.h" import "stdlib.h" @@ -81,9 +80,7 @@ struct Analysis { []StructRec structs []EnumRec enums []str imports - []str importKeys // the module each import is: btn -> ui/button Diag diag - []Diag warnings // shown in yellow; the program still builds } struct Server { @@ -127,22 +124,13 @@ fn uriToPath(str uri) -> str { i += 1 } } - p := str(out) - if p.len > 2 && p[0] == '/' && p[2] == ':' { - p = p.sub(1, p.len) // Windows: file:///C:/x -> C:/x - } - return p + return str(out) } fn pathToUri(str path) -> str { digits := "0123456789ABCDEF" []str parts = ["file://"] - p := path.replace("\\", "/") - if p.len > 1 && p[1] == ':' { - parts.push("/" + p.sub(0, 2)) // Windows: C:/x -> file:///C:/x - p = p.sub(2, p.len) - } - for c in p { + for c in path { n := int(c) if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "/-_.~".contains(str(c)) { parts.push(str(c)) @@ -307,7 +295,7 @@ fn parseDiag(str line) -> Diag { fn parseOutput(str output) -> Analysis { a := Analysis() - for line in output.replace("\r", "").split("\n") { // Windows ends the compiler's lines with \r\n + for line in output.split("\n") { if line.starts_with("@") { f := splitTabs(line) tag := f[0] @@ -325,15 +313,8 @@ fn parseOutput(str output) -> Analysis { a.structs[k].types.push(f[4]) } else if tag == "@E" && f.len >= 7 { a.enums.push(EnumRec(module: f[1], name: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5]), variants: f[6].split(","))) - } else if tag == "@W" && f.len >= 5 { - a.warnings.push(Diag(has: true, file: f[1], line: intOf(f[2]), col: intOf(f[3]), message: f[4])) } else if tag == "@I" && f.len >= 2 { a.imports.push(f[1]) - if f.len >= 3 { - a.importKeys.push(f[2]) - } else { - a.importKeys.push(f[1]) - } } } else if !a.diag.has { a.diag = parseDiag(line) @@ -367,7 +348,6 @@ fn Server.analyse(str path) { a.vars = g.vars if a.imports.len == 0 { a.imports = g.imports - a.importKeys = g.importKeys } } self.results[path] = a @@ -402,27 +382,6 @@ fn Server.publish(str path, Analysis a) { list.push(d) now.push(f) } - for w in a.warnings { - if w.file == f { - d := json.new_object() - wl := w.line - 1 - if wl < 0 { - wl = 0 - } - wc := w.col - 1 - if wc < 0 { - wc = 0 - } - d.set("range", range(wl, wc, wc + 1)) - d.set("severity", num(2)) - d.set("source", json.new_text("finch")) - d.set("message", json.new_text(w.message)) - list.push(d) - if !now.contains(f) { - now.push(f) - } - } - } p := json.new_object() p.set("uri", json.new_text(pathToUri(f))) p.set("diagnostics", list) @@ -578,7 +537,6 @@ fn completionAt(Analysis a, str text, str path, int line, int col) -> json.Json } // a module: its functions, structs and enums if a.imports.contains(owner) { - owner = a.importKeys[a.imports.find(owner)] for f in a.fns { if f.module == owner && f.recv.len == 0 { items.push(item(f.name, 3, f.signature, "")) @@ -819,7 +777,6 @@ fn Server.answer(json.Json msg) { caps.set("hoverProvider", json.new_bool(true)) caps.set("definitionProvider", json.new_bool(true)) caps.set("documentSymbolProvider", json.new_bool(true)) - caps.set("documentFormattingProvider", json.new_bool(true)) comp := json.new_object() dot := json.new_list() dot.push(json.new_text(".")) @@ -874,32 +831,6 @@ fn Server.answer(json.Json msg) { empty.set("diagnostics", json.new_list()) notify("textDocument/publishDiagnostics", empty) } - "textDocument/formatting" -> { - // finch fmt on the open text: one edit that puts the whole text in its place - path := pathOf(params) - edits := json.new_list() - if self.docs.has(path) { - old := self.docs[path] - neat := fmt.format(old) - if neat != old { - lines := old.split("\n") - whole := json.new_object() - start := json.new_object() - start.set("line", num(0)) - start.set("character", num(0)) - end := json.new_object() - end.set("line", num(lines.len)) - end.set("character", num(0)) - whole.set("start", start) - whole.set("end", end) - edit := json.new_object() - edit.set("range", whole) - edit.set("newText", json.new_text(neat)) - edits.push(edit) - } - } - reply(id, edits) - } "textDocument/hover", "textDocument/definition", "textDocument/completion", "textDocument/documentSymbol", "textDocument/signatureHelp" -> { path := pathOf(params) line := params.get("position").get("line").int_or(0) + 1 diff --git a/src/main.fch b/src/main.fch index f4c4f00..858271f 100644 --- a/src/main.fch +++ b/src/main.fch @@ -15,8 +15,6 @@ import linker import lsp import host import embedded -import manifest -import fmt import "stdlib.h" fn dirOf(str path) -> str { @@ -30,14 +28,6 @@ fn dirOf(str path) -> str { return path.sub(0, i) } -// the path from the root, so the same file is the same in the cache wherever finch is started -fn absolute(str path) -> str { - if path.starts_with("/") || (path.len > 2 && path[1] == ':') { - return path - } - return host.cwd() + "/" + path -} - fn stem(str path) -> str { name := path.sub(dirOf(path).len + 1, path.len) if dirOf(path) == "." && !path.starts_with("./") { @@ -57,7 +47,6 @@ struct Loader { str os // "windows", "macos" or "linux": a module may have a version for each (time_windows.fch) str std // the standard library's folder, or "" []str paths // FINCH_PATH: more folders to look for modules in - str root // the main file's folder: modules are named by their path from here (ui/button) []str overlayPaths // --overlay path=file: read `path` from `file` (the text an editor has, not saved yet) []str overlayFiles } @@ -100,27 +89,6 @@ fn findModule(ptr[Loader] l, str from, str name) -> str { return local } -// A module is named by its file, so `import button` in ui/menu.fch and `import ui/button` in main.fch -// are the same module, and two different util.fch files are two modules. -fn moduleKey(ptr[Loader] l, str file, str name) -> str { - []str roots - roots.push(l.root) - for d in l.paths { - roots.push(d) - } - roots.push(l.std) - for r in roots { - if r.len > 0 && file.starts_with(r + "/") && file.ends_with(".fch") { - key := file.sub(r.len + 1, file.len - 4) - if key.ends_with("_" + l.os) { - key = key.sub(0, key.len - l.os.len - 1) // time_windows.fch is the module time - } - return key - } - } - return name -} - fn load(ptr[Loader] l, str path, str module) { if !file_exists(path) && !l.overlayPaths.contains(path) { if module.len > 0 { @@ -136,21 +104,12 @@ fn load(ptr[Loader] l, str path, str module) { prog := parser.parse(lexer.lex(src, path), file, path) prog.module = module prog.path = path - if module.len == 0 { - l.root = dirOf(path) - } - []str files - for i in 0..prog.modules.len { - f := findModule(l, path, prog.modules[i]) - files.push(f) - prog.modules[i] = moduleKey(l, f, prog.modules[i]) - } - modules := prog.modules + imports := prog.imports l.progs.push(prog) - for i in 0..modules.len { - if !l.loaded.contains(modules[i]) { - l.loaded.push(modules[i]) - load(l, files[i], modules[i]) + for name in imports { + if !l.loaded.contains(name) { + l.loaded.push(name) + load(l, findModule(l, path, name), name) } } } @@ -160,10 +119,7 @@ fn usage() { print("usage:") print(" finch run [args...] build and run a program") print(" finch build [-o name] build a program") - print(" finch test run its test \"...\" { } blocks (and those of the modules it imports)") print(" finch ir print the LLVM IR") - print(" finch fmt [--check] [files/dirs] indent .fch files by { }, 4 spaces (--check: only list those it would change)") - print(" (with a finch.toml, run, build and test can leave out the file: its entry is used)") print(" finch version show the version") print(" finch lsp the language server, for editors (VS Code, Kate, ...)") print("options:") @@ -171,8 +127,6 @@ fn usage() { print(" -l link a library (the same as link \"lib\" in the program)") print(" -I look for imported C headers there too") print(" -g debug info (for gdb and lldb; use -O0 with it)") - print(" --trace runtime errors show the chain of calls (finch run, finch test, -g and -O0 do it anyway)") - print(" --no-trace they don't (finch build without -g or -O0 doesn't, it costs speed)") print(" --target build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple") exit(1) } @@ -194,104 +148,26 @@ fn unpack() -> str { return dir } -// finch fmt [--check] [files or folders]: the indentation and empty lines of .fch files (all under here by default) -fn format([]str args) -> int { - check := false - []str targets - for i in 2..args.len { - if args[i] == "--check" { - check = true - } else { - targets.push(args[i]) - } - } - if targets.len == 0 { - targets.push(".") - } - []str files - for t in targets { - if t.ends_with(".fch") { - files.push(t) - } else { - for f in host.listFiles(t, ".fch") { - files.push(f) - } - } - } - changed := 0 - for f in files { - text := read_file(f) or { - print("error: can't read " + f) - return 1 - } - neat := fmt.format(text) - if neat != text { - changed += 1 - if check { - print(f) - } else { - write_file(f, neat) - } - } - } - if check && changed > 0 { - print(str(changed) + " file(s) are not formatted (finch fmt changes them)") - return 1 - } - return 0 -} - fn main([]str args) -> int { if args.len == 2 && (args[1] == "version" || args[1] == "--version") { - print("Finch 2.5.0 (the compiler is written in Finch)") + print("Finch 2.4.0 (the compiler is written in Finch)") return 0 } - if args.len >= 2 && args[1] == "fmt" { - return format(args) - } if args.len == 2 && args[1] == "lsp" { return lsp.run(args[0]) // the language server, for editors } - if args.len < 2 { + if args.len < 3 { usage() } cmd := args[1] - if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" && cmd != "test" { + file := args[2] + if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" { usage() } - file := "" - ai := 2 - if args.len > 2 && !args[2].starts_with("-") { - file = args[2] - ai = 3 - } - // finch.toml: next to the file, or above it (with no file: from here, and its entry is the file) - from := host.cwd() - if file.len > 0 { - from = dirOf(absolute(file)) - } - man := manifest.find(from) or { - print(err) - return 1 - } - isEntry := file.len == 0 - if isEntry { - entry := man.value("entry", "") - if !man.found || entry.len == 0 { - print("error: which file? (finch " + cmd + " , or a finch.toml with entry = \"main.fch\")") - return 1 - } - file = man.resolve(entry) - } out := stem(file) - if isEntry && man.value("name", "").len > 0 { // another file of the project keeps its own name - out = man.value("name", "") - } outGiven := false shared := false debug := false - trace := cmd != "build" // finch build: only with --trace, -g or -O0 (the chain costs speed) - traceGiven := false targetName := "" opt := "-O2" []str extraLibs @@ -299,6 +175,7 @@ fn main([]str args) -> int { []str overlayPaths []str overlayFiles []str progArgs + ai := 3 while ai < args.len { a := args[ai] if cmd == "run" && progArgs.len > 0 { // after the first argument of the program, all of them are its @@ -326,12 +203,6 @@ fn main([]str args) -> int { ai += 1 } else if a == "-g" { debug = true - } else if a == "--no-trace" { - trace = false - traceGiven = true - } else if a == "--trace" { - trace = true - traceGiven = true } else if a == "--target" && ai + 1 < args.len { targetName = args[ai + 1] ai += 1 @@ -342,11 +213,8 @@ fn main([]str args) -> int { } ai += 1 } - if !traceGiven && (debug || opt == "-O0") { - trace = true - } tg := platform.select(targetName) - if cmd == "run" || cmd == "test" { + if cmd == "run" { out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe } else if shared && !outGiven { if tg.windows { @@ -377,48 +245,6 @@ fn main([]str args) -> int { } loader.overlayPaths = overlayPaths loader.overlayFiles = overlayFiles - // what finch.toml adds, for every system and for this one - []str targets - sys := "linux" - if tg.windows { - sys = "windows" - } else if tg.darwin { - sys = "macos" - } else if tg.android { - sys = "android" - } - arch := "x86_64" - if tg.arm64 { - arch = "arm64" - } - targets.push(sys) - targets.push(sys + "-" + arch) - ldflags := "" - cflags := "" - if man.found { - for d in man.list("paths", targets) { - loader.paths.push(man.resolve(d)) - } - for d in man.list("include", targets) { - extraDirs.push(man.resolve(d)) - } - for l in man.list("links", targets) { - if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".cpp") || l.ends_with(".cc") { - extraLibs.push(man.resolve(l)) - } else { - extraLibs.push(l) - } - } - for d in man.list("libdirs", targets) { - ldflags = ldflags + " -L" + linker.quote(man.resolve(d)) - } - for f in man.list("ldflags", targets) { - ldflags = ldflags + " " + linker.quote(f) - } - for f in man.list("cflags", targets) { - cflags = cflags + " " + linker.quote(f) - } - } for d in getenv("FINCH_PATH").split(host.pathSeparator()) { if d.len > 0 { loader.paths.push(d) @@ -443,7 +269,7 @@ fn main([]str args) -> int { } } for l in prog.links { - if linker.isOwnCode(l) { + if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") { links.push(d + "/" + l) // the program's own C code, next to the .fch file } else { links.push(l) @@ -458,13 +284,7 @@ fn main([]str args) -> int { print(err) return 1 } - []str testModules - for prog in loader.progs { - if !prog.path.starts_with(loader.std + "/") { - testModules.push(prog.module) // your own files' tests, not the standard library's - } - } - ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg, test: cmd == "test", testModules: testModules, trace: trace)) + ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg)) if cmd == "ir" || cmd == "index" { print(ir) return 0 @@ -494,15 +314,10 @@ fn main([]str args) -> int { if debug { extra = extra + " -g" // or clang drops the debug info that is in the IR } - []str cflagList - for d in dirs { - cflagList.push(" -I" + linker.quote(d)) // also the program's own folders (its headers, for inline C functions) - } - cflagList.push(cflags) - if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links, name: absolute(file), cflags: cflagList.join(""), ldflags: ldflags), tg, ci) != 0 { + if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links), tg, ci) != 0 { return 1 } - if cmd == "run" || cmd == "test" { + if cmd == "run" { []str quoted for a in progArgs { quoted.push(linker.quote(a)) diff --git a/src/manifest.fch b/src/manifest.fch deleted file mode 100644 index 2c846b0..0000000 --- a/src/manifest.fch +++ /dev/null @@ -1,243 +0,0 @@ -// finch.toml: what a project needs to build, in one file next to its code (instead of FINCH_PATH, -// C_INCLUDE_PATH, -I, -l and linker flags on the command line). -// -// name = "game" the program finch build makes -// entry = "src/main.fch" what finch run / build / test use when no file is given -// paths = ["src", "lib"] folders with modules -// include = ["vendor/SDL3/include"] folders with C headers (also for link "x.c" files) -// links = ["SDL3"] libraries -// libdirs = ["vendor/SDL3/lib"] folders with libraries -// cflags = ["-DFAST"] for the program's own C files -// ldflags = ["-Wl,-rpath,."] for the linker -// -// [target.windows] added only when building for that system: -// links = ["ws2_32"] windows, linux, macos, android, or with the CPU: linux-arm64 -// -// Folders are relative to finch.toml. Only this part of TOML is read: [sections], key = "text", -// key = ["a", "b"] (over several lines too), numbers, true/false and # comments. - -struct Manifest { - bool found - str path - str dir - map[str]str values // "section.key" (top level: "key") -> a single value - map[str][]str lists // "section.key" -> a list -} - -fn dirOf(str path) -> str { - i := path.len - 1 - while i >= 0 && path[i] != '/' && path[i] != '\\' { - i -= 1 - } - if i < 0 { - return "." - } - if i == 0 { - return "/" - } - return path.sub(0, i) -} - -// finch.toml in `dir` or the folders above it -fn find(str dir) -> Manifest! { - d := dir - while true { - p := d + "/finch.toml" - if d == "/" { - p = "/finch.toml" - } - if file_exists(p) { - return try parse(p, try read_file(p)) - } - up := dirOf(d) - if up == d || d == "." || d.len == 0 { - break - } - d = up - } - return Manifest() -} - -struct Reader { - str text - int i - int line - str path -} - -fn Reader.fail(str msg) -> str { - return self.path + ":" + str(self.line) + ": error: " + msg -} - -fn Reader.at() -> char { - if self.i < self.text.len { - return self.text[self.i] - } - return char(0) -} - -// past spaces, and past line ends and comments too when `lines` -fn Reader.skip(bool lines) { - while self.i < self.text.len { - c := self.text[self.i] - if c == ' ' || c == '\t' || c == '\r' { - self.i += 1 - } else if c == '#' { - while self.i < self.text.len && self.text[self.i] != '\n' { - self.i += 1 - } - } else if c == '\n' && lines { - self.i += 1 - self.line += 1 - } else { - return - } - } -} - -fn isKeyChar(char c) -> bool { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' -} - -fn Reader.word() -> str { - start := self.i - while self.i < self.text.len && isKeyChar(self.text[self.i]) { - self.i += 1 - } - return self.text.sub(start, self.i) -} - -// "text" (with \" \\ \n \t) or 'text' (as it is) -fn Reader.quoted() -> str! { - q := self.at() - self.i += 1 - []str out - while self.at() != q { - c := self.at() - if c == char(0) || c == '\n' { - return error(self.fail("this text is never closed")) - } - if c == '\\' && q == '"' { - self.i += 1 - e := self.at() - if e == 'n' { - c = '\n' - } else if e == 't' { - c = '\t' - } else { - c = e - } - } - out.push(str(c)) - self.i += 1 - } - self.i += 1 - return out.join("") -} - -// a single value: text, a number, true or false (all kept as text) -fn Reader.scalar() -> str! { - c := self.at() - if c == '"' || c == '\'' { - return try self.quoted() - } - w := self.word() - if w.len == 0 { - return error(self.fail("expected a value: \"text\", a number, true, false or a [list]")) - } - return w -} - -fn parse(str path, str text) -> Manifest! { - m := Manifest(found: true, path: path, dir: dirOf(path)) - r := Reader(text: text, i: 0, line: 1, path: path) - section := "" - while true { - r.skip(true) - if r.i >= r.text.len { - break - } - if r.at() == '[' { - r.i += 1 - r.skip(false) - section = r.word() - r.skip(false) - if r.at() != ']' || section.len == 0 { - return error(r.fail("a section looks like [name] or [target.windows]")) - } - r.i += 1 - continue - } - key := r.word() - if key.len == 0 { - return error(r.fail("expected a key, like name = \"game\"")) - } - r.skip(false) - if r.at() != '=' { - return error(r.fail("expected '=' after '" + key + "'")) - } - r.i += 1 - r.skip(false) - full := key - if section.len > 0 { - full = section + "." + key - } - if r.at() == '[' { - r.i += 1 - []str items - while true { - r.skip(true) - if r.at() == ']' { - r.i += 1 - break - } - items.push(try r.scalar()) - r.skip(true) - if r.at() == ',' { - r.i += 1 - } else if r.at() != ']' { - return error(r.fail("expected ',' or ']' in the list of '" + key + "'")) - } - } - m.lists[full] = items - } else { - m.values[full] = try r.scalar() - } - r.skip(false) - if r.i < r.text.len && r.at() != '\n' { - return error(r.fail("one key per line")) - } - } - return m -} - -fn Manifest.value(str key, str fallback) -> str { - return self.values.get(key, fallback) -} - -// the list `key` from the top level and from the sections for this system, in that order -fn Manifest.list(str key, []str targets) -> []str { - []str out - if self.lists.has(key) { - for x in self.lists[key] { - out.push(x) - } - } - for t in targets { - k := "target." + t + "." + key - if self.lists.has(k) { - for x in self.lists[k] { - out.push(x) - } - } - } - return out -} - -// a folder or file of the manifest, from where finch.toml is -fn Manifest.resolve(str p) -> str { - if p.starts_with("/") || (p.len > 2 && p[1] == ':') { - return p - } - return self.dir + "/" + p -} diff --git a/src/parser.fch b/src/parser.fch index ce3aff3..82c54ad 100644 --- a/src/parser.fch +++ b/src/parser.fch @@ -236,49 +236,18 @@ fn parseProgram(ptr[Parser] p) -> ast.Program { if !at(p, "ident") { fail(p, t, "expected a module name or a C header after 'import'") } - // import ui/button (a folder), import path as p (another name here) - key := next(p).text - name := key - while at(p, "/") && !cur(p).nl { - next(p) - name = expect(p, "ident", "a module name after '/'").text - key = key + "/" + name - } - if at(p, "ident") && cur(p).text == "as" && !cur(p).nl { - next(p) - name = expect(p, "ident", "the name to use for the module, like import path as p").text - } - if prog.imports.contains(name) { - fail(p, t, "the name '" + name + "' is already used by another import (pick one with: import " + key + " as other)") - } - prog.imports.push(name) - prog.modules.push(key) + prog.imports.push(next(p).text) } } else if at(p, "link") { lt := next(p) lib := expect(p, "string", "a library name in quotes, like link \"glfw\"").text - ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") || lib.ends_with(".cpp") || lib.ends_with(".cc") || lib.ends_with(".cxx") || lib.ends_with(".lib") // your own C or C++ code, next to the .fch file + ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") // your own C code, next to the .fch file if !ownCode && (lib.starts_with("lib") || lib.contains(".so") || lib.contains("/")) { fail(p, lt, "write just the library's name: for libglfw.so that's link \"glfw\"") } prog.links.push(lib) } else if at(p, "struct") { prog.structs.push(parseStruct(p)) - } else if at(p, "const") { - prog.consts.push(parseConst(p)) - } else if at(p, "ident") && cur(p).text == "test" && peekKind(p, 1) == "string" { - // test "adds up" { ... }: a function finch test runs (its name is the text) - t := next(p) - f := node(p, "fn") - f.line = t.line - f.col = t.col - f.text = next(p).text - f.name = "__test_" + str(prog.tests.len) - f.ty = ast.simple("void") - f.kids.push(parseBlock(p)) - f.endLine = f.kids[0].endLine - prog.tests.push(f.name) - prog.fns.push(f) } else if at(p, "enum") { e := parseEnum(p) prog.enums.push(e) @@ -292,24 +261,6 @@ fn parseProgram(ptr[Parser] p) -> ast.Program { return prog } -// const WIDTH = 640 const f32 GRAVITY = 9.8 const TITLE = "Game" -fn parseConst(ptr[Parser] p) -> ast.Node { - c := node(p, "const") - expect(p, "const", "'const'") - if !(at(p, "ident") && peekKind(p, 1) == "=") { - c.ty = parseType(p) - c.flag = true - } - t := expect(p, "ident", "the constant's name, like const WIDTH = 640") - c.name = t.text - c.line = t.line - c.col = t.col - expect(p, "=", "'=' and its value, like const WIDTH = 640") - c.kids.push(parseExpr(p)) - endOfStatement(p) - return c -} - // enum Color { Red, Green, Blue } enum Status { Ok = 200, NotFound = 404 } // The variants are numbered from 0 (or from the last number given) and are written Color.Red. // A variant can hold values: enum Shape { Circle(float), Rect(float, float), Point } (e.flag is then true). @@ -482,7 +433,7 @@ fn parseFn(ptr[Parser] p) -> ast.Node { if at(p, "ident") && isTypeName(cur(p).text) { fail(p, cur(p), "functions start with 'fn', like: fn add(int a, int b) -> int { ... }") } - expect(p, "fn", "'fn', 'struct', 'enum', 'const' or 'import'") + expect(p, "fn", "'fn', 'struct' or 'import'") f.name = expect(p, "ident", "a function name").text if at(p, "[") { f.tparams = parseTParams(p) // fn max[T](...) or the struct's, in fn Stack[T].push(...) @@ -661,33 +612,6 @@ fn parseIf(ptr[Parser] p) -> ast.Node { // ---------- expressions ---------- -// if as a value: x := if a > b { a } else { b } (each branch is one value; the else is needed) -fn parseIfValue(ptr[Parser] p) -> ast.Node { - e := node(p, "ifvalue") - expect(p, "if", "'if'") - e.kids.push(parseExpr(p)) - e.kids.push(branchValue(p)) - if !accept(p, "else") { - fail(p, cur(p), "an if that gives a value needs an else: if c { a } else { b }") - } - if at(p, "if") { - e.kids.push(parseIfValue(p)) - } else { - e.kids.push(branchValue(p)) - } - return e -} - -fn branchValue(ptr[Parser] p) -> ast.Node { - expect(p, "{", "'{'") - saved := p.depth - p.depth = 1 - v := parseExpr(p) - p.depth = saved - expect(p, "}", "'}' (a branch of an if that gives a value holds just that value)") - return v -} - fn binaryLevel(str op) -> int { if op == "||" { return 1 @@ -845,9 +769,6 @@ fn parsePrimary(ptr[Parser] p) -> ast.Node { if k == "match" { return parseMatch(p, true) } - if k == "if" { - return parseIfValue(p) - } if k == "[" { next(p) e.kind = "array" diff --git a/src/platform.fch b/src/platform.fch index 44a678f..b7a6992 100644 --- a/src/platform.fch +++ b/src/platform.fch @@ -11,7 +11,6 @@ struct Target { bool darwin // macOS bool arm64 // AAPCS64 bool cross // another system than the one the compiler runs on - bool android // Android (bionic): a .so that an app loads str exe // ".exe" or "" str cc // the C compiler that links (the IR is always compiled by clang) str runner // how to run a program for it here: "", "wine ", "qemu-aarch64 " @@ -46,8 +45,6 @@ fn select(str name) -> Target { t = "x86_64-unknown-linux-gnu" } else if name == "arm64" || name == "aarch64" || name == "linux-arm64" { t = "aarch64-unknown-linux-gnu" - } else if name == "android" { - t = "aarch64-linux-android24" } else if name == "macos" { if hostName.starts_with("aarch64") || hostName.starts_with("arm64") { t = "arm64-apple-macosx14.0.0" @@ -63,7 +60,6 @@ fn select(str name) -> Target { tg.msvc = t.contains("msvc") tg.darwin = t.contains("apple") || t.contains("darwin") || t.contains("macos") tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64") - tg.android = t.contains("android") hostArm := hostName.starts_with("aarch64") || hostName.starts_with("arm64") hostDarwin := hostName.contains("apple") || hostName.contains("darwin") hostWin := hostName.contains("windows") || hostName.contains("mingw") || hostName.contains("msvc") @@ -76,8 +72,6 @@ fn select(str name) -> Target { fc := getenv("FINCH_CC") if fc.len > 0 { tg.cc = fc - } else if tg.android { - tg.cc = androidClang(t) } else if tg.cross && tg.windows { tg.cc = "x86_64-w64-mingw32-gcc" } else if tg.cross && tg.arm64 && !tg.darwin { @@ -91,30 +85,12 @@ fn select(str name) -> Target { } if tg.cross && tg.windows { tg.runner = "wine " - } else if tg.cross && tg.arm64 && !tg.darwin && !tg.android { + } else if tg.cross && tg.arm64 && !tg.darwin { tg.runner = "qemu-aarch64 " } return tg } -// The NDK's clang for this Android triple (ANDROID_NDK_HOME or ANDROID_NDK_ROOT says where the NDK is) -fn androidClang(str triple) -> str { - ndk := getenv("ANDROID_NDK_HOME") - if ndk.len == 0 { - ndk = getenv("ANDROID_NDK_ROOT") - } - if ndk.len == 0 { - return triple + "-clang" - } - for h in ["linux-x86_64", "darwin-x86_64", "windows-x86_64"] { - cc := ndk + "/toolchains/llvm/prebuilt/" + h + "/bin/" + triple + "-clang" - if file_exists(cc) || file_exists(cc + ".cmd") { - return cc - } - } - return triple + "-clang" -} - // C's plain `char`: signed on x86 and Apple, not on ARM64 Linux fn charIsSigned(Target t) -> bool { return !t.arm64 || t.darwin || t.windows diff --git a/std/finch_fs.h b/std/finch_fs.h deleted file mode 100644 index f3df1df..0000000 --- a/std/finch_fs.h +++ /dev/null @@ -1,9 +0,0 @@ -/* The runtime's folder functions, for std/fs.fch (they are in runtime/finch_rt.c, linked into every program). */ -int finch_fs_is_dir(const char *path); -int finch_fs_exists(const char *path); -int finch_fs_mkdir(const char *path); -int finch_fs_remove_dir(const char *path); -void *finch_fs_list_open(const char *path); -const char *finch_fs_list_next(void *list); -void finch_fs_list_close(void *list); -const char *finch_fs_data_home(void); diff --git a/std/finch_thread.h b/std/finch_thread.h deleted file mode 100644 index c8a27b4..0000000 --- a/std/finch_thread.h +++ /dev/null @@ -1,8 +0,0 @@ -/* The runtime's thread functions, for std/thread.fch (they are in runtime/finch_rt.c, linked into every program). */ -void *finch_thread_spawn(void (*fn)(void *), void *arg); -int finch_thread_join(void *thread); -void *finch_mutex_new(void); -void finch_mutex_lock(void *mutex); -void finch_mutex_unlock(void *mutex); -void finch_mutex_free(void *mutex); -int finch_cpu_count(void); diff --git a/std/fs.fch b/std/fs.fch deleted file mode 100644 index afb6196..0000000 --- a/std/fs.fch +++ /dev/null @@ -1,70 +0,0 @@ -// Files and folders: bytes, folders, listing, and a place for a program's own files. -// -// import fs -// data := try fs.read_bytes("atlas.png") the whole file as []u8 (any bytes, NUL included) -// try fs.write_bytes("out.bin", data) -// try fs.mkdir("saves/slot1") with the folders above it -// names := try fs.list("saves") the names in a folder, sorted -// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p) -// dir := try fs.data_dir("MyGame") a folder for this user's saves, made if needed -import "finch_fs.h" - -fn read_bytes(str path) -> []u8! { - text := try read_file(path) - return text.bytes() -} - -fn write_bytes(str path, []u8 data) -> ! { - try write_file(path, str(data)) -} - -fn exists(str path) -> bool { - return finch_fs_exists(path) != 0 -} - -fn is_dir(str path) -> bool { - return finch_fs_is_dir(path) != 0 -} - -fn mkdir(str path) -> ! { - if finch_fs_mkdir(path) == 0 { - return error("can't make the folder \"" + path + "\"") - } -} - -// only an empty folder -fn remove_dir(str path) -> ! { - if finch_fs_remove_dir(path) == 0 { - return error("can't remove the folder \"" + path + "\" (is it empty?)") - } -} - -fn list(str path) -> []str! { - l := finch_fs_list_open(path) - if l == null { - return error("can't list the folder \"" + path + "\"") - } - []str names - while true { - name := finch_fs_list_next(l) - if name.len == 0 { - break - } - names.push(name) - } - finch_fs_list_close(l) - names.sort() - return names -} - -// The folder where a program keeps this user's files (saves, settings), made if it isn't there: -// ~/.local/share/ on Linux, ~/Library/Application Support/ on macOS, %APPDATA%\ on Windows. -fn data_dir(str app) -> str! { - home := finch_fs_data_home() - if home.len == 0 { - return error("this system doesn't say where programs keep their files") - } - dir := home + "/" + app - try mkdir(dir) - return dir -} diff --git a/std/thread.fch b/std/thread.fch deleted file mode 100644 index 275bc7d..0000000 --- a/std/thread.fch +++ /dev/null @@ -1,65 +0,0 @@ -// Threads: run a function next to the rest of the program, and a lock for what they share. -// -// import thread -// fn work(ptr arg) { a thread's function takes one ptr (to whatever it needs) -// ptr[Job] job = arg -// ... -// } -// t := try thread.spawn(addr(work), addr(job)) -// t.join() wait for it; `job` must live until then -// -// m := thread.mutex() -// defer m.close() -// m.lock() ... m.unlock() one thread at a time in between -// atomic_add(addr(count), 1) (built in) numbers changed by many threads at once -// -// What a thread is given is shared, not copied: don't let it outlive the block it comes from, and -// change it only under a lock (or with atomic_...). -import "finch_thread.h" - -struct Thread { - ptr handle -} - -struct Mutex { - ptr handle -} - -// start fn(arg) on a new thread; fn is addr(f) of a fn f(ptr arg) -fn spawn(ptr fn_, ptr arg) -> Thread! { - h := finch_thread_spawn(fn_, arg) - if h == null { - return error("can't start a thread") - } - return Thread(handle: h) -} - -// wait until the thread has finished -fn Thread.join() { - if self.handle != null { - finch_thread_join(self.handle) - self.handle = null - } -} - -fn mutex() -> Mutex { - return Mutex(handle: finch_mutex_new()) -} - -fn Mutex.lock() { - finch_mutex_lock(self.handle) -} - -fn Mutex.unlock() { - finch_mutex_unlock(self.handle) -} - -fn Mutex.close() { - finch_mutex_free(self.handle) - self.handle = null -} - -// how many threads the computer runs at the same time -fn cpus() -> int { - return int(finch_cpu_count()) -} diff --git a/tests/cross.sh b/tests/cross.sh index 8c03d2b..c1ea693 100755 --- a/tests/cross.sh +++ b/tests/cross.sh @@ -12,8 +12,7 @@ target=${1:?usage: tests/cross.sh windows|arm64} FINCH=${FINCH:-../build/finch} export WINEDEBUG=-all case $target in - windows) runner=wine; ext=.exe - wine cmd /c exit > /dev/null 2>&1 || true ;; # the first run sets Wine up and says so: not in a test's output + windows) runner=wine; ext=.exe ;; arm64) runner=qemu-aarch64; ext= ;; *) echo "unknown target $target"; exit 2 ;; esac @@ -34,8 +33,7 @@ for f in run/*.fch new/*.fch; do failed=$((failed + 1)); echo "FAIL (build) $f"; head -5 "$tmp/err"; continue fi got=$(run_exe "$exe" "$input") - # (warnings come from the build, which is not part of what the program prints) - if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out" | grep -v ': warning: ')" ]; then + if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out")" ]; then pass=$((pass + 1)) else failed=$((failed + 1)); echo "FAIL $f"; diff <(echo "$got") "${f%.fch}.out" | head -10 diff --git a/tests/lsp_test.py b/tests/lsp_test.py index ef579a0..25ead42 100755 --- a/tests/lsp_test.py +++ b/tests/lsp_test.py @@ -84,8 +84,7 @@ init = c.request("initialize", {"processId": None, "rootUri": None, "capabilitie check("initialize", init["capabilities"]["hoverProvider"] is True) c.notify("initialized", {}) c.notify("textDocument/didOpen", {"textDocument": {"uri": uri, "languageId": "finch", "version": 1, "text": src}}) -first = c.diagnostics(uri) -check("no errors in a good file", first == [], first) +check("no errors in a good file", c.diagnostics(uri) == []) h = c.request("textDocument/hover", {"textDocument": td, "position": pos(12, "total")}) check("hover: variable type", h and "int total" in h["contents"]["value"], h) @@ -125,12 +124,6 @@ syms = c.request("textDocument/documentSymbol", {"textDocument": td}) names = sorted(s["name"] for s in syms) check("document symbols", names == ["Point", "add", "main"], names) -messy = "fn main() {\n x := 1 \n\n\n\tprint(x)\n}" -c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 5}, "contentChanges": [{"text": messy}]}) -fm = c.request("textDocument/formatting", {"textDocument": td, "options": {"tabSize": 4, "insertSpaces": True}}) -check("formatting", fm and fm[0]["newText"] == "fn main() {\n x := 1\n\n print(x)\n}\n", fm) -c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 5}, "contentChanges": [{"text": src}]}) - bad = src.replace("total := add(p.x, p.y)", 'total := add(p.x, "y")') c.notify("textDocument/didChange", {"textDocument": {"uri": uri, "version": 6}, "contentChanges": [{"text": bad}]}) diags = c.diagnostics(uri) @@ -193,33 +186,6 @@ c.notify("textDocument/didChange", {"textDocument": {"uri": uri2, "version": 4}, diags = c.diagnostics(uri2) check("error: unhandled failure", diags and "can fail" in diags[0]["message"], diags) -# a big struct copied on every call: a warning (yellow), not an error -src4 = "struct V {\n" + "".join(" float f%d\n" % i for i in range(40)) + "}\n\nfn len2(V v) -> float {\n return v.f0\n}\n\nfn main() {\n print(len2(V()))\n}\n" -path4 = os.path.join(d, "big.fch") -open(path4, "w").write(src4) -uri4 = pathlib.Path(path4).resolve().as_uri() -c.notify("textDocument/didOpen", {"textDocument": {"uri": uri4, "languageId": "finch", "version": 1, "text": src4}}) -w4 = c.diagnostics(uri4) -check("warning for a big copy", w4 and len(w4) == 1 and w4[0]["severity"] == 2 and "copied on every call" in w4[0]["message"] and w4[0]["range"]["start"]["line"] == 43, w4) - -# a module imported under another name: completion after its name -src3 = "import path as pt\n\nfn main() {\n print(pt.base(\"a/b\"))\n}\n" -path3 = os.path.join(d, "alias.fch") -open(path3, "w").write(src3) -uri3 = pathlib.Path(path3).resolve().as_uri() -c.notify("textDocument/didOpen", {"textDocument": {"uri": uri3, "languageId": "finch", "version": 1, "text": src3}}) -check("import as: compiles", c.diagnostics(uri3) == []) -e3 = src3.replace(" print(", " pt.\n print(") -c.notify("textDocument/didChange", {"textDocument": {"uri": uri3, "version": 2}, "contentChanges": [{"text": e3}]}) -comp = c.request("textDocument/completion", {"textDocument": {"uri": uri3}, "position": {"line": 3, "character": 7}}) -labels = [i["label"] for i in comp["items"]] -ok3 = "base" in labels and "join" in labels -if not ok3: # show what the compiler's index says about the module, to see where the names differ - import subprocess - idx = subprocess.run([FINCH, "index", path3], capture_output=True, text=True).stdout - labels = [l for l in idx.splitlines() if l.startswith("@I") or (l.startswith("@F") and "base" in l)][:6] -check("completion: a module under another name", ok3, labels) - c.request("shutdown", None) c.notify("exit", None) c.p.wait(timeout=5) diff --git a/tests/new/auto_addr.fch b/tests/new/auto_addr.fch deleted file mode 100644 index ad3a68e..0000000 --- a/tests/new/auto_addr.fch +++ /dev/null @@ -1,23 +0,0 @@ -struct Body { - float x - float y -} - -fn push(ptr[Body] b, float dx) { - b.x += dx -} - -fn Body.moved(float dx) -> Body { - return Body(x: self.x + dx, y: self.y) -} - -fn main() { - b := Body(x: 1.0, y: 2.0) - push(b, 0.5) // the same as push(addr(b), 0.5) - push(addr(b), 0.5) - print(b.x) - p := addr(b) - push(p, 1.0) // already a ptr: as it is - print(b.x) - print(b.moved(1.0).x) -} diff --git a/tests/new/auto_addr.out b/tests/new/auto_addr.out deleted file mode 100644 index dcf37cd..0000000 --- a/tests/new/auto_addr.out +++ /dev/null @@ -1,3 +0,0 @@ -2 -3 -4 diff --git a/tests/new/big_copies.fch b/tests/new/big_copies.fch deleted file mode 100644 index f4a3eb0..0000000 --- a/tests/new/big_copies.fch +++ /dev/null @@ -1,52 +0,0 @@ -// a struct of 256 bytes or more copied without the code saying so: a warning (the program still builds) -struct M4 { - float a0 - float a1 - float a2 - float a3 - float b0 - float b1 - float b2 - float b3 - float c0 - float c1 - float c2 - float c3 - float d0 - float d1 - float d2 - float d3 -} - -struct Body { - M4 world - M4 local - int id -} - -fn weight(Body b) -> float { // warned: 264 bytes copied on every call - return b.world.a0 + float(b.id) -} - -fn nudge(ptr[Body] b) { // shared: no warning - b.world.a0 += 1.0 -} - -fn small(M4 m) -> float { // 128 bytes: no warning - return m.a0 -} - -fn main() { - b := Body(id: 2) - nudge(b) - print(weight(b), small(b.world)) - bodies := [b, b] - total := 0.0 - for x in bodies { // warned: each element copied into x - total += x.world.a0 - } - for i in 0..bodies.len { // in place: no warning - total += bodies[i].world.a0 - } - print(total) -} diff --git a/tests/new/big_copies.out b/tests/new/big_copies.out deleted file mode 100644 index 78da95f..0000000 --- a/tests/new/big_copies.out +++ /dev/null @@ -1,4 +0,0 @@ -new/big_copies.fch:27:11: warning: 'b' is a Body (264 bytes), copied on every call to weight; take ptr[Body] to share it instead (callers can still pass the variable as it is) -new/big_copies.fch:45:5: warning: each Body (264 bytes) is copied into 'x'; for i in 0..bodies.len with bodies[i] works on it in place -3 1 -4 diff --git a/tests/new/c_unions_macros.fch b/tests/new/c_unions_macros.fch deleted file mode 100644 index 30f93c7..0000000 --- a/tests/new/c_unions_macros.fch +++ /dev/null @@ -1,30 +0,0 @@ -// C unions (SDL_Event-style) and #defines that are expressions, strings or casts -import "cunion.h" -import "stdlib.h" -link "cunion.c" - -fn main() { - print(EV_MOTION, EV_KEY, FLAG_BIG, FLAGS_BOTH, HALF, GREETING, LONG_NAME) - u64 flags = FLAG_BIG | FLAG_GL - u32 small = EV_KEY - print(flags, small) - - Event e - if poll_event(addr(e)) != 0 && e.type == EV_MOTION { - print("motion", e.motion.x, e.motion.y) - } - k := Event(key: Key(type: EV_KEY, key: 32)) - print(k.type == EV_KEY, k.key.key) - []Event queue - queue.push(e) - queue.push(k) - print(queue[1].key.key, queue.len) - - Tagged t - t.as.f = 0.5 - print(t.as.f) - print(abs(-2.5), abs(-3), min(EV_KEY, 7)) // Finch's abs, not C's int-only one - bg := RAYWHITE // #define RAYWHITE (Color){ 245, 245, 245, 255 } - print(bg.r, bg.a, DARK.g, DARK.a, HALF_UP.x, HALF_UP.y) - print(twice(21)) // static inline in the header -} diff --git a/tests/new/c_unions_macros.out b/tests/new/c_unions_macros.out deleted file mode 100644 index 512e303..0000000 --- a/tests/new/c_unions_macros.out +++ /dev/null @@ -1,9 +0,0 @@ -1024 1025 1099511627776 1099511627778 0.5 hi there ab -1099511627778 1025 -motion 1.5 -2 -true 32 -32 2 -0.5 -2.5 3 7 -245 255 20 0 0.5 -1 -42 diff --git a/tests/new/cunion.c b/tests/new/cunion.c deleted file mode 100644 index 5170d36..0000000 --- a/tests/new/cunion.c +++ /dev/null @@ -1,7 +0,0 @@ -#include "cunion.h" -int poll_event(Event *e) { - e->motion.type = EV_MOTION; - e->motion.x = 1.5f; - e->motion.y = -2.0f; - return 1; -} diff --git a/tests/new/cunion.h b/tests/new/cunion.h deleted file mode 100644 index d478445..0000000 --- a/tests/new/cunion.h +++ /dev/null @@ -1,41 +0,0 @@ -// C unions and #define constants that are expressions (tests/new/c_unions_macros.fch) -#include - -typedef struct { uint32_t type; float x, y; } Motion; -typedef struct { uint32_t type; int32_t key; } Key; -typedef union { - uint32_t type; - Motion motion; - Key key; - uint8_t padding[64]; -} Event; - -typedef struct { int kind; union { int i; float f; } as; } Tagged; - -#define EV_MOTION 0x400u -#define EV_KEY (EV_MOTION + 1) -#define FLAG_BIG ((uint64_t)1 << 40) -#define FLAG_GL 0x2ULL -#define FLAGS_BOTH (FLAG_BIG | FLAG_GL) -#define HALF (1.0 / 2) -#define GREETING "hi\tthere" -#define LONG_NAME "a" "b" -#define NOT_A_CONSTANT do { } while (0) - -typedef struct { unsigned char r, g, b, a; } Color; -typedef struct { float x, y; } Vec2; -#define CLITERAL(type) (type) -#define RAYWHITE CLITERAL(Color){ 245, 245, 245, 255 } -#define DARK (Color){ 20, 20, 20 } -#define HALF_UP (Vec2){ 0.5f, -1 } -#define NOT_A_STRUCT_CONST (Color){ 1, 2, (unsigned char)(long)&poll_event, 4 } - -static inline int fill_key(Event *e, int key) { - e->key.type = EV_KEY; - e->key.key = key; - return 1; -} -int poll_event(Event *e); - -static inline int twice(int x) { return 2 * x; } -static inline int sum_all(int n, ...) { return n; } diff --git a/tests/new/defaults_numbers.fch b/tests/new/defaults_numbers.fch deleted file mode 100644 index e4b7d06..0000000 --- a/tests/new/defaults_numbers.fch +++ /dev/null @@ -1,43 +0,0 @@ -// struct defaults apply inside structs too; number literals and float text - -struct Audio { - int rate = 48000 - float volume = 0.8 -} - -struct Mixer { - Audio audio - []Audio extra - int voices = 5 -} - -struct Box[T] { - T item - Audio audio -} - -fn make() -> Mixer { - Mixer m - return m -} - -fn scale(f32 x) -> f32 { - return x * 2.0 + 1 -} - -fn main() { - a := Mixer() - Mixer b - c := Mixer(voices: 2) - list := [Mixer()] - m := ["x": Mixer()] - map[str]Mixer counts - counts["new"].voices += 1 - bx := Box[int]() - print(a.audio.rate, b.audio.volume, c.audio.rate, c.voices, list[0].audio.rate, m["x"].audio.rate) - print(make().audio.rate, counts["new"].audio.rate, counts["new"].voices, bx.audio.rate) - - print(1e3, 2.5e-3, 1E+6, 1_000.5e1, scale(1.5)) - print(1.0 / 3.0, 0.1 + 0.2, 100000000.0, 1e20, 1e-7, float(7), f32(0.1)) - print(str(3.14159, 2), str(2, 1), str(f32(0.1)), "x=" + str(0.25), [0.5, 1.25]) -} diff --git a/tests/new/defaults_numbers.out b/tests/new/defaults_numbers.out deleted file mode 100644 index ebe7250..0000000 --- a/tests/new/defaults_numbers.out +++ /dev/null @@ -1,5 +0,0 @@ -48000 0.8 48000 2 48000 48000 -48000 48000 6 48000 -1000 0.0025 1000000 10005 4 -0.3333333333333333 0.30000000000000004 100000000 1e+20 1e-07 7 0.1 -3.14 2.0 0.1 x=0.25 [0.5, 1.25] diff --git a/tests/new/exit_or.fch b/tests/new/exit_or.fch deleted file mode 100644 index 79b342b..0000000 --- a/tests/new/exit_or.fch +++ /dev/null @@ -1,12 +0,0 @@ -// exit(...) ends an or-block, like return -fn load(str p) -> int! { - return error("no such file") -} - -fn main() { - n := load("a.txt") or { - print("stopping:", err) - exit(0) - } - print("never", n) -} diff --git a/tests/new/exit_or.out b/tests/new/exit_or.out deleted file mode 100644 index 6b37a2b..0000000 --- a/tests/new/exit_or.out +++ /dev/null @@ -1 +0,0 @@ -stopping: no such file diff --git a/tests/new/fail/after_exit.fch b/tests/new/fail/after_exit.fch deleted file mode 100644 index 89b6c8a..0000000 --- a/tests/new/fail/after_exit.fch +++ /dev/null @@ -1,5 +0,0 @@ -// expect: this code can never run (it comes after return, break, continue or exit) -fn main() { - exit(1) - print("x") -} diff --git a/tests/new/fail/auto_addr_value.fch b/tests/new/fail/auto_addr_value.fch deleted file mode 100644 index a925084..0000000 --- a/tests/new/fail/auto_addr_value.fch +++ /dev/null @@ -1,13 +0,0 @@ -// expect: argument 'b' must be ptr[Body], but this is Body -struct Body { - float x -} -fn push(ptr[Body] b) { - b.x += 1.0 -} -fn make() -> Body { - return Body(x: 1.0) -} -fn main() { - push(make()) -} diff --git a/tests/new/fail/const_assign.fch b/tests/new/fail/const_assign.fch deleted file mode 100644 index 50bfb71..0000000 --- a/tests/new/fail/const_assign.fch +++ /dev/null @@ -1,5 +0,0 @@ -// expect: is a constant; it can't be changed -const W = 3 -fn main() { - W = 4 -} diff --git a/tests/new/fail/const_call.fch b/tests/new/fail/const_call.fch deleted file mode 100644 index 336d6eb..0000000 --- a/tests/new/fail/const_call.fch +++ /dev/null @@ -1,6 +0,0 @@ -// expect: a constant is made of numbers, text -fn f() -> int { return 1 } -const W = f() -fn main() { - print(W) -} diff --git a/tests/new/fail/const_loop.fch b/tests/new/fail/const_loop.fch deleted file mode 100644 index de8965e..0000000 --- a/tests/new/fail/const_loop.fch +++ /dev/null @@ -1,6 +0,0 @@ -// expect: is worked out from itself -const A = B + 1 -const B = A -fn main() { - print(A) -} diff --git a/tests/new/fail/ifvalue_else.fch b/tests/new/fail/ifvalue_else.fch deleted file mode 100644 index e91fc59..0000000 --- a/tests/new/fail/ifvalue_else.fch +++ /dev/null @@ -1,4 +0,0 @@ -// expect: an if that gives a value needs an else -fn main() { - x := if true { 1 } -} diff --git a/tests/new/fail/ifvalue_types.fch b/tests/new/fail/ifvalue_types.fch deleted file mode 100644 index b23e893..0000000 --- a/tests/new/fail/ifvalue_types.fch +++ /dev/null @@ -1,4 +0,0 @@ -// expect: both branches of the if have to give the same type -fn main() { - x := if true { 1 } else { "one" } -} diff --git a/tests/new/fail/import_clash.fch b/tests/new/fail/import_clash.fch deleted file mode 100644 index 46e6e49..0000000 --- a/tests/new/fail/import_clash.fch +++ /dev/null @@ -1,5 +0,0 @@ -// expect: is already used by another import -import path -import time as path -fn main() { -} diff --git a/tests/new/fail/longstr_indent.fch b/tests/new/fail/longstr_indent.fch deleted file mode 100644 index 82b5a0a..0000000 --- a/tests/new/fail/longstr_indent.fch +++ /dev/null @@ -1,6 +0,0 @@ -// expect: is indented less than the closing -fn main() { - s := """ - a - """ -} diff --git a/tests/new/fail/union_two_fields.fch b/tests/new/fail/union_two_fields.fch deleted file mode 100644 index 4000878..0000000 --- a/tests/new/fail/union_two_fields.fch +++ /dev/null @@ -1,5 +0,0 @@ -// expect: Event is a C union: give one field by name -import "../cunion.h" -fn main() { - e := Event(1, 2) -} diff --git a/tests/new/interpolation.fch b/tests/new/interpolation.fch deleted file mode 100644 index a95efc8..0000000 --- a/tests/new/interpolation.fch +++ /dev/null @@ -1,21 +0,0 @@ -// "\(x)" in text -struct P { - int x - float y -} - -fn main() { - name := "Ada" - hp := 7 - p := P(x: 3, y: 0.25) - print("hi \(name), hp \(hp), next \(hp + 1)") - print("p = (\(p.x), \(p.y, 2)) \("nested \(name)")") - map[str]int m - m["k"] = 5 - print("\(m["k"]) \(str(1.0 / 3.0, 3))") - print("no code here: \\(x) and \t tab") - long := """ - speed \(p.y * 4.0) - """ - print(long) -} diff --git a/tests/new/interpolation.out b/tests/new/interpolation.out deleted file mode 100644 index d1dbb2e..0000000 --- a/tests/new/interpolation.out +++ /dev/null @@ -1,5 +0,0 @@ -hi Ada, hp 7, next 8 -p = (3, 0.25) nested Ada -5 0.333 -no code here: \(x) and tab -speed 1 diff --git a/tests/new/lang_additions.fch b/tests/new/lang_additions.fch deleted file mode 100644 index d4462b7..0000000 --- a/tests/new/lang_additions.fch +++ /dev/null @@ -1,33 +0,0 @@ -// const, if as a value, min/max/abs/clamp, import a/b and import x as y, """ text -import ui/button -import path as p -import ui/button as b2 - -const W = 640 -const H = W / 2 -const f32 G = 9.8 -const TITLE = "Game " + "One" -const NEG = -W - -fn main() { - print(W, H, G, TITLE, NEG, button.SIZE) - bt := button.make("ok") - button.Button other - print(bt.label, bt.w, other.w, b2.make("x").w) - print(p.base("/a/b.txt")) - x := 7 - v := if x > 5 { "big" } else if x > 2 { "mid" } else { "small" } - f32 s = if x > 5 { 1.5 } else { 0.5 } - print(v, s, if x == 7 { x * 2 } else { 0 }) - print(min(3, 9), max(2.5, 1), abs(-4), abs(-2.5), clamp(15, 0, 10), clamp(-3, 0, 10), min(5, 2, 8)) - f32 sp = 2.0 - print(clamp(sp * 3.0, 0.0, 5.0), max(u8(3), u8(200))) - shader := """ - #version 330 - void main() { - gl_Position = vec4(0.0); - } - """ - print(shader) - print("""one "quoted" line""") -} diff --git a/tests/new/lang_additions.out b/tests/new/lang_additions.out deleted file mode 100644 index 9f67b4f..0000000 --- a/tests/new/lang_additions.out +++ /dev/null @@ -1,11 +0,0 @@ -640 320 9.8 Game One -640 3 -ok 30 30 30 -b.txt -big 1.5 14 -3 2.5 4 2.5 10 0 2 -5 200 -#version 330 -void main() { - gl_Position = vec4(0.0); -} -one "quoted" line diff --git a/tests/new/modules_by_file.fch b/tests/new/modules_by_file.fch deleted file mode 100644 index 4d85cef..0000000 --- a/tests/new/modules_by_file.fch +++ /dev/null @@ -1,10 +0,0 @@ -// a module is its file: ui/menu's "import button" is this file's "import ui/button", -// and ui/util.fch and util.fch are two different modules -import ui/button -import ui/menu -import util - -fn main() { - button.Button b = menu.first() - print(b.label, b.w, menu.whoami(), util.who()) -} diff --git a/tests/new/modules_by_file.out b/tests/new/modules_by_file.out deleted file mode 100644 index e39e691..0000000 --- a/tests/new/modules_by_file.out +++ /dev/null @@ -1 +0,0 @@ -menu 30 ui/util util diff --git a/tests/new/std_fs.fch b/tests/new/std_fs.fch deleted file mode 100644 index 40eb2ff..0000000 --- a/tests/new/std_fs.fch +++ /dev/null @@ -1,23 +0,0 @@ -// the fs module: bytes with NUL in them, folders, listing -import fs - -fn main() -> ! { - root := "fs_test_dir" - try fs.mkdir(root + "/a/b") - print(fs.exists(root), fs.is_dir(root + "/a/b"), fs.is_dir("nope")) - data := [u8(0), u8(255), u8(13), u8(10), u8(65)] - try fs.write_bytes(root + "/a/blob.bin", data) - back := try fs.read_bytes(root + "/a/blob.bin") - print(back.len, back[0], back[1], back[4], str(back) == str(data)) - write_file(root + "/z.txt", "z") - print(try fs.list(root)) - print(try fs.list(root + "/a")) - print(fs.list("no_such_dir") or ["(none)"]) - delete_file(root + "/a/blob.bin") - delete_file(root + "/z.txt") - try fs.remove_dir(root + "/a/b") - try fs.remove_dir(root + "/a") - try fs.remove_dir(root) - print(fs.exists(root)) - fs.remove_dir(root) or { print("gone:", err) } -} diff --git a/tests/new/std_fs.out b/tests/new/std_fs.out deleted file mode 100644 index 84c5f28..0000000 --- a/tests/new/std_fs.out +++ /dev/null @@ -1,7 +0,0 @@ -true true false -5 0 255 65 true -["a", "z.txt"] -["b", "blob.bin"] -["(none)"] -false -gone: can't remove the folder "fs_test_dir" (is it empty?) diff --git a/tests/new/threads.fch b/tests/new/threads.fch deleted file mode 100644 index b57ad8f..0000000 --- a/tests/new/threads.fch +++ /dev/null @@ -1,55 +0,0 @@ -import thread - -struct Shared { - []int seen -} - -struct Job { - ptr[int] counter - int adds - ptr[thread.Mutex] lock - ptr[Shared] common - int id -} - -fn work(ptr arg) { - ptr[Job] job = arg - for i in 0..job.adds { - atomic_add(job.counter, 1) - } - job.lock.lock() - job.common.seen.push(job.id) - job.lock.unlock() -} - -fn main() { - count := 0 - m := thread.mutex() - defer m.close() - both := Shared() - []Job jobs - for i in 0..4 { - jobs.push(Job(counter: addr(count), adds: 100000, lock: addr(m), common: addr(both), id: i)) - } - []thread.Thread ts - for i in 0..4 { - ts.push(thread.spawn(addr(work), addr(jobs[i])) or { - print("can't start") - exit(1) - }) - } - for i in 0..4 { - ts[i].join() - } - print(atomic_load(addr(count))) - print(both.seen.len) - print(thread.cpus() > 0) - - u32 flags = 5 - print(atomic_swap(addr(flags), 9)) - print(atomic_cas(addr(flags), 9, 1)) - print(atomic_cas(addr(flags), 9, 2)) - atomic_store(addr(flags), 7) - print(atomic_sub(addr(flags), 2)) - print(flags) -} diff --git a/tests/new/threads.out b/tests/new/threads.out deleted file mode 100644 index f5e1201..0000000 --- a/tests/new/threads.out +++ /dev/null @@ -1,8 +0,0 @@ -400000 -4 -true -5 -true -false -7 -5 diff --git a/tests/new/ui/button.fch b/tests/new/ui/button.fch deleted file mode 100644 index 46db929..0000000 --- a/tests/new/ui/button.fch +++ /dev/null @@ -1,7 +0,0 @@ -// a module in a folder, for tests/new/lang_additions.fch (import ui/button) -const SIZE = 3 -struct Button { - str label - int w = SIZE * 10 -} -fn make(str l) -> Button { return Button(label: l) } diff --git a/tests/new/ui/menu.fch b/tests/new/ui/menu.fch deleted file mode 100644 index 36005d4..0000000 --- a/tests/new/ui/menu.fch +++ /dev/null @@ -1,11 +0,0 @@ -// for tests/new/modules_by_file.fch: its sibling, imported without the folder -import button -import util - -fn first() -> button.Button { - return button.make("menu") -} - -fn whoami() -> str { - return util.who() -} diff --git a/tests/new/ui/util.fch b/tests/new/ui/util.fch deleted file mode 100644 index c8dba43..0000000 --- a/tests/new/ui/util.fch +++ /dev/null @@ -1,4 +0,0 @@ -// for tests/new/modules_by_file.fch: a util.fch in a folder -fn who() -> str { - return "ui/util" -} diff --git a/tests/new/util.fch b/tests/new/util.fch deleted file mode 100644 index 8b03465..0000000 --- a/tests/new/util.fch +++ /dev/null @@ -1,4 +0,0 @@ -// for tests/new/modules_by_file.fch: the top-level util.fch -fn who() -> str { - return "util" -} diff --git a/tests/run.sh b/tests/run.sh index fe172ff..c6176d5 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -56,30 +56,12 @@ for f in fail/*.fch new/fail/*.fch; do fi done -# the commands around the compiler: finch test, the chain of calls, finch fmt, a finch.toml project -check_tool() { # name, what it printed, the file with what it should print - if [ "$2" == "$(tr -d '\r' < "$3")" ]; then - pass=$((pass + 1)) - else - failed=$((failed + 1)) - echo "FAIL $1" - diff <(echo "$2") "$3" | head -20 - fi -} -check_tool "finch test" "$(cd tool && "$FINCH" test test_blocks.fch 2>&1 | tr -d '\r')" tool/test_blocks.out -check_tool "the chain of calls" "$(cd tool && "$FINCH" run trace.fch 2>&1 | tr -d '\r')" tool/trace.out -cp tool/fmt_in.fch "$tmp/fmt.fch" -"$FINCH" fmt "$tmp/fmt.fch" -check_tool "finch fmt" "$(cat "$tmp/fmt.fch")" tool/fmt_out.fch -check_tool "finch fmt (again)" "$("$FINCH" fmt --check tool/fmt_out.fch 2>&1)" /dev/null -check_tool "finch.toml" "$(cd tool/project && "$FINCH" run 2>&1 | tr -d '\r')" tool/project/expected.out - # the same programs with debug info (-g -O0): the metadata has to be valid for every kind of thing a program has for f in new/enums.fch new/generics.fch new/std_json.fch run/basics.fch run/methods_basic.fch run/maps.fch run/errors.fch \ run/defer.fch run/c_structs.fch run/memory.fch; do input="${f%.fch}.in" [ -f "$input" ] || input=/dev/null - if "$FINCH" build "$f" -g -O0 -o "$tmp/progg" > "$tmp/err" 2>&1 && [ "$("$tmp/progg" < "$input" 2>&1 | tr -d '\r')" == "$(tr -d '\r' < "${f%.fch}.out")" ]; then + if "$FINCH" build "$f" -g -O0 -o "$tmp/progg" > "$tmp/err" 2>&1 && [ "$("$tmp/progg" < "$input" 2>&1)" == "$(cat "${f%.fch}.out")" ]; then pass=$((pass + 1)) else failed=$((failed + 1)) @@ -88,12 +70,8 @@ for f in new/enums.fch new/generics.fch new/std_json.fch run/basics.fch run/meth fi done -# a library a C program loads (finch build --shared); the test's host uses dlopen and -rdynamic, so not on Windows -# (there a DLL can't call functions of the program that loads it without an import library) -case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) skip_shared=1 ;; *) skip_shared=0 ;; esac -if [ $skip_shared = 1 ]; then - : -elif FINCH="$FINCH" ./shared.sh > "$tmp/shared.out" 2>&1; then +# a library a C program loads (finch build --shared) +if FINCH="$FINCH" ./shared.sh > "$tmp/shared.out" 2>&1; then pass=$((pass + 1)) else failed=$((failed + 1)) diff --git a/tests/run/modules.out b/tests/run/modules.out index 572aef0..2886aed 100644 --- a/tests/run/modules.out +++ b/tests/run/modules.out @@ -1,5 +1,5 @@ 5 -(4, 5) of length 6.4031242374328485 +(4, 5) of length 6.40312 Vec(x: 0, y: 0) 10 Box(corner: Vec(x: 0, y: 0), size: Vec(x: 2, y: 5)) total 3 diff --git a/tests/tool/fmt_in.fch b/tests/tool/fmt_in.fch deleted file mode 100644 index 05d9628..0000000 --- a/tests/tool/fmt_in.fch +++ /dev/null @@ -1,24 +0,0 @@ -struct P { -int x - int y -} - - - -fn main() { - p := P(x: 1, - y: 2) - msg := """ - a { - b - """ - if p.x > 0 { -print("{ \(p.x) }") // } not counted - } else { - print(msg) - } - s := "a" + - "b" - print(s) -} - diff --git a/tests/tool/fmt_out.fch b/tests/tool/fmt_out.fch deleted file mode 100644 index 34e6850..0000000 --- a/tests/tool/fmt_out.fch +++ /dev/null @@ -1,21 +0,0 @@ -struct P { - int x - int y -} - -fn main() { - p := P(x: 1, - y: 2) - msg := """ - a { - b - """ - if p.x > 0 { - print("{ \(p.x) }") // } not counted - } else { - print(msg) - } - s := "a" + - "b" - print(s) -} diff --git a/tests/tool/project/expected.out b/tests/tool/project/expected.out deleted file mode 100644 index d9a45b2..0000000 --- a/tests/tool/project/expected.out +++ /dev/null @@ -1 +0,0 @@ -hello world 42 diff --git a/tests/tool/project/finch.toml b/tests/tool/project/finch.toml deleted file mode 100644 index 4b6e207..0000000 --- a/tests/tool/project/finch.toml +++ /dev/null @@ -1,14 +0,0 @@ -# a project: finch run / build / test here need no file and no flags -name = "mygame" -entry = "src/main.fch" -paths = ["src"] -include = ["native"] -links = [ - "native/glue.cpp", # C++, built with the C++ compiler -] -cflags = ["-DGLUE_BASE=40"] - -[target.linux] -links = ["m"] -[target.windows] -links = ["ws2_32"] diff --git a/tests/tool/project/native/glue.cpp b/tests/tool/project/native/glue.cpp deleted file mode 100644 index e4323a7..0000000 --- a/tests/tool/project/native/glue.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// C++ behind a C header: extern "C" gives the function a name C (and Finch) can call -#include "glue.h" -#include -#include - -extern "C" int glue_answer(void) { - std::vector v = {1, 1}; - return GLUE_BASE + std::accumulate(v.begin(), v.end(), 0); -} diff --git a/tests/tool/project/native/glue.h b/tests/tool/project/native/glue.h deleted file mode 100644 index dc207ab..0000000 --- a/tests/tool/project/native/glue.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifdef __cplusplus -extern "C" { -#endif -int glue_answer(void); -#ifdef __cplusplus -} -#endif diff --git a/tests/tool/project/src/game/world.fch b/tests/tool/project/src/game/world.fch deleted file mode 100644 index d184de1..0000000 --- a/tests/tool/project/src/game/world.fch +++ /dev/null @@ -1,3 +0,0 @@ -fn hello() -> str { - return "world" -} diff --git a/tests/tool/project/src/main.fch b/tests/tool/project/src/main.fch deleted file mode 100644 index 18b0f03..0000000 --- a/tests/tool/project/src/main.fch +++ /dev/null @@ -1,6 +0,0 @@ -import "glue.h" -import game/world - -fn main() { - print("hello \(world.hello()) \(glue_answer())") -} diff --git a/tests/tool/test_blocks.fch b/tests/tool/test_blocks.fch deleted file mode 100644 index c908c17..0000000 --- a/tests/tool/test_blocks.fch +++ /dev/null @@ -1,30 +0,0 @@ -// finch test: each test "..." block runs on its own; a failed assert or a runtime error fails only that one -fn add(int a, int b) -> int { - return a + b -} - -fn second([]int xs) -> int { - return xs[1] -} - -test "add works" { - assert(add(2, 3) == 5) - assert(add(-1, 1) == 0, "zero") -} - -test "a failing assert" { - x := add(1, 2) - assert(x == 4, "x is \(x), not 4") -} - -test "a runtime error fails only this test" { - print(second([1])) -} - -test "still runs after the others" { - assert("\(1.5, 2) and \(add(1, 1))" == "1.50 and 2") -} - -fn main() { - print("main is not run by finch test") -} diff --git a/tests/tool/test_blocks.out b/tests/tool/test_blocks.out deleted file mode 100644 index f948ba9..0000000 --- a/tests/tool/test_blocks.out +++ /dev/null @@ -1,8 +0,0 @@ -ok add works -FAIL a failing assert - test_blocks.fch:17: runtime error: assert failed: x is 3, not 4 -FAIL a runtime error fails only this test - test_blocks.fch:7: runtime error: index 1 is out of range (the length is 1) -ok still runs after the others - -2 passed, 2 failed diff --git a/tests/tool/trace.fch b/tests/tool/trace.fch deleted file mode 100644 index 278e11a..0000000 --- a/tests/tool/trace.fch +++ /dev/null @@ -1,16 +0,0 @@ -struct Enemy { - []int path -} - -fn Enemy.next(int i) -> int { - return self.path[i] -} - -fn step(Enemy e) -> int { - return e.next(3) -} - -fn main() { - e := Enemy(path: [1, 2]) - print(step(e)) -} diff --git a/tests/tool/trace.out b/tests/tool/trace.out deleted file mode 100644 index 8d0417c..0000000 --- a/tests/tool/trace.out +++ /dev/null @@ -1,4 +0,0 @@ -trace.fch:6: runtime error: index 3 is out of range (the length is 2) - in Enemy.next (trace.fch:6) - in step (trace.fch:10) - in main (trace.fch:15)