mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4bdeb5712 | ||
|
|
d557b5505e | ||
|
|
961ba951ec | ||
|
|
ff72f68804 | ||
|
|
72e96a3f71 | ||
|
|
bb9702aeba | ||
|
|
d428498efd | ||
|
|
c830c780fe | ||
|
|
3ba2dcd5d6 | ||
|
|
17863ecf7b | ||
|
|
f2d6480749 | ||
|
|
d0666cf788 | ||
|
|
6142e47e71 |
89 changed files with 4576 additions and 205 deletions
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -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
|
||||
|
|
|
|||
21
README.pl.md
21
README.pl.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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=""""" attribute="String" context="LongString" beginRegion="LongString"/>
|
||||
<DetectChar char=""" attribute="String" context="String"/>
|
||||
<DetectChar char="'" attribute="Char" context="Char"/>
|
||||
|
||||
<WordDetect String="fn" attribute="Keyword" context="FunctionName"/>
|
||||
<RegExpr String="\btest(?=\s*")" attribute="Keyword"/>
|
||||
<RegExpr String="(?<=\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\\"']" attribute="Escape"/>
|
||||
<RegExpr String="\\." attribute="Error"/>
|
||||
<DetectChar char=""" attribute="String" context="#pop"/>
|
||||
</context>
|
||||
|
||||
<context name="LongString" attribute="String" lineEndContext="#stay">
|
||||
<RegExpr String="\\\(" attribute="Escape" context="Interpolation"/>
|
||||
<RegExpr String="\\[ntr0\\"']" attribute="Escape"/>
|
||||
<StringDetect String=""""" 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\\"']" attribute="Escape"/>
|
||||
<DetectChar char="'" attribute="Char" context="#pop"/>
|
||||
|
|
|
|||
|
|
@ -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(...)`.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
256
src/cimport.fch
256
src/cimport.fch
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
16
src/docs.fch
16
src/docs.fch
|
|
@ -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
224
src/fmt.fch
Normal 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
|
||||
}
|
||||
735
src/gen.fch
735
src/gen.fch
File diff suppressed because it is too large
Load diff
15
src/host.fch
15
src/host.fch
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
251
src/lexer.fch
251
src/lexer.fch
|
|
@ -24,7 +24,7 @@ fn isHex(char c) -> bool {
|
|||
|
||||
fn isKeyword(str w) -> bool {
|
||||
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false",
|
||||
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export"]
|
||||
"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"
|
||||
|
|
|
|||
232
src/linker.fch
232
src/linker.fch
|
|
@ -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
|
||||
|
|
|
|||
75
src/lsp.fch
75
src/lsp.fch
|
|
@ -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
|
||||
|
|
|
|||
215
src/main.fch
215
src/main.fch
|
|
@ -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
243
src/manifest.fch
Normal 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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
9
std/finch_fs.h
Normal 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
8
std/finch_thread.h
Normal 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
70
std/fs.fch
Normal 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
65
std/thread.fch
Normal 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())
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
23
tests/new/auto_addr.fch
Normal 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
3
tests/new/auto_addr.out
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
2
|
||||
3
|
||||
4
|
||||
52
tests/new/big_copies.fch
Normal file
52
tests/new/big_copies.fch
Normal 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
4
tests/new/big_copies.out
Normal 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
|
||||
30
tests/new/c_unions_macros.fch
Normal file
30
tests/new/c_unions_macros.fch
Normal 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
|
||||
}
|
||||
9
tests/new/c_unions_macros.out
Normal file
9
tests/new/c_unions_macros.out
Normal 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
7
tests/new/cunion.c
Normal 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
41
tests/new/cunion.h
Normal 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; }
|
||||
43
tests/new/defaults_numbers.fch
Normal file
43
tests/new/defaults_numbers.fch
Normal 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])
|
||||
}
|
||||
5
tests/new/defaults_numbers.out
Normal file
5
tests/new/defaults_numbers.out
Normal 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
12
tests/new/exit_or.fch
Normal 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
1
tests/new/exit_or.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
stopping: no such file
|
||||
5
tests/new/fail/after_exit.fch
Normal file
5
tests/new/fail/after_exit.fch
Normal 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")
|
||||
}
|
||||
13
tests/new/fail/auto_addr_value.fch
Normal file
13
tests/new/fail/auto_addr_value.fch
Normal 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())
|
||||
}
|
||||
5
tests/new/fail/const_assign.fch
Normal file
5
tests/new/fail/const_assign.fch
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// expect: is a constant; it can't be changed
|
||||
const W = 3
|
||||
fn main() {
|
||||
W = 4
|
||||
}
|
||||
6
tests/new/fail/const_call.fch
Normal file
6
tests/new/fail/const_call.fch
Normal 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)
|
||||
}
|
||||
6
tests/new/fail/const_loop.fch
Normal file
6
tests/new/fail/const_loop.fch
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// expect: is worked out from itself
|
||||
const A = B + 1
|
||||
const B = A
|
||||
fn main() {
|
||||
print(A)
|
||||
}
|
||||
4
tests/new/fail/ifvalue_else.fch
Normal file
4
tests/new/fail/ifvalue_else.fch
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// expect: an if that gives a value needs an else
|
||||
fn main() {
|
||||
x := if true { 1 }
|
||||
}
|
||||
4
tests/new/fail/ifvalue_types.fch
Normal file
4
tests/new/fail/ifvalue_types.fch
Normal 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" }
|
||||
}
|
||||
5
tests/new/fail/import_clash.fch
Normal file
5
tests/new/fail/import_clash.fch
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// expect: is already used by another import
|
||||
import path
|
||||
import time as path
|
||||
fn main() {
|
||||
}
|
||||
6
tests/new/fail/longstr_indent.fch
Normal file
6
tests/new/fail/longstr_indent.fch
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// expect: is indented less than the closing
|
||||
fn main() {
|
||||
s := """
|
||||
a
|
||||
"""
|
||||
}
|
||||
5
tests/new/fail/union_two_fields.fch
Normal file
5
tests/new/fail/union_two_fields.fch
Normal 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)
|
||||
}
|
||||
21
tests/new/interpolation.fch
Normal file
21
tests/new/interpolation.fch
Normal 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)
|
||||
}
|
||||
5
tests/new/interpolation.out
Normal file
5
tests/new/interpolation.out
Normal 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
|
||||
33
tests/new/lang_additions.fch
Normal file
33
tests/new/lang_additions.fch
Normal 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""")
|
||||
}
|
||||
11
tests/new/lang_additions.out
Normal file
11
tests/new/lang_additions.out
Normal 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
|
||||
10
tests/new/modules_by_file.fch
Normal file
10
tests/new/modules_by_file.fch
Normal 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())
|
||||
}
|
||||
1
tests/new/modules_by_file.out
Normal file
1
tests/new/modules_by_file.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
menu 30 ui/util util
|
||||
23
tests/new/std_fs.fch
Normal file
23
tests/new/std_fs.fch
Normal 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
7
tests/new/std_fs.out
Normal 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
55
tests/new/threads.fch
Normal 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
8
tests/new/threads.out
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
400000
|
||||
4
|
||||
true
|
||||
5
|
||||
true
|
||||
false
|
||||
7
|
||||
5
|
||||
7
tests/new/ui/button.fch
Normal file
7
tests/new/ui/button.fch
Normal 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
11
tests/new/ui/menu.fch
Normal 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
4
tests/new/ui/util.fch
Normal 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
4
tests/new/util.fch
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// for tests/new/modules_by_file.fch: the top-level util.fch
|
||||
fn who() -> str {
|
||||
return "util"
|
||||
}
|
||||
28
tests/run.sh
28
tests/run.sh
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
24
tests/tool/fmt_in.fch
Normal 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
21
tests/tool/fmt_out.fch
Normal 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)
|
||||
}
|
||||
1
tests/tool/project/expected.out
Normal file
1
tests/tool/project/expected.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
hello world 42
|
||||
14
tests/tool/project/finch.toml
Normal file
14
tests/tool/project/finch.toml
Normal 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"]
|
||||
9
tests/tool/project/native/glue.cpp
Normal file
9
tests/tool/project/native/glue.cpp
Normal 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);
|
||||
}
|
||||
7
tests/tool/project/native/glue.h
Normal file
7
tests/tool/project/native/glue.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
int glue_answer(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
3
tests/tool/project/src/game/world.fch
Normal file
3
tests/tool/project/src/game/world.fch
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn hello() -> str {
|
||||
return "world"
|
||||
}
|
||||
6
tests/tool/project/src/main.fch
Normal file
6
tests/tool/project/src/main.fch
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import "glue.h"
|
||||
import game/world
|
||||
|
||||
fn main() {
|
||||
print("hello \(world.hello()) \(glue_answer())")
|
||||
}
|
||||
30
tests/tool/test_blocks.fch
Normal file
30
tests/tool/test_blocks.fch
Normal 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")
|
||||
}
|
||||
8
tests/tool/test_blocks.out
Normal file
8
tests/tool/test_blocks.out
Normal 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
16
tests/tool/trace.fch
Normal 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
4
tests/tool/trace.out
Normal 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)
|
||||
Loading…
Reference in a new issue