mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 08:54:25 +00:00
Finch 2.5.0: docs (EN, PL), Kate and VS Code for the new features
- cheat sheets, technicians' and engineers' guides: "\(x)", test blocks, finch fmt, finch.toml, the build cache, the chain of calls, threads and atomics, C++/Rust/Android, struct #defines, passing structs, why there is no warning for big copies - Kate (version 4) and VS Code: code inside text, test, min/max/abs/clamp/assert/atomic_... - finch.toml's name is used only for its entry; other files of the project keep their own name Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
This commit is contained in:
parent
d428498efd
commit
bb9702aeba
16 changed files with 778 additions and 60 deletions
|
|
@ -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"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -68,7 +89,7 @@ One statement per line, no semicolons, `{` on the same line, no brackets around
|
|||
| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` |
|
||||
| `bool` | `true`, `false` |
|
||||
| `char` | `'A'`, `'\n'` |
|
||||
| `str` | `"text\t\"quoted\""`, `"""` several lines `"""` (the closing quotes' indentation is removed) |
|
||||
| `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 |
|
||||
|
|
@ -226,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 |
|
||||
|
|
@ -280,6 +302,8 @@ Automatic only when nothing is lost: smaller → bigger number, any int → floa
|
|||
| `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 |
|
||||
|
||||
|
|
@ -290,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]` |
|
||||
|
|
@ -303,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.
|
||||
- `new(...)` + `free(...)` (often with `defer`) for linked lists, trees, sharing.
|
||||
- C memory: C's own functions (`malloc` / `free`, `fclose`, …).
|
||||
|
||||
|
|
@ -327,7 +352,7 @@ import ui/button as btn // another name: btn.draw()
|
|||
|---|---|
|
||||
| `import "x.h"` | functions, structs, enums, `#define` numbers, globals |
|
||||
| `link "name"` | libname.so / name.lib (pkg-config first) |
|
||||
| `link "file.c"` | 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 |
|
||||
|
|
@ -342,6 +367,10 @@ import ui/button as btn // another name: btn.draw()
|
|||
| `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).
|
||||
|
||||
|
|
@ -358,6 +387,26 @@ A C function's `char *` result is a `str` already; structs by value work with th
|
|||
| `can't turn "x" into int` | `int("x")` |
|
||||
| `can't read the file "…"` | `read_file` (without `or`) |
|
||||
| `the key "x" is not in the map` | `m["x"]` |
|
||||
| `assert failed: why` | `assert(cond, "why")` |
|
||||
|
||||
Below the message, the calls that led there (`finch run`, `finch test`, `-g`, `-O0`, `--trace`):
|
||||
|
||||
```
|
||||
game.fch:6: runtime error: index 3 is out of range (the length is 2)
|
||||
in Enemy.next (game.fch:6)
|
||||
in step (game.fch:10)
|
||||
in main (game.fch:15)
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```c
|
||||
test "jump height" { // next to the code; finch test file.fch runs them, finch run/build leave them out
|
||||
p := Player()
|
||||
p.jump()
|
||||
assert(p.y > 0.0, "y is \(p.y)")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -407,10 +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 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) |
|
||||
|
||||
---
|
||||
|
|
@ -166,7 +168,7 @@ offset 0, and a field is read through memory. They can't be passed by value to C
|
|||
`ir` prints the IR; `index` prints the records the language server uses (§11); `--shared` makes a shared library of the
|
||||
`export fn`s.
|
||||
|
||||
`--target windows|linux|arm64|macos|<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.
|
||||
|
|
@ -176,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
|
||||
|
|
@ -440,6 +474,41 @@ 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.
|
||||
|
||||
### 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.
|
||||
|
|
@ -449,4 +518,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
|
||||
|
|
@ -503,6 +521,13 @@ s[0] = 'J' // change a character
|
|||
| `s.bytes()` | `[]u8` with the bytes |
|
||||
| `s.ptr` | a `ptr[char]` for C (valid while `s` lives) |
|
||||
|
||||
Values go into text with `\(...)`: any code, and `, 2` after a number for two digits after the point.
|
||||
A `\` before the `(` you want to keep: `"\\(not code)"`.
|
||||
|
||||
```c
|
||||
print("hp \(hp) of \(max_hp), speed \(v, 2), name \(p.name.upper())")
|
||||
```
|
||||
|
||||
`str` keeps its length, so `.len` is instant, and it is always NUL-terminated, so passing it
|
||||
to C as `char*` works. The other way round, a C function that gives back a `char *` gives you a `str`
|
||||
(a copy is made when you keep it), and `str(p)` reads a `ptr[char]` you got some other way.
|
||||
|
|
@ -779,8 +804,8 @@ Both work directly; `examples/opengl.fch` and `examples/vulkan.fch` are complete
|
|||
|
||||
- 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) ...`) and macros that make a struct (raylib's `RAYWHITE`).
|
||||
Macros that are a number, a text or an expression of them (`(1u << 3)`, `((Uint64)1)`, `(A | B)`) work.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
|
@ -860,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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -939,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
|
||||
|
|
@ -1073,6 +1112,7 @@ library's second, so your own file of the same name wins.
|
|||
| `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).
|
||||
|
|
@ -1093,7 +1133,155 @@ 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. Android is built (`--target android`) but not yet tried on a device.
|
||||
|
||||
On purpose, not planned: a warning for every big struct that gets copied. A copy in Finch is always visible in the
|
||||
code (`b := a`, passing a struct to a `Body b` parameter), a method never copies `self`, and `ptr[Body]` (or passing
|
||||
the variable to a `ptr[Body]` parameter) shares instead. A warning that fires on ordinary code would be noise.
|
||||
|
||||
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: it is built and checked here, but not
|
||||
yet tried on a device.
|
||||
|
|
|
|||
Loading…
Reference in a new issue