Compare commits

..
Author SHA1 Message Date
Nexoniarz
a4bdeb5712 CI: package the macOS and Windows builds again (finch-macos-arm64.tar.gz, finch-windows-x64.zip)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 12:22:56 +01:00
Nexoniarz
d557b5505e Warnings for big copies: a struct with 256+ bytes of plain data as a parameter or in for x in list
- on stderr (finch ir stays the IR), as yellow warnings in editors (@W in the index), once per place
- str, arrays and maps inside a struct don't count: passing it copies only their handles
- none in the compiler, the examples or the tests other than the new one
- docs: the warning instead of "not planned"; what was and wasn't checked for Android

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 12:15:22 +01:00
Nexoniarz
961ba951ec Windows: the language server reads the compiler's \r\n lines (completion after an import's other name); Wine is set up before the cross tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 11:58:24 +01:00
Nexoniarz
ff72f68804 static inline C functions (mingw's printf and time), macOS libraries that call their host, lld's missing-library message
- a static inline function in a header is called through a pointer a small C file keeps
  (void *finch_inl_f = &f), built with the program's include folders and cached
- --shared on macOS links with -undefined dynamic_lookup, as a .so does on Linux
- "unable to find library -lx" (lld, MinGW) gives the library-not-found advice
- the language server test shows the index records when alias completion fails

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 11:53:24 +01:00
Nexoniarz
72e96a3f71 CI: find clang-c/Index.h and libclang where Ubuntu and Homebrew keep them; the language server on Windows paths
CI has failed since the embed merge (2026-10-25):
- Linux: bootstrap/build.sh asks llvm-config for the folder with clang-c/Index.h
- macOS: FINCH_LIBCLANG's -L folders also go to the compiler's own link "clang"
- Windows: the language server turns file:///C:/x into C:/x and back (diagnostics went to /C:/x);
  the -g tests compare without \r; shared.sh (dlopen, -rdynamic) is not run there

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 11:42:18 +01:00
Nexoniarz
bb9702aeba Finch 2.5.0: docs (EN, PL), Kate and VS Code for the new features
- cheat sheets, technicians' and engineers' guides: "\(x)", test blocks, finch fmt, finch.toml,
  the build cache, the chain of calls, threads and atomics, C++/Rust/Android, struct #defines,
  passing structs, why there is no warning for big copies
- Kate (version 4) and VS Code: code inside text, test, min/max/abs/clamp/assert/atomic_...
- finch.toml's name is used only for its entry; other files of the project keep their own name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 11:34:41 +01:00
Nexoniarz
d428498efd finch.toml, threads and atomics, finch fmt, C++ files, struct #defines, a struct variable given for its ptr
- finch.toml: name, entry, paths, include, links, libdirs, cflags, ldflags, and [target.windows] etc.;
  finch run / build / test with no file use its entry
- import thread: spawn, join, mutex, cpus; atomic_add/sub/load/store/swap/cas built in
- finch fmt [--check]: indentation by { }, trailing spaces, empty lines; also as LSP formatting
- link "x.cpp" (or in finch.toml): built with the C++ compiler, which then links (FINCH_CXX)
- #define RAYWHITE (Color){ 245, 245, 245, 255 } imports as a Color
- push(body) where push takes ptr[Body]: the same as push(addr(body))
- --target android (NDK clang from ANDROID_NDK_HOME, -llog)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 11:17:10 +01:00
Nexoniarz
c830c780fe "\(x)" in text, test blocks with assert and finch test, a build cache, the chain of calls on runtime errors
- "x is \(x)", "\(speed, 2)" (two digits after the point), also in """ text
- test "name" { ... } blocks; assert(cond) and assert(cond, why); finch test runs each on its own
- the program's object and its C files (link "x.c") are kept in the cache until they or their headers change
- runtime errors show the calls that led there (finch run, finch test, -g, -O0, --trace)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 10:59:30 +01:00
Nexoniarz
3ba2dcd5d6 Modules are known by their file: import button from ui/menu.fch and import ui/button are one module, two util.fch are two; min/max/abs/clamp win over C's int-only abs; completion after an import's other name; hover docs; engineers' guides
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 09:52:04 +01:00
Nexoniarz
17863ecf7b docs, Kate and VS Code: const, if as a value, """ text, 1e9, str(x, 2), min/max/abs/clamp, import a/b and as, fs, C unions and macros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 09:40:06 +01:00
Nexoniarz
f2d6480749 const, if as a value, """ text over several lines, min/max/abs/clamp, import a/b and import x as y, the fs module
- const NAME = value (numbers, text, other constants; typed: const f32 G = 9.8), also module.NAME
- x := if c { a } else { b }
- """ ... """: the closing quotes' indentation goes from every line
- min, max, abs, clamp for any numbers, unless your own function has the name
- import ui/button (a module in a folder), import path as p
- fs: read_bytes, write_bytes, exists, is_dir, mkdir (with parents), list, remove_dir, data_dir(app)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
2026-10-29 09:37:41 +01:00
Nexoniarz
d0666cf788 C import: unions (SDL_Event) and their fields; #defines that are expressions, casts or text; typedef'd structs no function mentions; FLAG_A | FLAG_B fits a u64
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:23:29 +01:00
Nexoniarz
6142e47e71 Struct defaults apply inside structs; exit() ends an or-block; 1e9 literals; x * 2.0 stays f32; floats print as the shortest exact text; str(x, digits)
Also frees main's args and the message of a failing main (valgrind was not clean on two tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:14:58 +01:00
89 changed files with 4576 additions and 205 deletions

View file

@ -89,6 +89,14 @@ 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
@ -116,6 +124,16 @@ 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

View file

@ -90,7 +90,7 @@ Pick the guide that fits you:
| | |
|---|---|
| Variables | `x := 5` (inferred) or `int x = 5` |
| Variables | `x := 5` (inferred) or `int x = 5`; `const W = 640` |
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]` |
| 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,15 +98,17 @@ 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 | `"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` |
| Text | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Control | `if` / `else if` / `else` (also as a value), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures |
| 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)`; the standard library: `json`, `path`, `time`, `proc` |
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks |
| 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 |
| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Tools | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` for gdb/lldb, `-O0` |
| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors |
## Examples
@ -146,14 +148,17 @@ 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`
- [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)
## 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
std/ the standard library, in Finch: json, path, time, proc, fs
runtime/ the small C runtime linked into every program
bootstrap/ build.sh and the seeds: the compiler's own LLVM IR, one per system
examples/ example programs

View file

@ -90,7 +90,7 @@ Wybierz przewodnik dla siebie:
| | |
|---|---|
| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` |
| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5`; `const W = 640` |
| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]` |
| Funkcje | `fn add(int a, int b) -> int { return a + b }` |
| Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
@ -98,15 +98,17 @@ 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 | `"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` |
| Tekst | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Sterowanie | `if` / `else if` / `else` (też jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie |
| Enumy, match | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (także jako wartość) |
| Generyki | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` |
| Moduły | `import ksztalty` → `ksztalty.pole(p)`; biblioteka standardowa: `json`, `path`, `time`, `proc` |
| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków |
| 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 |
| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Narzędzia | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` dla gdb/lldb, `-O0` |
| Narzędzia | `finch run/build/test/fmt/ir/lsp`, `finch.toml` dla projektu, cache budowania, `--target windows/arm64/macos/android`, `-g` dla gdb/lldb, `-O0`, łańcuch wywołań przy błędach w czasie działania |
## Przykłady
@ -146,14 +148,17 @@ 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`
- [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)
## 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
std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs
runtime/ mały runtime w C dołączany do każdego programu
bootstrap/ build.sh i ziarna: własny LLVM IR kompilatora, po jednym na system
examples/ przykładowe programy

View file

@ -27,8 +27,20 @@ case $seed in windows-*) EXE=.exe ;; esac
CLANG=${FINCH_CLANG:-clang}
LIBS=${FINCH_LIBCLANG:--lclang}
INC=
[ -z "${FINCH_LLVM_INCLUDE:-}" ] || INC="-I $FINCH_LLVM_INCLUDE" # where clang-c/Index.h is, if clang does not find it itself
# 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"
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

View file

@ -13,15 +13,36 @@ 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` / triple | build for another system |
| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | build for another system (`android`: the NDK's clang from `ANDROID_NDK_HOME`, usually with `--shared`) |
| `-g` | debug info (gdb, lldb, 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` / `CC` (the C compiler that links).
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"]
```
---
@ -53,9 +74,10 @@ 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 |
| `int x` | zero / `""` / `[]` / `null` / the struct's defaults (also of the structs inside it) |
| `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) |
---
@ -64,10 +86,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` |
| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` |
| `bool` | `true`, `false` |
| `char` | `'A'`, `'\n'` |
| `str` | `"text\t\"quoted\""` |
| `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) |
| `i8 i16 i32 i64` | signed, sized |
| `u8 u16 u32 u64` | unsigned, sized |
| `f32 f64` | floats |
@ -99,6 +121,7 @@ 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)
@ -224,6 +247,7 @@ 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 |
@ -234,7 +258,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) |
| `s.ptr` | `ptr[char]` (for C; a `str` also goes to a `const char *` parameter as it is) |
| `==` `!=` `<` `>` | compare |
---
@ -250,7 +274,8 @@ 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 |
| `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([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 |
@ -273,9 +298,12 @@ 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 |
| `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) |
| `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 |
@ -286,7 +314,7 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa
| Finch | C |
|---|---|
| `ptr[int] p` | `int *p` |
| `addr(x)` | `&x` |
| `addr(x)` | `&x` (a struct variable given where `ptr[ThatStruct]` goes: `push(body)` = `push(addr(body))`) |
| `p.value` | `*p` |
| `p.field` | `p->field` |
| `p[i]` | `p[i]` |
@ -299,6 +327,7 @@ 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`, …).
@ -311,6 +340,8 @@ 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()
```
---
@ -321,7 +352,7 @@ shapes.Box other // in declarations
|---|---|
| `import "x.h"` | functions, structs, enums, `#define` numbers, globals |
| `link "name"` | libname.so / name.lib (pkg-config first) |
| `link "file.c"` | compile your own C file |
| `link "file.c"`, `link "glue.cpp"` | compile your own C or C++ file (C++: functions in `extern "C"`) |
| `addr(myFn)` | pass a Finch function as a callback |
| C type | Finch |
@ -334,6 +365,14 @@ shapes.Box other // in declarations
| `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).
---
@ -348,6 +387,26 @@ shapes.Box other // in declarations
| `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)")
}
```
---
@ -397,9 +456,12 @@ 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`
`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`)
(and `self` inside methods)

View file

@ -76,7 +76,7 @@ To check it works, type:
./build/finch version
```
You should see something like `Finch 2.4.0`.
You should see something like `Finch 2.5.0`.
---

View file

@ -1,6 +1,6 @@
# Finch for Engineers
**The internals of Finch 2.4, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*,
**The internals of Finch 2.5, whose compiler is written in Finch.** How source becomes tokens, an AST and LLVM IR *as text*,
which `clang` compiles and links with a small C runtime; how C headers are read with libclang from Finch itself and how
the C calling conventions are reproduced by hand; the type and ownership rules; how the compiler builds itself from a
seed; and the language server.
@ -69,18 +69,20 @@ Design decisions that shape the rest:
| File | Lines | Responsibility |
|---|---|---|
| `src/lexer.fch` | 220 | Tokens, positions, indentation-free layout |
| `src/lexer.fch` | 470 | Tokens, positions, `"""` text, `"\(x)"` turned into `("…" + str(x) + "…")` tokens |
| `src/ast.fch` | 120 | One `Node` struct with a `kind` string, children, type names, positions (`line`, `endLine`) |
| `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/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/platform.fch` | 90 | `Target`: triple, data layout, C compiler, signedness of `char` per system |
| `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/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/docs.fch` | 110 | One description of the built-ins, shared by hover and completion |
| `std/*.fch` | 900 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`) |
| `runtime/finch_rt.c` | 760 | The C runtime |
| `std/*.fch` | 1040 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`), `fs`, `thread` |
| `runtime/finch_rt.c` | 1150 | The C runtime: text, arrays, maps, files, float printing, fs, threads, the test runner, the call chain |
| `bootstrap/` | | `build.sh`, `install.sh`, the seeds (§10) |
---
@ -98,6 +100,18 @@ 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 `_<os>` 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
@ -138,6 +152,14 @@ 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
@ -146,7 +168,7 @@ The same code is what lets the compiler call libclang, whose API passes `CXCurso
`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|<triple>` changes the triple and data layout, the C `char` signedness, the ABI rules
`--target windows|linux|arm64|macos|android|<triple>` 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.
@ -156,6 +178,38 @@ 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/<hash of main file + target + flags>.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/<hash of source + compiler + flags + target>.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<pid>` 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.<os>]` and `[target.<os>-<cpu>]`. 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/<host>/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_<n>`, 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
@ -201,8 +255,9 @@ 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; 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`.
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`.
### Explicit conversions: `convert()`
@ -419,6 +474,48 @@ 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 <line>, 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 <size>`, 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.
@ -428,4 +525,6 @@ test (22 checks).
- 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.

View file

@ -37,6 +37,10 @@ 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)
---
@ -137,17 +141,31 @@ kept in `~/.cache/finch`. `FINCH_CC` names a different compiler to link with; `F
```
finch run <file.fch> [args...] compile and run right away
finch build <file.fch> [-o name] compile into a program (default name: the file's name)
finch test <file.fch> run its test "..." { } blocks (section 27)
finch fmt [--check] [files/dirs] tidy the indentation of .fch files (all under here by default)
finch ir <file.fch> print the LLVM IR (for the curious)
finch lsp the language server, for editors
finch version show the version
options:
-l <lib> link a C library, same as link "lib" in the file
--target <name> build for another system: windows, linux, arm64, macos, or an LLVM triple
-I <dir> look for C headers there too
--target <name> build for another system: windows, linux, arm64, macos, android, 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
@ -214,7 +232,19 @@ 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 defer`
**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`.
---
@ -316,6 +346,13 @@ 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 {
...
@ -484,8 +521,29 @@ 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.
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`).
---
@ -642,6 +700,8 @@ 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.
---
@ -742,8 +802,10 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete
### What does not work yet
- 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.
- 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.
- `long double`, 128-bit integers.
---
@ -823,6 +885,19 @@ 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.
---
@ -902,13 +977,14 @@ 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
├── std/ the standard library, in Finch: json, path, time, proc, fs, thread
├── runtime/ finch_rt.c: the small runtime (text, arrays, maps, input, files)
├── bootstrap/ build.sh, install.sh and the seeds (the compiler's own IR, one per system)
├── examples/ hello, tour, types, structs, todo, guess, 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
@ -1035,6 +1111,11 @@ 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
@ -1052,7 +1133,165 @@ Not there yet:
- Interfaces.
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
- Threads.
- Windows on ARM64, 32-bit systems, WebAssembly.
- 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.
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.

View file

@ -1,6 +1,6 @@
# Finch dla inżynierów
**Wnętrze Fincha 2.4, którego kompilator jest napisany w Finchu.** Jak kod źródłowy staje się tokenami, drzewem AST i
**Wnętrze Fincha 2.5, którego kompilator jest napisany w Finchu.** Jak kod źródłowy staje się tokenami, drzewem AST i
LLVM IR *jako tekstem*, który `clang` kompiluje i linkuje z małym runtime'em w C; jak nagłówki C czyta libclang wywoływany
z samego Fincha i jak ręcznie odtwarzane są konwencje wywołań C; reguły typów i własności; jak kompilator buduje sam siebie
z ziarna; oraz serwer języka.
@ -68,18 +68,20 @@ Decyzje projektowe:
| Plik | Linie | Zadanie |
|---|---|---|
| `src/lexer.fch` | 220 | Tokeny i pozycje |
| `src/lexer.fch` | 470 | Tokeny i pozycje, tekst `"""`, `"\(x)"` zamienione na tokeny `("…" + str(x) + "…")` |
| `src/ast.fch` | 120 | Jedna struktura `Node` z napisem `kind`, dziećmi, nazwami typów, pozycjami (`line`, `endLine`) |
| `src/parser.fch` | 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/parser.fch` | 910 | Zejście rekurencyjne: deklaracje, instrukcje, wyrażenia, generyki (`[T]`), `enum`, `match`, `export`, `link`, `const`, `test "…" { }` |
| `src/gen.fch` | 6450 | Kontrola typów + IR: struktury, funkcje, metody, zakresy i zwolnienia, instrukcje, wyrażenia, wbudowane, mapy, błędy jako wartości, generyki (monomorfizacja), enumy i `match`, metadane `-g`, tryb `index` |
| `src/cimport.fch` | 790 | libclang: funkcje, struktury i unie (pola, offsety, wypełnienie), enumy, stałe, globalne, makra (także będące strukturą) |
| `src/platform.fch` | 90 | `Target`: triple, układ danych, kompilator C, znak `char` zależnie od systemu |
| `src/linker.fch` | 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/linker.fch` | 430 | Pamięć podręczna (runtime, obiekt programu, pliki C/C++), biblioteki (`link`, pkg-config), podpowiedzi przy błędach linkera |
| `src/main.fch` | 515 | Sterownik: CLI, loader z wyszukiwaniem modułów i nakładkami, `build/run/test/fmt/ir/index/lsp` |
| `src/manifest.fch` | 240 | `finch.toml`: mały czytnik TOML, listy zależne od celu |
| `src/fmt.fch` | 225 | `finch fmt` i formatowanie w LSP |
| `src/lsp.fch` | 910 | Serwer języka (§11) |
| `src/docs.fch` | 110 | Jeden opis wbudowanych funkcji dla podpowiedzi i uzupełniania |
| `std/*.fch` | 900 | `json`, `path`, `time` (+`_windows`), `proc` (+`_windows`) |
| `runtime/finch_rt.c` | 760 | Runtime w C |
| `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ń |
| `bootstrap/` | | `build.sh`, `install.sh`, ziarna (§10) |
---
@ -97,6 +99,18 @@ 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 `_<system>`, 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
@ -130,6 +144,14 @@ 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ść.
---
@ -139,7 +161,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|<triple>` zmienia triple i układ danych, znak `char`, reguły ABI powyżej, wariant OS
`--target windows|linux|arm64|macos|android|<triple>` 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.
@ -149,6 +171,38 @@ 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/<skrót pliku głównego + celu + flag>.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/<skrót źródła + kompilatora + flag + celu>.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<pid>` 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.<os>]` oraz `[target.<os>-<cpu>]`. Ś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/<host>/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_<n>`, 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
@ -194,9 +248,10 @@ 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; 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`.
Strona-literał dopasowuje się do drugiej (także literał float obok `f32`: `x * 2.0` zostaje `f32`); w przeciwnym razie
rośnie strona, która się rozszerza; `i32` + `u32` to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa;
f32 + f64 → f64. Nie ma promocji całkowitych z C: `u8 + u8` zostaje `u8`. Dwa literały całkowite składają się w literał,
więc `FLAG_A | FLAG_B` dalej mieści się w `u64` albo `u32`.
### Konwersje jawne: `convert()`
@ -412,6 +467,48 @@ 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 <linia>, 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 <rozmiar>`, 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.
@ -421,4 +518,6 @@ serwer podpowiada dalej z ostatniej wersji, która się skompilowała. Test akce
- 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.

View file

@ -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.4.0`.
Powinieneś zobaczyć coś w stylu `finch 2.5.0`.
---

View file

@ -37,6 +37,10 @@ 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)
---
@ -138,17 +142,31 @@ katalogi, w których szukać modułów.
```
finch run <plik.fch> [argumenty...] skompiluj i od razu uruchom
finch build <plik.fch> [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku)
finch test <plik.fch> 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 <plik.fch> pokaż LLVM IR (dla ciekawskich)
finch lsp serwer języka, dla edytorów
finch version pokaż wersję
opcje:
-l <lib> dolinkuj bibliotekę C, to samo co link "lib" w pliku
--target <nazwa> zbuduj na inny system: windows, linux, arm64, macos albo triple LLVM
-I <folder> szukaj nagłówków C także tam
--target <nazwa> zbuduj na inny system: windows, linux, arm64, macos, android 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
@ -214,7 +232,19 @@ 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 defer`
**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`.
---
@ -316,6 +346,13 @@ 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 {
...
@ -486,8 +523,29 @@ 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.
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`).
---
@ -644,6 +702,8 @@ 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.
---
@ -744,8 +804,10 @@ Oba działają bezpośrednio; `examples/opengl.fch` i `examples/vulkan.fch` to k
### Co jeszcze nie działa
- **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ą.
- **Unie** C i struktury z **polami bitowymi** przekazywane do funkcji C albo zwracane z niej **przez wartość**
(wskaźniki do nich działają, unie jako zmienne też: `SDL_Event e`, `e.type`, `e.key.key`, `SDL_PollEvent(addr(e))`).
- **Makra-funkcje** (`#define MAX(a, b) ...`). Makra będące liczbą, tekstem, wyrażeniem z nich
(`(1u << 3)`, `((Uint64)1)`, `(A | B)`) albo strukturą z liczb (`RAYWHITE` z raylib, `(Color){ 245, 245, 245, 255 }`) działają.
- `long double`, liczby 128-bitowe.
---
@ -826,6 +888,19 @@ 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.
---
@ -906,13 +981,14 @@ 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
├── std/ biblioteka standardowa, w Finchu: json, path, time, proc, fs, thread
├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, mapy, wejście, pliki)
├── bootstrap/ build.sh, install.sh i ziarna (własny IR kompilatora, po jednym na system)
├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm
├── tests/
│ ├── run/ new/ programy + dokładny wynik (.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
@ -1039,6 +1115,11 @@ 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
@ -1056,7 +1137,167 @@ Jeszcze nie ma:
- Interfejsów.
- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII.
- Wątków.
- Windowsa na ARM64, systemów 32-bitowych, WebAssembly.
- 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.
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.

View file

@ -12,15 +12,37 @@ 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` / triple | zbuduj dla innego systemu |
| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | zbuduj dla innego systemu (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) |
| `-g` | informacje dla debuggera (gdb, lldb, Visual Studio) |
| `-O0` | bez optymalizacji |
| `--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` / `CC` (kompilator C, który linkuje).
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"]
```
---
@ -52,9 +74,10 @@ 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 |
| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (też struktur w niej) |
| `x = 7` | przypisanie |
| `x += 1` `-=` `*=` `/=` `%=` | zmiana |
| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; `ksztalty.W` z modułu) |
---
@ -63,10 +86,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` |
| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` |
| `bool` | `true` (prawda), `false` (fałsz) |
| `char` | `'A'`, `'\n'` |
| `str` | `"tekst\t\"w cudzysłowie\""` |
| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykających cudzysłowów znika) |
| `i8 i16 i32 i64` | ze znakiem, z rozmiarem |
| `u8 u16 u32 u64` | bez znaku, z rozmiarem |
| `f32 f64` | zmiennoprzecinkowe |
@ -98,6 +121,7 @@ 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)
@ -223,6 +247,7 @@ 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 |
@ -233,7 +258,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) |
| `s.ptr` | `ptr[char]` (dla C; `str` trafia też do parametru `const char *` bez zmian) |
| `==` `!=` `<` `>` | porównanie |
---
@ -249,7 +274,8 @@ 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 |
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (float: najkrótszy tekst, który jest tą samą liczbą) |
| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po kropce) |
| `str([1, 2])`, `str(m)`, `str(punkt)` | ten sam tekst, który pokazuje `print` |
| `int("x") or 0`, `float(s) or 0.0` | liczba albo wartość zapasowa |
| `str(bajty)` | `[]u8` → tekst |
@ -272,9 +298,12 @@ 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 |
| `exit(kod)` | zakończ program od razu (kończy też blok `or { }`) |
| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, od, do)` | dla dowolnych liczb (twoja funkcja o tej nazwie wygrywa) |
| `error(komunikat)` | `return error("…")` w funkcji `-> T!` |
| `addr(x)` | wskaźnik na x; `addr(fn)` = 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ą |
@ -285,7 +314,7 @@ Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza → większa liczba, dowo
| Finch | C |
|---|---|
| `ptr[int] p` | `int *p` |
| `addr(x)` | `&x` |
| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie idzie `ptr[TaStruktura]`: `pchnij(cialo)` = `pchnij(addr(cialo))`) |
| `p.value` | `*p` |
| `p.pole` | `p->pole` |
| `p[i]` | `p[i]` |
@ -298,6 +327,7 @@ 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`, …).
@ -310,6 +340,8 @@ 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()
```
---
@ -320,7 +352,7 @@ ksztalty.Pudlo inne // w deklaracjach
|---|---|
| `import "x.h"` | funkcje, struktury, enumy, liczbowe `#define`, zmienne globalne |
| `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pkg-config) |
| `link "plik.c"` | skompiluj własny plik C |
| `link "plik.c"`, `link "glue.cpp"` | skompiluj własny plik C albo C++ (C++: funkcje w `extern "C"`) |
| `addr(mojaFn)` | funkcja Fincha jako callback |
| Typ C | Finch |
@ -333,6 +365,14 @@ ksztalty.Pudlo inne // w deklaracjach
| `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).
---
@ -347,6 +387,26 @@ ksztalty.Pudlo inne // w deklaracjach
| `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)")
}
```
---
@ -396,6 +456,8 @@ 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
@ -416,5 +478,9 @@ 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 |

View file

@ -5,7 +5,7 @@
Install: copy to ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml
(or run editors/kate/install.sh), then restart Kate.
-->
<language name="Finch" version="2" kateversion="5.0" section="Sources" extensions="*.fch"
<language name="Finch" version="4" kateversion="5.0" section="Sources" extensions="*.fch"
mimetype="text/x-finch" indenter="cstyle" author="The Finch authors" license="Apache-2.0">
<highlighting>
<list name="keywords">
@ -13,6 +13,7 @@
<item>struct</item>
<item>enum</item>
<item>export</item>
<item>const</item>
</list>
<list name="control">
<item>if</item>
@ -70,6 +71,17 @@
<item>new</item>
<item>free</item>
<item>error</item>
<item>min</item>
<item>max</item>
<item>abs</item>
<item>clamp</item>
<item>assert</item>
<item>atomic_add</item>
<item>atomic_sub</item>
<item>atomic_load</item>
<item>atomic_store</item>
<item>atomic_swap</item>
<item>atomic_cas</item>
</list>
<contexts>
@ -79,10 +91,13 @@
<Detect2Chars char="/" char1="*" attribute="Comment" context="BlockComment" beginRegion="Comment"/>
<DetectChar char="{" attribute="Symbol" beginRegion="Brace"/>
<DetectChar char="}" attribute="Symbol" endRegion="Brace"/>
<StringDetect String="&quot;&quot;&quot;" attribute="String" context="LongString" beginRegion="LongString"/>
<DetectChar char="&quot;" attribute="String" context="String"/>
<DetectChar char="'" attribute="Char" context="Char"/>
<WordDetect String="fn" attribute="Keyword" context="FunctionName"/>
<RegExpr String="\btest(?=\s*&quot;)" attribute="Keyword"/>
<RegExpr String="(?&lt;=\s)as(?=\s+[A-Za-z_])" attribute="Import"/>
<WordDetect String="struct" attribute="Keyword" context="StructName"/>
<keyword String="control" attribute="Control Flow"/>
<keyword String="imports" attribute="Import"/>
@ -91,7 +106,8 @@
<keyword String="builtins" attribute="Built-in"/>
<RegExpr String="\b0[xX][0-9a-fA-F_]+\b" attribute="Hex"/>
<RegExpr String="\b[0-9][0-9_]*\.[0-9][0-9_]*([eE][+-]?[0-9]+)?\b" attribute="Float"/>
<RegExpr String="\b[0-9][0-9_]*(\.[0-9][0-9_]*)?[eE][+-]?[0-9]+\b" attribute="Float"/>
<RegExpr String="\b[0-9][0-9_]*\.[0-9][0-9_]*\b" attribute="Float"/>
<RegExpr String="\b[0-9][0-9_]*\b" attribute="Decimal"/>
<!-- a call: name( ; method or field after a dot -->
@ -131,11 +147,30 @@
</context>
<context name="String" attribute="String" lineEndContext="#pop">
<RegExpr String="\\\(" attribute="Escape" context="Interpolation"/>
<RegExpr String="\\[ntr0\\&quot;']" attribute="Escape"/>
<RegExpr String="\\." attribute="Error"/>
<DetectChar char="&quot;" attribute="String" context="#pop"/>
</context>
<context name="LongString" attribute="String" lineEndContext="#stay">
<RegExpr String="\\\(" attribute="Escape" context="Interpolation"/>
<RegExpr String="\\[ntr0\\&quot;']" attribute="Escape"/>
<StringDetect String="&quot;&quot;&quot;" attribute="String" context="#pop" endRegion="LongString"/>
</context>
<!-- "hp \(hp + 1)": code inside text, up to the ) that closes \( -->
<context name="Interpolation" attribute="Normal Text" lineEndContext="#stay">
<DetectChar char=")" attribute="Escape" context="#pop"/>
<DetectChar char="(" attribute="Symbol" context="InterpolationParen"/>
<IncludeRules context="Normal"/>
</context>
<context name="InterpolationParen" attribute="Normal Text" lineEndContext="#stay">
<DetectChar char=")" attribute="Symbol" context="#pop"/>
<DetectChar char="(" attribute="Symbol" context="InterpolationParen"/>
<IncludeRules context="Normal"/>
</context>
<context name="Char" attribute="Char" lineEndContext="#pop">
<RegExpr String="\\[ntr0\\&quot;']" attribute="Escape"/>
<DetectChar char="'" attribute="Char" context="#pop"/>

View file

@ -1,5 +1,12 @@
# 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(...)`.

View file

@ -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.4.0",
"version": "2.5.0",
"publisher": "nexoniarz",
"license": "Apache-2.0",
"icon": "images/icon.png",

View file

@ -103,9 +103,16 @@
},
"statements": {
"patterns": [
{
"name": "keyword.other.test.finch",
"match": "\\btest(?=\\s*\")"
},
{
"include": "#comments"
},
{
"include": "#longstrings"
},
{
"include": "#strings"
},
@ -125,7 +132,7 @@
},
{
"name": "storage.type.struct.finch",
"match": "\\b(struct|enum)\\b"
"match": "\\b(struct|enum|const)\\b"
},
{
"name": "storage.modifier.finch",
@ -144,7 +151,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)\\s*(?=\\()",
"match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas)\\s*(?=\\()",
"captures": {
"1": {
"name": "support.function.builtin.finch"
@ -214,6 +221,9 @@
"begin": "\"",
"end": "\"|$",
"patterns": [
{
"include": "#interpolation"
},
{
"name": "constant.character.escape.finch",
"match": "\\\\[ntr0\\\\\"']"
@ -243,13 +253,62 @@
},
{
"name": "constant.numeric.float.finch",
"match": "\\b\\d[\\d_]*\\.\\d[\\d_]*\\b"
"match": "\\b\\d[\\d_]*(\\.\\d[\\d_]*([eE][+-]?\\d+)?|[eE][+-]?\\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"
}
]
}
}
}

View file

@ -26,6 +26,9 @@ 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'],
@ -43,6 +46,9 @@ 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 = [];

View file

@ -12,12 +12,24 @@
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <direct.h>
#include <io.h>
#else
#include <pthread.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>
#endif
@ -43,10 +55,38 @@ 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);
}
@ -74,6 +114,50 @@ 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) {
@ -153,12 +237,50 @@ 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 = snprintf(buf, sizeof buf, "%g", v);
int n = fmt_float(buf, sizeof buf, v, 0);
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"); }
@ -242,6 +364,7 @@ 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) {
@ -399,7 +522,14 @@ 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) { buf_fmt(b, "%g", 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_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);
@ -498,6 +628,12 @@ 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);
@ -608,6 +744,255 @@ 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.

View file

@ -114,11 +114,14 @@ struct Node {
struct Program {
str module
str path
[]str imports // Finch modules
[]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 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
}

View file

@ -18,14 +18,29 @@ 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 simple #define numbers
struct CConst { // enum values and #define constants
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 {
@ -44,6 +59,7 @@ 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 {
@ -75,6 +91,7 @@ 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 {
@ -169,6 +186,7 @@ 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))
@ -194,9 +212,10 @@ 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
@ -293,9 +312,6 @@ 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")
@ -366,19 +382,93 @@ fn parseInt(str lit) -> Number {
return Number(c: CConst(i: n), ok: true)
}
// Simple #defines: a single number, maybe negative, maybe in parentheses.
fn macroValue(ptr[Importer] imp, CXCursor c) -> Number {
// 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 {
ptr[CXToken] tokens = null
u32 n = 0
clang_tokenize(imp.tu, clang_getCursorExtent(c), addr(tokens), addr(n))
[]str parts
out := MacroTokens(safe: true)
allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ",", "{", "}"]
words := ["sizeof", "unsigned", "signed", "int", "long", "short", "char", "float", "double", "const", "_Bool"]
depth := 0
for k in 1..int(n) { // token 0 is the macro's name
if clang_getTokenKind(tokens[k]) == CXToken_Comment {
kind := clang_getTokenKind(tokens[k])
if kind == CXToken_Comment {
continue
}
parts.push(text(clang_getTokenSpelling(imp.tu, tokens[k])))
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
}
}
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)
@ -438,6 +528,7 @@ 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) {
@ -445,9 +536,7 @@ fn visit(ptr[Importer] imp, CXCursor c) {
}
f := CFunc(name: name, header: imp.header)
if clang_Cursor_getStorageClass(c) == CX_SC_Static {
f.unsupported = "is a static inline C function (it has no code to link against)"
imp.out.fns[name] = f
return
f.inline = true // mingw's printf and time, header-only helpers
}
ft := clang_getCanonicalType(clang_getCursorType(c))
if ft.kind == CXType_FunctionNoProto {
@ -483,7 +572,7 @@ fn visit(ptr[Importer] imp, CXCursor c) {
if g.ok {
imp.out.globals[name] = g.ty
}
} else if kind == CXCursor_StructDecl {
} else if kind == CXCursor_StructDecl || kind == CXCursor_UnionDecl {
if clang_isCursorDefinition(c) != 0 && name.len > 0 && !name.contains(" ") {
usr := text(clang_getCursorUSR(c))
if !imp.recordNames.has(usr) {
@ -500,9 +589,17 @@ 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 && !imp.out.consts.has(name) {
if v.ok {
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
}
}
}
@ -563,7 +660,134 @@ 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
}

View file

@ -21,11 +21,21 @@ 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."),
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("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."),
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("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."),
@ -86,7 +96,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"]
"link", "struct", "enum", "match", "export", "defer", "or", "try", "self", "const"]
}
fn typeNames() -> []str {

File diff suppressed because one or more lines are too long

224
src/fmt.fch Normal file
View file

@ -0,0 +1,224 @@
// 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
}

File diff suppressed because it is too large Load diff

View file

@ -81,3 +81,18 @@ 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
}

View file

@ -102,3 +102,18 @@ 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
}

View file

@ -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"]
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export", "const"]
return keywords.contains(w)
}
@ -78,6 +78,216 @@ 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'
@ -99,7 +309,12 @@ fn escape(ptr[Lexer] lx, char e) -> char {
}
fn lex(str src, str path) -> []Token {
lexer := Lexer(src: src, path: path, i: 0, line: 1, col: 1, nl: true)
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)
lx := addr(lexer)
[]Token out
while lx.i < lx.src.len {
@ -144,6 +359,8 @@ fn lex(str src, str path) -> []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)
@ -161,8 +378,38 @@ fn lex(str src, str path) -> []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"

View file

@ -18,10 +18,169 @@ 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") {
@ -100,6 +259,8 @@ 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 '") {
@ -203,6 +364,9 @@ 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.
@ -222,57 +386,77 @@ 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"
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)
obj := irObject(clang, target + " " + b.opt + b.extra, b.ir, b.name + "\n" + tg.triple, log) or {
print(err)
host.remove(log)
return 1
}
cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
// 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)
}
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"
cmd = cmd + " -dynamiclib -undefined dynamic_lookup" // like a .so: the host's functions are found when it is loaded
} else {
cmd = cmd + " -shared"
}
}
[]str temps
for l in b.links {
if l.ends_with(".c") {
// the program's own C file, built along with it
o := b.out + "." + str(temps.len) + ".o"
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)
pic := " -fPIC"
if tg.windows {
pic = ""
}
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)
compiler := cc
if isCpp(l) {
compiler = cxxFor(cc)
}
o := cObject(l, compiler, " -O2" + pic + b.cflags, tg, log) or {
print(err)
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") {
} else if l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") {
cmd = cmd + " " + quote(l)
} else {
cmd = cmd + " " + libFlags(l, tg)
@ -280,11 +464,7 @@ 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

View file

@ -7,6 +7,7 @@
import json
import host
import docs
import fmt
import "stdio.h"
import "stdlib.h"
@ -80,7 +81,9 @@ 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 {
@ -124,13 +127,22 @@ fn uriToPath(str uri) -> str {
i += 1
}
}
return str(out)
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
}
fn pathToUri(str path) -> str {
digits := "0123456789ABCDEF"
[]str parts = ["file://"]
for c in path {
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 {
n := int(c)
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "/-_.~".contains(str(c)) {
parts.push(str(c))
@ -295,7 +307,7 @@ fn parseDiag(str line) -> Diag {
fn parseOutput(str output) -> Analysis {
a := Analysis()
for line in output.split("\n") {
for line in output.replace("\r", "").split("\n") { // Windows ends the compiler's lines with \r\n
if line.starts_with("@") {
f := splitTabs(line)
tag := f[0]
@ -313,8 +325,15 @@ 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)
@ -348,6 +367,7 @@ 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
@ -382,6 +402,27 @@ 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)
@ -537,6 +578,7 @@ 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, ""))
@ -777,6 +819,7 @@ 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("."))
@ -831,6 +874,32 @@ 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

View file

@ -15,6 +15,8 @@ import linker
import lsp
import host
import embedded
import manifest
import fmt
import "stdlib.h"
fn dirOf(str path) -> str {
@ -28,6 +30,14 @@ 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("./") {
@ -47,6 +57,7 @@ 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
}
@ -89,6 +100,27 @@ 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 {
@ -104,12 +136,21 @@ fn load(ptr[Loader] l, str path, str module) {
prog := parser.parse(lexer.lex(src, path), file, path)
prog.module = module
prog.path = path
imports := prog.imports
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
l.progs.push(prog)
for name in imports {
if !l.loaded.contains(name) {
l.loaded.push(name)
load(l, findModule(l, path, name), name)
for i in 0..modules.len {
if !l.loaded.contains(modules[i]) {
l.loaded.push(modules[i])
load(l, files[i], modules[i])
}
}
}
@ -119,7 +160,10 @@ fn usage() {
print("usage:")
print(" finch run <file.fch> [args...] build and run a program")
print(" finch build <file.fch> [-o name] build a program")
print(" finch test <file.fch> run its test \"...\" { } blocks (and those of the modules it imports)")
print(" finch ir <file.fch> 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:")
@ -127,6 +171,8 @@ fn usage() {
print(" -l <lib> link a library (the same as link \"lib\" in the program)")
print(" -I <dir> 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 <name> build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple")
exit(1)
}
@ -148,26 +194,104 @@ 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.4.0 (the compiler is written in Finch)")
print("Finch 2.5.0 (the compiler is written in Finch)")
return 0
}
if args.len >= 2 && args[1] == "fmt" {
return format(args)
}
if args.len == 2 && args[1] == "lsp" {
return lsp.run(args[0]) // the language server, for editors
}
if args.len < 3 {
if args.len < 2 {
usage()
}
cmd := args[1]
file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" {
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" && cmd != "test" {
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 + " <file.fch>, 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
@ -175,7 +299,6 @@ 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
@ -203,6 +326,12 @@ 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
@ -213,8 +342,11 @@ fn main([]str args) -> int {
}
ai += 1
}
if !traceGiven && (debug || opt == "-O0") {
trace = true
}
tg := platform.select(targetName)
if cmd == "run" {
if cmd == "run" || cmd == "test" {
out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe
} else if shared && !outGiven {
if tg.windows {
@ -245,6 +377,48 @@ 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)
@ -269,7 +443,7 @@ fn main([]str args) -> int {
}
}
for l in prog.links {
if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") {
if linker.isOwnCode(l) {
links.push(d + "/" + l) // the program's own C code, next to the .fch file
} else {
links.push(l)
@ -284,7 +458,13 @@ fn main([]str args) -> int {
print(err)
return 1
}
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg))
[]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))
if cmd == "ir" || cmd == "index" {
print(ir)
return 0
@ -314,10 +494,15 @@ fn main([]str args) -> int {
if debug {
extra = extra + " -g" // or clang drops the debug info that is in the IR
}
if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links), tg, ci) != 0 {
[]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 {
return 1
}
if cmd == "run" {
if cmd == "run" || cmd == "test" {
[]str quoted
for a in progArgs {
quoted.push(linker.quote(a))

243
src/manifest.fch Normal file
View file

@ -0,0 +1,243 @@
// 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
}

View file

@ -236,18 +236,49 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
if !at(p, "ident") {
fail(p, t, "expected a module name or a C header after 'import'")
}
prog.imports.push(next(p).text)
// 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)
}
} 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") // your own C code, next to the .fch file
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
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)
@ -261,6 +292,24 @@ 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).
@ -433,7 +482,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' or 'import'")
expect(p, "fn", "'fn', 'struct', 'enum', 'const' 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(...)
@ -612,6 +661,33 @@ 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
@ -769,6 +845,9 @@ 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"

View file

@ -11,6 +11,7 @@ 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 "
@ -45,6 +46,8 @@ 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"
@ -60,6 +63,7 @@ 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")
@ -72,6 +76,8 @@ 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 {
@ -85,12 +91,30 @@ fn select(str name) -> Target {
}
if tg.cross && tg.windows {
tg.runner = "wine "
} else if tg.cross && tg.arm64 && !tg.darwin {
} else if tg.cross && tg.arm64 && !tg.darwin && !tg.android {
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

9
std/finch_fs.h Normal file
View file

@ -0,0 +1,9 @@
/* 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);

8
std/finch_thread.h Normal file
View file

@ -0,0 +1,8 @@
/* 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);

70
std/fs.fch Normal file
View file

@ -0,0 +1,70 @@
// 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/<app> on Linux, ~/Library/Application Support/<app> on macOS, %APPDATA%\<app> 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
}

65
std/thread.fch Normal file
View file

@ -0,0 +1,65 @@
// 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())
}

View file

@ -12,7 +12,8 @@ target=${1:?usage: tests/cross.sh windows|arm64}
FINCH=${FINCH:-../build/finch}
export WINEDEBUG=-all
case $target in
windows) runner=wine; ext=.exe ;;
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
arm64) runner=qemu-aarch64; ext= ;;
*) echo "unknown target $target"; exit 2 ;;
esac
@ -33,7 +34,8 @@ 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")
if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out")" ]; then
# (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
pass=$((pass + 1))
else
failed=$((failed + 1)); echo "FAIL $f"; diff <(echo "$got") "${f%.fch}.out" | head -10

View file

@ -84,7 +84,8 @@ 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}})
check("no errors in a good file", c.diagnostics(uri) == [])
first = c.diagnostics(uri)
check("no errors in a good file", first == [], first)
h = c.request("textDocument/hover", {"textDocument": td, "position": pos(12, "total")})
check("hover: variable type", h and "int total" in h["contents"]["value"], h)
@ -124,6 +125,12 @@ 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)
@ -186,6 +193,33 @@ 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)

23
tests/new/auto_addr.fch Normal file
View file

@ -0,0 +1,23 @@
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)
}

3
tests/new/auto_addr.out Normal file
View file

@ -0,0 +1,3 @@
2
3
4

52
tests/new/big_copies.fch Normal file
View file

@ -0,0 +1,52 @@
// 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)
}

4
tests/new/big_copies.out Normal file
View file

@ -0,0 +1,4 @@
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

View file

@ -0,0 +1,30 @@
// 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
}

View file

@ -0,0 +1,9 @@
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

7
tests/new/cunion.c Normal file
View file

@ -0,0 +1,7 @@
#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;
}

41
tests/new/cunion.h Normal file
View file

@ -0,0 +1,41 @@
// C unions and #define constants that are expressions (tests/new/c_unions_macros.fch)
#include <stdint.h>
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; }

View file

@ -0,0 +1,43 @@
// 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])
}

View file

@ -0,0 +1,5 @@
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]

12
tests/new/exit_or.fch Normal file
View file

@ -0,0 +1,12 @@
// 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)
}

1
tests/new/exit_or.out Normal file
View file

@ -0,0 +1 @@
stopping: no such file

View file

@ -0,0 +1,5 @@
// expect: this code can never run (it comes after return, break, continue or exit)
fn main() {
exit(1)
print("x")
}

View file

@ -0,0 +1,13 @@
// 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())
}

View file

@ -0,0 +1,5 @@
// expect: is a constant; it can't be changed
const W = 3
fn main() {
W = 4
}

View file

@ -0,0 +1,6 @@
// expect: a constant is made of numbers, text
fn f() -> int { return 1 }
const W = f()
fn main() {
print(W)
}

View file

@ -0,0 +1,6 @@
// expect: is worked out from itself
const A = B + 1
const B = A
fn main() {
print(A)
}

View file

@ -0,0 +1,4 @@
// expect: an if that gives a value needs an else
fn main() {
x := if true { 1 }
}

View file

@ -0,0 +1,4 @@
// expect: both branches of the if have to give the same type
fn main() {
x := if true { 1 } else { "one" }
}

View file

@ -0,0 +1,5 @@
// expect: is already used by another import
import path
import time as path
fn main() {
}

View file

@ -0,0 +1,6 @@
// expect: is indented less than the closing
fn main() {
s := """
a
"""
}

View file

@ -0,0 +1,5 @@
// expect: Event is a C union: give one field by name
import "../cunion.h"
fn main() {
e := Event(1, 2)
}

View file

@ -0,0 +1,21 @@
// "\(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)
}

View file

@ -0,0 +1,5 @@
hi Ada, hp 7, next 8
p = (3, 0.25) nested Ada
5 0.333
no code here: \(x) and tab
speed 1

View file

@ -0,0 +1,33 @@
// 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""")
}

View file

@ -0,0 +1,11 @@
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

View file

@ -0,0 +1,10 @@
// 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())
}

View file

@ -0,0 +1 @@
menu 30 ui/util util

23
tests/new/std_fs.fch Normal file
View file

@ -0,0 +1,23 @@
// 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) }
}

7
tests/new/std_fs.out Normal file
View file

@ -0,0 +1,7 @@
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?)

55
tests/new/threads.fch Normal file
View file

@ -0,0 +1,55 @@
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)
}

8
tests/new/threads.out Normal file
View file

@ -0,0 +1,8 @@
400000
4
true
5
true
false
7
5

7
tests/new/ui/button.fch Normal file
View file

@ -0,0 +1,7 @@
// 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) }

11
tests/new/ui/menu.fch Normal file
View file

@ -0,0 +1,11 @@
// 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()
}

4
tests/new/ui/util.fch Normal file
View file

@ -0,0 +1,4 @@
// for tests/new/modules_by_file.fch: a util.fch in a folder
fn who() -> str {
return "ui/util"
}

4
tests/new/util.fch Normal file
View file

@ -0,0 +1,4 @@
// for tests/new/modules_by_file.fch: the top-level util.fch
fn who() -> str {
return "util"
}

View file

@ -56,12 +56,30 @@ 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)" == "$(cat "${f%.fch}.out")" ]; then
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
pass=$((pass + 1))
else
failed=$((failed + 1))
@ -70,8 +88,12 @@ 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)
if FINCH="$FINCH" ./shared.sh > "$tmp/shared.out" 2>&1; then
# 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
pass=$((pass + 1))
else
failed=$((failed + 1))

View file

@ -1,5 +1,5 @@
5
(4, 5) of length 6.40312
(4, 5) of length 6.4031242374328485
Vec(x: 0, y: 0)
10 Box(corner: Vec(x: 0, y: 0), size: Vec(x: 2, y: 5))
total 3

24
tests/tool/fmt_in.fch Normal file
View file

@ -0,0 +1,24 @@
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)
}

21
tests/tool/fmt_out.fch Normal file
View file

@ -0,0 +1,21 @@
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)
}

View file

@ -0,0 +1 @@
hello world 42

View file

@ -0,0 +1,14 @@
# 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"]

View file

@ -0,0 +1,9 @@
// C++ behind a C header: extern "C" gives the function a name C (and Finch) can call
#include "glue.h"
#include <numeric>
#include <vector>
extern "C" int glue_answer(void) {
std::vector<int> v = {1, 1};
return GLUE_BASE + std::accumulate(v.begin(), v.end(), 0);
}

View file

@ -0,0 +1,7 @@
#ifdef __cplusplus
extern "C" {
#endif
int glue_answer(void);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,3 @@
fn hello() -> str {
return "world"
}

View file

@ -0,0 +1,6 @@
import "glue.h"
import game/world
fn main() {
print("hello \(world.hello()) \(glue_answer())")
}

View file

@ -0,0 +1,30 @@
// 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")
}

View file

@ -0,0 +1,8 @@
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

16
tests/tool/trace.fch Normal file
View file

@ -0,0 +1,16 @@
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))
}

4
tests/tool/trace.out Normal file
View file

@ -0,0 +1,4 @@
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)