Compare commits

..
229 changed files with 9966 additions and 615491 deletions

4
.gitattributes vendored
View file

@ -1,4 +0,0 @@
# Finch sources, tests and expected outputs always use \n line endings, on every system
* text=auto eol=lf
*.png binary
*.vsix binary

View file

@ -1,187 +0,0 @@
name: CI
# The compiler is written in Finch. It is built from bootstrap/<system>.ll, its own LLVM IR (see bootstrap/README.md),
# then from itself, and tested, on every system it runs on: Linux x86-64 and ARM64, macOS ARM64 and x86-64, Windows x86-64.
# A tag v* also makes the release: every package below is attached to it.
on:
push:
branches: [main, "ci/**"]
tags: ["v*"]
pull_request:
jobs:
linux:
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-24.04, arch: x64 }
- { os: ubuntu-24.04-arm, arch: arm64 }
name: linux-${{ matrix.arch }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install clang 21, libclang, tools
run: |
wget -q https://apt.llvm.org/llvm.sh
sudo bash llvm.sh 21
sudo apt-get install -y libclang-21-dev pkg-config valgrind
echo "/usr/lib/llvm-21/bin" >> $GITHUB_PATH
- name: Build the compiler (seed, then itself, and the two must agree)
run: |
export FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21"
bootstrap/build.sh
build/finch version
- name: Tests (programs, refusals, -g, a shared library, the language server)
run: tests/run.sh
- name: Tests under valgrind
if: matrix.arch == 'x64'
run: MEMCHECK=1 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-linux-${{ matrix.arch }}.tar.gz -C dist finch
- uses: actions/upload-artifact@v4
with:
name: finch-linux-${{ matrix.arch }}
path: finch-linux-${{ matrix.arch }}.tar.gz
cross:
name: programs for ${{ matrix.target }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
target: [windows, arm64]
steps:
- uses: actions/checkout@v4
- name: Install clang 21, libclang, cross compilers and emulators
run: |
wget -q https://apt.llvm.org/llvm.sh
sudo bash llvm.sh 21
sudo apt-get install -y libclang-21-dev pkg-config gcc-mingw-w64-x86-64 wine64 gcc-aarch64-linux-gnu qemu-user libc6-arm64-cross
echo "/usr/lib/llvm-21/bin" >> $GITHUB_PATH
- name: Build the compiler
run: |
export FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21"
bootstrap/build.sh
- name: Run every test on the other system
run: |
if [ "${{ matrix.target }}" = windows ]; then
FINCH_CC=x86_64-w64-mingw32-gcc tests/cross.sh windows
else
FINCH_CC=aarch64-linux-gnu-gcc QEMU_LD_PREFIX=/usr/aarch64-linux-gnu tests/cross.sh arm64
fi
macos:
strategy:
fail-fast: false
matrix:
include:
- { os: macos-15, arch: arm64 }
- { os: macos-15-intel, arch: x64 }
name: macos-${{ matrix.arch }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install LLVM 21 (libclang)
run: brew install llvm@21 pkg-config
- name: Build the compiler
run: |
export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang"
export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang"
export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include"
bootstrap/build.sh
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-${{ matrix.arch }}.tar.gz -C dist finch
- uses: actions/upload-artifact@v4
with:
name: finch-macos-${{ matrix.arch }}
path: finch-macos-${{ matrix.arch }}.tar.gz
windows:
runs-on: windows-2022
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Download llvm-mingw (clang with MinGW headers) and LLVM for Windows (libclang.dll)
run: |
curl -sSL -o mingw.zip https://github.com/mstorsjo/llvm-mingw/releases/download/20251216/llvm-mingw-20251216-ucrt-x86_64.zip
7z x -y mingw.zip -o/c/ > /dev/null
curl -sSL -o llvm.exe https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.8/LLVM-21.1.8-win64.exe
7z x -y llvm.exe -o/c/llvm > /dev/null
- name: Build the compiler
run: |
export PATH="/c/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH"
export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll"
export FINCH_LLVM_INCLUDE="C:/llvm/include"
export FINCH_LDFLAGS="-LC:/llvm/bin"
bootstrap/build.sh
build/finch.exe version
- name: Tests
run: |
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
defaults:
run:
working-directory: editors/vscode
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Grammar test
run: npm test
- name: Package
run: npx vsce package --out finch-lang.vsix
- uses: actions/upload-artifact@v4
with:
name: finch-vscode
path: editors/vscode/finch-lang.vsix
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux, cross, macos, windows, vscode]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Attach the packages to the release (made if it isn't there yet)
uses: softprops/action-gh-release@v2
with:
name: Finch ${{ github.ref_name }}
generate_release_notes: true
files: |
dist/finch-linux-x64.tar.gz
dist/finch-linux-arm64.tar.gz
dist/finch-macos-arm64.tar.gz
dist/finch-macos-x64.tar.gz
dist/finch-windows-x64.zip
dist/finch-lang.vsix

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
build/ build/
*.o *.o
core.*

47
CMakeLists.txt Normal file
View file

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.20)
project(finch VERSION 2.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Using LLVM ${LLVM_PACKAGE_VERSION}")
add_executable(finch
src/main.cpp
src/lexer.cpp
src/parser.cpp
src/codegen.cpp
src/codegen_types.cpp
src/codegen_builtins.cpp
src/abi.cpp
src/cimport.cpp
)
# The runtime (runtime/finch_rt.c) is embedded into the compiler as a string;
# finch compiles it once into ~/.cache/finch and links it into every program.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c)
file(READ ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c FINCH_RT_SOURCE)
file(WRITE ${CMAKE_BINARY_DIR}/rt_source.inc "static const char *kRuntimeSource = R\"FINCHRT(${FINCH_RT_SOURCE})FINCHRT\";\n")
target_include_directories(finch PRIVATE ${CMAKE_BINARY_DIR})
# libclang reads C headers for `import "stdio.h"`
find_path(LIBCLANG_INCLUDE clang-c/Index.h HINTS ${LLVM_INCLUDE_DIRS})
find_library(LIBCLANG NAMES clang libclang HINTS ${LLVM_LIBRARY_DIRS})
if(NOT LIBCLANG_INCLUDE OR NOT LIBCLANG)
message(FATAL_ERROR "libclang not found (on Nix: run inside nix-shell)")
endif()
target_include_directories(finch SYSTEM PRIVATE ${LLVM_INCLUDE_DIRS} ${LIBCLANG_INCLUDE})
target_compile_definitions(finch PRIVATE ${LLVM_DEFINITIONS} FINCH_VERSION="${PROJECT_VERSION}")
target_compile_options(finch PRIVATE -Wall -Wextra)
# Link the one shared libLLVM when available: libclang uses it too, and two copies
# of LLVM in one process crash on exit.
if(TARGET LLVM)
set(LLVM_LIBS LLVM)
else()
llvm_map_components_to_libnames(LLVM_LIBS core support passes native)
endif()
target_link_libraries(finch PRIVATE ${LLVM_LIBS} ${LIBCLANG})

View file

@ -38,10 +38,6 @@ fn main() {
- **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Out-of-range - **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Out-of-range
indexes, division by zero and null pointers stop with a clear runtime error instead of undefined behavior. indexes, division by zero and null pointers stop with a clear runtime error instead of undefined behavior.
Operator precedence that does not bite (`x & 1 == 0` means what it says). Operator precedence that does not bite (`x & 1 == 0` means what it says).
- **Practical.** Maps, methods, errors as values (`int(s) or 0`, `try read_file(p)`), modules, any C
library, and a language server for your editor. Runs on Linux, macOS and Windows, on x86-64 and ARM64.
- **SIMD built in.** `f32x8`, `i8x32`, `f64x4`: numbers side by side, added and multiplied all at once. LLVM turns them
into SSE, AVX2, AVX-512 or NEON instructions, so one piece of code is fast on a desktop, a laptop and ARM.
- **Errors that talk like a person:** - **Errors that talk like a person:**
``` ```
game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) game.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
@ -50,36 +46,26 @@ fn main() {
``` ```
- **Any C library, directly.** `import "raylib.h"` + `link "raylib"`, and you can call it, structs by value - **Any C library, directly.** `import "raylib.h"` + `link "raylib"`, and you can call it, structs by value
and callbacks included. Headers are read by libclang, so there is nothing to declare by hand. and callbacks included. Headers are read by libclang, so there is nothing to declare by hand.
- **Written in itself.** The compiler is Finch code, and nothing else: `src/*.fch`, about 12k lines. It is built from a - **Written in itself, too.** `boot/` is a Finch compiler written in Finch that compiles itself to a
seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical. byte-identical result.
## Quick start ## Quick start
**Ready builds** for Linux (x64, ARM64), macOS (Apple Silicon, Intel) and Windows (x64) are in the Linux x86-64, LLVM 21.
[releases](https://github.com/Nexoniarz/Finch/releases). Unpack one and keep `runtime/` and `std/` next to `finch`. What
each system needs besides is in the [technicians' guide](docs/en/for-technicians.md#2-installing): clang and libclang
from LLVM 21 on Linux, `brew install llvm@21` on a Mac, llvm-mingw on Windows.
**Building it yourself** (on Linux, macOS, or Windows with Git Bash; needs `clang` and libclang, LLVM 21):
```sh ```sh
git clone https://github.com/Nexoniarz/Finch.git git clone https://github.com/Nexoniarz/Finch.git
cd Finch cd Finch
nix-shell # or install clang + libclang (LLVM 21) + pkg-config yourself nix-shell # or install LLVM 21 + libclang + cmake + ninja yourself
bootstrap/build.sh # the compiler is build/finch: built from the seed, then from itself cmake -S . -B build -G Ninja
ninja -C build
./build/finch run examples/hello.fch ./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour ./build/finch build examples/tour.fch -o tour && ./tour
tests/run.sh # every program, every expected error, -g, a shared library, the language server tests/run.sh # 59 passed, 0 failed
tests/boot.sh # the compiler written in Finch builds itself
``` ```
The same `bootstrap/build.sh` works on a Mac (with Homebrew's `llvm@21`) and on Windows (with llvm-mingw): see
[bootstrap/README.md](bootstrap/README.md). Programs for another system are built with `--target windows|linux|arm64|macos`
(see the [guide](docs/en/for-technicians.md#building-for-another-system)).
Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any
editor with LSP support (`finch lsp`).
## Documentation ## Documentation
Pick the guide that fits you: Pick the guide that fits you:
@ -89,33 +75,23 @@ Pick the guide that fits you:
| **Beginners** | Never programmed: students, seniors, anyone curious | [for-beginners.md](docs/en/for-beginners.md) | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | | **Beginners** | Never programmed: students, seniors, anyone curious | [for-beginners.md](docs/en/for-beginners.md) | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) |
| **Technicians** | Know computers, new to programming: install, whole language, C libraries, troubleshooting | [for-technicians.md](docs/en/for-technicians.md) | [dla-technikow.md](docs/pl/dla-technikow.md) | | **Technicians** | Know computers, new to programming: install, whole language, C libraries, troubleshooting | [for-technicians.md](docs/en/for-technicians.md) | [dla-technikow.md](docs/pl/dla-technikow.md) |
| **Engineers** | Everything: lexer, grammar, AST, type rules, IR lowering, libclang import, ABI, internals | [for-engineers.md](docs/en/for-engineers.md) | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | | **Engineers** | Everything: lexer, grammar, AST, type rules, IR lowering, libclang import, ABI, internals | [for-engineers.md](docs/en/for-engineers.md) | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) |
| **Cheat sheet** | Every function, method, operator and command on one page | [cheatsheet.md](docs/en/cheatsheet.md) | [sciaga.md](docs/pl/sciaga.md) |
## The language at a glance ## The language at a glance
| | | | | |
|---|---| |---|---|
| Variables | `x := 5` (inferred) or `int x = 5`; `const W = 640` | | Variables | `x := 5` (inferred) or `int x = 5` |
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]`, vectors `f32x8` `i8x32`… | | Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, pointers `ptr[T]` |
| Functions | `fn add(int a, int b) -> int { return a + b }` | | 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)` | | Structs | `struct Point { … }` with one field per line (`int x`, `int y = 0`); create with `Point(1, 2)` or `Point(x: 1)` |
| Methods | `fn Point.move(int dx) { self.x += dx }` → `p.move(3)` | | Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for n in nums { }` |
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for i, n in nums { }` | | Text | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` | | Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` |
| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(list, i)` |
| Processors | `--cpu native`; `@cpu("avx2") fn …` with `if cpu_has("avx2")`; floats computed as written (`fma`/`muladd` to fuse) |
| 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 | | 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) | | Modules | `import shapes` → `shapes.area(b)` |
| Generics | `fn max[T](T a, T b) -> T`, `struct Stack[T]`, `Stack[int]()` | | 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` | | I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `--cpu native`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors | | Tools | `finch run/build/ir`, `-g` for gdb, `-O0` |
## Examples ## Examples
@ -125,17 +101,15 @@ Pick the guide that fits you:
| [`examples/tour.fch`](examples/tour.fch) | variables, functions, conditions, loops | | [`examples/tour.fch`](examples/tour.fch) | variables, functions, conditions, loops |
| [`examples/guess.fch`](examples/guess.fch) | a guessing game: input, loops, C's `rand` | | [`examples/guess.fch`](examples/guess.fch) | a guessing game: input, loops, C's `rand` |
| [`examples/todo.fch`](examples/todo.fch) | structs, arrays, strings and files | | [`examples/todo.fch`](examples/todo.fch) | structs, arrays, strings and files |
| [`examples/wordcount.fch`](examples/wordcount.fch) | maps, methods and errors as values: the most frequent words in a file |
| [`examples/structs.fch`](examples/structs.fch) | structs, arrays of structs, a linked list with `new`/`free` | | [`examples/structs.fch`](examples/structs.fch) | structs, arrays of structs, a linked list with `new`/`free` |
| [`examples/types.fch`](examples/types.fch) | sized numbers and pointers | | [`examples/types.fch`](examples/types.fch) | sized numbers and pointers |
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` | | [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
| [`examples/simd.fch`](examples/simd.fch) | vectors: a dot product eight numbers at a time, and the time it saves |
| [`examples/window.fch`](examples/window.fch) | a GLFW + OpenGL window | | [`examples/window.fch`](examples/window.fch) | a GLFW + OpenGL window |
| [`examples/opengl.fch`](examples/opengl.fch) | modern OpenGL 3.3: shaders, buffers, a spinning triangle | | [`examples/opengl.fch`](examples/opengl.fch) | modern OpenGL 3.3: shaders, buffers, a spinning triangle |
| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: list the GPUs, their memory and queues, create a device | | [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: list the GPUs, their memory and queues, create a device |
| [`examples/raylib.fch`](examples/raylib.fch) | raylib with C structs by value | | [`examples/raylib.fch`](examples/raylib.fch) | raylib with C structs by value |
| [`examples/llvm.fch`](examples/llvm.fch) | Finch building LLVM IR through the LLVM-C API | | [`examples/llvm.fch`](examples/llvm.fch) | Finch building LLVM IR through the LLVM-C API |
| [`src/`](src/) | the compiler, written in Finch (the seed to build it is in [`bootstrap/`](bootstrap/)) | | [`boot/`](boot/) | the Finch compiler, written in Finch |
## Roadmap ## Roadmap
@ -151,30 +125,18 @@ Pick the guide that fits you:
- [x] Finch modules (`import name`) - [x] Finch modules (`import name`)
- [x] Debug info (`-g`) - [x] Debug info (`-g`)
- [x] Bootstrap: a Finch compiler written in Finch that builds itself - [x] Bootstrap: a Finch compiler written in Finch that builds itself
- [x] Windows: `finch.exe`, `.exe` programs, the Microsoft x64 ABI; cross-building from Linux (`--target windows`) - [ ] Methods on structs, maps, `match`
- [x] Language server (`finch lsp`) and a VS Code extension - [ ] Error values instead of stopping the program
- [x] macOS and ARM64 (Linux and Apple Silicon), with their C calling conventions; Kate support - [ ] More platforms (ARM64, macOS)
- [x] Methods on structs, maps, errors as values (`!`, `or`, `try`)
- [x] Generics (`fn max[T]`, `struct Stack[T]`), `match`, enums
- [x] A standard library in Finch: `json`, `path`, `time`, `proc`, `fs`
- [x] `const`, `if` as a value, `"\(x)"` in text, `"""` text, C unions and `#define` expressions
- [x] `test` blocks and `finch test`, `finch fmt`, `finch.toml`, a build cache, the chain of calls on runtime errors
- [x] Threads (`import thread`) and atomics; C++ files; `--target android`
- [x] The compiler on every system it builds for: Linux, macOS (Apple Silicon and Intel), Windows; releases made by CI
- [x] Vector types (`f32x8`, `i8x32`, …) for SIMD, and `--cpu native`
- [x] Masks, shuffles, byte lookup and `dot_add` for kernels; `@cpu` and `cpu_has`; `fma`/`muladd`, float bits, f16 and bf16
## Layout ## Layout
``` ```
src/ the compiler, in Finch: lexer, parser, AST, code generation (to LLVM IR as text), C import (libclang) and src/ compiler: lexer, parser, AST, codegen, C import + ABI, driver (C++17, ~4.5k lines)
the C calling conventions, the language server, the driver
std/ the standard library, in Finch: json, path, time, proc, fs, thread
runtime/ the small C runtime linked into every program runtime/ the small C runtime linked into every program
bootstrap/ build.sh and the seeds: the compiler's own LLVM IR, one per system boot/ the Finch compiler written in Finch (~3k lines)
examples/ example programs examples/ example programs
editors/ VS Code extension, Kate highlighting and LSP setup tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh
tests/ run/ new/ (golden output), fail/ new/fail/ (expected errors), run.sh, cross.sh, shared.sh, lsp_test.py
docs/ en/ and pl/ guides for three audiences docs/ en/ and pl/ guides for three audiences
``` ```

View file

@ -5,8 +5,8 @@
</picture> </picture>
</p> </p>
**Mały, szybki, celny.** Język programowania oparty na LLVM, ze składnią w stylu C. Nauczysz się go w godzinę, **Mały, zwinny, skuteczny.** Język programowania na LLVM ze składnią podobną do C,
a programy działają tak szybko jak w C. na tyle prosty, że nauczysz się go w godzinę, i tak szybki jak C.
> 🇬🇧 [English version](README.md) > 🇬🇧 [English version](README.md)
@ -19,7 +19,7 @@ fn przeciwprostokatna(float a, float b) -> float {
fn main() { fn main() {
imie := "Finch" imie := "Finch"
print("Cześć, tu", imie, "!") print("Cześć od", imie, "!")
for i in 1..4 { for i in 1..4 {
print(i, "→", przeciwprostokatna(float(i), 1.0)) print(i, "→", przeciwprostokatna(float(i), 1.0))
@ -29,155 +29,115 @@ fn main() {
## Dlaczego Finch ## Dlaczego Finch
- **Prosty.** Nie ma średników, plików nagłówkowych ani preprocesora. Na każdą rzecz jest jeden sposób. - **Prosty.** Bez średników, bez plików nagłówkowych, bez preprocesora. Każdą rzecz robi się na jeden sposób.
- **Szybki.** Kod przechodzi przez optymalizacje LLVM (`-O2`) i staje się zwykłym programem, bez garbage collectora. - **Szybki.** Kompiluje się przez potok `-O2` z LLVM do kodu natywnego, bez garbage collectora.
Rekurencyjne `fib(40)` liczy się ok. 0,19 s (ten sam kod w C z `clang -O2`: ok. 0,23 s), a pętle po tablicach Rekurencyjne `fib(40)` trwa ok. 0,19 s (`clang -O2` na tym samym kodzie C: ok. 0,23 s), a kod na tablicach
ze sprawdzaniem zakresu są tak samo szybkie jak w C. z kontrolą zakresu dorównuje C.
- **Pamięć bez kłopotów.** Listy, teksty i struktury są wartościami: przypisanie robi kopię, a koniec bloku sprząta. - **Pamięć bez bólu.** Listy, teksty i struktury są wartościami: przypisanie kopiuje, a koniec bloku je zwalnia.
Nie ma `free`, o którym da się zapomnieć, ani odwołań do zwolnionej pamięci. Valgrind nie znajduje błędów w żadnym teście. Nie ma `free`, o którym można zapomnieć, nie ma użycia po zwolnieniu, a każdy test jest czysty pod valgrindem.
- **Bezpieczniejszy niż C tam, gdzie nic to nie kosztuje.** Liczby nie zmieniają po cichu typu, a liczb ze znakiem - **Bezpieczniejszy niż C tam, gdzie to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem
i bez znaku nie da się przypadkiem pomieszać. Indeks poza tablicą, dzielenie przez zero czy `null` kończą program i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast
jasnym komunikatem, a nie niezdefiniowanym zachowaniem. Kolejność działań jest taka, jakiej się spodziewasz powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać).
(`x & 1 == 0` znaczy dokładnie to, co widać). - **Błędy mówią po ludzku:**
- **Praktyczny.** Mapy, metody, błędy jako wartości (`int(s) or 0`, `try read_file(p)`), moduły, dowolna biblioteka C
i serwer języka dla edytora. Działa na Linuksie, macOS i Windowsie, na procesorach x86-64 i ARM64.
- **SIMD w języku.** `f32x8`, `i8x32`, `f64x4`: kilka liczb obok siebie, dodawanych i mnożonych naraz. LLVM zamienia je
na instrukcje SSE, AVX2, AVX-512 albo NEON, więc ten sam kod jest szybki na komputerze stacjonarnym, laptopie i ARM-ie.
- **Komunikaty, które da się zrozumieć:**
``` ```
gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert) gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
3 | i32 y = x 3 | i32 y = x
| ^ | ^
``` ```
- **Biblioteki C bez pośredników.** Wystarczy `import "raylib.h"` i `link "raylib"`, żeby z niej korzystać, łącznie - **Każda biblioteka C, bezpośrednio.** Piszesz `import "raylib.h"` i `link "raylib"`, i możesz z niej korzystać,
ze strukturami przekazywanymi przez wartość i funkcjami zwrotnymi. Nagłówki czyta libclang, więc niczego nie trzeba także ze struktur przekazywanych przez wartość i z callbacków. Nagłówki czyta libclang, więc nie trzeba niczego deklarować ręcznie.
deklarować ręcznie. - **Napisany także w sobie.** `boot/` to kompilator Fincha napisany w Finchu, który kompiluje sam siebie
- **Napisany w samym sobie.** Kompilator to wyłącznie kod w Finchu: `src/*.fch`, ok. 12 tys. linii. Buduje się go do identycznego co do bajtu wyniku.
z pliku startowego (jego własnego kodu LLVM IR, w `bootstrap/`), a potem jeszcze raz nim samym. Oba wyniki są identyczne.
## Na start ## Szybki start
**Gotowe kompilatory** na Linuksa (x64, ARM64), macOS (Apple Silicon, Intel) i Windowsa (x64) znajdziesz w Linux x86-64, LLVM 21.
[wydaniach](https://github.com/Nexoniarz/Finch/releases). Rozpakuj archiwum i trzymaj katalogi `runtime/` i `std/` obok
pliku `finch`. Co jeszcze trzeba doinstalować, opisuje [przewodnik dla techników](docs/pl/dla-technikow.md#2-instalacja):
na Linuksie clang i libclang z LLVM 21, na Macu `brew install llvm@21`, na Windowsie llvm-mingw.
**Samodzielne budowanie** (na Linuksie, macOS albo Windowsie w Git Bash; potrzebne są `clang` i libclang z LLVM 21):
```sh ```sh
git clone https://github.com/Nexoniarz/Finch.git git clone https://github.com/Nexoniarz/Finch.git
cd Finch cd Finch
nix-shell # albo zainstaluj clang, libclang (LLVM 21) i pkg-config po swojemu nix-shell # albo zainstaluj samodzielnie LLVM 21 + libclang + cmake + ninja
bootstrap/build.sh # gotowy kompilator to build/finch cmake -S . -B build -G Ninja
ninja -C build
./build/finch run examples/hello.fch ./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour ./build/finch build examples/tour.fch -o tour && ./tour
tests/run.sh # wszystkie programy testowe, oczekiwane błędy, -g, biblioteka współdzielona, serwer języka tests/run.sh # 59 passed, 0 failed
tests/boot.sh # kompilator napisany w Finchu buduje sam siebie
``` ```
Ten sam `bootstrap/build.sh` działa na Macu (z `llvm@21` z Homebrew) i na Windowsie (z llvm-mingw); szczegóły są w
[bootstrap/README.md](bootstrap/README.md). Program na inny system zbudujesz opcją `--target windows|linux|arm64|macos`
(zob. [przewodnik](docs/pl/dla-technikow.md#budowanie-na-inny-system)).
Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo każdy edytor,
który obsługuje LSP (`finch lsp`).
## Dokumentacja ## Dokumentacja
Wybierz przewodnik dla siebie: Wybierz przewodnik dla siebie:
| Przewodnik | Dla kogo | Polski | English | | Przewodnik | Dla kogo | Polski | English |
|---|---|---|---| |---|---|---|---|
| **Dla początkujących** | Dla tych, którzy nigdy nie programowali: uczniów, seniorów, ciekawych świata | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) | | **Początkujący** | Nigdy nie programowałeś: uczniowie, seniorzy, ciekawscy | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) |
| **Dla techników** | Znasz komputery, ale programowanie jest dla ciebie nowe: instalacja, cały język, biblioteki C, rozwiązywanie problemów | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) | | **Technicy** | Znasz komputery, programowanie jest nowe: instalacja, cały język, biblioteki C, problemy | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) |
| **Dla inżynierów** | Wszystko: lekser, gramatyka, AST, reguły typów, generowanie IR, import przez libclang, ABI, budowa kompilatora | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) | | **Inżynierowie** | Wszystko: lekser, gramatyka, AST, reguły typów, tłumaczenie na IR, import przez libclang, ABI, wnętrze | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) |
| **Ściąga** | Wszystkie funkcje, metody, operatory i polecenia na jednej stronie | [sciaga.md](docs/pl/sciaga.md) | [cheatsheet.md](docs/en/cheatsheet.md) |
## Język w pigułce ## Język w pigułce
| | | | | |
|---|---| |---|---|
| Zmienne | `x := 5` (typ wynika z wartości) albo `int x = 5`; `const W = 640` | | Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` |
| Typy | `int` `float` `bool` `char` `str`, o podanym rozmiarze: `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, mapy `map[K]V`, wskaźniki `ptr[T]`, wektory `f32x8` `i8x32`… | | Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, wskaźniki `ptr[T]` |
| Funkcje | `fn dodaj(int a, int b) -> int { return a + b }` | | Funkcje | `fn add(int a, int b) -> int { return a + b }` |
| Struktury | `struct Punkt { … }`, jedno pole w linii (`int x`, `int y = 0`); tworzy się je przez `Punkt(1, 2)` albo `Punkt(x: 1)` | | Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
| Metody | `fn Punkt.przesun(int dx) { self.x += dx }` → `p.przesun(3)` | | Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for n in liczby { }` |
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` | | Tekst | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cyryl")`, `ile[slowo] += 1`, `for k, v in wiek { }` | | Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
| Błędy | `fn parsuj(str s) -> int! { … return error("zła liczba") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` | | Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie |
| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(lista, i)` | | Moduły | `import ksztalty` → `ksztalty.pole(p)` |
| Procesory | `--cpu native`; `@cpu("avx2") fn …` razem z `if cpu_has("avx2")`; liczby zmiennoprzecinkowe liczone tak, jak zapisane (`fma`/`muladd` do łączenia) | | Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków |
| Tekst | `"hp: \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | | Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Sterowanie | `if` / `else if` / `else` (również jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` | | Narzędzia | `finch run/build/ir`, `-g` dla gdb, `-O0` |
| Pamięć | tablice, teksty i struktury zwalniają się same; `new(...)` / `free(...)` dla własnych struktur na stercie |
| Enumy i `match` | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (również jako wartość) |
| Typy generyczne | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` |
| Moduły | `import ksztalty` → `ksztalty.pole(p)`, `import ui/button as btn`; biblioteka standardowa: `json`, `path`, `time`, `proc`, `fs`, `thread` |
| Wątki | `thread.spawn(addr(praca), addr(dane))`, `t.join()`, `thread.mutex()`, `atomic_add(addr(n), 1)` |
| Testy | `test "nazwa" { assert(x == 3) }` tuż obok kodu, uruchamiane przez `finch test` |
| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"` / `"glue.cpp"`, unie, `#define` (także `(Color){…}`), `addr(fn)` jako funkcja zwrotna |
| Wejście i wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Narzędzia | `finch run/build/test/fmt/ir/lsp`, `finch.toml` dla projektu, pamięć podręczna kompilacji, `--target windows/arm64/macos/android`, `--cpu native`, `-g` dla gdb i lldb, `-O0`, ścieżka wywołań przy błędzie w trakcie działania |
## Przykłady ## Przykłady
| Plik | Co pokazuje | | Plik | Pokazuje |
|---|---| |---|---|
| [`examples/hello.fch`](examples/hello.fch) | najkrótszy program | | [`examples/hello.fch`](examples/hello.fch) | najmniejszy program |
| [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle | | [`examples/tour.fch`](examples/tour.fch) | zmienne, funkcje, warunki, pętle |
| [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie liczby: wczytywanie, pętle, `rand` z C | | [`examples/guess.fch`](examples/guess.fch) | gra w zgadywanie: wejście, pętle, `rand` z C |
| [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki | | [`examples/todo.fch`](examples/todo.fch) | struktury, tablice, teksty i pliki |
| [`examples/wordcount.fch`](examples/wordcount.fch) | mapy, metody i błędy jako wartości: najczęstsze słowa w pliku | | [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` |
| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista jednokierunkowa z `new`/`free` | | [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki |
| [`examples/types.fch`](examples/types.fch) | liczby o podanym rozmiarze i wskaźniki |
| [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` | | [`examples/c_import.fch`](examples/c_import.fch) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
| [`examples/simd.fch`](examples/simd.fch) | wektory: iloczyn skalarny liczony po osiem liczb naraz i zysk na czasie | | [`examples/window.fch`](examples/window.fch) | okno GLFW + OpenGL |
| [`examples/window.fch`](examples/window.fch) | okno GLFW z OpenGL |
| [`examples/opengl.fch`](examples/opengl.fch) | nowoczesny OpenGL 3.3: shadery, bufory, obracający się trójkąt | | [`examples/opengl.fch`](examples/opengl.fch) | nowoczesny OpenGL 3.3: shadery, bufory, obracający się trójkąt |
| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych z ich pamięcią i kolejkami, utworzenie urządzenia | | [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych, ich pamięć i kolejki, tworzenie urządzenia |
| [`examples/raylib.fch`](examples/raylib.fch) | raylib i struktury C przekazywane przez wartość | | [`examples/raylib.fch`](examples/raylib.fch) | raylib ze strukturami C przez wartość |
| [`examples/llvm.fch`](examples/llvm.fch) | Finch, który buduje LLVM IR przez API LLVM-C | | [`examples/llvm.fch`](examples/llvm.fch) | Finch budujący LLVM IR przez API LLVM-C |
| [`src/`](src/) | kompilator napisany w Finchu (plik startowy do jego zbudowania leży w [`bootstrap/`](bootstrap/)) | | [`boot/`](boot/) | kompilator Fincha napisany w Finchu |
## Plan rozwoju ## Plan rozwoju
- [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2 - [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2
- [x] Typy, w tym liczby o podanym rozmiarze, wskaźniki i konwersje; funkcje; `if`/`while`/`for` - [x] Typy, w tym liczby z rozmiarem, wskaźniki, konwersje; funkcje; `if`/`while`/`for`
- [x] Czytelne błędy kompilacji; sprawdzanie w trakcie działania (dzielenie, `null`, zakres tablicy) - [x] Czytelne błędy kompilacji; kontrole w czasie działania (dzielenie, null, zakres)
- [x] Operatory bitowe z rozsądną kolejnością działań - [x] Operatory bitowe z rozsądnymi priorytetami
- [x] Nagłówki C przez libclang; `link` z pkg-config; podpowiedzi przy błędach linkera - [x] Nagłówki C przez libclang; `link` z pkg-config; porady przy błędach linkowania
- [x] `struct`, w tym struktury C przekazywane przez wartość (ABI System V) i funkcje zwrotne - [x] `struct`, także struktury C przez wartość (ABI System V) i callbacki
- [x] Tablice z `.len`, sprawdzaniem zakresu i metodami; `for x in lista` - [x] Tablice z `.len`, kontrolą zakresu i metodami; `for x in lista`
- [x] Pamięć: wartości zwalniane na końcu bloku, `defer`, `new`/`free` - [x] Pamięć: zwalnianie na końcu bloku, `defer`, `new`/`free`
- [x] Teksty: `+`, `.len`, indeksowanie, metody, `str(x)`, `int(s)`; `input()`; pliki - [x] Teksty: `+`, `.len`, indeksowanie, metody, `str(x)`, `int(s)`; `input()`; pliki
- [x] Moduły Fincha (`import nazwa`) - [x] Moduły Fincha (`import nazwa`)
- [x] Informacje dla debuggera (`-g`) - [x] Informacje dla debuggera (`-g`)
- [x] Bootstrap: kompilator Fincha napisany w Finchu, który kompiluje sam siebie - [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie
- [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`) - [ ] Metody w strukturach, mapy, `match`
- [x] Serwer języka (`finch lsp`) i rozszerzenie do VS Code - [ ] Błędy jako wartości zamiast zatrzymywania programu
- [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate - [ ] Więcej platform (ARM64, macOS)
- [x] Metody struktur, mapy, błędy jako wartości (`!`, `or`, `try`)
- [x] Typy generyczne (`fn max[T]`, `struct Stos[T]`), `match`, enumy
- [x] Biblioteka standardowa w Finchu: `json`, `path`, `time`, `proc`, `fs`
- [x] `const`, `if` jako wartość, `"\(x)"` w tekście, tekst `"""`, unie C i wyrażenia w `#define`
- [x] Bloki `test` i `finch test`, `finch fmt`, `finch.toml`, pamięć podręczna kompilacji, ścieżka wywołań przy błędach
- [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android`
- [x] Kompilator na każdym systemie, na który buduje: Linux, macOS (Apple Silicon i Intel), Windows; wydania robi CI
- [x] Typy wektorowe (`f32x8`, `i8x32`, …) dla SIMD i `--cpu native`
- [x] Maski, przestawianie, tabele bajtów i `dot_add` dla jąder obliczeniowych; `@cpu` i `cpu_has`; `fma`/`muladd`, bity liczb, f16 i bf16
## Co gdzie leży ## Struktura
``` ```
src/ kompilator w Finchu: lekser, parser, AST, generowanie kodu (LLVM IR jako tekst), import C (libclang) src/ kompilator: lekser, parser, AST, codegen, import C + ABI, sterownik (C++17, ok. 4,5 tys. linii)
i konwencje wywołań C, serwer języka, polecenie finch runtime/ mały runtime w C dołączany do każdego programu
std/ biblioteka standardowa w Finchu: json, path, time, proc, fs, thread boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii)
runtime/ mały runtime w C, dołączany do każdego programu
bootstrap/ build.sh i pliki startowe: własny LLVM IR kompilatora, osobno dla każdego systemu
examples/ przykładowe programy examples/ przykładowe programy
editors/ rozszerzenie do VS Code, podświetlanie składni i konfiguracja LSP dla Kate tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh
tests/ run/ new/ (wzorcowe wyniki), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
docs/ przewodniki po angielsku (en/) i po polsku (pl/) dla trzech grup czytelników
``` ```
## Licencja ## Licencja

94
boot/ast.fch Normal file
View file

@ -0,0 +1,94 @@
// The syntax tree of the self-hosted compiler.
// A type as written or resolved.
// kind: "void" "int" "bool" "char" "float" "str" "null" "ptr" "array" "named" "struct"
// elem: the element of an array or the target of a ptr (zero or one entry)
// name, module: structs ("" module = the main file)
struct Type {
str kind
[]Type elem
str name
str module
}
fn simple(str kind) -> Type {
return Type(kind: kind)
}
fn arrayOf(Type t) -> Type {
out := Type(kind: "array")
out.elem.push(t)
return out
}
fn ptrTo(Type t) -> Type {
out := Type(kind: "ptr")
out.elem.push(t)
return out
}
fn same(Type a, Type b) -> bool {
if a.kind != b.kind {
return false
}
if a.kind == "struct" || a.kind == "named" {
return a.name == b.name && a.module == b.module
}
if a.kind == "array" || a.kind == "ptr" {
if a.elem.len != b.elem.len {
return false
}
if a.elem.len == 0 {
return true
}
return same(a.elem[0], b.elem[0])
}
return true
}
fn show(Type t) -> str {
if t.kind == "array" {
return "[]" + show(t.elem[0])
}
if t.kind == "ptr" {
if t.elem.len == 0 {
return "ptr"
}
return "ptr[" + show(t.elem[0]) + "]"
}
if t.kind == "struct" || t.kind == "named" {
if t.module.len > 0 {
return t.module + "." + t.name
}
return t.name
}
if t.kind == "void" {
return "nothing"
}
return t.kind
}
// One node of the tree, for expressions, statements, functions and structs alike.
// expressions: int float str char bool null var unary binary call member index array method
// statements: decl assign expr if while for each return break continue block
// top level: fn param struct field
struct Node {
str kind
str name // identifier, operator, callee, field or method name
str text // literal text
Type ty // declared type (decl, param, field, fn result)
bool flag // decl: has a type; fn: has a body
[]Node kids
[]str names // named arguments: Point(x: 1)
int line
int col
int file
}
struct Program {
str module
str path
[]str imports // Finch modules
[]Node structs
[]Node fns
}

2161
boot/gen.fch Normal file

File diff suppressed because it is too large Load diff

210
boot/lexer.fch Normal file
View file

@ -0,0 +1,210 @@
// The Finch lexer, written in Finch.
// A token's kind is a word: "ident", "int", "float", "string", "char", "eof",
// or the keyword / operator itself ("fn", "{", "+=", ...).
struct Token {
str kind
str text
int line
int col
bool nl // a newline came before this token
}
fn isAlpha(char c) -> bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}
fn isDigit(char c) -> bool {
return c >= '0' && c <= '9'
}
fn isHex(char c) -> bool {
return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
fn isKeyword(str w) -> bool {
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false",
"null", "import", "link", "struct", "defer"]
return keywords.contains(w)
}
struct Lexer {
str src
str path
int i
int line
int col
bool nl
}
fn fail(ptr[Lexer] lx, int line, int col, str msg) {
print(lx.path + ":" + str(line) + ":" + str(col) + ": error: " + msg)
exit(1)
}
fn peek(ptr[Lexer] lx, int off) -> char {
if lx.i + off < lx.src.len {
return lx.src[lx.i + off]
}
return char(0)
}
fn advance(ptr[Lexer] lx) {
c := lx.src[lx.i]
if c == '\n' {
lx.line += 1
lx.col = 1
lx.nl = true
} else if int(c) & 0xC0 != 0x80 {
lx.col += 1
}
lx.i += 1
}
// operators, longest first
fn operator(ptr[Lexer] lx) -> str {
twos := ["->", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "..", ":=", "+=", "-=", "*=", "/=", "%="]
if lx.i + 1 < lx.src.len {
two := lx.src.sub(lx.i, lx.i + 2)
if twos.contains(two) {
return two
}
}
ones := "(){}[],.:+-*/%&|^~<>=!"
c := lx.src.sub(lx.i, lx.i + 1)
if ones.contains(c) {
return c
}
return ""
}
fn escape(ptr[Lexer] lx, char e) -> 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, lx.line, lx.col - 1, "unknown escape \\" + str(e))
return e
}
fn lex(str src, str path) -> []Token {
lexer := Lexer(src: src, path: path, i: 0, line: 1, col: 1, nl: true)
lx := addr(lexer)
[]Token out
while lx.i < lx.src.len {
c := lx.src[lx.i]
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
advance(lx)
continue
}
if c == '/' && peek(lx, 1) == '/' {
while lx.i < lx.src.len && lx.src[lx.i] != '\n' {
advance(lx)
}
continue
}
if c == '/' && peek(lx, 1) == '*' {
line := lx.line
col := lx.col
advance(lx)
advance(lx)
while lx.i < lx.src.len && !(lx.src[lx.i] == '*' && peek(lx, 1) == '/') {
advance(lx)
}
if lx.i >= lx.src.len {
fail(lx, line, col, "comment is never closed (missing */)")
}
advance(lx)
advance(lx)
continue
}
t := Token(kind: "", text: "", line: lx.line, col: lx.col, nl: lx.nl)
lx.nl = false
start := lx.i
if isAlpha(c) {
while lx.i < lx.src.len && (isAlpha(lx.src[lx.i]) || isDigit(lx.src[lx.i])) {
advance(lx)
}
t.text = lx.src.sub(start, lx.i)
t.kind = "ident"
if isKeyword(t.text) {
t.kind = t.text
}
} else if isDigit(c) {
t.kind = "int"
if c == '0' && (peek(lx, 1) == 'x' || peek(lx, 1) == 'X') {
advance(lx)
advance(lx)
while lx.i < lx.src.len && (isHex(lx.src[lx.i]) || lx.src[lx.i] == '_') {
advance(lx)
}
} else {
while lx.i < lx.src.len && (isDigit(lx.src[lx.i]) || lx.src[lx.i] == '_') {
advance(lx)
}
if peek(lx, 0) == '.' && isDigit(peek(lx, 1)) {
t.kind = "float"
advance(lx)
while lx.i < lx.src.len && (isDigit(lx.src[lx.i]) || lx.src[lx.i] == '_') {
advance(lx)
}
}
}
t.text = lx.src.sub(start, lx.i).replace("_", "")
} else if c == '"' || c == '\'' {
quote := c
t.kind = "string"
if quote == '\'' {
t.kind = "char"
}
advance(lx)
[]str parts
while peek(lx, 0) != quote {
if peek(lx, 0) == char(0) || peek(lx, 0) == '\n' {
if quote == '"' {
fail(lx, t.line, t.col, "string is never closed (missing \")")
}
fail(lx, t.line, t.col, "char is never closed (missing ')")
}
ch := peek(lx, 0)
advance(lx)
if ch == '\\' {
e := peek(lx, 0)
advance(lx)
ch = escape(lx, e)
}
parts.push(str(ch))
}
advance(lx)
t.text = parts.join("")
if quote == '\'' && t.text.len != 1 {
fail(lx, t.line, t.col, "a char holds exactly one character, like 'a'")
}
} else {
op := operator(lx)
if op.len == 0 {
fail(lx, t.line, t.col, "unexpected character '" + str(c) + "'")
}
for k in 0..op.len {
advance(lx)
}
t.kind = op
t.text = op
}
out.push(t)
}
out.push(Token(kind: "eof", text: "", line: lx.line, col: lx.col, nl: true))
return out
}

116
boot/main.fch Normal file
View file

@ -0,0 +1,116 @@
// finch-boot: the Finch compiler written in Finch.
//
// finch-boot build <file.fch> [-o name] compile a program
// finch-boot ir <file.fch> print the LLVM IR
//
// It reads the program and the modules it imports, checks it, writes LLVM IR as text
// and lets clang turn that into a program, linked with Finch's C runtime.
import lexer
import ast
import parser
import gen
fn dirOf(str path) -> str {
i := path.len - 1
while i >= 0 && path[i] != '/' {
i -= 1
}
if i < 0 {
return "."
}
return path.sub(0, i)
}
fn stem(str path) -> str {
name := path.sub(dirOf(path).len + 1, path.len)
if dirOf(path) == "." && !path.starts_with("./") {
name = path
}
dot := name.find(".")
if dot > 0 {
return name.sub(0, dot)
}
return name
}
struct Loader {
[]ast.Program progs
[]str files
[]str loaded
}
fn load(ptr[Loader] l, str path, str module) {
if !file_exists(path) {
if module.len > 0 {
print("error: can't find the module '" + module + "' (looked for " + path + ")")
} else {
print("error: can't open '" + path + "'")
}
exit(1)
}
src := read_file(path)
file := l.files.len
l.files.push(path)
prog := parser.parse(lexer.lex(src, path), file, path)
prog.module = module
prog.path = path
imports := prog.imports
l.progs.push(prog)
for name in imports {
if !l.loaded.contains(name) {
l.loaded.push(name)
load(l, dirOf(path) + "/" + name + ".fch", name)
}
}
}
fn usage() {
print("finch-boot - the Finch compiler written in Finch")
print("usage: finch-boot build <file.fch> [-o name]")
print(" finch-boot ir <file.fch>")
exit(1)
}
fn main([]str args) -> int {
if args.len < 3 {
usage()
}
cmd := args[1]
file := args[2]
out := stem(file)
if args.len == 5 && args[3] == "-o" {
out = args[4]
} else if args.len != 3 {
usage()
}
if cmd != "build" && cmd != "ir" {
usage()
}
loader := Loader()
load(addr(loader), file, "")
ir := gen.generate(loader.progs, loader.files)
if cmd == "ir" {
print(ir)
return 0
}
// the runtime is Finch's C file; FINCH_RT can point somewhere else
rt := dirOf(args[0]) + "/../runtime/finch_rt.c"
if file_exists("runtime/finch_rt.c") {
rt = "runtime/finch_rt.c"
}
if !file_exists(rt) {
print("error: can't find the Finch runtime (runtime/finch_rt.c)")
return 1
}
llPath := out + ".ll"
write_file(llPath, ir)
code := shell("clang -O2 -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm")
shell("rm -f '" + llPath + "'")
if code != 0 {
print("error: clang failed")
return 1
}
return 0
}

501
boot/parser.fch Normal file
View file

@ -0,0 +1,501 @@
// The Finch parser, written in Finch: tokens -> ast.Program.
import lexer
import ast
struct Parser {
[]lexer.Token toks
int i
int depth // inside ( ) and [ ] newlines don't end anything
int file
str path
}
fn fail(ptr[Parser] p, lexer.Token t, str msg) {
print(p.path + ":" + str(t.line) + ":" + str(t.col) + ": error: " + msg)
exit(1)
}
fn cur(ptr[Parser] p) -> lexer.Token {
return p.toks[p.i]
}
fn curKind(ptr[Parser] p) -> str {
return p.toks[p.i].kind
}
fn peekKind(ptr[Parser] p, int off) -> str {
k := p.i + off
if k >= p.toks.len {
k = p.toks.len - 1
}
return p.toks[k].kind
}
fn at(ptr[Parser] p, str k) -> bool {
return p.toks[p.i].kind == k
}
fn next(ptr[Parser] p) -> lexer.Token {
t := p.toks[p.i]
p.i += 1
return t
}
fn accept(ptr[Parser] p, str k) -> bool {
if at(p, k) {
p.i += 1
return true
}
return false
}
fn describe(lexer.Token t) -> str {
if t.kind == "ident" {
return "'" + t.text + "'"
}
if t.kind == "eof" {
return "end of file"
}
if t.kind == "int" || t.kind == "float" {
return "a number"
}
if t.kind == "string" {
return "a string"
}
if t.kind == "char" {
return "a char"
}
return "'" + t.kind + "'"
}
fn expect(ptr[Parser] p, str k, str what) -> lexer.Token {
if !at(p, k) {
fail(p, cur(p), "expected " + what + ", found " + describe(cur(p)))
}
return next(p)
}
fn sameLine(ptr[Parser] p) -> bool {
return p.depth > 0 || !p.toks[p.i].nl
}
fn node(ptr[Parser] p, str kind) -> ast.Node {
t := p.toks[p.i]
return ast.Node(kind: kind, line: t.line, col: t.col, file: p.file)
}
fn isTypeName(str n) -> bool {
return n == "int" || n == "float" || n == "bool" || n == "char" || n == "str" || n == "ptr"
}
// int, str, ptr[T], []T, Name, module.Name
fn parseType(ptr[Parser] p) -> ast.Type {
if accept(p, "[") {
expect(p, "]", "']' (an array type looks like []int)")
return ast.arrayOf(parseType(p))
}
if !at(p, "ident") {
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (expected a type like int, str, []int or a struct name)")
}
name := next(p).text
if isTypeName(name) {
if name == "ptr" && accept(p, "[") {
t := ast.ptrTo(parseType(p))
expect(p, "]", "']'")
return t
}
return ast.simple(name)
}
t := ast.Type(kind: "named", name: name)
if at(p, ".") && peekKind(p, 1) == "ident" {
next(p)
t.module = name
t.name = next(p).text
}
return t
}
fn atDeclaration(ptr[Parser] p) -> bool {
if at(p, "[") {
return peekKind(p, 1) == "]"
}
if !at(p, "ident") {
return false
}
n := peekKind(p, 1)
if n == "ident" {
return !p.toks[p.i + 1].nl
}
if cur(p).text == "ptr" && n == "[" {
return true
}
return n == "." && peekKind(p, 2) == "ident" && peekKind(p, 3) == "ident" && !p.toks[p.i + 3].nl
}
fn endOfStatement(ptr[Parser] p) {
if at(p, "}") || at(p, "eof") || cur(p).nl {
return
}
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (put each statement on its own line)")
}
// ---------- top level ----------
fn parseProgram(ptr[Parser] p) -> ast.Program {
prog := ast.Program()
while !at(p, "eof") {
if at(p, "import") {
t := next(p)
if !at(p, "ident") {
fail(p, t, "the self-hosted compiler only imports Finch modules (import name)")
}
prog.imports.push(next(p).text)
} else if at(p, "struct") {
prog.structs.push(parseStruct(p))
} else {
prog.fns.push(parseFn(p))
}
}
return prog
}
fn parseStruct(ptr[Parser] p) -> ast.Node {
s := node(p, "struct")
expect(p, "struct", "'struct'")
s.name = expect(p, "ident", "a struct name").text
expect(p, "{", "'{'")
while !at(p, "}") {
if at(p, "eof") {
fail(p, cur(p), "a struct is never closed (missing '}')")
}
f := node(p, "field")
if !atDeclaration(p) {
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (a field looks like: int x)")
}
f.ty = parseType(p)
f.name = expect(p, "ident", "a field name").text
if accept(p, "=") {
f.kids.push(parseExpr(p))
}
s.kids.push(f)
endOfStatement(p)
}
next(p)
return s
}
fn parseFn(ptr[Parser] p) -> ast.Node {
f := node(p, "fn")
expect(p, "fn", "'fn', 'struct' or 'import'")
f.name = expect(p, "ident", "a function name").text
expect(p, "(", "'('")
p.depth += 1
if !at(p, ")") {
while true {
prm := node(p, "param")
prm.ty = parseType(p)
prm.name = expect(p, "ident", "a parameter name").text
f.kids.push(prm)
if !accept(p, ",") {
break
}
}
}
p.depth -= 1
expect(p, ")", "')'")
f.ty = ast.simple("void")
if accept(p, "->") {
f.ty = parseType(p)
}
f.kids.push(parseBlock(p))
return f
}
// ---------- statements ----------
fn parseBlock(ptr[Parser] p) -> ast.Node {
blk := node(p, "block")
expect(p, "{", "'{'")
saved := p.depth
p.depth = 0
while !at(p, "}") {
if at(p, "eof") {
fail(p, cur(p), "a '{' is never closed (missing '}')")
}
blk.kids.push(parseStatement(p))
endOfStatement(p)
}
p.depth = saved
next(p)
return blk
}
fn parseStatement(ptr[Parser] p) -> ast.Node {
k := curKind(p)
if k == "if" {
return parseIf(p)
}
if k == "while" {
s := node(p, "while")
next(p)
s.kids.push(parseExpr(p))
s.kids.push(parseBlock(p))
return s
}
if k == "for" {
s := node(p, "for")
next(p)
s.name = expect(p, "ident", "a loop variable name").text
expect(p, "in", "'in'")
s.kids.push(parseExpr(p))
if accept(p, "..") {
s.kids.push(parseExpr(p))
} else {
s.kind = "each"
}
s.kids.push(parseBlock(p))
return s
}
if k == "return" {
s := node(p, "return")
next(p)
if !at(p, "}") && !cur(p).nl {
s.kids.push(parseExpr(p))
}
return s
}
if k == "break" || k == "continue" {
s := node(p, k)
next(p)
return s
}
if k == "{" {
return parseBlock(p)
}
if k == "fn" || k == "struct" || k == "import" || k == "defer" || k == "link" {
fail(p, cur(p), "'" + k + "' can't be used here in the self-hosted compiler")
}
if atDeclaration(p) {
s := node(p, "decl")
s.flag = true
s.ty = parseType(p)
s.name = expect(p, "ident", "a variable name").text
if accept(p, "=") {
s.kids.push(parseExpr(p))
}
return s
}
if at(p, "ident") && peekKind(p, 1) == ":=" {
s := node(p, "decl")
s.name = next(p).text
next(p)
s.kids.push(parseExpr(p))
return s
}
start := cur(p)
e := parseExpr(p)
ops := ["=", "+=", "-=", "*=", "/=", "%="]
if ops.contains(curKind(p)) && sameLine(p) {
if e.kind != "var" && e.kind != "member" && e.kind != "index" {
fail(p, start, "only a variable, a field, an element or p.value can be assigned to")
}
s := ast.Node(kind: "assign", line: start.line, col: start.col, file: p.file)
s.name = next(p).kind
s.kids.push(e)
s.kids.push(parseExpr(p))
return s
}
if e.kind != "call" && e.kind != "method" {
fail(p, start, "this value is computed but never used")
}
s := ast.Node(kind: "expr", line: start.line, col: start.col, file: p.file)
s.kids.push(e)
return s
}
fn parseIf(ptr[Parser] p) -> ast.Node {
s := node(p, "if")
expect(p, "if", "'if'")
s.kids.push(parseExpr(p))
s.kids.push(parseBlock(p))
if accept(p, "else") {
if at(p, "if") {
s.kids.push(parseIf(p))
} else {
s.kids.push(parseBlock(p))
}
}
return s
}
// ---------- expressions ----------
fn binaryLevel(str op) -> int {
if op == "||" {
return 1
}
if op == "&&" {
return 2
}
if op == "==" || op == "!=" || op == "<" || op == "<=" || op == ">" || op == ">=" {
return 3
}
if op == "+" || op == "-" || op == "|" || op == "^" {
return 4
}
if op == "*" || op == "/" || op == "%" || op == "<<" || op == ">>" || op == "&" {
return 5
}
return 0
}
fn parseExpr(ptr[Parser] p) -> ast.Node {
return parseBinary(p, 1)
}
// precedence climbing; all binary operators are left-associative
fn parseBinary(ptr[Parser] p, int level) -> ast.Node {
if level > 5 {
return parseUnary(p)
}
left := parseBinary(p, level + 1)
while sameLine(p) && binaryLevel(curKind(p)) == level {
e := node(p, "binary")
e.name = next(p).kind
e.kids.push(left)
e.kids.push(parseBinary(p, level + 1))
left = e
}
return left
}
fn parseUnary(ptr[Parser] p) -> ast.Node {
if at(p, "-") || at(p, "!") || at(p, "~") {
e := node(p, "unary")
e.name = next(p).kind
e.kids.push(parseUnary(p))
return e
}
return parsePostfix(p)
}
fn parseArgs(ptr[Parser] p, ptr[ast.Node] call) {
expect(p, "(", "'('")
p.depth += 1
if !at(p, ")") {
while !at(p, ")") {
name := ""
if at(p, "ident") && peekKind(p, 1) == ":" {
name = next(p).text
next(p)
}
call.names.push(name)
call.kids.push(parseExpr(p))
if !accept(p, ",") {
break
}
}
}
p.depth -= 1
expect(p, ")", "')'")
}
fn parsePostfix(ptr[Parser] p) -> ast.Node {
e := parsePrimary(p)
while sameLine(p) {
if at(p, ".") {
m := node(p, "member")
next(p)
m.name = expect(p, "ident", "a name after '.'").text
m.kids.push(e)
if at(p, "(") && !cur(p).nl {
m.kind = "method"
parseArgs(p, addr(m))
}
e = m
} else if at(p, "[") {
ix := node(p, "index")
next(p)
p.depth += 1
ix.kids.push(e)
ix.kids.push(parseExpr(p))
p.depth -= 1
expect(p, "]", "']'")
e = ix
} else {
return e
}
}
return e
}
fn parsePrimary(ptr[Parser] p) -> ast.Node {
t := cur(p)
e := node(p, "")
k := t.kind
if k == "int" || k == "float" || k == "char" {
next(p)
e.kind = k
e.text = t.text
return e
}
if k == "string" {
next(p)
e.kind = "str"
e.text = t.text
return e
}
if k == "true" || k == "false" {
next(p)
e.kind = "bool"
e.text = k
return e
}
if k == "null" {
next(p)
e.kind = "null"
return e
}
if k == "(" {
next(p)
p.depth += 1
inner := parseExpr(p)
p.depth -= 1
expect(p, ")", "')'")
return inner
}
if k == "[" {
next(p)
e.kind = "array"
p.depth += 1
while !at(p, "]") {
e.kids.push(parseExpr(p))
if !accept(p, ",") {
break
}
}
p.depth -= 1
expect(p, "]", "']'")
return e
}
if k == "ident" {
next(p)
e.name = t.text
if at(p, "(") && !cur(p).nl {
e.kind = "call"
parseArgs(p, addr(e))
return e
}
e.kind = "var"
return e
}
fail(p, t, "unexpected " + describe(t) + " (expected a value)")
return e
}
fn parse([]lexer.Token toks, int file, str path) -> ast.Program {
parser := Parser(toks: toks, i: 0, depth: 0, file: file, path: path)
return parseProgram(addr(parser))
}

View file

@ -1,50 +0,0 @@
# The seed
Finch's compiler is written in Finch. To build it you need something that already runs Finch: the **seed**, which is
the compiler's own LLVM IR, one file per system:
| File | System | Built and tested in CI on |
|---|---|---|
| `linux-x86_64.ll` | Linux x86-64 | ubuntu-24.04 |
| `linux-aarch64.ll` | Linux ARM64 | ubuntu-24.04-arm |
| `macos-arm64.ll` | macOS, Apple Silicon | macos-15 |
| `macos-x86_64.ll` | macOS, Intel | macos-15-intel |
| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | windows-2022 |
Each CI job builds the compiler from its seed, then from itself, runs every test, and packs the result; a tag `v*` attaches
the packages to the release.
`bootstrap/build.sh` compiles the seed with `clang` (and the runtime, and libclang) into `build/finch0`; `finch0` builds
`src/` into `finch1`; `finch1` builds `src/` again into `build/finch`; the two must make the same IR, or the script stops.
## Why one per system
The seed includes how this system's C compiler passes structs by value, because the compiler calls libclang with
`CXCursor`, `CXType` and `CXString` (the calling convention is different on x86-64, on ARM64 and on Windows). So a seed
made for one system doesn't run on another, and a seed needs the target system's C headers to be made.
## When to make a new one
When the compiler's own source starts to use a feature that the committed seed doesn't have, `build.sh` fails at the first
step. Make the seed from a working compiler:
```sh
bootstrap/build.sh seed # writes bootstrap/<this system>.ll
```
For another system, from a machine that has that system's headers and a cross compiler (what `tests/cross.sh` uses):
```sh
FINCH_CC=aarch64-linux-gnu-gcc build/finch ir src/main.fch --target arm64 > bootstrap/linux-aarch64.ll
```
## Windows and macOS
Windows: `windows-x86_64.ll` is made by `bootstrap/winseed.sh` on Linux (llvm-mingw's MinGW headers and the headers of an
official LLVM for Windows); `bootstrap/winbuild.sh` turns it into `finch.exe` for trying under Wine. On a Windows machine
`bootstrap/build.sh` does the same from Git Bash: put llvm-mingw's `bin` in PATH and set `FINCH_LIBCLANG` (the path of
`libclang.dll`), `FINCH_LLVM_INCLUDE` (its `include` folder) and `FINCH_LDFLAGS=-L<folder of libclang.dll>`.
macOS: the seeds were made with the macOS 11.3 SDK. `brew install llvm@21`
gives clang and libclang; set `FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE` as the CI job does (the
[technicians' guide](../docs/en/for-technicians.md#option-b-build-it-yourself) has the lines).

View file

@ -1,69 +0,0 @@
#!/bin/sh
# Builds the Finch compiler without a compiler for Finch: from the seed (the compiler's own LLVM IR),
# then from itself, and checks that the two come out the same.
#
# bootstrap/build.sh build/finch
# bootstrap/build.sh seed also write bootstrap/<system>.ll anew (do this when the compiler got a feature
# its own source uses, or the seed is old: the next build starts from it)
#
# Needs clang and libclang (the C headers are read through it). The compiler is then in build/finch. It carries the
# runtime and the standard library inside itself (src/embedded.fch), so it runs from anywhere.
set -eu
cd "$(dirname "$0")/.."
mkdir -p build
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) seed=linux-x86_64 ;;
Linux-aarch64|Linux-arm64) seed=linux-aarch64 ;;
Darwin-arm64) seed=macos-arm64 ;;
Darwin-x86_64) seed=macos-x86_64 ;;
MINGW*|MSYS*|CYGWIN*) seed=windows-x86_64 ;;
*) echo "bootstrap: no seed for $(uname -s) $(uname -m) (see bootstrap/README.md)" >&2; exit 1 ;;
esac
EXE=
case $seed in windows-*) EXE=.exe ;; esac
[ -f "bootstrap/$seed.ll" ] || { echo "bootstrap: bootstrap/$seed.ll is missing" >&2; exit 1; }
CLANG=${FINCH_CLANG:-clang}
LIBS=${FINCH_LIBCLANG:--lclang}
INC=
# where clang-c/Index.h is, if clang does not find it itself (Debian and Ubuntu keep it in /usr/lib/llvm-N/include)
if [ -z "${FINCH_LLVM_INCLUDE:-}" ] && command -v llvm-config > /dev/null 2>&1 \
&& [ -f "$(llvm-config --includedir)/clang-c/Index.h" ]; then
FINCH_LLVM_INCLUDE=$(llvm-config --includedir)
fi
[ -z "${FINCH_LLVM_INCLUDE:-}" ] || INC="-I $FINCH_LLVM_INCLUDE"
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
echo "bootstrap: seed ($seed) -> build/finch0"
$CLANG -O1 -w "bootstrap/$seed.ll" runtime/finch_rt.c -o build/finch0$EXE -lm $LIBS
echo "bootstrap: build/finch0$EXE packs the runtime and std into src/embedded.fch"
bootstrap/pack.sh build/finch0$EXE
echo "bootstrap: build/finch0$EXE builds the compiler -> build/finch1"
build/finch0$EXE build src/main.fch -o build/finch1$EXE $INC
echo "bootstrap: build/finch1$EXE builds the compiler -> build/finch"
build/finch1$EXE build src/main.fch -o build/finch$EXE $INC
# a compiler that builds itself must come out the same however many times it does
build/finch1$EXE ir src/main.fch $INC > build/stage1.ll
build/finch$EXE ir src/main.fch $INC > build/stage2.ll
if cmp -s build/stage1.ll build/stage2.ll; then
echo "bootstrap: build/finch1$EXE and build/finch make the same IR ($(wc -l < build/stage2.ll) lines): done"
else
echo "bootstrap: the compiler does not reproduce itself (build/stage1.ll and build/stage2.ll differ)" >&2
exit 1
fi
if [ "${1:-}" = seed ]; then
cp build/stage2.ll "bootstrap/$seed.ll"
echo "bootstrap: wrote bootstrap/$seed.ll"
fi

View file

@ -1,14 +0,0 @@
#!/bin/sh
# Installs the compiler you built (bootstrap/build.sh) under a prefix:
# bootstrap/install.sh /usr/local (needs sudo)
# bootstrap/install.sh ~/.local
# finch goes to <prefix>/bin; the runtime and the standard library go to <prefix>/share/finch, where finch looks for them.
set -eu
cd "$(dirname "$0")/.."
prefix=${1:-/usr/local}
[ -x build/finch ] || { echo "install: run bootstrap/build.sh first" >&2; exit 1; }
mkdir -p "$prefix/bin" "$prefix/share/finch"
cp build/finch "$prefix/bin/finch"
rm -rf "$prefix/share/finch/runtime" "$prefix/share/finch/std"
cp -r runtime std "$prefix/share/finch/"
echo "installed: $prefix/bin/finch ($("$prefix/bin/finch" version))"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +0,0 @@
#!/bin/sh
# Writes src/embedded.fch: the runtime and the standard library as text, so the compiler can carry them inside itself.
# bootstrap/pack.sh <a Finch compiler> [out.fch]
set -eu
cd "$(dirname "$0")/.."
FINCH_RT=$PWD/runtime/finch_rt.c "$1" run tools/pack.fch "${2:-src/embedded.fch}" runtime/finch_rt.c std/*.fch std/*.h

View file

@ -1,8 +0,0 @@
#!/bin/sh
# Builds build/win/finch.exe from bootstrap/windows-x86_64.ll on Linux (to try the Windows compiler under Wine):
# bootstrap/winbuild.sh <llvm-mingw dir> <LLVM-for-Windows dir>
set -eu
cd "$(dirname "$0")/.."
mkdir -p build/win
"$1/bin/x86_64-w64-mingw32-clang" -O1 -w bootstrap/windows-x86_64.ll runtime/finch_rt.c -o build/win/finch.exe "$2/bin/libclang.dll"
cp "$2/bin/libclang.dll" build/win/

File diff suppressed because one or more lines are too long

View file

@ -1,9 +0,0 @@
#!/bin/sh
# Makes bootstrap/windows-x86_64.ll from Linux, with a Linux-hosted llvm-mingw (MinGW headers) and the headers of an
# official LLVM for Windows (clang-c/Index.h):
# bootstrap/winseed.sh <finch> <llvm-mingw dir> <LLVM-for-Windows dir>
set -eu
cd "$(dirname "$0")/.."
finch=$1; mingw=$2; llvm=$3
PATH="$mingw/bin:$PATH" FINCH_CC="$mingw/bin/x86_64-w64-mingw32-clang" "$finch" ir src/main.fch --target windows -I "$llvm/include" > bootstrap/windows-x86_64.ll
echo "wrote bootstrap/windows-x86_64.ll"

View file

@ -1,501 +0,0 @@
# Finch Cheat Sheet
Everything in Finch on one page. For explanations, see the [guides](../../README.md#documentation).
> 🇵🇱 Polska wersja: [sciaga.md](../pl/sciaga.md)
---
## The `finch` command
| Command | Does |
|---|---|
| `finch run file.fch [args…]` | compile and run (args go to the program) |
| `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) |
| `finch build file.fch --shared [-I dir]` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) |
| `finch test file.fch` | run its `test "…" { }` blocks, each on its own (and those of the modules it imports) |
| `finch fmt [--check] [files/folders]` | indent by `{ }` with 4 spaces, no spaces at line ends, at most one empty line (`--check`: only list what it would change) |
| `finch run` / `build` / `test` with no file | in a folder with a `finch.toml`: its `entry` |
| `finch ir file.fch` | print the LLVM IR |
| `finch lsp` | the language server (editors use it) |
| `finch version` | version |
| `-l lib` | link a C library (like `link "lib"`) |
| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | build for another system (`android`: the NDK's clang from `ANDROID_NDK_HOME`, usually with `--shared`) |
| `-g` | debug info (gdb, lldb) |
| `-O0` | no optimizations |
| `--cpu native` / `x86-64-v3` / `apple-m1` … | use every instruction of that processor (AVX2, AVX-512, NEON); the program then needs one like it |
| `--trace` / `--no-trace` | runtime errors show the chain of calls; on for `run`, `test`, `-g`, `-O0`, off for a plain `build` (it costs speed) |
Environment: `FINCH_PATH` (folders with modules), `FINCH_CC` (the C compiler that links), `FINCH_CXX` (for `.cpp` files).
A program's compiled code and its `link "x.c"` files are kept in the cache (`~/.cache/finch`) until they or their headers change.
`finch.toml` (next to the code; `finch run` there needs no file and no flags):
```toml
name = "game" # what finch build makes
entry = "src/main.fch"
paths = ["src"] # folders with modules
include = ["vendor/SDL3/include"] # C headers
links = ["SDL3", "native/glue.cpp"] # libraries and your own .c / .cpp / .o / .a
libdirs = ["vendor/SDL3/lib"]
cflags = ["-DFAST"] # for your .c / .cpp files
ldflags = ["-Wl,-rpath,."]
cpu = "native" # the same as --cpu native
[target.windows] # only when building for that system (also linux, macos, android, linux-arm64)
links = ["ws2_32"]
```
---
## A file
```c
import "stdio.h" // a C header
import shapes // a Finch module: shapes.fch
link "m" // a C library, or "mine.c" / ".o" / ".a"
struct Point {
int x
int y = 0 // default value
}
fn main() {
print("Hello")
}
```
`main` can be `fn main()`, `fn main() -> int` (exit code) or `fn main([]str args)`.
One statement per line, no semicolons, `{` on the same line, no brackets around conditions.
---
## Variables
| Code | Meaning |
|---|---|
| `x := 5` | new variable, type inferred |
| `int x = 5` | new variable, type written |
| `int x` | zero / `""` / `[]` / `null` / the struct's defaults (also of the structs inside it) |
| `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) |
---
## Types
| Type | Example |
|---|---|
| `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` |
| `bool` | `true`, `false` |
| `char` | `'A'`, `'\n'` |
| `str` | `"text\t\"quoted\""`, `"hp \(hp), speed \(v, 2)"` (code in the text; `, 2` = two digits after the point), `"""` several lines `"""` (the closing quotes' indentation is removed) |
| `i8 i16 i32 i64` | signed, sized |
| `u8 u16 u32 u64` | unsigned, sized |
| `f32 f64` | floats |
| `[]T` | array: `[1, 2, 3]`, `[]str names` |
| `map[K]V` | map: `["a": 1]`, `map[str]int counts`, `[:]` |
| `ptr[T]`, `ptr` | pointer, untyped pointer |
| `f32x8`, `i8x32`, `f64x4`, `u16x16` … | vector: N numbers worked on at once (SIMD), see below |
| `Name` / `module.Name` | a struct |
Escapes: `\n \t \r \0 \\ \" \'`.
---
## Operators
| Priority | Operators |
|---|---|
| 5 | `*` `/` `%` `<<` `>>` `&` |
| 4 | `+` `-` `\|` `^` (`+` also joins text) |
| 3 | `==` `!=` `<` `<=` `>` `>=` |
| 2 | `&&` |
| 1 | `\|\|` |
| unary | `-x` `!x` `~x` |
`x & 1 == 0` means `(x & 1) == 0`.
---
## Control flow
```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)
for i, item in list { … } // with its index
for key, value in m { … } // every map entry, in insertion order
break continue return value
defer cleanup() // runs when the block ends
```
---
## Functions
```c
fn add(int a, int b) -> int {
return a + b
}
fn greet(str name) { // no result
print("Hi", name)
}
```
Parameters behave like copies. Order in the file doesn't matter. A comma after the last argument is allowed.
---
## Methods
```c
fn Point.move(int dx, int dy) { // self is the Point it's called on (not a copy)
self.x += dx
self.y += dy
}
fn Point.dist() -> float { … }
p.move(1, 2) list[0].move(1, 1) ptr_to_point.move(0, 1)
```
Also on structs from C: `fn Vector2.length() -> f32 { … }`.
---
## Errors as values
```c
fn parse(str s) -> int! { // ! = can fail
if s.len == 0 {
return error("empty")
}
return int(s) or { return error("not a number: " + s) }
}
fn save(str p, str t) -> ! { // can fail, no result
try write_file(p, t)
}
```
| Code | On failure |
|---|---|
| `x := parse(s) or 0` | use 0 |
| `x := parse(s) or { print(err); return }` | run the block (`err`: the message) |
| `x := try parse(s)` | fail the current function too |
| `fn main() -> !` | a failure prints `error: …`, exit code 1 |
Fallible built-ins: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
---
## Structs
| Code | Meaning |
|---|---|
| `struct P { … }` | define (one field per line, `= default` optional) |
| `P(1, 2)` | create, all fields in order |
| `P(x: 1)` | create by name, the rest default/zero |
| `p.x`, `p.x = 5` | read, change a field |
| `b := a` | a full copy |
---
## Arrays `[]T`
| Code | Gives |
|---|---|
| `a.len` | number of elements |
| `a[i]`, `a[i] = x` | element (checked) |
| `a.push(x)` | add at the end |
| `a.pop()` | remove the last, give it back |
| `a.insert(i, x)` | put at position i |
| `a.remove(i)` | remove position i, give it back |
| `a.clear()` | empty it |
| `a.resize(n)` | length n (new elements are zero) |
| `a.find(x)` | index or -1 |
| `a.contains(x)` | bool |
| `a.sort()` | sort (numbers, chars, str) |
| `a.reverse()` | reverse |
| `a.slice(s, e)` | new array of s … e-1 |
| `a.join(sep)` | `[]str` → one str |
| `a.ptr` | address of the first element (for C) |
---
## Maps `map[K]V`
| Code | Gives |
|---|---|
| `m := ["a": 1, "b": 2]` | a map (keys: numbers, char, bool, str) |
| `m[k]` | value (missing key: the program stops) |
| `m[k] = v` | add / replace |
| `m[k] += 1`, `m[k].push(x)` | a missing key starts at the default (0, `[]`, …) |
| `m.len` | number of keys |
| `m.has(k)` | bool |
| `m.get(k, default)` | value or default |
| `m.remove(k)` | bool: was it there |
| `m.clear()` | empty it |
| `m.keys()`, `m.values()` | arrays, insertion order |
---
## Vectors `f32x8` (SIMD)
Types: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i64x2/4/8`, and `u…` the same; at most 512 bits.
| Code | Gives |
|---|---|
| `f32x8(x)` | `x` in every lane |
| `f32x8(a, b, …, h)` | one number per lane |
| `f32x8(list, i)` | `list[i]` … `list[i+7]` (a `[]f32` or `ptr[f32]`) |
| `f32x8(v)` | an 8-lane vector converted lane by lane (`f32x8(ints)`, `i32x8(floats)`) |
| `v + w`, `v * 2.0`, `-v` | lane by lane; a plain number counts for every lane (`/` only on floats) |
| `& \| ^ << >> ~` | lane by lane, whole-number lanes |
| `v < w`, `v == w`, … | a mask `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` |
| `select(m, a, b)` | lane by lane: `a` where `m` is true, else `b` |
| `v[i]`, `v[i] = x`, `v.len` | a lane; the number of lanes |
| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane |
| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | one number from all the lanes |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) |
| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: one rounding always / the fastest way here |
| `v.store(list, i)` | lanes into `list[i]` … |
| `f32x8(list, i, fill)`, `v.store_part(list, i)` | the end of a list: only the lanes inside it (the others are `fill`) |
| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | lanes rearranged (shuffle takes numbers written in the code) |
| `table.lookup(idx)` | `table[idx % table.len]` per lane (`pshufb`, `tbl`) |
| `acc.dot_add(a, b)` | `i32x8` sums gain 4 byte products each (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` where there |
| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | a function that may use these features; check at run time before calling it |
---
## Text `str`
| Code | Gives |
|---|---|
| `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 |
| `s.starts_with(t)`, `s.ends_with(t)` | bool |
| `s.split(sep)` | `[]str` |
| `s.trim()` | no spaces at the ends |
| `s.upper()`, `s.lower()` | A–Z / a–z |
| `s.replace(a, b)` | every a → b |
| `s.repeat(n)` | n times |
| `s.bytes()` | `[]u8` |
| `s.ptr` | `ptr[char]` (for C; a `str` also goes to a `const char *` parameter as it is) |
| `==` `!=` `<` `>` | compare |
---
## Conversions
| Code | Result |
|---|---|
| `int(3.9)` | `3` |
| `int("42")` | `42` (stops the program if it isn't a number) |
| `int('A')` | `65` |
| `float(7)`, `float("2.5")` | `7`, `2.5` |
| `u8(300)` | `44` (keeps the low bits) |
| `char(66)` | `'B'` |
| `bool(0)` | `false` |
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | text (a float: the shortest text that is the same number) |
| `str(3.14159, 2)` | `"3.14"` (that many digits after the point) |
| `str([1, 2])`, `str(m)`, `str(point)` | the same text `print` shows |
| `int("x") or 0`, `float(s) or 0.0` | a number, or the fallback |
| `str(bytes)` | `[]u8` → text |
| `str(p)` | C `char*` → text |
| `ptr(p)`, `ptr(16)` | untyped pointer, from a pointer or a number |
| `int(p)` | pointer → address |
Automatic only when nothing is lost: smaller → bigger number, any int → float, `f32` → `f64`.
---
## Built-in functions
| Function | Does |
|---|---|
| `print(a, b, …)` | print, spaces between, newline at the end |
| `input()`, `input("Question? ")` | read a line |
| `read_file(path)` | file → str |
| `write_file(path, text)` | str → file, `true` if it worked |
| `file_exists(path)` | bool |
| `delete_file(path)` | bool |
| `shell(command)` | run a command, its exit code |
| `exit(code)` | stop the program now (also ends an `or { }` block) |
| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | for any numbers (your own function of that name wins) |
| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` with one rounding / fused where the processor can (`a * b + c` written out is never fused) |
| `f32_bits(x)`, `f32_from_bits(u)`, `f64_bits`, `f64_from_bits` | a float's bits as `u32`/`u64` and back, unchanged |
| `f16_to_f32(h)`, `f32_to_f16(x)`, `bf16_to_f32(b)`, `f32_to_bf16(x)` | half and bfloat16 numbers (in a `u16`) ↔ `f32`; also on vectors |
| `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 |
---
## Pointers
| Finch | C |
|---|---|
| `ptr[int] p` | `int *p` |
| `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]` |
| `null` | `NULL` |
| `ptr` | `void *` |
---
## Memory
- 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`, …).
---
## Modules
```c
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()
```
---
## C
| Code | Meaning |
|---|---|
| `import "x.h"` | functions, structs, enums, `#define` numbers, globals |
| `link "name"` | libname.so / name.lib (pkg-config first) |
| `link "file.c"`, `link "glue.cpp"` | compile your own C or C++ file (C++: functions in `extern "C"`) |
| `addr(myFn)` | pass a Finch function as a callback |
| C type | Finch |
|---|---|
| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (by size) |
| `float` / `double` | `f32` / `f64` |
| `char *` (parameter, result) | `str` |
| `char *` (field, behind a pointer) | `ptr[char]` |
| `struct X` | `X` |
| `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).
---
## Runtime errors (the program stops with file and line)
| Message | Cause |
|---|---|
| `division by zero` | `/` or `%` by 0 |
| `index 5 is out of range (the length is 3)` | `a[5]` |
| `pop() on an empty array` | |
| `used .value on a null pointer` | `.value` / `.field` / `[i]` through `null` |
| `can't turn "x" into int` | `int("x")` |
| `can't read the file "…"` | `read_file` (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)")
}
```
---
## Editors
Kate: `editors/kate/install.sh`, then enable the **LSP Client** plugin; run with **Build & Run** → `finch run %f`.
Any LSP editor: `finch lsp`.
### VS Code (extension "Finch")
| Key / action | Does |
|---|---|
| ▶ / `Ctrl+F5` | run the file |
| `Finch: Build This File` | build; errors go to Problems |
| `F12` | go to definition |
| hover | type / signature / docs |
| `Ctrl+Space` | completion (after `.`: fields, methods, module members) |
| `Ctrl+Shift+O` | functions and structs in the file |
| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | snippets |
| setting `finch.path` | where finch / finch.exe is |
---
## Enums and `match`
```
enum Color { Red, Green, Blue } enum Status { Ok = 200, NotFound = 404 }
c := Color.Green int(c) Color(2) c == Color.Red // == and != only; Color(7) is an error
fn Color.next() -> Color { ... } // methods work
match c { Red -> ... Green, Blue -> { ... } else -> ... } // bare variant names; every variant or else
x := match n { 1, 2 -> "few" else -> "many" } // an expression: every arm the same type
```
## Generics
```
fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // worked out, or written out
struct Stack[T] { []T items } fn Stack[T].push(T x) { ... } // the type goes with it:
s := Stack[int]() Stack[int] t Pair[str, int]("age", 31) // never just Stack()
```
## Standard library (`import name`; your own file of that name wins)
| | |
|---|---|
| `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)

View file

@ -53,9 +53,8 @@ The computer reads your program from top to bottom, line by line.
## 2. Getting Finch ready ## 2. Getting Finch ready
Finch runs on **Linux**, **macOS** and **Windows** computers. If a teacher, parent or friend has already set it up Finch runs on **Linux** computers. If a teacher, parent or friend has already set it up
for you, skip to the next section. (On a Mac or on Windows, ask someone to follow the for you, skip to the next section.
[guide for technicians](for-technicians.md#2-installing): it has a ready-made Finch for both.)
Otherwise, open the **Terminal** program (a window where you type commands) and type Otherwise, open the **Terminal** program (a window where you type commands) and type
these lines one by one, pressing **Enter** after each: these lines one by one, pressing **Enter** after each:
@ -64,7 +63,8 @@ these lines one by one, pressing **Enter** after each:
git clone https://github.com/Nexoniarz/Finch.git git clone https://github.com/Nexoniarz/Finch.git
cd Finch cd Finch
nix-shell nix-shell
bootstrap/build.sh cmake -S . -B build -G Ninja
ninja -C build
``` ```
If the last line finishes without the word `error`, Finch is ready. If the last line finishes without the word `error`, Finch is ready.
@ -77,7 +77,7 @@ To check it works, type:
./build/finch version ./build/finch version
``` ```
You should see something like `Finch 2.6.0`. You should see something like `finch 2.0.0`.
--- ---
@ -505,68 +505,8 @@ fn main() {
} }
``` ```
If you also want to know *where* you are in the list, give `for` two names: the first one counts
0, 1, 2…, the second one is the value:
```c
fn main() {
for i, fruit in ["apple", "banana", "cherry"] {
print(i + 1, fruit)
}
}
```
```
1 apple
2 banana
3 cherry
```
Lists can do more: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`. Lists can do more: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`.
The [guide for technicians](for-technicians.md#10-arrays-and-maps) lists them all. The [guide for technicians](for-technicians.md#arrays) lists them all.
### Looking things up by name: maps
A list finds things by their place: `fruits[0]`. Sometimes you want to find things by a **name**
instead, like looking up a word in a dictionary, or a phone number in a phone book.
That's a **map**: each **key** (the name) has a **value**:
```c
fn main() {
phone := ["Ola": "555-1234", "Jan": "555-9876"]
print(phone["Ola"]) // 555-1234
phone["Ewa"] = "555-0000" // add a new name
print(phone.len) // 3
if phone.has("Tom") {
print("Tom is here")
} else {
print("no Tom")
}
for name, number in phone { // every name with its number
print(name, number)
}
}
```
- `["key": value, ...]` makes a map. Keys are often text, but numbers work too.
- `phone["Ola"]` gets the value for a key. Asking for a key that isn't there stops the program,
so check first with `phone.has("Tom")`, or use `phone.get("Tom", "unknown")`.
- `for name, number in phone` visits every key with its value, in the order you added them.
Maps are great for **counting**. A new key starts at 0, so `+= 1` just works:
```c
fn main() {
map[str]int votes // an empty map: text keys, whole-number values
for v in ["cat", "dog", "cat", "cat", "fish"] {
votes[v] += 1
}
print(votes) // {"cat": 3, "dog": 1, "fish": 1}
}
```
--- ---
@ -678,36 +618,6 @@ fn main() {
And you can keep many of them in a list: `[]Book shelf`, then `shelf.push(b)`. And you can keep many of them in a list: `[]Book shelf`, then `shelf.push(b)`.
### Commands for your struct: methods
A struct can have its own commands. Write the struct's name, a dot, and the command's name.
Inside, `self` is the struct the command was used on:
```c
struct Player {
str name
int lives = 3
}
fn Player.hit() {
self.lives -= 1
print(self.name, "has", self.lives, "lives left")
}
fn Player.is_alive() -> bool {
return self.lives > 0
}
fn main() {
p := Player(name: "Ola")
p.hit() // Ola has 2 lives left
p.hit() // Ola has 1 lives left
print(p.is_alive()) // true
}
```
`p.hit()` changes `p` itself, so after two hits `p.lives` really is 1.
--- ---
## 15. Saving to a file ## 15. Saving to a file
@ -770,57 +680,6 @@ Here we simply misspelled `age`.
| `index 5 is out of range (the length is 3)` | You asked a list for a place it doesn't have | Remember counting starts at 0; check `.len` | | `index 5 is out of range (the length is 3)` | You asked a list for a place it doesn't have | Remember counting starts at 0; check `.len` |
| `can't turn "abc" into int` | `int(...)` got text that isn't a number | Check what the person typed | | `can't turn "abc" into int` | `int(...)` got text that isn't a number | Check what the person typed |
| `can't use '+' on str and int` | Text plus a number | Use `str(...)` on the number | | `can't use '+' on str and int` | Text plus a number | Use `str(...)` on the number |
| `'f' can fail, so say what happens then` | You used a command that can fail without saying what to do | Add `or ...` (see below) |
### Things that can fail: `or` and `try`
Some things can go wrong *while the program runs*: the person types `abc` where you wanted a number,
or a file isn't there. Instead of stopping the program, you can say what to do then, with **`or`**:
```c
fn main() {
age := int(input("Your age? ")) or 0 // not a number? use 0
print("Next year you'll be", age + 1)
note := read_file("note.txt") or "(no note yet)"
print(note)
}
```
`or` can also be followed by a block. Inside it, `err` is the message that says what went wrong:
```c
fn main() {
while true {
n := int(input("Pick a number: ")) or {
print("That's not a number, try again")
continue
}
print("You picked", n)
break
}
}
```
Your own functions can fail too. Put `!` after the result type, and use `return error("...")`:
```c
fn safe_divide(int a, int b) -> int! {
if b == 0 {
return error("can't divide by zero")
}
return a / b
}
fn main() {
print(safe_divide(10, 2) or -1) // 5
print(safe_divide(10, 0) or -1) // -1
}
```
Finch won't let you forget: calling `safe_divide(10, 0)` without `or` is an error that tells you
to handle it. (You can also pass the problem on with `try`; the
[guide for technicians](for-technicians.md#errors-as-values) explains it.)
--- ---
@ -1023,13 +882,10 @@ struct Pet { // your own kind of value
int age = 1 int age = 1
} }
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } for i, n in nums { } // lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { }
// maps: ages := ["Ola": 9] ages["Jan"] = 10 ages["Ola"] ages.has("Tom") for name, age in ages { }
// text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x") // text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
// ask: name := input("Name? ") // ask: name := input("Name? ")
// files: write_file("f.txt", text) read_file("f.txt") // files: write_file("f.txt", text) read_file("f.txt")
// methods: fn Pet.grow() { self.age += 1 } p.grow()
// failing: n := int(text) or 0 x := read_file("f.txt") or { print(err) ... }
``` ```
--- ---
@ -1049,9 +905,7 @@ struct Pet { // your own kind of value
| **condition** | A yes/no question, like `x > 5` | | **condition** | A yes/no question, like `x > 5` |
| **list / array** | Many values kept in order, like `[1, 2, 3]` | | **list / array** | Many values kept in order, like `[1, 2, 3]` |
| **index** | The position of a value in a list, starting at 0 | | **index** | The position of a value in a list, starting at 0 |
| **struct** | Your own kind of value made of named parts | | **struct** | Your own kind of value, made of named parts |
| **method** | A command that belongs to a struct, like `p.hit()` |
| **map** | Values you find by a key (a name), like a dictionary |
| **input** | What the person types into the program | | **input** | What the person types into the program |
| **error** | A message saying something is wrong, and where | | **error** | A message saying something is wrong, and where |
| **bug** | A mistake in a program | | **bug** | A mistake in a program |

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
# Finch dla początkujących # Finch dla początkujących
**Programowanie to dla ciebie coś zupełnie nowego? Ten przewodnik jest dla ciebie.** **Ten przewodnik jest dla ciebie, jeśli jeszcze nigdy nie pisałeś programu.**
Nie musisz nic umieć: wystarczy matematyka ze szkoły podstawowej. Nie potrzebujesz żadnego doświadczenia ani matematyki ponad to, czego uczy się w podstawówce.
Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak najlepiej zostaje w głowie. Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. Tak zostaje w głowie najlepiej.
> 🇬🇧 English version: [for-beginners.md](../en/for-beginners.md) > 🇬🇧 English version: [for-beginners.md](../en/for-beginners.md)
@ -13,7 +13,7 @@ Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak
1. [Co to jest program?](#1-co-to-jest-program) 1. [Co to jest program?](#1-co-to-jest-program)
2. [Przygotowanie Fincha](#2-przygotowanie-fincha) 2. [Przygotowanie Fincha](#2-przygotowanie-fincha)
3. [Twój pierwszy program](#3-twój-pierwszy-program) 3. [Twój pierwszy program](#3-twój-pierwszy-program)
4. [Wypisywanie na ekranie](#4-wypisywanie-na-ekranie) 4. [Wypisywanie na ekran](#4-wypisywanie-na-ekran)
5. [Pudełka z nazwami: zmienne](#5-pudełka-z-nazwami-zmienne) 5. [Pudełka z nazwami: zmienne](#5-pudełka-z-nazwami-zmienne)
6. [Rodzaje wartości](#6-rodzaje-wartości) 6. [Rodzaje wartości](#6-rodzaje-wartości)
7. [Liczenie](#7-liczenie) 7. [Liczenie](#7-liczenie)
@ -28,61 +28,63 @@ Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak
16. [Kiedy coś pójdzie nie tak](#16-kiedy-coś-pójdzie-nie-tak) 16. [Kiedy coś pójdzie nie tak](#16-kiedy-coś-pójdzie-nie-tak)
17. [Małe projekty do zrobienia](#17-małe-projekty-do-zrobienia) 17. [Małe projekty do zrobienia](#17-małe-projekty-do-zrobienia)
18. [Ściąga](#18-ściąga) 18. [Ściąga](#18-ściąga)
19. [Słowa, które będziesz słyszeć](#19-słowa-które-będziesz-słyszeć) 19. [Słowniczek](#19-słowniczek)
--- ---
## 1. Co to jest program? ## 1. Co to jest program?
Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu każesz, krok po kroku. Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu się każe,
krok po kroku.
**Program** to spisana lista takich kroków. Trochę jak przepis: **Program** to spisana lista takich kroków. Trochę jak przepis kuchenny:
> 1. Weź dwa jajka. > 1. Weź dwa jajka.
> 2. Wbij je do miski. > 2. Wbij je do miski.
> 3. Wymieszaj. > 3. Wymieszaj.
**Język programowania** to sposób zapisania takiego przepisu tak, żeby komputer go zrozumiał. **Język programowania** to sposób zapisania takiego przepisu, który komputer rozumie.
**Finch** to jeden z takich języków. Powstał po to, żeby był mały i prosty, więc szybko się go nauczysz. **Finch** (po polsku: zięba) jest jednym z takich języków. Zrobiono go tak, żeby był
mały i prosty, więc nauczysz się go szybko.
Komputer czyta program od góry do dołu, linia po linii. Komputer czyta program od góry do dołu, linijka po linijce.
--- ---
## 2. Przygotowanie Fincha ## 2. Przygotowanie Fincha
Finch działa na komputerach z **Linuksem**, **macOS** i **Windowsem**. Jeśli nauczyciel, rodzic albo znajomy Finch działa na komputerach z **Linuksem**. Jeśli nauczyciel, rodzic albo znajomy już go
już go dla ciebie przygotował, przejdź do następnego rozdziału. (Na Macu i na Windowsie poproś kogoś o skorzystanie dla ciebie zainstalował, przejdź od razu do następnego rozdziału.
z [przewodnika dla techników](dla-technikow.md#2-instalacja): są tam gotowe wersje Fincha na oba systemy.)
W przeciwnym razie otwórz program **Terminal** (okno, w którym wpisuje się polecenia) i wpisz po kolei te linie, W przeciwnym razie otwórz program **Terminal** (okienko, w którym wpisuje się polecenia)
naciskając po każdej **Enter**: i wpisz te linijki po kolei, naciskając **Enter** po każdej:
```sh ```sh
git clone https://github.com/Nexoniarz/Finch.git git clone https://github.com/Nexoniarz/Finch.git
cd Finch cd Finch
nix-shell nix-shell
bootstrap/build.sh cmake -S . -B build -G Ninja
ninja -C build
``` ```
Jeśli ostatnie polecenie skończy się bez słowa `error`, Finch jest gotowy. Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.
(Jeśli na twoim komputerze nie ma `nix-shell`, poproś kogoś o skorzystanie z (Jeśli komputer nie zna polecenia `nix-shell`, poproś kogoś o skorzystanie z
[przewodnika dla techników](dla-technikow.md#2-instalacja). Jest tam opisany inny sposób.) [przewodnika dla techników](dla-technikow.md#2-instalacja), gdzie opisano inny sposób.)
Żeby sprawdzić, czy wszystko działa, wpisz: Żeby sprawdzić, czy działa, wpisz:
```sh ```sh
./build/finch version ./build/finch version
``` ```
Powinien pojawić się napis w rodzaju `Finch 2.6.0`. Powinieneś zobaczyć coś w stylu `finch 2.0.0`.
--- ---
## 3. Twój pierwszy program ## 3. Twój pierwszy program
1. Otwórz dowolny edytor tekstu (np. *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*). 1. Otwórz dowolny edytor tekstu (na przykład *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*).
2. Przepisz dokładnie to: 2. Wpisz dokładnie to:
```c ```c
fn main() { fn main() {
@ -90,11 +92,11 @@ fn main() {
} }
``` ```
3. Zapisz plik w katalogu `Finch` pod nazwą **`hello.fch`**. Końcówka `.fch` mówi, że to program w Finchu. 3. Zapisz plik w folderze `Finch` pod nazwą **`czesc.fch`**. Końcówka `.fch` mówi, że to program w Finchu.
4. W Terminalu wpisz: 4. W Terminalu wpisz:
```sh ```sh
./build/finch run hello.fch ./build/finch run czesc.fch
``` ```
Zobaczysz: Zobaczysz:
@ -103,7 +105,7 @@ Zobaczysz:
Cześć! Cześć!
``` ```
🎉 **Właśnie udało ci się napisać i uruchomić program.** 🎉 **Właśnie napisałeś i uruchomiłeś program.**
### Co właściwie napisaliśmy? ### Co właściwie napisaliśmy?
@ -113,26 +115,26 @@ fn main() { ← „tu zaczyna się główna część mojego programu
} ← „tu główna część się kończy” } ← „tu główna część się kończy”
``` ```
- `fn main()` to miejsce, w którym **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`. - `fn main()` to miejsce, od którego **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`.
- Nawiasy klamrowe `{` i `}` obejmują wszystko, co należy do `main`, jak dwie okładki książki. - Nawiasy klamrowe `{` i `}` obejmują wszystko, co należy do `main`, jak dwie okładki książki.
- `print(...)` pokazuje coś na ekranie. - `print(...)` pokazuje coś na ekranie.
- Tekst zawsze stoi w cudzysłowie: `"Cześć!"`. - Tekst zawsze piszemy w cudzysłowie: `"Cześć!"`.
Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku nawiasów. Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku
Zawsze je dodawaj. To dobry nawyk. klamer. Zawsze je dodawaj, to dobry nawyk.
--- ---
## 4. Wypisywanie na ekranie ## 4. Wypisywanie na ekran
`print` potrafi pokazać tekst i liczby. Oddziel je przecinkami, a Finch sam wstawi między nimi spacje: `print` umie pokazać tekst i liczby. Między kolejne rzeczy wstaw przecinek, a Finch sam doda spację:
```c ```c
fn main() { fn main() {
print("Mam", 10, "lat") print("Mam", 10, "lat")
print("2 + 2 =", 2 + 2) print("2 + 2 =", 2 + 2)
print() print()
print("Linia wyżej jest pusta") print("Linijka wyżej jest pusta")
} }
``` ```
@ -142,10 +144,10 @@ Pokaże:
Mam 10 lat Mam 10 lat
2 + 2 = 4 2 + 2 = 4
Linia wyżej jest pusta Linijka wyżej jest pusta
``` ```
Każde `print` zaczyna nową linię. `print()` bez niczego w środku robi pustą linię. Każdy `print` zaczyna nową linijkę. `print()` bez niczego w środku robi pustą linijkę.
### Notatki dla siebie: komentarze ### Notatki dla siebie: komentarze
@ -162,8 +164,8 @@ fn main() {
## 5. Pudełka z nazwami: zmienne ## 5. Pudełka z nazwami: zmienne
Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a później sprawdzić, co jest w środku, Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a potem sprawdzić,
czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**. co jest w środku, czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**.
```c ```c
fn main() { fn main() {
@ -172,9 +174,9 @@ fn main() {
} }
``` ```
`wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”.* `wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”*.
Później możesz zmienić zawartość pudełka, pisząc samo `=`: Później możesz zmienić zawartość pudełka, używając samego `=`:
```c ```c
fn main() { fn main() {
@ -195,39 +197,42 @@ Punkty: 5
Punkty: 6 Punkty: 6
``` ```
`punkty = punkty + 1` czyta się tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”.* `punkty = punkty + 1` czytamy tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”*.
To samo można napisać krócej: `punkty += 1`. To samo można zapisać krócej: `punkty += 1`.
**Zapamiętaj:** **Zapamiętaj:**
- `:=` robi **nowe** pudełko. Dla każdej nazwy używasz tego tylko raz. - `:=` robi **nowe** pudełko. Używasz tego tylko raz dla danej nazwy.
- `=` wkłada nową wartość do pudełka, **które już istnieje**. - `=` wkłada nową wartość do pudełka, **które już istnieje**.
### Nazywanie pudełek ### Nazywanie pudełek
W nazwach można używać liter od `a` do `z` i od `A` do `Z`, cyfr i znaku `_`, ale nazwa nie może zaczynać się od cyfry: Nazwy mogą zawierać angielskie litery `a`–`z` i `A`–`Z`, cyfry i znak `_`, ale nie mogą zaczynać się od cyfry:
`wiek`, `suma_punktow`, `gracz2` są w porządku, a `2gracz` nie. `wiek`, `suma_punktow`, `gracz2` są w porządku. `2gracz` już nie.
Polskich liter (`ą`, `ę`, `ż`…) nie wolno używać w nazwach, tylko w tekstach w cudzysłowie: `print("Łódź")` jest w porządku.
⚠️ **Bez polskich liter w nazwach!** `ą`, `ę`, `ł`, `ż` i inne mogą być tylko w tekście w cudzysłowie.
Pisz `zolw`, a nie `żółw`. Natomiast `print("Żółw")` działa bez problemu.
Wybieraj nazwy, które mówią, co jest w środku: `cena` jest lepsza niż `c`. Wybieraj nazwy, które mówią, co jest w środku: `cena` jest lepsza niż `c`.
--- ---
## 6. Rodzaje wartości ## 6. Rodzaje wartości
Finch pilnuje, jaki *rodzaj* rzeczy leży w każdym pudełku. Taki rodzaj to **typ**. Finch pilnuje, *jakiego rodzaju* rzecz jest w każdym pudełku. Ten rodzaj nazywa się **typem**.
Najczęściej będziesz używać tych: Najczęściej będziesz używać tych:
| Typ | Co przechowuje | Przykłady | | Typ | Co przechowuje | Przykłady |
|---------|-----------------------------|--------------------------| |---------|---------------------------|--------------------------|
| `int` | liczby całkowite | `0`, `42`, `-7` | | `int` | liczby całkowite | `0`, `42`, `-7` |
| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` | | `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` |
| `str` | tekst | `"Cześć"`, `"Ola"` | | `str` | tekst | `"Cześć"`, `"Ola"` |
| `bool` | tak albo nie | `true`, `false` | | `bool` | tak albo nie | `true` (prawda), `false` (fałsz) |
| `char` | jedną literę albo znak | `'A'`, `'z'`, `'?'` | | `char` | jeden znak | `'A'`, `'z'`, `'?'` |
Uwaga: w programach część ułamkową oddziela się **kropką**, nie przecinkiem: `3.14`, a nie `3,14`. ⚠️ W liczbach zamiast przecinka piszemy **kropkę**: `3.14`, a nie `3,14`. Tak jest w prawie wszystkich językach programowania.
Przy `:=` Finch sam rozpoznaje typ: Kiedy używasz `:=`, Finch sam rozpoznaje typ:
```c ```c
imie := "Ola" // str imie := "Ola" // str
@ -236,7 +241,7 @@ wzrost := 1.42 // float
lubi_koty := true // bool lubi_koty := true // bool
``` ```
Typ można też napisać samemu, przed nazwą: Typ możesz też napisać sam, przed nazwą:
```c ```c
int wiek = 10 int wiek = 10
@ -245,23 +250,24 @@ str imie = "Ola"
Oba sposoby robią to samo. Wybierz ten, który ci bardziej pasuje. Oba sposoby robią to samo. Wybierz ten, który ci bardziej pasuje.
**Pudełko trzyma się swojego typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu. **Pudełko nie zmienia typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu.
Finch się zatrzyma i ci o tym powie, a to pomaga wcześnie wyłapywać pomyłki. Finch zatrzyma się i powie o tym, co pomaga wcześnie wyłapywać pomyłki.
--- ---
## 7. Liczenie ## 7. Liczenie
| Znak | Znaczenie | Przykład | Wynik | | Znak | Co robi | Przykład | Wynik |
|------|--------------------------------|----------|--------| |------|-----------------------------|----------|-------|
| `+` | dodawanie | `7 + 2` | `9` | | `+` | dodawanie | `7 + 2` | `9` |
| `-` | odejmowanie | `7 - 2` | `5` | | `-` | odejmowanie | `7 - 2` | `5` |
| `*` | mnożenie | `7 * 2` | `14` | | `*` | mnożenie | `7 * 2` | `14` |
| `/` | dzielenie | `7 / 2` | `3` | | `/` | dzielenie | `7 / 2` | `3` |
| `%` | reszta z dzielenia | `7 % 2` | `1` | | `%` | reszta z dzielenia | `7 % 2` | `1` |
**Uważaj przy dzieleniu liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`, bo obie liczby są całkowite, więc wynik **Uwaga na dzielenie liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`. Obie liczby są
też jest całkowity (część po kropce znika). Jeśli chcesz `3.5`, użyj liczby z kropką: `7.0 / 2`. całkowite, więc wynik też jest całkowity (część po kropce jest odrzucana).
Jeśli chcesz dostać `3.5`, użyj liczby z kropką: `7.0 / 2`.
Mnożenie i dzielenie wykonują się przed dodawaniem i odejmowaniem, tak jak w szkole. Mnożenie i dzielenie wykonują się przed dodawaniem i odejmowaniem, tak jak w szkole.
Nawiasy zmieniają kolejność: Nawiasy zmieniają kolejność:
@ -271,28 +277,28 @@ print(2 + 3 * 4) // 14
print((2 + 3) * 4) // 20 print((2 + 3) * 4) // 20
``` ```
`%` przydaje się do sprawdzania, czy liczba jest parzysta: liczba parzysta dzielona przez 2 nie daje reszty, `%` przydaje się do sprawdzania, czy liczba jest parzysta. Liczba parzysta dzielona przez 2
więc `n % 2` wynosi `0`. nie daje reszty, więc `n % 2` wynosi `0`.
**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linii to się stało. **Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linijce to się stało.
--- ---
## 8. Podejmowanie decyzji: if ## 8. Podejmowanie decyzji: if
Program może wybierać, co zrobić. `if` znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*. Program może wybierać, co zrobić. `if` (czyt. „if”, po polsku „jeżeli”) znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*.
```c ```c
fn main() { fn main() {
temperatura := 25 temperatura := 25
if temperatura > 20 { if temperatura > 20 {
print("Jest ciepło, włóż koszulkę") print("Jest ciepło, załóż koszulkę")
} }
} }
``` ```
Dopisz `else`, żeby powiedzieć *„a jeśli nie, zrób to”*: Dodaj `else` („w przeciwnym razie”):
```c ```c
if temperatura > 20 { if temperatura > 20 {
@ -302,11 +308,12 @@ if temperatura > 20 {
} }
``` ```
A `else if` daje więcej możliwości. Finch sprawdza je od góry do dołu i wykonuje tylko **pierwszą**, która jest prawdą: A `else if` dodaje kolejne możliwości. Finch sprawdza je od góry i wykonuje tylko
**pierwszą** prawdziwą:
```c ```c
if temperatura > 25 { if temperatura > 25 {
print("Upał!") print("Gorąco!")
} else if temperatura > 15 { } else if temperatura > 15 {
print("Przyjemnie") print("Przyjemnie")
} else { } else {
@ -322,18 +329,18 @@ if temperatura > 25 {
| `!=` | nie jest równe | | `!=` | nie jest równe |
| `<` | jest mniejsze niż | | `<` | jest mniejsze niż |
| `>` | jest większe niż | | `>` | jest większe niż |
| `<=` | jest mniejsze albo równe | | `<=` | jest mniejsze lub równe |
| `>=` | jest większe albo równe | | `>=` | jest większe lub równe |
⚠️ **Jedno `=` wkłada wartość do pudełka. Dwa `==` pytają „czy to jest równe?”.** ⚠️ **Jedno `=` wkłada wartość do pudełka. Dwa `==` pytają „czy to jest równe?”.**
Porównywać można też teksty: `if imie == "Ola" { ... }`. Tekst też można porównywać: `if imie == "Ola" { ... }`.
### Łączenie pytań ### Łączenie pytań
- `&&` znaczy **i**: oba warunki muszą być prawdziwe. `if wiek > 6 && wiek < 12 { ... }` - `&&` znaczy **i**: oba warunki muszą być prawdziwe. `if wiek > 6 && wiek < 12 { ... }`
- `||` znaczy **lub**: przynajmniej jeden musi być prawdziwy. `if dzien == "sob" || dzien == "niedz" { ... }` - `||` znaczy **lub**: wystarczy, że jeden jest prawdziwy. `if dzien == "sob" || dzien == "nd" { ... }`
- `!` znaczy **nie**: zamienia prawdę w fałsz i odwrotnie. `if !pada { ... }` - `!` znaczy **nie**: odwraca prawdę i fałsz. `if !pada { ... }`
--- ---
@ -359,7 +366,7 @@ Liczba 4
Liczba 5 Liczba 5
``` ```
`for i in 1..6` znaczy: *„licz w `i` od 1 do 6, ale **bez** 6”*. `for i in 1..6` znaczy: *„licz za pomocą `i` od 1 do 6, ale **bez** 6”*.
Czyli `0..10` daje dziesięć liczb: 0, 1, 2 … 9. Czyli `0..10` daje dziesięć liczb: 0, 1, 2 … 9.
### Powtarzanie, dopóki coś jest prawdą: `while` ### Powtarzanie, dopóki coś jest prawdą: `while`
@ -382,8 +389,8 @@ fn main() {
Start! Start!
``` ```
⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się bez końca. ⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się
Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać. w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać.
### Wcześniejsze wyjście i pomijanie ### Wcześniejsze wyjście i pomijanie
@ -393,9 +400,9 @@ Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać
```c ```c
for i in 1..100 { for i in 1..100 {
if i == 5 { if i == 5 {
break // zatrzymaj się, gdy dojdziemy do 5 break // stop, gdy dojdziemy do 5
} }
print(i) // wypisze 1 2 3 4 print(i) // pokaże 1 2 3 4
} }
``` ```
@ -403,7 +410,7 @@ for i in 1..100 {
## 10. Własne polecenia: funkcje ## 10. Własne polecenia: funkcje
Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany blok to **funkcja**. Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany kawałek kodu to **funkcja**.
Jedną już znasz: `main`. Jedną już znasz: `main`.
```c ```c
@ -422,12 +429,12 @@ Cześć, Ola
Cześć, Tomek Cześć, Tomek
``` ```
`str imie` w nawiasach znaczy: *„ta funkcja potrzebuje jednego tekstu i w środku będziemy go nazywać `imie`”.* `str imie` w nawiasie znaczy: *„ta funkcja potrzebuje jednego tekstu, a w środku nazywamy go `imie`”*.
To, co wpiszesz w nawiasach przy jej użyciu, trafia do środka. To, co wpiszesz w nawias przy wywołaniu, zostanie przekazane do środka.
### Funkcje, które dają odpowiedź ### Funkcje, które oddają odpowiedź
Napisz `->` i typ odpowiedzi, a odpowiedź oddaj przez `return`: Napisz `->` i typ odpowiedzi, a odpowiedź oddaj słowem `return`:
```c ```c
fn dodaj(int a, int b) -> int { fn dodaj(int a, int b) -> int {
@ -440,14 +447,14 @@ fn main() {
} }
``` ```
Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad `main` czy pod nim. Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad czy pod `main`.
--- ---
## 11. Listy: wiele wartości w jednym pudełku ## 11. Listy: wiele wartości w jednym pudełku
Czasem jedno pudełko nie wystarcza. Na liście zakupów jest wiele rzeczy. Czasem jedno pudełko nie wystarczy. Na liście zakupów jest wiele rzeczy.
**Lista** (programiści mówią też **tablica**) przechowuje wiele wartości po kolei: **Lista** (programiści mówią też **tablica**) trzyma wiele wartości po kolei:
```c ```c
fn main() { fn main() {
@ -464,34 +471,34 @@ Mam 3 owoce
Pierwszy to jabłko Pierwszy to jabłko
``` ```
- Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddziela się przecinkami. - Nawiasy kwadratowe `[ ]` tworzą listę. Wartości oddzielamy przecinkami.
- `.len` mówi, ile rzeczy jest na liście. - `.len` mówi, ile rzeczy jest na liście.
- `owoce[0]` to **pierwsza** rzecz. Liczy się od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia. - `owoce[0]` to **pierwsza** rzecz. Liczymy od **0**, więc `owoce[1]` to druga, a `owoce[2]` trzecia.
Jeśli zapytasz o miejsce, którego nie ma, np. `owoce[10]`, program się zatrzyma i powie: Jeśli zapytasz o miejsce, którego nie ma, np. `owoce[10]`, program się zatrzyma i powie:
`index 10 is out of range (the length is 3)` (indeks 10 jest poza listą, która ma długość 3). `index 10 is out of range (the length is 3)`, czyli „indeks 10 jest poza zakresem (długość to 3)”.
### Dodawanie i zabieranie ### Dodawanie i zabieranie
```c ```c
fn main() { fn main() {
[]int wyniki // pusta lista liczb całkowitych []int wyniki // pusta lista liczb całkowitych
wyniki.push(10) // dodaj 10 na końcu wyniki.push(10) // dołóż 10 na koniec
wyniki.push(25) wyniki.push(25)
wyniki.push(7) wyniki.push(7)
print(wyniki) // [10, 25, 7] print(wyniki) // [10, 25, 7]
ostatni := wyniki.pop() // zdejmij ostatni ostatni := wyniki.pop() // zabierz ostatni
print(ostatni, wyniki) // 7 [10, 25] print(ostatni, wyniki) // 7 [10, 25]
wyniki.sort() wyniki.sort() // posortuj
print(wyniki) print(wyniki)
} }
``` ```
### Przechodzenie po liście ### Przechodzenie po liście
`for ... in` odwiedza po kolei każdą wartość: `for ... in` odwiedza każdą wartość po kolei:
```c ```c
fn main() { fn main() {
@ -503,73 +510,14 @@ fn main() {
} }
``` ```
Jeśli chcesz też wiedzieć, *w którym miejscu* listy jesteś, podaj w `for` dwie nazwy: pierwsza liczy 0, 1, 2…, Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń),
a druga to wartość: `sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#tablice).
```c
fn main() {
for i, owoc in ["jabłko", "banan", "wiśnia"] {
print(i + 1, owoc)
}
}
```
```
1 jabłko
2 banan
3 wiśnia
```
Listy potrafią więcej: `contains`, `find`, `insert`, `remove`, `sort`, `reverse`, `join`.
Wszystkie są opisane w [przewodniku dla techników](dla-technikow.md#10-tablice-i-mapy).
### Szukanie po nazwie: mapy
Lista znajduje rzeczy po miejscu: `owoce[0]`. Czasem jednak chcesz szukać po **nazwie**, tak jak szuka się słowa
w słowniku albo numeru w książce telefonicznej. Do tego służy **mapa**: każdy **klucz** (nazwa) ma swoją **wartość**:
```c
fn main() {
telefon := ["Ola": "555-1234", "Jan": "555-9876"]
print(telefon["Ola"]) // 555-1234
telefon["Ewa"] = "555-0000" // dodaj nowe imię
print(telefon.len) // 3
if telefon.has("Tomek") {
print("Tomek jest")
} else {
print("nie ma Tomka")
}
for imie, numer in telefon { // każde imię razem z numerem
print(imie, numer)
}
}
```
- `["klucz": wartość, ...]` tworzy mapę. Kluczami są często teksty, ale mogą być też liczby.
- `telefon["Ola"]` daje wartość dla klucza. Pytanie o klucz, którego nie ma, zatrzymuje program,
więc najpierw sprawdź `telefon.has("Tomek")` albo użyj `telefon.get("Tomek", "nieznany")`.
- `for imie, numer in telefon` odwiedza każdy klucz z jego wartością, w kolejności, w jakiej je dodano.
Mapy świetnie nadają się do **liczenia**. Nowy klucz zaczyna od 0, więc `+= 1` po prostu działa:
```c
fn main() {
map[str]int glosy // pusta mapa: klucze to teksty, wartości to liczby całkowite
for g in ["kot", "pies", "kot", "kot", "rybka"] {
glosy[g] += 1
}
print(glosy) // {"kot": 3, "pies": 1, "rybka": 1}
}
```
--- ---
## 12. Zabawa tekstem ## 12. Zabawa tekstem
Teksty można sklejać przez `+`: Teksty sklejasz znakiem `+`:
```c ```c
fn main() { fn main() {
@ -593,16 +541,16 @@ fn main() {
slowo := "Finch" slowo := "Finch"
print(slowo.len) // 5 liter print(slowo.len) // 5 liter
print(slowo[0]) // F (pierwsza litera) print(slowo[0]) // F (pierwsza litera)
print(slowo.upper()) // FINCH print(slowo.upper()) // FINCH (wielkimi literami)
print(slowo.lower()) // finch print(slowo.lower()) // finch (małymi literami)
print(slowo.contains("in")) // true print(slowo.contains("in")) // true (czy zawiera "in")
for litera in slowo { for litera in slowo {
print(litera) // F, i, n, c, h, każda w osobnej linii print(litera) // F, i, n, c, h, każda w osobnej linijce
} }
} }
``` ```
(`.len` liczy bajty, a polskie litery zajmują po dwa, więc `"żółw".len` to 7, a nie 4.) ⚠️ `.len` liczy bajty, a polskie litery (ą, ę, ż…) zajmują po 2 bajty. `"żółw".len` to 7, a nie 4.
--- ---
@ -627,15 +575,15 @@ fn main() {
} }
``` ```
Jeśli ktoś wpisze coś, co nie jest liczbą, program się zatrzyma z komunikatem Jeśli ktoś wpisze coś, co nie jest liczbą, program zatrzyma się z komunikatem
w rodzaju `can't turn "abc" into int` (nie da się zamienić "abc" na liczbę). w stylu `can't turn "abc" into int` („nie da się zamienić "abc" na int”).
--- ---
## 14. Własne rodzaje wartości: struktury ## 14. Własne rodzaje wartości: struktury
Pomyśl o karcie w bibliotece: każda ma tytuł, autora i rok wydania. Wyobraź sobie kartę w bibliotece: każda karta ma tytuł, autora i rok.
**Struktura** pozwala stworzyć własny rodzaj wartości z nazwanymi częściami: **Struktura** (`struct`) pozwala zrobić własny rodzaj wartości z nazwanymi częściami:
```c ```c
struct Ksiazka { struct Ksiazka {
@ -657,11 +605,11 @@ Hobbit napisał Tolkien
Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1938) Ksiazka(tytul: "Hobbit", autor: "Tolkien", rok: 1938)
``` ```
- `struct Ksiazka { ... }` opisuje, co ma każda książka. Umieść to **poza** `main`. - `struct Ksiazka { ... }` opisuje, co ma każda książka. Pisz to **poza** `main`.
- `Ksiazka(tytul: ..., autor: ..., rok: ...)` tworzy jedną książkę. - `Ksiazka(tytul: ..., autor: ..., rok: ...)` tworzy jedną książkę.
- `k.tytul` odczytuje jedną część. `k.rok = 1938` ją zmienia. - `k.tytul` odczytuje jedną część, a `k.rok = 1938` ją zmienia.
Częściom można nadać wartość początkową. Wtedy nie trzeba ich wypełniać: Częściom możesz nadać wartość początkową. Wtedy nie trzeba ich podawać:
```c ```c
struct Gracz { struct Gracz {
@ -675,37 +623,7 @@ fn main() {
} }
``` ```
Wiele takich wartości można trzymać na liście: `[]Ksiazka polka`, a potem `polka.push(k)`. Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`.
### Polecenia dla struktury: metody
Struktura może mieć własne polecenia. Napisz nazwę struktury, kropkę i nazwę polecenia.
W środku `self` to struktura, na której polecenie użyto:
```c
struct Gracz {
str imie
int zycia = 3
}
fn Gracz.trafiony() {
self.zycia -= 1
print(self.imie, "- pozostałe życia:", self.zycia)
}
fn Gracz.zyje() -> bool {
return self.zycia > 0
}
fn main() {
g := Gracz(imie: "Ola")
g.trafiony() // Ola - pozostałe życia: 2
g.trafiony() // Ola - pozostałe życia: 1
print(g.zyje()) // true
}
```
`g.trafiony()` zmienia samo `g`, więc po dwóch trafieniach `g.zycia` naprawdę wynosi 1.
--- ---
@ -724,14 +642,14 @@ fn main() {
- `read_file(nazwa)` oddaje tekst z pliku. - `read_file(nazwa)` oddaje tekst z pliku.
- `file_exists(nazwa)` mówi, czy plik istnieje (`true` albo `false`). - `file_exists(nazwa)` mówi, czy plik istnieje (`true` albo `false`).
Plik pojawi się w katalogu, w którym uruchomiono program. Plik pojawi się w folderze, w którym uruchomiłeś program.
--- ---
## 16. Kiedy coś pójdzie nie tak ## 16. Kiedy coś pójdzie nie tak
Każdy, kto programuje, robi błędy, nawet eksperci, codziennie. Każdy się myli przy programowaniu, nawet eksperci, codziennie.
Finch bardzo się stara wyjaśnić prostymi słowami, co poszło nie tak. Przykład: Finch stara się tłumaczyć, co poszło nie tak, prostymi słowami (na razie po angielsku). Przykład:
```c ```c
fn main() { fn main() {
@ -741,91 +659,41 @@ fn main() {
``` ```
``` ```
hello.fch:3:11: error: there is no variable named 'wiekk' czesc.fch:3:11: error: there is no variable named 'wiekk'
3 | print(wiekk) 3 | print(wiekk)
| ^ | ^
``` ```
Jak to czytać: Jak to czytać:
- `hello.fch` to plik, - `czesc.fch` to plik,
- `3` to numer **linii**, - `3` to numer **linijki**,
- `11` mówi, jak daleko w linii (to **kolumna**), - `11` to który znak w linijce (**kolumna**),
- dalej jest wyjaśnienie, a pod nim ta linia ze znakiem `^`, który wskazuje problem. - potem wyjaśnienie, a pod nim ta linijka ze strzałką `^` wskazującą problem.
Komunikaty są po angielsku; tutaj znaczy to „nie ma zmiennej o nazwie 'wiekk'”. Po prostu literówka w `wiek`. Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o nazwie 'wiekk'”.
### Częste pomyłki ### Częste błędy
| Finch mówi | Co to znaczy | Jak naprawić | | Finch mówi | Co to znaczy | Jak naprawić |
|---|---|---| |---|---|---|
| `there is no variable named 'x'` | Literówka w nazwie albo takie pudełko nigdy nie powstało | Sprawdź pisownię. Utwórz je przez `x := ...` | | `there is no variable named 'x'` | Nie ma zmiennej `x`: literówka albo zapomniałeś ją zrobić | Sprawdź pisownię. Utwórz ją przez `x := ...` |
| `a variable named 'x' already exists here` | Użyto `:=` dwa razy dla tej samej nazwy | Za drugim razem napisz `=` | | `a variable named 'x' already exists here` | Użyłeś `:=` dwa razy dla tej samej nazwy | Za drugim razem użyj `=` |
| `'x' must be int, but this is str` | Zły rodzaj wartości dla tego pudełka | Włóż wartość właściwego rodzaju | | `'x' must be int, but this is str` | Zły rodzaj wartości dla tego pudełka | Włóż wartość właściwego rodzaju |
| `put each statement on its own line` | Dwa polecenia w jednej linii | Rozdziel je Enterem | | `put each statement on its own line` | Dwa polecenia w jednej linijce | Wciśnij Enter między nimi |
| `this '{' is never closed` | Brakuje `}` | Dopisz `}` | | `this '{' is never closed` | Brakuje `}` | Dopisz `}` |
| `a condition must be bool` | `if` potrzebuje pytania typu tak/nie | Porównaj coś: `if x > 0` | | `a condition must be bool` | `if` potrzebuje pytania tak/nie | Porównaj coś: `if x > 0` |
| `runtime error: division by zero` | W trakcie działania programu nastąpiło dzielenie przez 0 | Sprawdź liczbę przed dzieleniem | | `unexpected character 'ż'` | Polska litera w nazwie | Zmień na zwykłą literę: `zolw` |
| `index 5 is out of range (the length is 3)` | Pytanie listy o miejsce, którego nie ma | Pamiętaj, że liczy się od 0; sprawdź `.len` | | `runtime error: division by zero` | Program podzielił przez 0 w trakcie działania | Sprawdź liczbę przed dzieleniem |
| `can't turn "abc" into int` | `int(...)` dostało tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik | | `index 5 is out of range (the length is 3)` | Zapytałeś listę o miejsce, którego nie ma | Pamiętaj, że liczymy od 0; sprawdź `.len` |
| `can't use '+' on str and int` | Tekst plus liczba | Użyj `str(...)` na liczbie | | `can't turn "abc" into int` | `int(...)` dostał tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
| `'f' can fail, so say what happens then` | Użyto polecenia, które może się nie udać, bez powiedzenia, co wtedy zrobić | Dopisz `or ...` (zob. niżej) | | `can't use '+' on str and int` | Tekst plus liczba | Zamień liczbę na tekst przez `str(...)` |
### Rzeczy, które mogą się nie udać: `or` i `try`
Niektóre rzeczy mogą się nie udać *w trakcie działania programu*: ktoś wpisze `abc` tam, gdzie miała być liczba,
albo nie ma pliku. Zamiast zatrzymywać program, możesz przez **`or`** powiedzieć, co wtedy zrobić:
```c
fn main() {
wiek := int(input("Ile masz lat? ")) or 0 // to nie liczba? weź 0
print("Za rok będziesz mieć", wiek + 1)
notatka := read_file("notatka.txt") or "(jeszcze nie ma notatki)"
print(notatka)
}
```
Po `or` może też stać blok. W jego środku `err` to komunikat, który mówi, co poszło nie tak:
```c
fn main() {
while true {
n := int(input("Wybierz liczbę: ")) or {
print("To nie jest liczba, spróbuj jeszcze raz")
continue
}
print("Wybrana liczba:", n)
break
}
}
```
Twoje własne funkcje też mogą się nie udać. Postaw `!` po typie wyniku i użyj `return error("...")`:
```c
fn bezpieczne_dzielenie(int a, int b) -> int! {
if b == 0 {
return error("nie można dzielić przez zero")
}
return a / b
}
fn main() {
print(bezpieczne_dzielenie(10, 2) or -1) // 5
print(bezpieczne_dzielenie(10, 0) or -1) // -1
}
```
Finch nie pozwoli o tym zapomnieć: wywołanie `bezpieczne_dzielenie(10, 0)` bez `or` to błąd, który każe ci to obsłużyć.
(Problem można też przekazać dalej przez `try`; wyjaśnia to
[przewodnik dla techników](dla-technikow.md#błędy-jako-wartości).)
--- ---
## 17. Małe projekty do zrobienia ## 17. Małe projekty do zrobienia
Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero wtedy, gdy utkniesz. Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero, gdy utkniesz.
### Projekt 1: Tabliczka mnożenia ### Projekt 1: Tabliczka mnożenia
@ -844,7 +712,7 @@ fn main() {
### Projekt 2: Parzysta czy nieparzysta ### Projekt 2: Parzysta czy nieparzysta
Dla każdej liczby od 1 do 10 pokaż, czy jest parzysta, czy nieparzysta. Dla każdej liczby od 1 do 10 napisz, czy jest parzysta, czy nieparzysta.
<details><summary>Rozwiązanie</summary> <details><summary>Rozwiązanie</summary>
@ -863,7 +731,7 @@ fn main() {
### Projekt 3: Stopnie Celsjusza na Fahrenheita ### Projekt 3: Stopnie Celsjusza na Fahrenheita
Pokaż tabelę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`. Pokaż tabelkę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`.
<details><summary>Rozwiązanie</summary> <details><summary>Rozwiązanie</summary>
@ -885,8 +753,8 @@ fn main() {
### Projekt 4: Fizz Buzz ### Projekt 4: Fizz Buzz
Policz od 1 do 20. Dla liczb podzielnych przez 3 powiedz `Fizz`, przez 5 `Buzz`, Policz od 1 do 20. Dla liczb podzielnych przez 3 napisz `Fizz`, przez 5 napisz `Buzz`,
przez obie `FizzBuzz`, a w pozostałych przypadkach samą liczbę. przez obie naraz napisz `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
<details><summary>Rozwiązanie</summary> <details><summary>Rozwiązanie</summary>
@ -909,7 +777,7 @@ fn main() {
### Projekt 5: Największa z trzech ### Projekt 5: Największa z trzech
Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która zwraca największą liczbę. Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która oddaje największą z trzech liczb.
<details><summary>Rozwiązanie</summary> <details><summary>Rozwiązanie</summary>
@ -932,8 +800,7 @@ fn main() {
### Projekt 6: Zgadnij liczbę ### Projekt 6: Zgadnij liczbę
Komputer wybiera tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem Komputer ma tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem mówi „więcej” albo „mniej”.
mówi „więcej” albo „mniej”.
<details><summary>Rozwiązanie</summary> <details><summary>Rozwiązanie</summary>
@ -949,7 +816,7 @@ fn main() {
} else if strzal > tajna { } else if strzal > tajna {
print("Mniej!") print("Mniej!")
} else { } else {
print("Tak! Liczba prób:", proby) print("Tak! Potrzebowałeś", proby, "prób")
return return
} }
} }
@ -961,7 +828,7 @@ fn main() {
### Projekt 7: Średnia ocen ### Projekt 7: Średnia ocen
Zapytaj o 3 oceny, zapisz je na liście i pokaż listę oraz średnią. Zapytaj o 3 oceny, zapisz je na liście, a potem pokaż listę i średnią.
<details><summary>Rozwiązanie</summary> <details><summary>Rozwiązanie</summary>
@ -991,9 +858,9 @@ fn main() {
fn main() { // tu zaczyna się program fn main() { // tu zaczyna się program
print("tekst", 42) // pokaż coś print("tekst", 42) // pokaż coś
x := 5 // nowe pudełko (typ rozpoznany sam) x := 5 // nowe pudełko (typ zgadnięty)
int y = 10 // nowe pudełko (typ podany) int y = 10 // nowe pudełko (typ podany)
x = 7 // zmiana zawartości pudełka x = 7 // zmień zawartość
x += 1 // dodaj 1 do x x += 1 // dodaj 1 do x
if x > 5 { // decyzja if x > 5 { // decyzja
@ -1013,49 +880,62 @@ fn main() { // tu zaczyna się program
} }
} }
fn dodaj(int a, int b) -> int { // twoje własne polecenie fn dodaj(int a, int b) -> int { // własne polecenie
return a + b return a + b
} }
struct Zwierzak { // twój własny rodzaj wartości struct Zwierzak { // własny rodzaj wartości
str imie str imie
int wiek = 1 int wiek = 1
} }
// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } for i, n in liczby { } // listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { }
// mapy: wiek := ["Ola": 9] wiek["Jan"] = 10 wiek["Ola"] wiek.has("Tomek") for imie, w in wiek { }
// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x") // tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
// pytanie: imie := input("Imię? ") // pytanie: imie := input("Imię? ")
// pliki: write_file("p.txt", tekst) read_file("p.txt") // pliki: write_file("p.txt", tekst) read_file("p.txt")
// metody: fn Zwierzak.urosnij() { self.wiek += 1 } z.urosnij()
// porażki: n := int(tekst) or 0 x := read_file("p.txt") or { print(err) ... }
``` ```
--- ---
## 19. Słowa, które będziesz słyszeć ## 19. Słowniczek
| Słowo | Znaczenie | | Słowo | Znaczenie |
|---|---| |---|---|
| **program** | Lista kroków dla komputera | | **program** | Lista kroków dla komputera |
| **kod** | Tekst programu | | **kod** | Tekst programu |
| **uruchomić** | Kazać komputerowi wykonać kroki | | **uruchomić** | Kazać komputerowi wykonać kroki |
| **skompilować** | Przetłumaczyć kod na coś, co komputer potrafi uruchomić. `finch run` robi to za ciebie | | **kompilować** | Przetłumaczyć kod na coś, co komputer umie uruchomić. `finch run` robi to za ciebie |
| **zmienna** | Pudełko z nazwą, w którym leży wartość | | **zmienna** | Pudełko z nazwą, w którym jest wartość |
| **typ** | Rodzaj wartości: liczba, tekst, tak/nie… | | **typ** | Rodzaj wartości: liczba, tekst, tak/nie… |
| **funkcja** | Nazwana grupa kroków, której można używać wiele razy | | **funkcja** | Nazwana grupa kroków, której można używać wiele razy |
| **pętla** | Kroki, które się powtarzają | | **pętla** | Kroki, które się powtarzają |
| **warunek** | Pytanie typu tak/nie, np. `x > 5` | | **warunek** | Pytanie tak/nie, np. `x > 5` |
| **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` | | **lista / tablica** | Wiele wartości po kolei, np. `[1, 2, 3]` |
| **indeks** | Miejsce wartości na liście, liczone od 0 | | **indeks** | Pozycja wartości na liście, liczona od 0 |
| **struktura** | Twój własny rodzaj wartości z nazwanych części | | **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części |
| **metoda** | Polecenie należące do struktury, np. `g.trafiony()` | | **wejście** (input) | To, co użytkownik wpisuje do programu |
| **mapa** | Wartości wyszukiwane po kluczu (nazwie), jak w słowniku | | **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie |
| **wejście** (ang. *input*) | To, co ktoś wpisuje do programu |
| **błąd** (ang. *error*) | Komunikat, że coś jest nie tak, i gdzie |
| **bug** | Pomyłka w programie | | **bug** | Pomyłka w programie |
### Słówka po angielsku, które spotkasz w Finchu
| Słowo | Po polsku |
|---|---|
| `print` | wypisz |
| `if` / `else` | jeżeli / w przeciwnym razie |
| `for` … `in` | dla … w |
| `while` | dopóki |
| `break` / `continue` | przerwij / kontynuuj |
| `return` | zwróć (oddaj) |
| `true` / `false` | prawda / fałsz |
| `fn` (od *function*) | funkcja |
| `struct` | struktura |
| `push` / `pop` | dołóż / zabierz |
| `input` | wejście (pytanie) |
| `len` (od *length*) | długość |
| `error` | błąd |
--- ---
**Co dalej?** Kiedy to wszystko będzie już łatwe, przejdź do **Co dalej?** Kiedy to wszystko wyda ci się łatwe, przejdź do
[przewodnika dla techników](dla-technikow.md). Opisuje każdą część Fincha. [przewodnika dla techników](dla-technikow.md). Opisuje każdą część Fincha.

File diff suppressed because it is too large Load diff

View file

@ -1,501 +0,0 @@
# Ściąga z Fincha
Cały Finch na jednej stronie. Wyjaśnienia znajdziesz w [przewodnikach](../../README.pl.md#dokumentacja).
> 🇬🇧 English version: [cheatsheet.md](../en/cheatsheet.md)
---
## Polecenie `finch`
| Polecenie | Co robi |
|---|---|
| `finch run plik.fch [argumenty…]` | kompiluje i uruchamia (argumenty trafiają do programu) |
| `finch build plik.fch [-o nazwa]` | tworzy program (na Windowsie `nazwa.exe`) |
| `finch build plik.fch --shared [-I katalog]` | tworzy bibliotekę (`libplik.so`, `plik.dll`), którą wczyta program w C; jej funkcjami C są funkcje `export fn` (liczby, bool, char, wskaźniki) |
| `finch test plik.fch` | uruchamia każdy blok `test "…" { }` osobno (także w importowanych modułach) |
| `finch fmt [--check] [pliki/katalogi]` | wcięcia po 4 spacje na każdy poziom `{ }`, bez spacji na końcach linii, najwyżej jedna pusta linia z rzędu (`--check`: tylko wypisuje, co by zmienił) |
| `finch run` / `build` / `test` bez pliku | w katalogu z `finch.toml`: plik z pola `entry` |
| `finch ir plik.fch` | wypisuje LLVM IR |
| `finch lsp` | serwer języka (korzystają z niego edytory) |
| `finch version` | wersja |
| `-l bib` | dolinkowuje bibliotekę C (to samo co `link "bib"`) |
| `--target windows` / `linux` / `arm64` / `macos` / `android` / triple | buduje na inny system (`android`: clang z NDK wskazanego przez `ANDROID_NDK_HOME`, zwykle z `--shared`) |
| `-g` | informacje dla debuggera (gdb, lldb) |
| `-O0` | bez optymalizacji |
| `--cpu native` / `x86-64-v3` / `apple-m1` … | używa wszystkich instrukcji danego procesora (AVX2, AVX-512, NEON); program będzie wtedy wymagał podobnego procesora |
| `--trace` / `--no-trace` | błąd w trakcie działania pokazuje ścieżkę wywołań; domyślnie włączone dla `run`, `test`, `-g` i `-O0`, wyłączone dla zwykłego `build` (spowalnia program) |
Zmienne środowiskowe: `FINCH_PATH` (katalogi z modułami), `FINCH_CC` (kompilator C, który linkuje), `FINCH_CXX` (dla plików `.cpp`).
Skompilowany program i jego pliki z `link "x.c"` zostają w pamięci podręcznej (`~/.cache/finch`), dopóki nie zmienią się one same albo ich nagłówki.
`finch.toml` (obok kodu; wtedy `finch run` nie potrzebuje ani nazwy pliku, ani opcji):
```toml
name = "gra" # co zbuduje finch build
entry = "src/main.fch"
paths = ["src"] # katalogi z modułami
include = ["vendor/SDL3/include"] # nagłówki C
links = ["SDL3", "native/glue.cpp"] # biblioteki i twoje pliki .c / .cpp / .o / .a
libdirs = ["vendor/SDL3/lib"]
cflags = ["-DFAST"] # dla twoich plików .c / .cpp
ldflags = ["-Wl,-rpath,."]
cpu = "native" # to samo co --cpu native
[target.windows] # tylko przy budowaniu na ten system (są też linux, macos, android, linux-arm64)
links = ["ws2_32"]
```
---
## Plik
```c
import "stdio.h" // nagłówek C
import ksztalty // moduł Fincha: ksztalty.fch
link "m" // biblioteka C albo "moje.c" / ".o" / ".a"
struct Punkt {
int x
int y = 0 // wartość domyślna
}
fn main() {
print("Cześć")
}
```
`main` może wyglądać tak: `fn main()`, `fn main() -> int` (zwraca kod wyjścia) albo `fn main([]str args)`.
Jedna instrukcja w linii, bez średników, `{` w tej samej linii, warunki bez nawiasów.
---
## Zmienne
| Kod | Znaczenie |
|---|---|
| `x := 5` | nowa zmienna, typ wynika z wartości |
| `int x = 5` | nowa zmienna z podanym typem |
| `int x` | zero / `""` / `[]` / `null` / wartości domyślne struktury (także struktur w jej środku) |
| `x = 7` | przypisanie |
| `x += 1` `-=` `*=` `/=` `%=` | zmiana |
| `const W = 640`, `const f32 G = 9.8` | stała (na najwyższym poziomie pliku; z modułu: `ksztalty.W`) |
---
## Typy
| Typ | Przykład |
|---|---|
| `int` (= `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
| `float` (= `f64`) | `3.14`, `1e9`, `2.5e-3` |
| `bool` | `true`, `false` |
| `char` | `'A'`, `'\n'` |
| `str` | `"tekst\t\"w cudzysłowie\""`, `"hp \(hp), prędkość \(v, 2)"` (kod w tekście; `, 2` = dwie cyfry po przecinku), `"""` kilka linii `"""` (wcięcie zamykającego cudzysłowu znika) |
| `i8 i16 i32 i64` | liczby całkowite ze znakiem, o podanym rozmiarze |
| `u8 u16 u32 u64` | liczby całkowite bez znaku |
| `f32 f64` | liczby zmiennoprzecinkowe |
| `[]T` | tablica: `[1, 2, 3]`, `[]str imiona` |
| `map[K]V` | mapa: `["a": 1]`, `map[str]int ile`, `[:]` |
| `ptr[T]`, `ptr` | wskaźnik, wskaźnik bez typu |
| `f32x8`, `i8x32`, `f64x4`, `u16x16` … | wektor: N liczb liczonych naraz (SIMD), zob. niżej |
| `Nazwa` / `modul.Nazwa` | struktura |
Znaki specjalne: `\n \t \r \0 \\ \" \'`.
---
## Operatory
| Priorytet | Operatory |
|---|---|
| 5 | `*` `/` `%` `<<` `>>` `&` |
| 4 | `+` `-` `\|` `^` (`+` łączy też teksty) |
| 3 | `==` `!=` `<` `<=` `>` `>=` |
| 2 | `&&` |
| 1 | `\|\|` |
| jednoargumentowe | `-x` `!x` `~x` |
`x & 1 == 0` znaczy `(x & 1) == 0`.
---
## Sterowanie
```c
if a > b { … } else if a == b { … } else { … }
x := if a > b { a } else { b } // jako wartość: po jednej wartości w każdej gałęzi, else obowiązkowe
while x > 0 { … }
for i in 0..10 { … } // 0 … 9
for el in lista { … } // każdy element (albo znak tekstu)
for i, el in lista { … } // razem z indeksem
for klucz, wartosc in m { … } // każdy wpis mapy, w kolejności dodania
break continue return wartosc
defer sprzatnij() // wykona się, gdy blok się skończy
```
---
## Funkcje
```c
fn dodaj(int a, int b) -> int {
return a + b
}
fn przywitaj(str imie) { // bez wyniku
print("Cześć", imie)
}
```
Parametry zachowują się jak kopie. Kolejność funkcji w pliku nie ma znaczenia. Po ostatnim argumencie może stać przecinek.
---
## Metody
```c
fn Punkt.przesun(int dx, int dy) { // self to punkt, na którym ją wywołano (nie kopia)
self.x += dx
self.y += dy
}
fn Punkt.odleglosc() -> float { … }
p.przesun(1, 2) lista[0].przesun(1, 1) wskaznik_na_punkt.przesun(0, 1)
```
Działają też na strukturach z C: `fn Vector2.length() -> f32 { … }`.
---
## Błędy jako wartości
```c
fn parsuj(str s) -> int! { // ! = może się nie udać
if s.len == 0 {
return error("pusty tekst")
}
return int(s) or { return error("to nie liczba: " + s) }
}
fn zapisz(str p, str t) -> ! { // może się nie udać, bez wyniku
try write_file(p, t)
}
```
| Kod | Gdy się nie uda |
|---|---|
| `x := parsuj(s) or 0` | weź 0 |
| `x := parsuj(s) or { print(err); return }` | wykonaj blok (`err` to komunikat) |
| `x := try parsuj(s)` | przerwij też bieżącą funkcję z tym samym błędem |
| `fn main() -> !` | błąd wypisuje `error: …`, kod wyjścia 1 |
Wbudowane funkcje, które mogą się nie udać: `int(s)`, `float(s)`, `read_file`, `write_file`, `delete_file`.
---
## Struktury
| Kod | Znaczenie |
|---|---|
| `struct P { … }` | definicja (jedno pole w linii, `= wartość` opcjonalnie) |
| `P(1, 2)` | utworzenie, wszystkie pola po kolei |
| `P(x: 1)` | utworzenie przez nazwy pól, reszta domyślna albo zero |
| `p.x`, `p.x = 5` | odczyt, zmiana pola |
| `b := a` | pełna kopia |
---
## Tablice `[]T`
| Kod | Wynik |
|---|---|
| `a.len` | liczba elementów |
| `a[i]`, `a[i] = x` | element (zakres jest sprawdzany) |
| `a.push(x)` | dodaje na końcu |
| `a.pop()` | zdejmuje ostatni i go zwraca |
| `a.insert(i, x)` | wstawia na pozycję i |
| `a.remove(i)` | usuwa pozycję i i ją zwraca |
| `a.clear()` | opróżnia |
| `a.resize(n)` | długość n (nowe elementy są zerami) |
| `a.find(x)` | indeks albo -1 |
| `a.contains(x)` | bool |
| `a.sort()` | sortuje (liczby, znaki, teksty) |
| `a.reverse()` | odwraca kolejność |
| `a.slice(p, k)` | nowa tablica z elementów p … k-1 |
| `a.join(sep)` | `[]str` → jeden tekst |
| `a.ptr` | adres pierwszego elementu (dla C) |
---
## Mapy `map[K]V`
| Kod | Wynik |
|---|---|
| `m := ["a": 1, "b": 2]` | mapa (klucze: liczby, char, bool, str) |
| `m[k]` | wartość (brak klucza zatrzymuje program) |
| `m[k] = v` | dodaje albo podmienia |
| `m[k] += 1`, `m[k].push(x)` | brakujący klucz zaczyna od wartości domyślnej (0, `[]`, …) |
| `m.len` | liczba kluczy |
| `m.has(k)` | bool |
| `m.get(k, domyslna)` | wartość albo `domyslna` |
| `m.remove(k)` | bool: czy klucz był |
| `m.clear()` | opróżnia |
| `m.keys()`, `m.values()` | tablice, w kolejności dodania |
---
## Wektory `f32x8` (SIMD)
Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i64x2/4/8` i tak samo `u…`; najwyżej 512 bitów.
| Kod | Wynik |
|---|---|
| `f32x8(x)` | `x` w każdym elemencie |
| `f32x8(a, b, …, h)` | po jednej liczbie na element |
| `f32x8(lista, i)` | `lista[i]` … `lista[i+7]` (z `[]f32` albo `ptr[f32]`) |
| `f32x8(v)` | 8-elementowy wektor przeliczony element po elemencie (`f32x8(calkowite)`, `i32x8(ulamki)`) |
| `v + w`, `v * 2.0`, `-v` | element po elemencie; zwykła liczba działa na każdy element (`/` tylko dla liczb zmiennoprzecinkowych) |
| `& \| ^ << >> ~` | element po elemencie, dla liczb całkowitych |
| `v < w`, `v == w`, … | maska `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` |
| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, a gdzie nie, `b` |
| `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów |
| `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie |
| `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | jedna liczba ze wszystkich elementów |
| `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) |
| `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: zawsze z jednym zaokrągleniem / najszybciej na tym procesorze |
| `v.store(lista, i)` | zapisuje elementy do `lista[i]` … |
| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | koniec listy: tylko elementy, które są w niej (pozostałe to `wypelnienie`) |
| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | przestawione elementy (shuffle przyjmuje numery zapisane w kodzie) |
| `tabela.lookup(idx)` | `tabela[idx % tabela.len]` dla każdego elementu (`pshufb`, `tbl`) |
| `acc.dot_add(a, b)` | sumy `i32x8` dostają po 4 iloczyny bajtów (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` tam, gdzie są |
| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | funkcja, która może używać tych rozszerzeń; przed jej wywołaniem sprawdź procesor |
---
## Tekst `str`
| Kod | Wynik |
|---|---|
| `s.len` | liczba bajtów |
| `s[i]`, `s[i] = 'x'` | znak |
| `a + b` | połączony tekst |
| `"x = \(x)"` | wartość dowolnego kodu wstawiona w tekst (`\(x, 2)`: liczba z dwiema cyframi po przecinku) |
| `s.sub(a, b)` | fragment a … b-1 |
| `s.find(t)` | pozycja albo -1 |
| `s.contains(t)` | bool |
| `s.starts_with(t)`, `s.ends_with(t)` | bool |
| `s.split(sep)` | `[]str` |
| `s.trim()` | bez spacji na końcach |
| `s.upper()`, `s.lower()` | A–Z / a–z |
| `s.replace(a, b)` | każde a zamienione na b |
| `s.repeat(n)` | tekst n razy |
| `s.bytes()` | `[]u8` |
| `s.ptr` | `ptr[char]` (dla C; parametr `const char *` przyjmie też samo `str`) |
| `==` `!=` `<` `>` | porównanie |
---
## Zamiana typów
| Kod | Wynik |
|---|---|
| `int(3.9)` | `3` |
| `int("42")` | `42` (zatrzymuje program, jeśli to nie liczba) |
| `int('A')` | `65` |
| `float(7)`, `float("2.5")` | `7`, `2.5` |
| `u8(300)` | `44` (zostają najniższe bity) |
| `char(66)` | `'B'` |
| `bool(0)` | `false` |
| `str(42)`, `str(1.5)`, `str(true)`, `str('c')` | tekst (liczba zmiennoprzecinkowa: najkrótszy zapis tej samej liczby) |
| `str(3.14159, 2)` | `"3.14"` (tyle cyfr po przecinku) |
| `str([1, 2])`, `str(m)`, `str(punkt)` | ten sam tekst, który pokazuje `print` |
| `int("x") or 0`, `float(s) or 0.0` | liczba albo wartość zapasowa |
| `str(bajty)` | `[]u8` → tekst |
| `str(p)` | `char*` z C → tekst |
| `ptr(p)`, `ptr(16)` | wskaźnik bez typu, ze wskaźnika albo z liczby |
| `int(p)` | wskaźnik → adres |
Automatycznie tylko wtedy, gdy nic nie ginie: mniejsza liczba → większa, liczba całkowita → zmiennoprzecinkowa, `f32` → `f64`.
---
## Funkcje wbudowane
| Funkcja | Co robi |
|---|---|
| `print(a, b, …)` | wypisuje wartości oddzielone spacjami i kończy linię |
| `input()`, `input("Pytanie? ")` | wczytuje linię |
| `read_file(sciezka)` | plik → str |
| `write_file(sciezka, tekst)` | str → plik, `true`, jeśli się udało |
| `file_exists(sciezka)` | bool |
| `delete_file(sciezka)` | bool |
| `shell(polecenie)` | uruchamia polecenie, zwraca jego kod wyjścia |
| `exit(kod)` | od razu kończy program (kończy też blok `or { }`) |
| `min(a, b, …)`, `max(a, b, …)`, `abs(x)`, `clamp(x, lo, hi)` | dla dowolnych liczb (twoja funkcja o tej samej nazwie ma pierwszeństwo) |
| `fma(a, b, c)`, `muladd(a, b, c)` | `a * b + c` z jednym zaokrągleniem / jedną instrukcją, gdzie procesor potrafi (zapisane `a * b + c` nigdy nie jest łączone) |
| `f32_bits(x)`, `f32_from_bits(u)`, `f64_bits`, `f64_from_bits` | bity liczby zmiennoprzecinkowej jako `u32`/`u64` i z powrotem, bez zmian |
| `f16_to_f32(h)`, `f32_to_f16(x)`, `bf16_to_f32(b)`, `f32_to_bf16(x)` | liczby połówkowej precyzji i bfloat16 (w `u16`) ↔ `f32`; także na wektorach |
| `error(komunikat)` | `return error("…")` w funkcji `-> T!` |
| `addr(x)` | wskaźnik na x; `addr(fn)` = funkcja zwrotna dla C |
| `assert(warunek)`, `assert(warunek, "dlaczego")` | zatrzymuje program, 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, którą zmieniają też inne wątki, przez jej `ptr` (`addr(licznik)`) |
| `new(wartosc)` | wartość na stercie → `ptr[T]` |
| `free(p)` | oddaje tę pamięć |
---
## Wskaźniki
| Finch | C |
|---|---|
| `ptr[int] p` | `int *p` |
| `addr(x)` | `&x` (zmienna-struktura podana tam, gdzie oczekiwany jest `ptr[TaStruktura]`: `push(cialo)` = `push(addr(cialo))`) |
| `p.value` | `*p` |
| `p.pole` | `p->pole` |
| `p[i]` | `p[i]` |
| `null` | `NULL` |
| `ptr` | `void *` |
---
## Pamięć
- Tablice, teksty i struktury zwalniają się same na końcu swojego bloku. `free` nie jest potrzebne.
- `b := a` kopiuje; wynik funkcji jest przenoszony; `return x` przenosi.
- Przekazywanie struktury: `fn f(Cialo c)` pracuje na własnej kopii (struktura wywołującego się nie zmienia), `fn f(ptr[Cialo] c)` zmienia strukturę wywołującego (`f(cialo)` albo `f(addr(cialo))`), metoda dostaje `self` przez adres, nigdy jako kopię. Struktura z co najmniej 256 bajtami liczb, przekazywana przez wartość albo w `for x in lista`, dostaje ostrzeżenie.
- `new(...)` z `free(...)` (często razem z `defer`) przydaje się do list wiązanych, drzew i współdzielenia.
- Pamięć z C zwalniasz funkcjami C (`malloc` / `free`, `fclose`, …).
---
## Moduły
```c
import ksztalty // ksztalty.fch obok tego pliku (albo w FINCH_PATH)
ksztalty.pole(b) // jego funkcje
ksztalty.Pudelko(2, 3) // jego struktury
ksztalty.Pudelko inne // w deklaracjach
import ui/button // ui/button.fch: button.draw()
import ui/button as btn // pod inną nazwą: btn.draw()
```
---
## C
| Kod | Znaczenie |
|---|---|
| `import "x.h"` | funkcje, struktury, enumy, liczby z `#define`, zmienne globalne |
| `link "nazwa"` | libnazwa.so / nazwa.lib (najpierw pyta pkg-config) |
| `link "plik.c"`, `link "glue.cpp"` | kompiluje twój plik C albo C++ (w C++ funkcje w `extern "C"`) |
| `addr(mojaFn)` | przekazuje funkcję Fincha jako funkcję zwrotną |
| Typ w C | W Finchu |
|---|---|
| `int` / `long long` / `unsigned` | `i32` / `i64` / `u32` (według rozmiaru) |
| `float` / `double` | `f32` / `f64` |
| `char *` (parametr, wynik) | `str` |
| `char *` (pole, za wskaźnikiem) | `ptr[char]` |
| `struct X` | `X` |
| `T *` | `ptr[T]` |
| `void *`, uchwyty | `ptr` |
| `T x[N]` (w strukturze) | `[N]T` |
| `union U` | `U`: `U u`, `u.pole`, `U(pole: v)`, do C przez wskaźnik (nie przez wartość) |
| `#define N 4`, `"tekst"`, `(1u << 3)`, `((Uint64)2)` | stałe (bez makr z parametrami) |
| `#define RAYWHITE (Color){ 245, 245, 245, 255 }` | wartość typu `Color` |
**`char *` to `str`**: funkcja C, która przyjmuje albo zwraca `char *`, przyjmuje albo zwraca `str` Fincha (bez `.ptr` i bez zamiany); tylko pole `char *` i `char **` zostają jako `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"`), z nagłówkiem (może go napisać cbindgen).
Wynik `char *` funkcji C od razu jest `str`; struktury przez wartość działają w konwencjach wywołań System V, Windows x64 i ARM64 (Linux i macOS).
---
## Błędy w trakcie działania (program kończy się z plikiem i linią)
| Komunikat | Przyczyna |
|---|---|
| `division by zero` | `/` albo `%` przez 0 |
| `index 5 is out of range (the length is 3)` | `a[5]` |
| `pop() on an empty array` | |
| `used .value on a null pointer` | `.value` / `.pole` / `[i]` przez `null` |
| `can't turn "x" into int` | `int("x")` |
| `can't read the file "…"` | `read_file` (bez `or`) |
| `the key "x" is not in the map` | `m["x"]` |
| `assert failed: dlaczego` | `assert(warunek, "dlaczego")` |
Pod komunikatem pojawiają się 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 Wrog.nastepny (gra.fch:6)
in krok (gra.fch:10)
in main (gra.fch:15)
```
## Testy
```c
test "wysokość skoku" { // obok kodu; uruchamia je finch test plik.fch, finch run/build je pomijają
p := Gracz()
p.skocz()
assert(p.y > 0.0, "y wynosi \(p.y)")
}
```
---
## Edytory
Kate: `editors/kate/install.sh`, potem włącz wtyczkę **LSP Client**; uruchamianie przez **Build & Run** → `finch run %f`.
Dowolny edytor z LSP: `finch lsp`.
### VS Code (rozszerzenie „Finch”)
| Klawisz / akcja | Co robi |
|---|---|
| ▶ / `Ctrl+F5` | uruchamia plik |
| `Finch: Build This File` | buduje; błędy trafiają do panelu Problems |
| `F12` | przejście do definicji |
| najechanie kursorem | typ / sygnatura / opis |
| `Ctrl+Spacja` | podpowiedzi (po `.`: pola, metody, zawartość modułu) |
| `Ctrl+Shift+O` | funkcje i struktury w pliku |
| `main`, `fn`, `fnr`, `fnf`, `method`, `struct`, `for`, `foreach`, `forkv`, `map`, `orb`, `if`, `ife`, `while`, `input` + Tab | gotowe fragmenty kodu |
| ustawienie `finch.path` | gdzie leży finch / finch.exe |
---
## Enumy i `match`
```
enum Kolor { Czerwony, Zielony, Niebieski } enum Status { Ok = 200, NotFound = 404 }
k := Kolor.Zielony int(k) Kolor(2) k == Kolor.Czerwony // tylko == i !=; Kolor(7) to błąd
fn Kolor.nastepny() -> Kolor { ... } // metody działają
match k { Czerwony -> ... Zielony, Niebieski -> { ... } else -> ... } // same nazwy wariantów; każdy wariant albo else
x := match n { 1, 2 -> "kilka" else -> "dużo" } // wyrażenie: każda gałąź tego samego typu
```
## Typy generyczne
```
fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // typ wywnioskowany albo podany
struct Stos[T] { []T el } fn Stos[T].push(T x) { ... } // typ zawsze idzie z nazwą:
s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // nigdy samo Stos()
```
## Biblioteka standardowa (`import nazwa`; twój plik o tej samej nazwie ma pierwszeństwo)
| | |
|---|---|
| `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, wzor)` `days_in_month(r, m)` |
| `proc` | `run(polecenie)` `output(polecenie)` (przy błędzie zwraca jego wyjście) `env(nazwa, domyslna)` `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(katalog)` `remove_dir(p)` `data_dir(aplikacja)` |
| `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
`fn return if else while for in break continue true false null import link struct enum match defer or try const export`
(`test` i `as` tylko tam, gdzie coś znaczą: `test "nazwa" { }`, `import x as y`)
(oraz `self` wewnątrz metod)

View file

@ -1,41 +0,0 @@
# Finch in Kate
Syntax highlighting and the Finch language server for **Kate** (and KWrite / KDevelop, which share
Kate's highlighting engine).
## Install
```sh
editors/kate/install.sh
```
It copies `finch.xml` to `~/.local/share/org.kde.syntax-highlighting/syntax/` and adds a `finch` entry
to `~/.config/kate/lspclient/settings.json` (your other servers are kept). Then:
1. Restart Kate.
2. **Settings → Configure Kate → Plugins**: enable **LSP Client** (and **Build & Run** for the next part).
3. Make sure `finch` is on your PATH (`finch version` in a terminal).
Open a `.fch` file: errors appear as you type, hovering shows types and docs, `Ctrl+Space` completes
(fields and methods after `.`), **Go to Definition** and the **Symbols** outline work, and parameter hints
pop up inside `(…)`.
## Running and building
**Build & Run** plugin → **Add target set**, then add targets with these commands (the working folder
can be `%d`, the folder of the current file):
| Target | Command |
|---|---|
| Run | `finch run %f` |
| Build | `finch build %f` |
Give one of them a shortcut in **Settings → Configure Keyboard Shortcuts** (search for "Build"). Finch's
errors have the `file:line:column: error:` form, so Kate lists them and jumps to the line on click.
## By hand
- Highlighting: copy `finch.xml` into `~/.local/share/org.kde.syntax-highlighting/syntax/`
(Windows: `%LOCALAPPDATA%\org.kde.syntax-highlighting\syntax\`).
- Language server: **Settings → Configure Kate → LSP Client → User Server Settings**, and add the
`finch` entry from `lspclient.json` inside `"servers"`.

View file

@ -1,227 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE language>
<!--
Finch syntax highlighting for Kate, KWrite and KDevelop (KSyntaxHighlighting).
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="5" 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">
<item>fn</item>
<item>struct</item>
<item>enum</item>
<item>export</item>
<item>const</item>
</list>
<list name="control">
<item>if</item>
<item>else</item>
<item>while</item>
<item>for</item>
<item>in</item>
<item>break</item>
<item>continue</item>
<item>return</item>
<item>defer</item>
<item>or</item>
<item>try</item>
<item>match</item>
</list>
<list name="imports">
<item>import</item>
<item>link</item>
</list>
<list name="constants">
<item>true</item>
<item>false</item>
<item>null</item>
<item>self</item>
</list>
<list name="types">
<item>int</item>
<item>float</item>
<item>bool</item>
<item>char</item>
<item>str</item>
<item>ptr</item>
<item>map</item>
<item>i8</item>
<item>i16</item>
<item>i32</item>
<item>i64</item>
<item>u8</item>
<item>u16</item>
<item>u32</item>
<item>u64</item>
<item>f32</item>
<item>f64</item>
</list>
<list name="builtins">
<item>print</item>
<item>input</item>
<item>read_file</item>
<item>write_file</item>
<item>file_exists</item>
<item>delete_file</item>
<item>shell</item>
<item>exit</item>
<item>addr</item>
<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>
<item>select</item>
<item>concat</item>
<item>fma</item>
<item>muladd</item>
<item>cpu_has</item>
<item>f32_bits</item>
<item>f32_from_bits</item>
<item>f64_bits</item>
<item>f64_from_bits</item>
<item>f16_to_f32</item>
<item>f32_to_f16</item>
<item>bf16_to_f32</item>
<item>f32_to_bf16</item>
</list>
<contexts>
<context name="Normal" attribute="Normal Text" lineEndContext="#stay">
<DetectSpaces/>
<Detect2Chars char="/" char1="/" attribute="Comment" context="LineComment"/>
<Detect2Chars char="/" char1="*" attribute="Comment" context="BlockComment" beginRegion="Comment"/>
<DetectChar char="{" attribute="Symbol" beginRegion="Brace"/>
<DetectChar char="}" attribute="Symbol" endRegion="Brace"/>
<StringDetect String="&quot;&quot;&quot;" attribute="String" context="LongString" beginRegion="LongString"/>
<DetectChar char="&quot;" attribute="String" context="String"/>
<DetectChar char="'" attribute="Char" context="Char"/>
<WordDetect String="fn" attribute="Keyword" context="FunctionName"/>
<RegExpr String="\btest(?=\s*&quot;)" attribute="Keyword"/>
<RegExpr String="(?&lt;=\s)as(?=\s+[A-Za-z_])" attribute="Import"/>
<WordDetect String="struct" attribute="Keyword" context="StructName"/>
<keyword String="control" attribute="Control Flow"/>
<keyword String="imports" attribute="Import"/>
<keyword String="constants" attribute="Constant"/>
<keyword String="types" attribute="Data Type"/>
<!-- vectors: f32x8, i8x32, u16x16, f64x4 ... -->
<RegExpr String="\b([iuf](8|16|32|64)|bool)x(2|4|8|16|32|64)\b" attribute="Data Type"/>
<RegExpr String="@cpu\b" attribute="Keyword"/>
<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_]*\b" attribute="Float"/>
<RegExpr String="\b[0-9][0-9_]*\b" attribute="Decimal"/>
<!-- a call: name( ; method or field after a dot -->
<RegExpr String="\b[A-Za-z_][A-Za-z0-9_]*(?=\s*\()" attribute="Function"/>
<!-- struct names are written with a capital letter by convention -->
<RegExpr String="\b[A-Z][A-Za-z0-9_]*\b" attribute="Data Type"/>
<DetectIdentifier/>
<Detect2Chars char=":" char1="=" attribute="Operator"/>
<Detect2Chars char="-" char1="&gt;" attribute="Operator"/>
<Detect2Chars char="." char1="." attribute="Operator"/>
<AnyChar String="+-*/%=&lt;&gt;!&amp;|^~" attribute="Operator"/>
<AnyChar String="()[],.:" attribute="Symbol"/>
</context>
<!-- fn name( or fn Type.method( -->
<context name="FunctionName" attribute="Normal Text" lineEndContext="#pop" fallthroughContext="#pop">
<DetectSpaces/>
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*(?=\.)" attribute="Data Type"/>
<DetectChar char="." attribute="Symbol"/>
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*" attribute="Function Definition" context="#pop"/>
</context>
<context name="StructName" attribute="Normal Text" lineEndContext="#pop" fallthroughContext="#pop">
<DetectSpaces/>
<RegExpr String="[A-Za-z_][A-Za-z0-9_]*" attribute="Data Type" context="#pop"/>
</context>
<context name="LineComment" attribute="Comment" lineEndContext="#pop">
<DetectSpaces/>
<IncludeRules context="##Comments"/>
</context>
<context name="BlockComment" attribute="Comment" lineEndContext="#stay">
<Detect2Chars char="*" char1="/" attribute="Comment" context="#pop" endRegion="Comment"/>
<IncludeRules context="##Comments"/>
</context>
<context name="String" attribute="String" lineEndContext="#pop">
<RegExpr String="\\\(" attribute="Escape" context="Interpolation"/>
<RegExpr String="\\[ntr0\\&quot;']" attribute="Escape"/>
<RegExpr String="\\." attribute="Error"/>
<DetectChar char="&quot;" attribute="String" context="#pop"/>
</context>
<context name="LongString" attribute="String" lineEndContext="#stay">
<RegExpr String="\\\(" attribute="Escape" context="Interpolation"/>
<RegExpr String="\\[ntr0\\&quot;']" attribute="Escape"/>
<StringDetect String="&quot;&quot;&quot;" attribute="String" context="#pop" endRegion="LongString"/>
</context>
<!-- "hp \(hp + 1)": code inside text, up to the ) that closes \( -->
<context name="Interpolation" attribute="Normal Text" lineEndContext="#stay">
<DetectChar char=")" attribute="Escape" context="#pop"/>
<DetectChar char="(" attribute="Symbol" context="InterpolationParen"/>
<IncludeRules context="Normal"/>
</context>
<context name="InterpolationParen" attribute="Normal Text" lineEndContext="#stay">
<DetectChar char=")" attribute="Symbol" context="#pop"/>
<DetectChar char="(" attribute="Symbol" context="InterpolationParen"/>
<IncludeRules context="Normal"/>
</context>
<context name="Char" attribute="Char" lineEndContext="#pop">
<RegExpr String="\\[ntr0\\&quot;']" attribute="Escape"/>
<DetectChar char="'" attribute="Char" context="#pop"/>
</context>
</contexts>
<itemDatas>
<itemData name="Normal Text" defStyleNum="dsNormal" spellChecking="false"/>
<itemData name="Keyword" defStyleNum="dsKeyword" spellChecking="false"/>
<itemData name="Control Flow" defStyleNum="dsControlFlow" spellChecking="false"/>
<itemData name="Import" defStyleNum="dsImport" spellChecking="false"/>
<itemData name="Constant" defStyleNum="dsConstant" spellChecking="false"/>
<itemData name="Data Type" defStyleNum="dsDataType" spellChecking="false"/>
<itemData name="Built-in" defStyleNum="dsBuiltIn" spellChecking="false"/>
<itemData name="Function" defStyleNum="dsFunction" spellChecking="false"/>
<itemData name="Function Definition" defStyleNum="dsFunction" bold="true" spellChecking="false"/>
<itemData name="Decimal" defStyleNum="dsDecVal" spellChecking="false"/>
<itemData name="Hex" defStyleNum="dsBaseN" spellChecking="false"/>
<itemData name="Float" defStyleNum="dsFloat" spellChecking="false"/>
<itemData name="String" defStyleNum="dsString"/>
<itemData name="Char" defStyleNum="dsChar" spellChecking="false"/>
<itemData name="Escape" defStyleNum="dsSpecialChar" spellChecking="false"/>
<itemData name="Operator" defStyleNum="dsOperator" spellChecking="false"/>
<itemData name="Symbol" defStyleNum="dsNormal" spellChecking="false"/>
<itemData name="Comment" defStyleNum="dsComment"/>
<itemData name="Error" defStyleNum="dsError" spellChecking="false"/>
</itemDatas>
</highlighting>
<general>
<comments>
<comment name="singleLine" start="//"/>
<comment name="multiLine" start="/*" end="*/" region="Comment"/>
</comments>
<keywords casesensitive="1"/>
<folding indentationsensitive="0"/>
</general>
</language>

View file

@ -1,27 +0,0 @@
#!/usr/bin/env bash
# Installs Finch support for Kate (also KWrite and KDevelop):
# - syntax highlighting: ~/.local/share/org.kde.syntax-highlighting/syntax/finch.xml
# - the language server: adds "finch" to ~/.config/kate/lspclient/settings.json
# Then restart Kate and enable Settings → Configure Kate → Plugins → "LSP Client".
set -eu
here="$(cd "$(dirname "$0")" && pwd)"
data="${XDG_DATA_HOME:-$HOME/.local/share}/org.kde.syntax-highlighting/syntax"
conf="${XDG_CONFIG_HOME:-$HOME/.config}/kate/lspclient"
mkdir -p "$data" "$conf"
cp "$here/finch.xml" "$data/finch.xml"
echo "highlighting: $data/finch.xml"
# merge into the user's LSP settings instead of replacing them
python3 - "$conf/settings.json" "$here/lspclient.json" <<'PY'
import json, os, sys
path, ours = sys.argv[1], json.load(open(sys.argv[2]))
cur = {}
if os.path.exists(path) and os.path.getsize(path) > 0:
cur = json.load(open(path))
cur.setdefault("servers", {}).update(ours["servers"])
with open(path, "w") as f:
json.dump(cur, f, indent=4)
f.write("\n")
print("language server: " + path)
PY
command -v finch > /dev/null || echo "note: 'finch' isn't on your PATH yet; Kate starts the language server with 'finch lsp'"

View file

@ -1,9 +0,0 @@
{
"servers": {
"finch": {
"command": ["finch", "lsp"],
"url": "https://github.com/Nexoniarz/Finch",
"highlightingModeRegex": "^Finch$"
}
}
}

View file

@ -1,2 +0,0 @@
node_modules/
*.vsix

View file

@ -1,8 +0,0 @@
.vscodeignore
test/**
node_modules/.package-lock.json
node_modules/vscode-textmate/**
node_modules/vscode-oniguruma/**
node_modules/@vscode/**
**/*.map
.gitignore

View file

@ -1,23 +0,0 @@
# Changelog
## 2.6.0
- Vector types (`f32x8`, `i8x32`, `f64x4`, …) and masks (`boolx8`) highlighted as types; completion of their methods (`sum`, `dot_add`, `shuffle`, `lookup`, `store`, …).
- `@cpu(...)` and the new built-ins (`select`, `concat`, `fma`, `muladd`, `cpu_has`, `f32_bits`, `f16_to_f32`, …) highlighted.
## 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(...)`.
- Completion for struct methods and map methods; parameter hints for method calls.
- Snippets: `method`, `fnf` (a function that can fail), `map`, `forkv`, `orb`.
## 2.3.0
- First release: highlighting, snippets, language server, Run and Build.

View file

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,15 +0,0 @@
# Finch for Visual Studio Code
Support for the [Finch](https://github.com/Nexoniarz/Finch) programming language (`.fch` files):
- **Syntax highlighting**, bracket matching, comments with `Ctrl+/`
- **Errors as you type**: the real compiler checks the file (`finch lsp`)
- **Completion**: variables, functions, structs, keywords, built-ins, struct fields and array/str methods after `.`, names from imported C headers
- **Hover** shows types and signatures; **Go to Definition** (F12); **Outline** of functions and structs; **parameter hints**
- **Run** button (▶ in the editor title, or `Ctrl+F5`) and **Finch: Build This File**; compile errors land in the Problems panel
- Snippets: `main`, `fn`, `fnr`, `struct`, `if`, `ife`, `for`, `foreach`, `while`, `input`, …
## Requirements
Finch 2.3 or newer. The extension runs `finch` from your PATH; if it lives somewhere else,
set **Finch: Path** (`finch.path`) to the full path of `finch` / `finch.exe`.

View file

@ -1,77 +0,0 @@
// The Finch extension: starts `finch lsp` for editor features and adds Run / Build.
const vscode = require('vscode');
const { LanguageClient, TransportKind } = require('vscode-languageclient/node');
let client = null;
function finchPath() {
return vscode.workspace.getConfiguration('finch').get('path') || 'finch';
}
async function startServer(context) {
if (!vscode.workspace.getConfiguration('finch').get('languageServer')) return;
const command = finchPath();
client = new LanguageClient(
'finch',
'Finch',
{ command, args: ['lsp'], transport: TransportKind.stdio },
{ documentSelector: [{ scheme: 'file', language: 'finch' }] }
);
try {
await client.start();
} catch (err) {
client = null;
const pick = await vscode.window.showErrorMessage(
`Finch: couldn't start "${command} lsp". Is Finch installed? You can set its path in the settings.`,
'Open Settings'
);
if (pick) vscode.commands.executeCommand('workbench.action.openSettings', 'finch.path');
}
}
// Run or build the current file as a task: no shell quoting, and the problem matcher
// turns compile errors into entries in the Problems panel.
async function finchTask(verb) {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'finch') {
vscode.window.showWarningMessage('Finch: open a .fch file first.');
return;
}
if (editor.document.isDirty) await editor.document.save();
const file = editor.document.uri.fsPath;
const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri);
const task = new vscode.Task(
{ type: 'finch', verb, file },
folder || vscode.TaskScope.Workspace,
`${verb} ${require('path').basename(file)}`,
'finch',
new vscode.ProcessExecution(finchPath(), [verb, file], { cwd: require('path').dirname(file) }),
'$finch'
);
task.presentationOptions = { reveal: vscode.TaskRevealKind.Always, focus: verb === 'run', clear: true };
vscode.tasks.executeTask(task);
}
async function activate(context) {
context.subscriptions.push(
vscode.commands.registerCommand('finch.run', () => finchTask('run')),
vscode.commands.registerCommand('finch.build', () => finchTask('build')),
vscode.commands.registerCommand('finch.restartServer', async () => {
if (client) await client.stop();
await startServer(context);
}),
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration('finch')) {
if (client) await client.stop();
await startServer(context);
}
})
);
await startServer(context);
}
async function deactivate() {
if (client) await client.stop();
}
module.exports = { activate, deactivate };

View file

@ -1,23 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<title>Finch</title>
<g transform="translate(0 -26)">
<!-- tail: two feathers -->
<path d="M178 316 L60 360 Q50 374 64 382 L192 348 Z" fill="#F5F3EF"/>
<path d="M182 334 L78 398 Q72 412 88 414 L200 360 Z" fill="#C9C4BC"/>
<!-- body -->
<path d="M150 330 C150 220 240 140 330 140 C400 140 440 190 440 245 C440 330 370 392 280 395 C220 398 168 372 150 330 Z" fill="#FF7A1A"/>
<!-- belly light -->
<path d="M236 390 C336 398 428 330 438 254 C432 300 404 330 372 350 C336 372 290 384 236 390 Z" fill="#FFA24D"/>
<!-- wing, swept back -->
<path d="M352 256 C306 232 228 256 160 340 C244 336 318 316 352 256 Z" fill="#1B1F2A"/>
<path d="M330 260 C292 252 246 270 206 306" fill="none" stroke="#FFFFFF" stroke-width="9" stroke-linecap="round"/>
<!-- beak -->
<path d="M434 212 L494 234 L434 254 Z" fill="#FFC145"/>
<!-- eye -->
<circle cx="382" cy="206" r="16" fill="#1B1F2A"/>
<circle cx="388" cy="200" r="5" fill="#FFFFFF"/>
<!-- legs and branch -->
<path d="M276 394 L270 420 M316 390 L318 420" stroke="#F5F3EF" stroke-width="10" stroke-linecap="round"/>
<path d="M120 424 L440 424" stroke="#F5F3EF" stroke-width="14" stroke-linecap="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,23 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<title>Finch</title>
<g transform="translate(0 -26)">
<!-- tail: two feathers -->
<path d="M178 316 L60 360 Q50 374 64 382 L192 348 Z" fill="#1B1F2A"/>
<path d="M182 334 L78 398 Q72 412 88 414 L200 360 Z" fill="#2E3445"/>
<!-- body -->
<path d="M150 330 C150 220 240 140 330 140 C400 140 440 190 440 245 C440 330 370 392 280 395 C220 398 168 372 150 330 Z" fill="#FF7A1A"/>
<!-- belly light -->
<path d="M236 390 C336 398 428 330 438 254 C432 300 404 330 372 350 C336 372 290 384 236 390 Z" fill="#FFA24D"/>
<!-- wing, swept back -->
<path d="M352 256 C306 232 228 256 160 340 C244 336 318 316 352 256 Z" fill="#1B1F2A"/>
<path d="M330 260 C292 252 246 270 206 306" fill="none" stroke="#FFFFFF" stroke-width="9" stroke-linecap="round"/>
<!-- beak -->
<path d="M434 212 L494 234 L434 254 Z" fill="#FFC145"/>
<!-- eye -->
<circle cx="382" cy="206" r="16" fill="#1B1F2A"/>
<circle cx="388" cy="200" r="5" fill="#FFFFFF"/>
<!-- legs and branch -->
<path d="M276 394 L270 420 M316 390 L318 420" stroke="#1B1F2A" stroke-width="10" stroke-linecap="round"/>
<path d="M120 424 L440 424" stroke="#1B1F2A" stroke-width="14" stroke-linecap="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,19 +0,0 @@
{
"comments": { "lineComment": "//", "blockComment": ["/*", "*/"] },
"brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "/*", "close": " */", "notIn": ["string"] }
],
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""], ["'", "'"]],
"indentationRules": {
"increaseIndentPattern": "^.*\\{[^}\"']*$|^.*\\([^)\"']*$|^.*\\[[^\\]\"']*$",
"decreaseIndentPattern": "^\\s*[}\\])]"
},
"wordPattern": "[A-Za-z_][A-Za-z0-9_]*|-?\\d[\\d_]*(\\.\\d+)?",
"folding": { "markers": { "start": "^\\s*//\\s*#?region\\b", "end": "^\\s*//\\s*#?endregion\\b" } }
}

File diff suppressed because it is too large Load diff

View file

@ -1,154 +0,0 @@
{
"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.6.0",
"publisher": "nexoniarz",
"license": "Apache-2.0",
"icon": "images/icon.png",
"repository": {
"type": "git",
"url": "https://github.com/Nexoniarz/Finch"
},
"homepage": "https://github.com/Nexoniarz/Finch",
"bugs": {
"url": "https://github.com/Nexoniarz/Finch/issues"
},
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Programming Languages",
"Linters",
"Snippets"
],
"keywords": [
"finch",
"llvm",
"compiler",
"language"
],
"main": "./extension.js",
"activationEvents": [
"onLanguage:finch"
],
"contributes": {
"languages": [
{
"id": "finch",
"aliases": [
"Finch",
"finch"
],
"extensions": [
".fch"
],
"configuration": "./language-configuration.json",
"icon": {
"light": "./images/file-light.svg",
"dark": "./images/file-dark.svg"
}
}
],
"grammars": [
{
"language": "finch",
"scopeName": "source.finch",
"path": "./syntaxes/finch.tmLanguage.json"
}
],
"snippets": [
{
"language": "finch",
"path": "./snippets/finch.json"
}
],
"configuration": {
"title": "Finch",
"properties": {
"finch.path": {
"type": "string",
"default": "finch",
"description": "The finch compiler: a command on your PATH, or the full path to finch / finch.exe."
},
"finch.languageServer": {
"type": "boolean",
"default": true,
"description": "Use `finch lsp` for errors as you type, completion, hover and go to definition."
}
}
},
"commands": [
{
"command": "finch.run",
"title": "Finch: Run This File",
"icon": "$(play)"
},
{
"command": "finch.build",
"title": "Finch: Build This File",
"icon": "$(tools)"
},
{
"command": "finch.restartServer",
"title": "Finch: Restart Language Server"
}
],
"menus": {
"editor/title/run": [
{
"command": "finch.run",
"when": "resourceLangId == finch",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "finch.run",
"when": "editorLangId == finch"
},
{
"command": "finch.build",
"when": "editorLangId == finch"
}
]
},
"keybindings": [
{
"command": "finch.run",
"key": "ctrl+f5",
"mac": "cmd+f5",
"when": "editorLangId == finch"
}
],
"problemMatchers": [
{
"name": "finch",
"owner": "finch",
"fileLocation": [
"autoDetect",
"${workspaceFolder}"
],
"pattern": {
"regexp": "^(.*?):(\\d+):(\\d+): error: (.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
}
]
},
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"scripts": {
"test": "node test/grammar.js",
"package": "vsce package"
},
"devDependencies": {
"vscode-textmate": "^9.2.0",
"vscode-oniguruma": "^2.0.1",
"@vscode/vsce": "^3.2.0"
}
}

View file

@ -1,198 +0,0 @@
{
"main": {
"prefix": "main",
"body": [
"fn main() {",
"\t$0",
"}"
],
"description": "The program's start"
},
"main with arguments": {
"prefix": "mainargs",
"body": [
"fn main([]str args) {",
"\t$0",
"}"
],
"description": "main that gets the command-line arguments"
},
"function": {
"prefix": "fn",
"body": [
"fn ${1:name}(${2}) {",
"\t$0",
"}"
],
"description": "A function"
},
"function with result": {
"prefix": "fnr",
"body": [
"fn ${1:name}(${2}) -> ${3:int} {",
"\t$0",
"}"
],
"description": "A function that returns a value"
},
"struct": {
"prefix": "struct",
"body": [
"struct ${1:Name} {",
"\t${2:int} ${3:field}",
"}"
],
"description": "A struct"
},
"if": {
"prefix": "if",
"body": [
"if ${1:condition} {",
"\t$0",
"}"
]
},
"if else": {
"prefix": "ife",
"body": [
"if ${1:condition} {",
"\t$2",
"} else {",
"\t$0",
"}"
]
},
"for range": {
"prefix": "for",
"body": [
"for ${1:i} in ${2:0}..${3:n} {",
"\t$0",
"}"
],
"description": "Count from start to end-1"
},
"for each": {
"prefix": "foreach",
"body": [
"for ${1:item} in ${2:list} {",
"\t$0",
"}"
],
"description": "Every element of an array or str"
},
"while": {
"prefix": "while",
"body": [
"while ${1:condition} {",
"\t$0",
"}"
]
},
"print": {
"prefix": "print",
"body": [
"print(${1})"
]
},
"input": {
"prefix": "input",
"body": [
"${1:answer} := input(\"${2:Question? }\")"
]
},
"import C header": {
"prefix": "importc",
"body": [
"import \"${1:stdio.h}\""
]
},
"defer": {
"prefix": "defer",
"body": [
"defer ${1}"
],
"description": "Run this when the block ends"
},
"method": {
"prefix": "method",
"body": [
"fn ${1:Type}.${2:name}(${3}) {",
"\t$0",
"}"
],
"description": "A method: self is the struct it is called on"
},
"fallible function": {
"prefix": "fnf",
"body": [
"fn ${1:name}(${2}) -> ${3:int}! {",
"\t$0",
"}"
],
"description": "A function that can fail: return error(\"...\")"
},
"map": {
"prefix": "map",
"body": [
"map[${1:str}]${2:int} ${3:name}"
],
"description": "An empty map"
},
"or block": {
"prefix": "orb",
"body": [
"or {",
"\t${1:print(err)}",
"\t${2:return}",
"}"
],
"description": "Handle a failure: err is the message"
},
"for key, value": {
"prefix": "forkv",
"body": [
"for ${1:key}, ${2:value} in ${3:map} {",
"\t$0",
"}"
],
"description": "Every key and value of a map (or index and element of an array)"
},
"enum": {
"prefix": "enum",
"body": [
"enum ${1:Name} {",
"\t${2:First},",
"\t${3:Second}",
"}"
],
"description": "An enum: a type with a few named values"
},
"match": {
"prefix": "match",
"body": [
"match ${1:value} {",
"\t${2:pattern} -> $0",
"\telse -> ",
"}"
],
"description": "match: pick what to do from a value"
},
"generic function": {
"prefix": "fng",
"body": [
"fn ${1:name}[${2:T}](${3:T x}) -> ${2:T} {",
"\t$0",
"}"
],
"description": "A function that works for any type"
},
"generic struct": {
"prefix": "structg",
"body": [
"struct ${1:Name}[${2:T}] {",
"\t$0",
"}"
],
"description": "A struct that holds any type"
}
}

View file

@ -1,318 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Finch",
"scopeName": "source.finch",
"fileTypes": [
"fch"
],
"patterns": [
{
"include": "#comments"
},
{
"include": "#imports"
},
{
"include": "#struct"
},
{
"include": "#function"
},
{
"include": "#statements"
}
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.line.double-slash.finch",
"match": "//.*$"
},
{
"name": "comment.block.finch",
"begin": "/\\*",
"end": "\\*/"
}
]
},
"imports": {
"patterns": [
{
"match": "^\\s*(import)\\s+(\"[^\"]*\")",
"captures": {
"1": {
"name": "keyword.control.import.finch"
},
"2": {
"name": "string.quoted.double.include.finch"
}
}
},
{
"match": "^\\s*(import)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": {
"name": "keyword.control.import.finch"
},
"2": {
"name": "entity.name.namespace.finch"
}
}
},
{
"match": "^\\s*(link)\\s+(\"[^\"]*\")",
"captures": {
"1": {
"name": "keyword.control.import.finch"
},
"2": {
"name": "string.quoted.double.finch"
}
}
}
]
},
"struct": {
"match": "\\b(struct|enum)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": {
"name": "storage.type.struct.finch"
},
"2": {
"name": "entity.name.type.struct.finch"
}
}
},
"function": {
"match": "\\b(fn)\\s+(?:([A-Za-z_][A-Za-z0-9_]*)(\\.))?([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": {
"name": "storage.type.function.finch"
},
"2": {
"name": "entity.name.type.finch"
},
"3": {
"name": "punctuation.accessor.finch"
},
"4": {
"name": "entity.name.function.finch"
}
}
},
"statements": {
"patterns": [
{
"name": "keyword.other.test.finch",
"match": "\\btest(?=\\s*\")"
},
{
"include": "#comments"
},
{
"include": "#longstrings"
},
{
"include": "#strings"
},
{
"include": "#chars"
},
{
"include": "#numbers"
},
{
"name": "keyword.control.finch",
"match": "\\b(if|else|while|for|in|break|continue|return|defer|or|try|match)\\b"
},
{
"name": "storage.type.function.finch",
"match": "\\bfn\\b"
},
{
"name": "storage.type.struct.finch",
"match": "\\b(struct|enum|const)\\b"
},
{
"name": "storage.modifier.finch",
"match": "\\bexport\\b"
},
{
"name": "constant.language.finch",
"match": "\\b(true|false|null)\\b"
},
{
"name": "variable.language.self.finch",
"match": "\\bself\\b"
},
{
"name": "storage.modifier.attribute.finch",
"match": "@cpu\\b"
},
{
"name": "storage.type.primitive.finch",
"match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|(?:[iuf](?:8|16|32|64)|bool)x(?:2|4|8|16|32|64))\\b"
},
{
"match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas|select|concat|fma|muladd|cpu_has|f32_bits|f32_from_bits|f64_bits|f64_from_bits|f16_to_f32|f32_to_f16|bf16_to_f32|f32_to_bf16)\\s*(?=\\()",
"captures": {
"1": {
"name": "support.function.builtin.finch"
}
}
},
{
"match": "(?<=\\.)(len|ptr|value)\\b(?!\\s*\\()",
"captures": {
"1": {
"name": "variable.other.property.finch"
}
}
},
{
"match": "\\b([A-Z][A-Za-z0-9_]*)\\s*(?=\\()",
"captures": {
"1": {
"name": "entity.name.type.finch"
}
}
},
{
"match": "\\b([a-z_][A-Za-z0-9_]*)\\s*(?=\\()",
"captures": {
"1": {
"name": "entity.name.function.call.finch"
}
}
},
{
"name": "entity.name.type.finch",
"match": "\\b[A-Z][A-Za-z0-9_]*\\b"
},
{
"name": "keyword.operator.assignment.finch",
"match": ":=|\\+=|-=|\\*=|/=|%=|(?<![=!<>])=(?!=)"
},
{
"name": "keyword.operator.comparison.finch",
"match": "==|!=|<=|>=|<|>"
},
{
"name": "keyword.operator.logical.finch",
"match": "&&|\\|\\||!"
},
{
"name": "keyword.operator.arrow.finch",
"match": "->"
},
{
"name": "keyword.operator.range.finch",
"match": "\\.\\."
},
{
"name": "keyword.operator.arithmetic.finch",
"match": "<<|>>|[-+*/%&|^~]"
},
{
"name": "variable.other.finch",
"match": "\\b[a-z_][A-Za-z0-9_]*\\b"
}
]
},
"strings": {
"name": "string.quoted.double.finch",
"begin": "\"",
"end": "\"|$",
"patterns": [
{
"include": "#interpolation"
},
{
"name": "constant.character.escape.finch",
"match": "\\\\[ntr0\\\\\"']"
},
{
"name": "invalid.illegal.escape.finch",
"match": "\\\\."
}
]
},
"chars": {
"name": "string.quoted.single.finch",
"begin": "'",
"end": "'|$",
"patterns": [
{
"name": "constant.character.escape.finch",
"match": "\\\\[ntr0\\\\\"']"
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.hex.finch",
"match": "\\b0[xX][0-9A-Fa-f_]+\\b"
},
{
"name": "constant.numeric.float.finch",
"match": "\\b\\d[\\d_]*(\\.\\d[\\d_]*([eE][+-]?\\d+)?|[eE][+-]?\\d+)\\b"
},
{
"name": "constant.numeric.integer.finch",
"match": "\\b\\d[\\d_]*\\b"
}
]
},
"longstrings": {
"name": "string.quoted.triple.finch",
"begin": "\"\"\"",
"end": "\"\"\"",
"patterns": [
{
"include": "#interpolation"
},
{
"name": "constant.character.escape.finch",
"match": "\\\\[ntr0\\\\\"']"
}
]
},
"interpolation": {
"name": "meta.embedded.line.finch",
"begin": "\\\\\\(",
"end": "\\)",
"beginCaptures": {
"0": {
"name": "punctuation.section.embedded.begin.finch"
}
},
"endCaptures": {
"0": {
"name": "punctuation.section.embedded.end.finch"
}
},
"patterns": [
{
"include": "#interpolation-parens"
},
{
"include": "$self"
}
]
},
"interpolation-parens": {
"begin": "\\(",
"end": "\\)",
"patterns": [
{
"include": "#interpolation-parens"
},
{
"include": "$self"
}
]
}
}
}

View file

@ -1,72 +0,0 @@
// Tokenizes Finch code with the same engine VS Code uses and checks the scopes.
const fs = require('fs'), path = require('path');
const vsctm = require('vscode-textmate'), oniguruma = require('vscode-oniguruma');
const grammarPath = path.join(__dirname, "..", "syntaxes", "finch.tmLanguage.json");
const wasm = fs.readFileSync(require.resolve("vscode-oniguruma/release/onig.wasm")).buffer;
const onigLib = oniguruma.loadWASM(wasm).then(() => ({
createOnigScanner: (s) => new oniguruma.OnigScanner(s),
createOnigString: (s) => new oniguruma.OnigString(s),
}));
const registry = new vsctm.Registry({ onigLib, loadGrammar: async () => vsctm.parseRawGrammar(fs.readFileSync(grammarPath, 'utf8'), grammarPath) });
const code = [
'import "stdio.h"',
'import shapes',
'struct Point {',
' int x = 0x1F',
'}',
'fn add(int a, float b) -> int {',
' s := "hi\\n" + str(a) // note',
' for i in 0..10 { print(i, \'c\', 3.5, true, null) }',
' p := Point(x: 1)',
' return a + p.x',
'}',
'fn Point.move(int dx) -> int! {',
' map[str]int m',
' n := try parse(s) or 0',
' self.x += dx',
' return error("no")',
'}',
'test "lives \\(hp + f(1)) left" {',
' assert(clamp(x, 0, 9) == 3)',
'}',
'fn dot(f32x8 a, i8x32 b) {',
'@cpu("avx2") fn k(boolx8 m) { select(m, a, b) }',
];
const expect = [ // [line, token text, scope that must be present]
[0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'],
[1, 'shapes', 'entity.name.namespace'],
[2, 'struct', 'storage.type.struct'], [2, 'Point', 'entity.name.type.struct'],
[3, 'int', 'storage.type.primitive'], [3, '0x1F', 'constant.numeric.hex'],
[5, 'fn', 'storage.type.function'], [5, 'add', 'entity.name.function'], [5, 'float', 'storage.type.primitive'], [5, '->', 'keyword.operator.arrow'],
[6, ':=', 'keyword.operator.assignment'], [6, '\\n', 'constant.character.escape'], [6, '// note', 'comment.line'], [6, 'str', 'storage.type.primitive'],
[7, 'for', 'keyword.control'], [7, 'in', 'keyword.control'], [7, '..', 'keyword.operator.range'], [7, 'print', 'support.function.builtin'],
[7, 'c', 'string.quoted.single'], [7, '3.5', 'constant.numeric.float'], [7, 'true', 'constant.language'], [7, 'null', 'constant.language'],
[8, 'Point', 'entity.name.type'],
[9, 'return', 'keyword.control'],
[11, 'Point', 'entity.name.type'], [11, 'move', 'entity.name.function'], [11, '!', 'keyword.operator'],
[12, 'map', 'storage.type.primitive'],
[13, 'try', 'keyword.control'], [13, 'or', 'keyword.control'],
[14, 'self', 'variable.language.self'],
[15, 'error', 'support.function.builtin'],
[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'],
[20, 'f32x8', 'storage.type.primitive'], [20, 'i8x32', 'storage.type.primitive'],
[21, '@cpu', 'storage.modifier.attribute'], [21, 'boolx8', 'storage.type.primitive'], [21, 'select', 'support.function.builtin'],
];
registry.loadGrammar('source.finch').then((g) => {
let state = vsctm.INITIAL, toks = [];
code.forEach((line, i) => {
const r = g.tokenizeLine(line, state);
r.tokens.forEach((t) => toks.push([i, line.substring(t.startIndex, t.endIndex), t.scopes.join(' ')]));
state = r.ruleStack;
});
let bad = 0;
for (const [line, text, scope] of expect) {
const hit = toks.find((t) => t[0] === line && t[1].trim() === text);
const ok = hit && hit[2].includes(scope);
if (!ok) { bad++; console.log('FAIL', line, JSON.stringify(text), 'wanted', scope, 'got', hit ? hit[2] : '(no token)'); }
}
console.log(bad ? `grammar: ${bad} failed` : `grammar: all ${expect.length} ok`);
process.exit(bad ? 1 : 0);
});

View file

@ -1,69 +0,0 @@
// Vectors (SIMD): eight f32 at once instead of one.
//
// finch run examples/simd.fch runs on any x86-64 or ARM64 processor
// finch run examples/simd.fch --cpu native uses all of this one's instructions (AVX2, AVX-512, NEON)
import time
// one number at a time
fn dot_plain([]f32 a, []f32 b) -> f32 {
f32 total = 0.0
for i in 0..a.len {
total += a[i] * b[i]
}
return total
}
// eight at a time: f32x8(list, i) takes list[i] ... list[i+7]; muladd multiplies and adds, in one step where it can
fn dot_simd([]f32 a, []f32 b) -> f32 {
acc := f32x8(0.0)
i := 0
while i + 8 <= a.len {
acc = f32x8(a, i).muladd(f32x8(b, i), acc)
i += 8
}
total := acc.sum()
while i < a.len { // the few that are left
total += a[i] * b[i]
i += 1
}
return total
}
// brighten every color: the same work on every lane, with min keeping it below 1
fn brighten([]f32 pixels, f32 amount) {
i := 0
while i + 8 <= pixels.len {
v := min(f32x8(pixels, i) * (1.0 + amount), 1.0)
v.store(pixels, i)
i += 8
}
}
fn main() {
[]f32 a
[]f32 b
for i in 0..4_000_000 {
a.push(f32(i % 100) / 100.0)
b.push(0.5)
}
start := time.millis()
x := 0.0
for round in 0..20 {
x = float(dot_plain(a, b))
}
plain := time.millis() - start
start = time.millis()
y := 0.0
for round in 0..20 {
y = float(dot_simd(a, b))
}
simd := time.millis() - start
print("one at a time: ", str(x, 1), "in", plain, "ms")
print("eight at a time:", str(y, 1), "in", simd, "ms")
brighten(a, 0.5)
print("brightened:", a[10], a[90], a.len)
}

View file

@ -1,62 +0,0 @@
// Counts the words in a file: maps, methods and errors as values.
// finch run examples/wordcount.fch README.md
struct Counter {
map[str]int counts
int total
}
// lower-case letters and digits only: "Hello," -> "hello"
fn clean(str word) -> str {
out := ""
for c in word.lower() {
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {
out += str(c)
}
}
return out
}
fn Counter.add(str text) {
for word in text.split(" ") {
w := clean(word)
if w.len > 0 {
self.counts[w] += 1 // a new word starts at 0
self.total += 1
}
}
}
// the n most frequent words, most frequent first
fn Counter.top(int n) -> []str {
words := self.counts.keys()
for i in 0..words.len {
for j in i + 1..words.len {
if self.counts[words[j]] > self.counts[words[i]] {
w := words[i]
words[i] = words[j]
words[j] = w
}
}
}
if words.len > n {
words.resize(n)
}
return words
}
fn main([]str args) -> ! {
if args.len < 2 {
return error("usage: wordcount <file>")
}
text := try read_file(args[1]) // a missing file ends the program with a clear message
Counter c
for line in text.split("\n") {
c.add(line)
}
print(c.total, "words,", c.counts.len, "different")
for i, w in c.top(10) {
print(i + 1, w, c.counts[w])
}
}

1
notatka.txt Normal file
View file

@ -0,0 +1 @@
Kupić mleko

1
note.txt Normal file
View file

@ -0,0 +1 @@
Buy milk

View file

@ -2,36 +2,20 @@
// It is embedded in the compiler, compiled once into a cache folder, and linked // It is embedded in the compiler, compiled once into a cache folder, and linked
// statically into every Finch program. Everything here is plain C with no state. // statically into every Finch program. Everything here is plain C with no state.
// //
// Layout shared with the compiler (src/gen.fch): // Layout shared with the compiler (src/codegen.cpp):
// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated // str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated
// cap > 0 : heap memory owned by the value (cap bytes allocated) // cap > 0 : heap memory owned by the value (cap bytes allocated)
// cap == 0 : a string literal, valid forever, never written to // cap == 0 : a string literal, valid forever, never written to
// cap == -1: borrowed from C, must be copied before it is kept // cap == -1: borrowed from C, must be copied before it is kept
// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0 // []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0
// map[K]V: see "maps" below
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
#include <math.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#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> #include <sys/wait.h>
#endif
typedef struct { typedef struct {
char *ptr; char *ptr;
@ -49,52 +33,10 @@ static char empty[1] = "";
// ---------- runtime errors ---------- // ---------- runtime errors ----------
// A program that loads Finch libraries (finch build --shared) can take runtime errors over
// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler
// gets the message and must not return (longjmp out, or exit itself).
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);
}
__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { __attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {
char full[1024];
snprintf(full, sizeof full, "%s:%lld: runtime error: %s", file, (long long)line, msg);
fflush(stdout); fflush(stdout);
fatal(full); fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg);
exit(1);
} }
__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) { __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {
@ -106,7 +48,8 @@ __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t
static void *must(void *p) { static void *must(void *p) {
if (!p) { if (!p) {
fflush(stdout); fflush(stdout);
fatal("runtime error: out of memory"); fprintf(stderr, "runtime error: out of memory\n");
exit(1);
} }
return p; return p;
} }
@ -114,50 +57,6 @@ static void *must(void *p) {
void *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); } 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)); } 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 ---------- // ---------- strings ----------
static void str_heap(FStr *out, const char *p, int64_t len) { static void str_heap(FStr *out, const char *p, int64_t len) {
@ -237,136 +136,43 @@ void finch_str_from_uint(FStr *out, uint64_t v) {
str_heap(out, buf, n); 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) { void finch_str_from_float(FStr *out, double v) {
char buf[64]; char buf[64];
int n = fmt_float(buf, sizeof buf, v, 0); int n = snprintf(buf, sizeof buf, "%g", v);
str_heap(out, buf, n); 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_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"); } void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); }
static void number_error(char *msg, size_t size, const FStr *s, const char *kind) { static void bad_number(const FStr *s, const char *kind, const char *file, int64_t line) {
snprintf(msg, size, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : "", kind); char msg[200];
snprintf(msg, sizeof msg, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr, kind);
finch_panic(file, line, msg);
} }
static int parse_int(const FStr *s, int64_t *out) { int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
const char *p = s->ptr ? s->ptr : ""; const char *p = s->ptr;
while (isspace((unsigned char)*p)) p++; while (isspace((unsigned char)*p)) p++;
char *end; char *end;
errno = 0; errno = 0;
long long v = strtoll(p, &end, 10); long long v = strtoll(p, &end, 10);
while (isspace((unsigned char)*end)) end++; while (isspace((unsigned char)*end)) end++;
*out = v; if (end == p || *end || errno == ERANGE) bad_number(s, "int", file, line);
return !(end == p || *end || errno == ERANGE);
}
static int parse_float(const FStr *s, double *out) {
const char *p = s->ptr ? s->ptr : "";
while (isspace((unsigned char)*p)) p++;
char *end;
double v = strtod(p, &end);
while (isspace((unsigned char)*end)) end++;
*out = v;
return !(end == p || *end);
}
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
int64_t v;
if (!parse_int(s, &v)) {
char msg[200];
number_error(msg, sizeof msg, s, "int");
finch_panic(file, line, msg);
}
return v; return v;
} }
double finch_str_to_float(const FStr *s, const char *file, int64_t line) { double finch_str_to_float(const FStr *s, const char *file, int64_t line) {
double v; const char *p = s->ptr;
if (!parse_float(s, &v)) { while (isspace((unsigned char)*p)) p++;
char msg[200]; char *end;
number_error(msg, sizeof msg, s, "float"); double v = strtod(p, &end);
finch_panic(file, line, msg); while (isspace((unsigned char)*end)) end++;
} if (end == p || *end) bad_number(s, "float", file, line);
return v; return v;
} }
// ---------- errors as values: int(s) or 0, try read_file(p) ... ----------
// Each gives back 1 on success; on failure 0, with the message in *err.
static void set_error(FStr *err, const char *msg) { str_heap(err, msg, (int64_t)strlen(msg)); }
int finch_str_try_int(const FStr *s, int64_t *out, FStr *err) {
if (parse_int(s, out)) return 1;
char msg[200];
number_error(msg, sizeof msg, s, "int");
set_error(err, msg);
*out = 0;
return 0;
}
int finch_str_try_float(const FStr *s, double *out, FStr *err) {
if (parse_float(s, out)) return 1;
char msg[200];
number_error(msg, sizeof msg, s, "float");
set_error(err, msg);
*out = 0;
return 0;
}
static void file_error(FStr *err, const char *what, const FStr *path) {
char msg[300];
snprintf(msg, sizeof msg, "can't %s the file \"%.*s\": %s", what, (int)(path->len > 200 ? 200 : path->len),
path->ptr ? path->ptr : "", strerror(errno));
set_error(err, msg);
}
// when `fn main() -> !` fails
void finch_report_error(const FStr *msg) {
fflush(stdout);
fprintf(stderr, "error: %.*s\n", (int)msg->len, msg->ptr ? msg->ptr : "");
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) { void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {
if (start < 0 || end > s->len || start > end) { if (start < 0 || end > s->len || start > end) {
char msg[160]; char msg[160];
@ -495,65 +301,6 @@ void finch_str_repeat(FStr *out, const FStr *s, int64_t n) {
void finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); } void finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }
// ---------- building text: print(x) and str(x) of arrays, maps and structs ----------
// A buffer starts as an empty str and grows on the heap.
void finch_buf_add(FStr *b, const char *p, int64_t n) {
if (b->len + n + 1 > b->cap) {
int64_t cap = b->cap > 0 ? b->cap * 2 : 32;
while (cap < b->len + n + 1) cap *= 2;
b->ptr = must(realloc(b->cap > 0 ? b->ptr : NULL, (size_t)cap));
b->cap = cap;
}
if (n) memcpy(b->ptr + b->len, p, (size_t)n);
b->len += n;
b->ptr[b->len] = 0;
}
static void buf_fmt(FStr *b, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void buf_fmt(FStr *b, const char *fmt, ...) {
char t[64];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(t, sizeof t, fmt, ap);
va_end(ap);
finch_buf_add(b, t, n);
}
void finch_buf_int(FStr *b, int64_t v) { buf_fmt(b, "%lld", (long long)v); }
void finch_buf_uint(FStr *b, uint64_t v) { buf_fmt(b, "%llu", (unsigned long long)v); }
void finch_buf_float(FStr *b, double v) {
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);
else finch_buf_add(b, "null", 4);
}
void finch_buf_char(FStr *b, char c, int quoted) {
if (quoted) finch_buf_add(b, "'", 1);
finch_buf_add(b, &c, 1);
if (quoted) finch_buf_add(b, "'", 1);
}
void finch_buf_str(FStr *b, const FStr *s, int quoted) {
if (quoted) finch_buf_add(b, "\"", 1);
finch_buf_add(b, s->ptr, s->len);
if (quoted) finch_buf_add(b, "\"", 1);
}
// print the buffer and free it
void finch_buf_print(FStr *b) {
if (b->len) fwrite(b->ptr, 1, (size_t)b->len, stdout);
if (b->cap > 0) free(b->ptr);
}
// ---------- arrays ---------- // ---------- arrays ----------
void finch_arr_reserve(FArr *a, int64_t need, int64_t es) { void finch_arr_reserve(FArr *a, int64_t need, int64_t es) {
@ -628,35 +375,21 @@ 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])); 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. // input("Name? ") prints the prompt and reads one line, without the newline.
void finch_input(FStr *out, const FStr *prompt) { void finch_input(FStr *out, const FStr *prompt) {
if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout); if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);
fflush(stdout); fflush(stdout);
// read one line of any length (getline isn't available everywhere) char *line = NULL;
size_t size = 128, n = 0; size_t size = 0;
char *line = finch_alloc((int64_t)size); ssize_t n = getline(&line, &size, stdin);
int c, got = 0; if (n < 0) {
while ((c = fgetc(stdin)) != EOF) {
got = 1;
if (c == '\n') break;
if (n + 1 >= size) line = must(realloc(line, size *= 2));
line[n++] = (char)c;
}
if (!got) {
free(line); free(line);
str_static(out, empty); str_static(out, empty);
return; return;
} }
while (n > 0 && line[n - 1] == '\r') n--; while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = 0;
line[n] = 0;
out->ptr = line; out->ptr = line;
out->len = (int64_t)n; out->len = n;
out->cap = (int64_t)size; out->cap = (int64_t)size;
} }
@ -666,27 +399,8 @@ int finch_file_exists(const FStr *path) {
return f != NULL; return f != NULL;
} }
int finch_try_read_file(FStr *out, const FStr *path, FStr *err) {
FILE *f = fopen(path->ptr ? path->ptr : "", "rb");
if (!f) {
file_error(err, "read", path);
return 0;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *m = finch_alloc(size + 1);
size_t got = fread(m, 1, (size_t)size, f);
fclose(f);
m[got] = 0;
out->ptr = m;
out->len = (int64_t)got;
out->cap = size + 1;
return 1;
}
void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) { void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {
FILE *f = fopen(path->ptr ? path->ptr : "", "rb"); FILE *f = fopen(path->ptr, "rb");
if (!f) { if (!f) {
char msg[300]; char msg[300];
snprintf(msg, sizeof msg, "can't read the file \"%.*s\": %s", (int)(path->len > 200 ? 200 : path->len), snprintf(msg, sizeof msg, "can't read the file \"%.*s\": %s", (int)(path->len > 200 ? 200 : path->len),
@ -709,534 +423,14 @@ void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line
int64_t finch_shell(const FStr *cmd) { int64_t finch_shell(const FStr *cmd) {
fflush(stdout); fflush(stdout);
int status = system(cmd->ptr ? cmd->ptr : ""); int status = system(cmd->ptr ? cmd->ptr : "");
#ifdef _WIN32
return status; // Windows gives the exit code directly
#else
if (status == -1) return -1; if (status == -1) return -1;
if (WIFEXITED(status)) return WEXITSTATUS(status); if (WIFEXITED(status)) return WEXITSTATUS(status);
return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0); return 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);
#endif
} }
int finch_delete_file(const FStr *path) { return remove(path->ptr ? path->ptr : "") == 0; }
int finch_try_delete_file(const FStr *path, FStr *err) {
if (remove(path->ptr ? path->ptr : "") == 0) return 1;
file_error(err, "delete", path);
return 0;
}
// stdin/stdout/stderr are macros on some systems (Windows), so Finch asks for them here
void *finch_std_stream(int i) { return i == 0 ? (void *)stdin : i == 1 ? (void *)stdout : (void *)stderr; }
int finch_write_file(const FStr *path, const FStr *text) { int finch_write_file(const FStr *path, const FStr *text) {
FILE *f = fopen(path->ptr ? path->ptr : "", "wb"); FILE *f = fopen(path->ptr, "wb");
if (!f) return 0; if (!f) return 0;
size_t put = text->len ? fwrite(text->ptr, 1, (size_t)text->len, f) : 0; size_t put = fwrite(text->ptr, 1, (size_t)text->len, f);
return fclose(f) == 0 && put == (size_t)text->len; return fclose(f) == 0 && put == (size_t)text->len;
} }
int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {
errno = 0;
if (finch_write_file(path, text)) return 1;
if (!errno) errno = EIO;
file_error(err, "write", path);
return 0;
}
// ---------- 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.
// A removed entry keeps its slot (hash 0) until the next resize packs the slots.
// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.
// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.
// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.
// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).
typedef struct {
char *entries;
int64_t len, used, cap;
int64_t *index;
int64_t icap;
int64_t adds;
} FMap;
static uint64_t map_hash(const void *key, int64_t ks, int kstr) {
const unsigned char *p;
size_t n;
if (kstr) {
const FStr *s = key;
p = (const unsigned char *)s->ptr;
n = (size_t)s->len;
} else {
p = key;
n = (size_t)ks;
}
uint64_t h = 1469598103934665603ULL; // FNV-1a, then mixed so the low bits spread well
for (size_t i = 0; i < n; i++) {
h ^= p[i];
h *= 1099511628211ULL;
}
h ^= h >> 31;
h *= 0xbf58476d1ce4e5b9ULL;
h ^= h >> 29;
return h | 1; // 0 marks a removed entry
}
static int map_key_eq(const char *a, const void *b, int64_t ks, int kstr) {
if (!kstr) return memcmp(a, b, (size_t)ks) == 0;
const FStr *x = (const FStr *)a, *y = b;
return x->len == y->len && (x->len == 0 || memcmp(x->ptr, y->ptr, (size_t)x->len) == 0);
}
static void map_reindex(FMap *m, int64_t es) {
if (m->len != m->used) { // pack the slots of removed entries away
int64_t j = 0;
for (int64_t i = 0; i < m->used; i++) {
char *e = m->entries + i * es;
if (*(uint64_t *)e == 0) continue;
if (i != j) memmove(m->entries + j * es, e, (size_t)es);
j++;
}
m->used = j;
}
int64_t icap = 16;
while (icap < m->cap * 2) icap *= 2;
free(m->index);
m->index = finch_alloc(icap * (int64_t)sizeof(int64_t));
memset(m->index, 0xff, (size_t)icap * sizeof(int64_t));
m->icap = icap;
for (int64_t i = 0; i < m->used; i++) {
uint64_t h = *(uint64_t *)(m->entries + i * es);
int64_t s = (int64_t)(h & (uint64_t)(icap - 1));
while (m->index[s] != -1) s = (s + 1) & (icap - 1);
m->index[s] = i;
}
}
// the entry number of `key`, or -1
int64_t finch_map_find(const FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr) {
if (m->len == 0) return -1;
uint64_t h = map_hash(key, ks, kstr);
for (int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));; s = (s + 1) & (m->icap - 1)) {
int64_t i = m->index[s];
if (i == -1) return -1;
if (i < 0) continue;
char *e = m->entries + i * es;
if (*(uint64_t *)e == h && map_key_eq(e + 8, key, ks, kstr)) return i;
}
}
// The entry for `key`. A new one is zeroed except its hash (*added = 1); the compiler then stores the key.
int64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t kstr, int32_t *added) {
int64_t i = finch_map_find(m, key, es, ks, kstr);
*added = i < 0;
if (i >= 0) return i;
if (m->used == m->cap) {
if (m->len * 4 < m->cap * 3) { // many removed slots: pack them instead of growing
map_reindex(m, es);
} else {
m->cap = m->cap ? m->cap * 2 : 8;
m->entries = must(realloc(m->entries, (size_t)(m->cap * es)));
map_reindex(m, es);
}
}
uint64_t h = map_hash(key, ks, kstr);
i = m->used++;
char *e = m->entries + i * es;
memset(e, 0, (size_t)es);
*(uint64_t *)e = h;
int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));
while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);
m->index[s] = i;
m->len++;
m->adds++;
return i;
}
// Forget entry i (its key and value were already dropped by the compiler).
void finch_map_remove_at(FMap *m, int64_t i, int64_t es) {
char *e = m->entries + i * es;
uint64_t h = *(uint64_t *)e;
int64_t s = (int64_t)(h & (uint64_t)(m->icap - 1));
while (m->index[s] != i) s = (s + 1) & (m->icap - 1);
m->index[s] = -2;
*(uint64_t *)e = 0;
if (--m->len == 0) {
m->used = 0;
memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));
}
}
void finch_map_clear(FMap *m) {
m->len = m->used = 0;
if (m->index) memset(m->index, 0xff, (size_t)m->icap * sizeof(int64_t));
}
void finch_map_free(FMap *m) {
free(m->entries);
free(m->index);
}
// A copy of the slots and the index; the compiler then copies the keys and values that own memory.
void finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {
*out = *m;
if (!m->cap) return;
out->entries = finch_alloc(m->cap * es);
memcpy(out->entries, m->entries, (size_t)(m->used * es));
out->index = finch_alloc(m->icap * (int64_t)sizeof(int64_t));
memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));
}
__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {
finch_panic(file, line, "a key was added to the map while looping over it (collect the new keys, and add them after the loop)");
}
__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {
char msg[200];
snprintf(msg, sizeof msg, "the key %.*s is not in the map (check with .has(key), or use .get(key, default))",
(int)(key->len > 100 ? 100 : key->len), key->ptr ? key->ptr : "");
finch_panic(file, line, msg);
}
// ---------- cpu_has("avx2"): what the processor running the program can do ----------
//
// x86: CPUID, and for the AVX and AVX-512 families also XGETBV (the system must save those registers, or the
// instructions fault). ARM64: the kernel's hwcaps on Linux and Android, sysctl on macOS. A name of the other
// architecture is false.
#if defined(__x86_64__) || defined(__i386__)
#include <cpuid.h>
static int x86_leaf(unsigned leaf, unsigned sub, unsigned r[4]) {
unsigned max = __get_cpuid_max(0, 0);
if (leaf > max) return 0;
__cpuid_count(leaf, sub, r[0], r[1], r[2], r[3]);
return 1;
}
static unsigned long long x86_xcr0(void) {
unsigned lo, hi;
__asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
return ((unsigned long long)hi << 32) | lo;
}
static int finch_cpu_has_c(const char *n) {
unsigned l1[4] = {0}, l7[4] = {0}, l71[4] = {0};
x86_leaf(1, 0, l1);
x86_leaf(7, 0, l7);
x86_leaf(7, 1, l71);
int osx = (l1[2] >> 27) & 1; // OSXSAVE
unsigned long long xcr0 = osx ? x86_xcr0() : 0;
int ymm = osx && (xcr0 & 6) == 6; // the system saves xmm and ymm
int zmm = ymm && (xcr0 & 0xe0) == 0xe0; // and the AVX-512 registers
if (!strcmp(n, "sse3")) return l1[2] & 1;
if (!strcmp(n, "ssse3")) return (l1[2] >> 9) & 1;
if (!strcmp(n, "sse4.1")) return (l1[2] >> 19) & 1;
if (!strcmp(n, "sse4.2")) return (l1[2] >> 20) & 1;
if (!strcmp(n, "popcnt")) return (l1[2] >> 23) & 1;
if (!strcmp(n, "avx")) return ymm && ((l1[2] >> 28) & 1);
if (!strcmp(n, "fma")) return ymm && ((l1[2] >> 12) & 1);
if (!strcmp(n, "f16c")) return ymm && ((l1[2] >> 29) & 1);
if (!strcmp(n, "avx2")) return ymm && ((l7[1] >> 5) & 1);
if (!strcmp(n, "bmi1")) return (l7[1] >> 3) & 1;
if (!strcmp(n, "bmi2")) return (l7[1] >> 8) & 1;
if (!strcmp(n, "avxvnni")) return ymm && ((l71[0] >> 4) & 1);
if (!strcmp(n, "avx512f")) return zmm && ((l7[1] >> 16) & 1);
if (!strcmp(n, "avx512dq")) return zmm && ((l7[1] >> 17) & 1);
if (!strcmp(n, "avx512bw")) return zmm && ((l7[1] >> 30) & 1);
if (!strcmp(n, "avx512vl")) return zmm && ((l7[1] >> 31) & 1);
if (!strcmp(n, "avx512vnni")) return zmm && ((l7[2] >> 11) & 1);
if (!strcmp(n, "avx512fp16")) return zmm && ((l7[3] >> 23) & 1);
if (!strcmp(n, "avx512bf16")) return zmm && ((l71[0] >> 5) & 1);
return 0;
}
#elif defined(__aarch64__) && defined(__APPLE__)
#include <sys/sysctl.h>
static int sysctl_flag(const char *name) {
int v = 0;
size_t len = sizeof v;
if (sysctlbyname(name, &v, &len, NULL, 0) != 0) return 0;
return v != 0;
}
static int finch_cpu_has_c(const char *n) {
if (!strcmp(n, "neon")) return 1;
if (!strcmp(n, "fp16")) return sysctl_flag("hw.optional.arm.FEAT_FP16");
if (!strcmp(n, "dotprod")) return sysctl_flag("hw.optional.arm.FEAT_DotProd");
if (!strcmp(n, "i8mm")) return sysctl_flag("hw.optional.arm.FEAT_I8MM");
if (!strcmp(n, "bf16")) return sysctl_flag("hw.optional.arm.FEAT_BF16");
return 0; // no SVE on Apple chips
}
#elif defined(__aarch64__) && defined(__linux__)
#include <sys/auxv.h>
static int finch_cpu_has_c(const char *n) {
unsigned long h = getauxval(AT_HWCAP), h2 = getauxval(AT_HWCAP2);
if (!strcmp(n, "neon")) return 1;
if (!strcmp(n, "fp16")) return (h >> 10) & 1; // HWCAP_ASIMDHP
if (!strcmp(n, "dotprod")) return (h >> 20) & 1; // HWCAP_ASIMDDP
if (!strcmp(n, "sve")) return (h >> 22) & 1; // HWCAP_SVE
if (!strcmp(n, "sve2")) return (h2 >> 1) & 1; // HWCAP2_SVE2
if (!strcmp(n, "i8mm")) return (h2 >> 13) & 1; // HWCAP2_I8MM
if (!strcmp(n, "bf16")) return (h2 >> 14) & 1; // HWCAP2_BF16
return 0;
}
#else
static int finch_cpu_has_c(const char *n) {
(void)n;
return 0;
}
#endif
int finch_cpu_has(const FStr *name) {
char n[32];
int64_t len = name->len < 31 ? name->len : 31;
memcpy(n, name->ptr ? name->ptr : "", (size_t)len);
n[len] = 0;
return finch_cpu_has_c(n) ? 1 : 0;
}

View file

@ -4,9 +4,11 @@ pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
# building Finch (the clang wrapper first, so `clang` finds the system headers) # building Finch (the clang wrapper first, so `clang` finds the system headers)
llvmPackages.clang llvmPackages.clang
llvmPackages.llvm
llvmPackages.libclang # reads C headers for `import "stdio.h"` llvmPackages.libclang # reads C headers for `import "stdio.h"`
cmake
ninja
pkg-config # finds the right flags for `link "..."` pkg-config # finds the right flags for `link "..."`
python3 # tests/lsp_test.py
# for the graphics examples (window, opengl, raylib, vulkan) # for the graphics examples (window, opengl, raylib, vulkan)
glfw glfw
@ -15,7 +17,4 @@ pkgs.mkShell {
vulkan-headers vulkan-headers
vulkan-loader vulkan-loader
]; ];
# Nix's clang wrapper drops -march=native unless this is 0 (finch --cpu native also tells it so itself)
NIX_ENFORCE_NO_NATIVE = 0;
} }

309
src/abi.cpp Normal file
View file

@ -0,0 +1,309 @@
// The C calling convention for structs passed and returned by value (System V x86-64),
// the way clang does it: small structs travel in registers, split into eightbytes that are
// either INTEGER (general registers) or SSE (xmm registers); bigger ones go through memory.
#include "codegen_impl.h"
using namespace llvm;
namespace finch {
namespace {
enum Class { None, Integer, Sse };
struct Leaf {
uint64_t offset;
uint64_t size;
bool isFloat;
bool isDouble;
};
} // namespace
// Flatten a value into its scalar pieces with byte offsets.
static void leaves(Codegen &cg, const FType &t, uint64_t base, std::vector<Leaf> &out) {
switch (t.kind) {
case FType::Struct: {
const StructLayout *lay = cg.dl->getStructLayout(t.info->llvm);
for (auto &f : t.info->fields) leaves(cg, f.type, base + lay->getElementOffset(f.llvmIndex), out);
break;
}
case FType::Fixed: {
uint64_t es = cg.dl->getTypeAllocSize(cg.ty(*t.elem));
for (long long i = 0; i < t.count; i++) leaves(cg, *t.elem, base + i * es, out);
break;
}
default:
out.push_back({base, cg.dl->getTypeAllocSize(cg.ty(t)), t.kind == FType::F32 || t.kind == FType::F64,
t.kind == FType::F64});
}
}
AbiArg Codegen::classify(const FType &t) {
AbiArg a;
if (t.kind != FType::Struct) return a; // Direct
uint64_t size = dl->getTypeAllocSize(ty(t));
if (size > 16 || size == 0) {
a.kind = AbiArg::Memory;
return a;
}
std::vector<Leaf> ls;
leaves(*this, t, 0, ls);
unsigned n = (size + 7) / 8;
Class cls[2] = {None, None};
bool hasDouble[2] = {false, false};
for (const Leaf &l : ls) {
if (l.offset % std::min<uint64_t>(l.size, 8) != 0) { // misaligned field (packed struct): memory
a.kind = AbiArg::Memory;
return a;
}
unsigned k = l.offset / 8;
cls[k] = (cls[k] == Integer || !l.isFloat) ? Integer : Sse;
hasDouble[k] |= l.isDouble;
}
a.kind = AbiArg::Expand;
for (unsigned k = 0; k < n; k++) {
uint64_t bytes = std::min<uint64_t>(8, size - 8 * k);
if (cls[k] == Sse) {
if (hasDouble[k]) a.parts.push_back(b.getDoubleTy());
else if (bytes > 4) a.parts.push_back(FixedVectorType::get(b.getFloatTy(), 2));
else a.parts.push_back(b.getFloatTy());
} else {
a.parts.push_back(b.getIntNTy(bytes * 8));
}
}
a.coerced = a.parts.size() == 1 ? a.parts[0] : StructType::get(ctx, a.parts);
return a;
}
namespace {
struct Plan {
AbiArg ret;
std::vector<AbiArg> params;
llvm::FunctionType *fty = nullptr;
};
} // namespace
// Decide how every parameter travels, counting the 6 integer and 8 SSE argument registers.
static Plan makePlan(Codegen &cg, const FType &ret, const std::vector<FType> &params, bool variadic,
std::vector<llvm::Type *> *extraTypes = nullptr) {
Plan pl;
int intRegs = 6, sseRegs = 8;
std::vector<llvm::Type *> types;
llvm::Type *retTy = cg.ty(ret);
if (ret.kind == FType::Str) retTy = cg.b.getPtrTy();
pl.ret = cg.classify(ret);
if (pl.ret.kind == AbiArg::Memory) {
types.push_back(cg.b.getPtrTy()); // sret
retTy = cg.b.getVoidTy();
intRegs--;
} else if (pl.ret.kind == AbiArg::Expand) {
retTy = pl.ret.coerced;
}
for (const FType &t : params) {
AbiArg a = cg.classify(t);
if (a.kind == AbiArg::Expand) {
int ni = 0, ns = 0;
for (llvm::Type *p : a.parts) (p->isIntegerTy() ? ni : ns)++;
if (ni <= intRegs && ns <= sseRegs) {
intRegs -= ni;
sseRegs -= ns;
for (llvm::Type *p : a.parts) types.push_back(p);
} else {
a.kind = AbiArg::Memory; // out of registers: the whole struct goes on the stack
}
}
if (a.kind == AbiArg::Memory) types.push_back(cg.b.getPtrTy());
if (a.kind == AbiArg::Direct) {
if (t.isFloat()) sseRegs--;
else intRegs--;
types.push_back(t.kind == FType::Str ? cg.b.getPtrTy() : cg.ty(t));
}
pl.params.push_back(a);
}
(void)extraTypes;
pl.fty = FunctionType::get(retTy, types, variadic);
return pl;
}
static Attribute::AttrKind extAttr(const FType &t) {
if (t.bits() >= 32 || !(t.isInt() || t.kind == FType::Bool || t.kind == FType::Char)) return Attribute::None;
return t.isSigned() || t.kind == FType::Char ? Attribute::SExt : Attribute::ZExt;
}
// Attributes that tell LLVM how C passes each piece: sret, byval, signext/zeroext.
static AttributeList attrsFor(Codegen &cg, const Plan &pl, const FType &ret, const std::vector<FType> &params) {
LLVMContext &ctx = cg.ctx;
AttributeList al;
unsigned idx = 0;
if (pl.ret.kind == AbiArg::Memory) {
al = al.addParamAttribute(ctx, idx, Attribute::getWithStructRetType(ctx, cg.ty(ret)));
idx++;
} else if (Attribute::AttrKind k = extAttr(ret); k != Attribute::None) {
al = al.addRetAttribute(ctx, k);
}
for (size_t i = 0; i < params.size(); i++) {
const AbiArg &a = pl.params[i];
if (a.kind == AbiArg::Memory) {
uint64_t align = std::max<uint64_t>(8, params[i].info->isC ? params[i].info->cdecl->align : 8);
al = al.addParamAttribute(ctx, idx, Attribute::getWithByValType(ctx, cg.ty(params[i])));
al = al.addParamAttribute(ctx, idx, Attribute::getWithAlignment(ctx, Align(align)));
idx++;
} else if (a.kind == AbiArg::Expand) {
idx += a.parts.size();
} else {
if (Attribute::AttrKind k = extAttr(params[i]); k != Attribute::None) al = al.addParamAttribute(ctx, idx, k);
idx++;
}
}
return al;
}
static std::vector<FType> resolvedParams(Codegen &cg, const CFunc &f) {
std::vector<FType> ps;
for (const FType &t : f.params) ps.push_back(cg.resolve(t, Pos{}));
return ps;
}
Function *Codegen::declareC(const CFunc &f) {
if (Function *fn = mod->getFunction(f.name)) return fn;
FType ret = resolve(f.ret, Pos{});
std::vector<FType> ps = resolvedParams(*this, f);
for (const FType &t : ps)
if (t.kind == FType::Struct && !t.info->unsupported.empty()) return nullptr;
Plan pl = makePlan(*this, ret, ps, f.variadic);
Function *fn = Function::Create(pl.fty, Function::ExternalLinkage, f.name, mod.get());
fn->setAttributes(attrsFor(*this, pl, ret, ps));
return fn;
}
// A stack slot big enough for both the struct and its register form.
static Value *abiSlot(Codegen &cg, const FType &t, const AbiArg &a) {
llvm::Type *st = cg.ty(t);
llvm::Type *big = a.coerced && cg.dl->getTypeAllocSize(a.coerced) > cg.dl->getTypeAllocSize(st) ? a.coerced : st;
return cg.tmpOf(big);
}
Value_ Codegen::emitCCall(const CFunc &f, Function *fn, std::vector<Value_> args, Pos p) {
FType ret = resolve(f.ret, p);
std::vector<FType> ps = resolvedParams(*this, f);
for (const FType &t : ps)
if (t.kind == FType::Struct && !t.info->unsupported.empty())
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' takes the struct " + t.info->name +
" by value, which Finch can't pass yet: it " + t.info->unsupported);
if (ret.kind == FType::Struct && !ret.info->unsupported.empty())
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' returns the struct " + ret.info->name +
" by value, which Finch can't receive yet: it " + ret.info->unsupported);
Plan pl = makePlan(*this, ret, ps, f.variadic);
std::vector<Value *> ll;
Value *sret = nullptr;
if (pl.ret.kind == AbiArg::Memory) {
sret = tmpOf(ty(ret));
cast<AllocaInst>(sret)->setAlignment(Align(16));
ll.push_back(sret);
}
for (size_t i = 0; i < args.size(); i++) {
const Value_ &v = args[i];
if (i >= ps.size()) { // the '...' part: C's default promotions
const FType &t = v.type;
Value *x = v.v;
if (t.kind == FType::F32) x = b.CreateFPExt(x, b.getDoubleTy());
else if (t.kind == FType::Str) x = cstr(x);
else if (t.kind == FType::Bool || ((t.isInt() || t.kind == FType::Char) && t.bits() < 32))
x = b.CreateIntCast(x, b.getInt32Ty(), t.isSigned() || t.kind == FType::Char);
ll.push_back(x);
continue;
}
const AbiArg &a = pl.params[i];
if (ps[i].kind == FType::Str) {
ll.push_back(cstr(v.v));
} else if (a.kind == AbiArg::Memory) {
Value *s = tmpOf(ty(ps[i]));
cast<AllocaInst>(s)->setAlignment(Align(16));
b.CreateStore(v.v, s);
ll.push_back(s);
} else if (a.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, ps[i], a);
b.CreateStore(v.v, s);
for (size_t k = 0; k < a.parts.size(); k++)
ll.push_back(b.CreateLoad(a.parts[k], b.CreateConstInBoundsGEP1_64(b.getInt8Ty(), s, 8 * k)));
} else {
ll.push_back(v.v);
}
}
CallInst *call = b.CreateCall(pl.fty, fn, ll);
AttributeList al = attrsFor(*this, pl, ret, ps);
call->setAttributes(al);
if (ret.kind == FType::Void) return {nullptr, FType::Void};
if (pl.ret.kind == AbiArg::Memory) return {b.CreateLoad(ty(ret), sret), ret};
if (pl.ret.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, ret, pl.ret);
b.CreateStore(call, s);
return {b.CreateLoad(ty(ret), s), ret};
}
if (ret.kind == FType::Str) return strFromC(call); // borrowed: copied if it is kept
return {call, ret};
}
// A C-callable door into a Finch function, for addr(fn) (callbacks like qsort's or libclang's).
Function *Codegen::cThunk(Fn &fn, Pos p) {
if (fn.cThunk) return fn.cThunk;
auto cOk = [](const FType &t) {
return t.isNumber() || t.kind == FType::Bool || t.kind == FType::Char || t.isPtr() || t.kind == FType::Void ||
(t.kind == FType::Struct && t.info->isC);
};
for (const FType &t : fn.params)
if (!cOk(t))
failAt(p.file, p.line, p.col, "addr(" + fn.decl->name + ") gives C a function pointer, so its parameters must be C types (numbers, ptr, C structs), not " + t.show());
if (!cOk(fn.ret)) failAt(p.file, p.line, p.col, "addr(" + fn.decl->name + "): a function given to C can't return " + fn.ret.show());
Plan pl = makePlan(*this, fn.ret, fn.params, false);
Function *th = Function::Create(pl.fty, Function::InternalLinkage, "finch.c." + fn.decl->name, mod.get());
th->setAttributes(attrsFor(*this, pl, fn.ret, fn.params));
fn.cThunk = th;
IRBuilderBase::InsertPointGuard guard(b);
DISubprogram *savedFn = diFn;
diFn = nullptr;
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", th));
b.SetCurrentDebugLocation(DebugLoc());
unsigned ai = 0;
Value *sret = nullptr;
if (pl.ret.kind == AbiArg::Memory) sret = th->getArg(ai++);
std::vector<Value *> args;
for (size_t i = 0; i < fn.params.size(); i++) {
const AbiArg &a = pl.params[i];
if (a.kind == AbiArg::Memory) {
args.push_back(b.CreateLoad(ty(fn.params[i]), th->getArg(ai++)));
} else if (a.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, fn.params[i], a);
for (size_t k = 0; k < a.parts.size(); k++)
b.CreateStore(th->getArg(ai++), b.CreateConstInBoundsGEP1_64(b.getInt8Ty(), s, 8 * k));
args.push_back(b.CreateLoad(ty(fn.params[i]), s));
} else {
args.push_back(th->getArg(ai++));
}
}
Value *r = b.CreateCall(fn.llvm, args);
if (fn.ret.kind == FType::Void) {
b.CreateRetVoid();
} else if (pl.ret.kind == AbiArg::Memory) {
b.CreateStore(r, sret);
b.CreateRetVoid();
} else if (pl.ret.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, fn.ret, pl.ret);
b.CreateStore(r, s);
b.CreateRet(b.CreateLoad(pl.ret.coerced, s));
} else {
b.CreateRet(r);
}
diFn = savedFn;
return th;
}
} // namespace finch

View file

@ -1,172 +0,0 @@
// The syntax tree of the self-hosted compiler.
// A type as written or resolved.
// kind: "void" "int" "bool" "char" "float" "str" "null" "ptr" "array" "named" "struct"
// elem: the element of an array or the target of a ptr (zero or one entry)
// name, module: structs ("" module = the main file)
struct Type {
str kind
[]Type elem
str name
str module
int count // "fixed": [N]T, only inside C structs
}
fn simple(str kind) -> Type {
return Type(kind: kind)
}
fn arrayOf(Type t) -> Type {
out := Type(kind: "array")
out.elem.push(t)
return out
}
// map[K]V: elem holds the key, then the value
fn mapOf(Type k, Type v) -> Type {
out := Type(kind: "map")
out.elem.push(k)
out.elem.push(v)
return out
}
fn ptrTo(Type t) -> Type {
out := Type(kind: "ptr")
out.elem.push(t)
return out
}
// f32x8, i8x32, u16x16, f64x4: N numbers of one type side by side, worked on all at once (SIMD).
// Its kind is "vec", elem[0] the type of one lane, count the number of lanes. kind "" if `name` is no such type.
fn vecType(str name) -> Type {
x := name.find("x")
if x < 2 || x + 1 >= name.len {
return Type()
}
lane := name.sub(0, x)
if !["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "bool"].contains(lane) {
return Type()
}
n := 0
for c in name.sub(x + 1, name.len) {
if c < '0' || c > '9' {
return Type()
}
n = n * 10 + (int(c) - int('0'))
}
if n != 2 && n != 4 && n != 8 && n != 16 && n != 32 && n != 64 {
return Type()
}
bits := int(lane.sub(1, lane.len)) or 1 // bool: a mask, one bit a lane
if n * bits > 512 {
return Type() // the widest registers (AVX-512) hold 512 bits
}
if lane == "i64" {
lane = "int"
} else if lane == "f64" {
lane = "float"
}
out := Type(kind: "vec", count: n)
out.elem.push(simple(lane))
return out
}
fn same(Type a, Type b) -> bool {
if a.kind != b.kind {
return false
}
if a.kind == "struct" || a.kind == "named" || a.kind == "enum" {
return a.name == b.name && a.module == b.module
}
if (a.kind == "fixed" || a.kind == "vec") && a.count != b.count {
return false
}
if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" || a.kind == "vec" {
if a.elem.len != b.elem.len {
return false
}
for i in 0..a.elem.len {
if !same(a.elem[i], b.elem[i]) {
return false
}
}
return true
}
return true
}
fn show(Type t) -> str {
if t.kind == "array" {
return "[]" + show(t.elem[0])
}
if t.kind == "fixed" {
return "[" + str(t.count) + "]" + show(t.elem[0])
}
if t.kind == "vec" {
lane := t.elem[0].kind
if lane == "int" {
lane = "i64"
} else if lane == "float" {
lane = "f64"
}
return lane + "x" + str(t.count)
}
if t.kind == "map" {
return "map[" + show(t.elem[0]) + "]" + show(t.elem[1])
}
if t.kind == "ptr" {
if t.elem.len == 0 {
return "ptr"
}
return "ptr[" + show(t.elem[0]) + "]"
}
if t.kind == "struct" || t.kind == "named" || t.kind == "enum" {
if t.module.len > 0 {
return t.module + "." + t.name
}
return t.name
}
if t.kind == "void" {
return "nothing"
}
return t.kind
}
// One node of the tree, for expressions, statements, functions and structs alike.
// expressions: int float str char bool null var unary binary call member index array method
// statements: decl assign expr if while for each return break continue block
// top level: fn param struct field enum variant
// match: match (kids: subject, arms...), arm (kids: patterns node, body; flag: the else arm), patterns
struct Node {
str kind
str name // identifier, operator, callee, field or method name
str recv // fn Point.move(...): "Point"; empty for a plain function
[]str tparams // fn max[T](...), struct Stack[T], fn Stack[T].push: the type parameters
str text // literal text
Type ty // declared type (decl, param, field, fn result)
bool flag // decl: has a type; or: the right side is a { block }
bool fallible // fn: -> T! or -> !
bool exported // export fn: a C function of a library (finch build --shared)
str cpu // @cpu("avx2,fma") fn: the processor features the function may use
[]Node kids
[]str names // named arguments: Point(x: 1)
int line
int col
int file
int endLine // block, fn: the line of the closing }
}
struct Program {
str module
str path
[]str imports // Finch modules, by the name this file uses: import ui/button -> "button"
[]str modules // which module each is: "ui/button" (import path as p: "path")
[]str 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
}

312
src/ast.h Normal file
View file

@ -0,0 +1,312 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
struct StructInfo; // filled in by codegen: fields, LLVM type, ownership
struct Type {
enum Kind {
Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null,
Array, // []T: growable, owns its elements
Fixed, // [N]T: only inside C structs
Struct, // a resolved struct
Named, // a struct name as written, before codegen resolves it
};
Kind kind = Void;
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed
long long count = 0; // Fixed
std::string name; // Named / Struct
std::string module; // Named: written qualifier ("" = none); Struct: defining module
StructInfo *info = nullptr; // Struct
Type() = default;
Type(Kind k) : kind(k) {}
static Type ptrTo(const Type &t) {
Type p(Ptr);
p.elem = std::make_shared<Type>(t);
return p;
}
static Type arrayOf(const Type &t) {
Type a(Array);
a.elem = std::make_shared<Type>(t);
return a;
}
bool isInt() const { return kind >= I8 && kind <= U64; }
bool isSigned() const { return kind >= I8 && kind <= I64; }
bool isUnsigned() const { return kind >= U8 && kind <= U64; }
bool isFloat() const { return kind == F32 || kind == F64; }
bool isNumber() const { return isInt() || isFloat(); }
bool isPtr() const { return kind == Ptr; }
int bits() const {
switch (kind) {
case Bool: return 1;
case Char: case I8: case U8: return 8;
case I16: case U16: return 16;
case I32: case U32: case F32: return 32;
default: return 64;
}
}
bool operator==(const Type &o) const {
if (kind != o.kind) return false;
switch (kind) {
case Ptr:
if (!elem || !o.elem) return !elem && !o.elem;
return *elem == *o.elem;
case Array: return *elem == *o.elem;
case Fixed: return count == o.count && *elem == *o.elem;
case Struct: return info == o.info;
case Named: return name == o.name && module == o.module;
default: return true;
}
}
bool operator!=(const Type &o) const { return !(*this == o); }
// How the type is written in Finch, for error messages.
std::string show() const {
switch (kind) {
case Void: return "nothing";
case Bool: return "bool";
case Char: return "char";
case I8: return "i8";
case I16: return "i16";
case I32: return "i32";
case I64: return "int";
case U8: return "u8";
case U16: return "u16";
case U32: return "u32";
case U64: return "u64";
case F32: return "f32";
case F64: return "float";
case Str: return "str";
case Ptr: return elem ? "ptr[" + elem->show() + "]" : "ptr";
case Null: return "null";
case Array: return "[]" + elem->show();
case Fixed: return "[" + std::to_string(count) + "]" + elem->show();
case Struct:
case Named: return module.empty() ? name : module + "." + name;
}
return "?";
}
};
// Built-in type names. `int` is i64 and `float` is f64: the everyday defaults.
bool typeFromName(const std::string &name, Type &out);
struct Pos {
int line = 0, col = 0;
int file = 0; // index into the list of source files (see error.h)
};
// ---------- expressions ----------
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method };
struct Expr {
ExprKind kind;
Pos pos;
Expr(ExprKind k, Pos p) : kind(k), pos(p) {}
virtual ~Expr() = default;
};
using ExprPtr = std::unique_ptr<Expr>;
struct IntExpr : Expr {
long long value;
IntExpr(Pos p, long long v) : Expr(ExprKind::Int, p), value(v) {}
};
struct FloatExpr : Expr {
double value;
FloatExpr(Pos p, double v) : Expr(ExprKind::Float, p), value(v) {}
};
struct BoolExpr : Expr {
bool value;
BoolExpr(Pos p, bool v) : Expr(ExprKind::Bool, p), value(v) {}
};
struct CharExpr : Expr {
char value;
CharExpr(Pos p, char v) : Expr(ExprKind::Char, p), value(v) {}
};
struct StrExpr : Expr {
std::string value;
StrExpr(Pos p, std::string v) : Expr(ExprKind::Str, p), value(std::move(v)) {}
};
struct NullExpr : Expr {
explicit NullExpr(Pos p) : Expr(ExprKind::Null, p) {}
};
struct VarExpr : Expr {
std::string name;
VarExpr(Pos p, std::string n) : Expr(ExprKind::Var, p), name(std::move(n)) {}
};
struct UnaryExpr : Expr {
char op; // '-', '!' or '~'
ExprPtr operand;
UnaryExpr(Pos p, char o, ExprPtr e) : Expr(ExprKind::Unary, p), op(o), operand(std::move(e)) {}
};
enum class BinOp {
Add, Sub, Mul, Div, Mod,
BitAnd, BitOr, BitXor, Shl, Shr,
Eq, NotEq, Less, LessEq, Greater, GreaterEq,
And, Or,
};
struct BinaryExpr : Expr {
BinOp op;
ExprPtr lhs, rhs;
BinaryExpr(Pos p, BinOp o, ExprPtr l, ExprPtr r)
: Expr(ExprKind::Binary, p), op(o), lhs(std::move(l)), rhs(std::move(r)) {}
};
// name(args): a function, a built-in, a conversion or a struct constructor.
struct CallExpr : Expr {
std::string callee;
std::vector<ExprPtr> args;
std::vector<std::string> argNames; // "" for positional; Point(x: 1, y: 2)
CallExpr(Pos p, std::string c) : Expr(ExprKind::Call, p), callee(std::move(c)) {}
};
struct MemberExpr : Expr { // obj.field, p.value, a.len
ExprPtr obj;
std::string field;
MemberExpr(Pos p, ExprPtr o, std::string f)
: Expr(ExprKind::Member, p), obj(std::move(o)), field(std::move(f)) {}
};
struct IndexExpr : Expr { // a[i]
ExprPtr obj, index;
IndexExpr(Pos p, ExprPtr o, ExprPtr i) : Expr(ExprKind::Index, p), obj(std::move(o)), index(std::move(i)) {}
};
struct ArrayLitExpr : Expr { // [1, 2, 3]
std::vector<ExprPtr> elems;
explicit ArrayLitExpr(Pos p) : Expr(ExprKind::ArrayLit, p) {}
};
// obj.name(args): a.push(x), s.find("x"), and math.sqrt(2) / math.Vec(1, 2) for modules
struct MethodExpr : Expr {
ExprPtr obj;
std::string name;
std::vector<ExprPtr> args;
std::vector<std::string> argNames;
MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {}
};
// ---------- statements ----------
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, ForEach, Return, Break, Continue, Block, Defer };
struct Stmt {
StmtKind kind;
Pos pos;
Stmt(StmtKind k, Pos p) : kind(k), pos(p) {}
virtual ~Stmt() = default;
};
using StmtPtr = std::unique_ptr<Stmt>;
struct BlockStmt : Stmt {
std::vector<StmtPtr> body;
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
};
using BlockPtr = std::unique_ptr<BlockStmt>;
struct VarDeclStmt : Stmt {
bool hasType; // `int x = 5` vs `x := 5`
Type type;
std::string name;
ExprPtr init; // may be null only when hasType
VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {}
};
struct AssignStmt : Stmt {
ExprPtr target; // a variable, field, element or p.value
char op; // '=', '+', '-', '*', '/', '%'
ExprPtr value;
AssignStmt(Pos p) : Stmt(StmtKind::Assign, p) {}
};
struct ExprStmt : Stmt {
ExprPtr expr;
ExprStmt(Pos p, ExprPtr e) : Stmt(StmtKind::Expr, p), expr(std::move(e)) {}
};
struct IfStmt : Stmt {
ExprPtr cond;
BlockPtr then;
StmtPtr otherwise; // null, BlockStmt, or IfStmt (else if)
IfStmt(Pos p) : Stmt(StmtKind::If, p) {}
};
struct WhileStmt : Stmt {
ExprPtr cond;
BlockPtr body;
WhileStmt(Pos p) : Stmt(StmtKind::While, p) {}
};
struct ForStmt : Stmt {
std::string var;
ExprPtr from, to; // for var in from..to (to is exclusive)
BlockPtr body;
ForStmt(Pos p) : Stmt(StmtKind::For, p) {}
};
struct ForEachStmt : Stmt {
std::string var;
ExprPtr list; // for var in list (an array or a str)
BlockPtr body;
ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, p) {}
};
struct ReturnStmt : Stmt {
ExprPtr value; // may be null
ReturnStmt(Pos p) : Stmt(StmtKind::Return, p) {}
};
struct BreakStmt : Stmt {
explicit BreakStmt(Pos p) : Stmt(StmtKind::Break, p) {}
};
struct ContinueStmt : Stmt {
explicit ContinueStmt(Pos p) : Stmt(StmtKind::Continue, p) {}
};
struct DeferStmt : Stmt { // defer <statement>: runs when the block ends
StmtPtr body;
DeferStmt(Pos p, StmtPtr s) : Stmt(StmtKind::Defer, p), body(std::move(s)) {}
};
// ---------- top level ----------
struct Param {
Type type;
std::string name;
Pos pos;
};
struct FnDecl {
Pos pos;
std::string name;
Type ret; // Void when there is no `-> type`
std::vector<Param> params;
BlockPtr body;
};
struct Field {
Type type;
std::string name;
ExprPtr init; // default value, may be null
Pos pos;
};
struct StructDecl {
Pos pos;
std::string name;
std::vector<Field> fields;
};
struct Import {
Pos pos;
std::string path; // "stdio.h" (C header, isC) or "math" (Finch module)
bool isC = true;
};
struct Link {
Pos pos;
std::string lib; // "glfw" -> libglfw.so; "helpers.c" / ".o" / ".a" -> a file next to the .fch file
};
// One .fch file.
struct Program {
std::string module; // "" for the main file, else the module's name
std::string path;
std::vector<Import> imports;
std::vector<Link> links;
std::vector<StructDecl> structs;
std::vector<FnDecl> fns;
};

367
src/cimport.cpp Normal file
View file

@ -0,0 +1,367 @@
#include "cimport.h"
#include "error.h"
#include <clang-c/Index.h>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace {
// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would.
std::vector<std::string> systemIncludeDirs() {
const char *cc = std::getenv("CC");
std::string cmd = std::string(cc ? cc : "cc") + " -E -v -x c /dev/null 2>&1";
std::vector<std::string> dirs;
FILE *p = popen(cmd.c_str(), "r");
if (!p) return dirs;
char line[4096];
bool inList = false;
while (std::fgets(line, sizeof line, p)) {
std::string l = line;
if (l.rfind("#include <...> search starts here:", 0) == 0) { inList = true; continue; }
if (l.rfind("End of search list.", 0) == 0) break;
if (!inList) continue;
size_t a = l.find_first_not_of(' ');
size_t z = l.find_last_not_of(" \n");
if (a == std::string::npos) continue;
std::string d = l.substr(a, z - a + 1);
if (d.size() > 20 && d.compare(d.size() - 20, 20, " (framework directory)") == 0) continue;
dirs.push_back(d);
}
pclose(p);
return dirs;
}
std::string str(CXString s) {
std::string out = clang_getCString(s) ? clang_getCString(s) : "";
clang_disposeString(s);
return out;
}
// Where a C type is used decides how `char *` looks in Finch: in function signatures it is
// a `str`; inside structs, globals and behind pointers it stays a raw `ptr[char]`.
enum class Ctx { Signature, Data };
struct Importer {
CImports &out;
CXTranslationUnit tu = nullptr;
std::string header;
std::map<std::string, std::string> recordNames; // record USR -> Finch name (typedef wins)
std::map<std::string, int> defining; // USR -> 1 while its fields are being mapped
int anon = 0;
explicit Importer(CImports &o) : out(o) {}
// The Finch name of a C record: its typedef name, its tag, or a made-up one.
std::string recordName(CXType t) {
CXCursor decl = clang_getTypeDeclaration(t);
std::string usr = str(clang_getCursorUSR(decl));
auto it = recordNames.find(usr);
if (it != recordNames.end()) return it->second;
std::string name = str(clang_getCursorSpelling(decl));
if (name.empty() || name.find(' ') != std::string::npos) name = "anon_struct_" + std::to_string(++anon);
recordNames[usr] = name;
return name;
}
// Fill out.structs[name] for a complete record; returns its name, or "" if it can't be one.
std::string defineRecord(CXType t) {
t = clang_getCanonicalType(t);
CXCursor decl = clang_getTypeDeclaration(t);
std::string usr = str(clang_getCursorUSR(decl));
std::string name = recordName(t);
if (out.structs.count(name) || defining.count(usr)) return name;
long long size = clang_Type_getSizeOf(t);
if (size < 0) return ""; // incomplete (opaque) struct
defining[usr] = 1;
CStruct s;
s.name = name;
s.size = size;
s.align = clang_Type_getAlignOf(t);
if (clang_getCursorKind(decl) == CXCursor_UnionDecl) s.unsupported = "is a C union";
struct Ctx2 { Importer *self; CStruct *s; } c2{this, &s};
clang_Type_visitFields(t, [](CXCursor field, CXClientData data) -> CXVisitorResult {
auto &c = *static_cast<Ctx2 *>(data);
CField f;
f.name = str(clang_getCursorSpelling(field));
CXType ft = clang_getCanonicalType(clang_getCursorType(field));
f.offset = clang_Cursor_getOffsetOfField(field) / 8;
f.size = clang_Type_getSizeOf(ft);
f.align = clang_Type_getAlignOf(ft);
std::string why;
if (clang_Cursor_isBitField(field)) {
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 (!c.self->mapType(ft, f.type, why, Ctx::Data)) {
f.hidden = true;
}
if (f.hidden) c.s->hasHidden = true;
if (f.name.empty()) f.hidden = true;
c.s->fields.push_back(f);
return CXVisit_Continue;
}, &c2);
defining.erase(usr);
out.structs[name] = s;
return name;
}
// C type -> Finch type. Returns false (with a reason) if Finch can't express it.
bool mapType(CXType t, Type &res, std::string &why, Ctx ctx) {
t = clang_getCanonicalType(t);
long long size = clang_Type_getSizeOf(t);
auto sized = [&](bool isSigned) {
switch (size) {
case 1: res = isSigned ? Type::I8 : Type::U8; return true;
case 2: res = isSigned ? Type::I16 : Type::U16; return true;
case 4: res = isSigned ? Type::I32 : Type::U32; return true;
case 8: res = isSigned ? Type::I64 : Type::U64; return true;
}
why = "uses a " + std::to_string(size * 8) + "-bit number";
return false;
};
switch (t.kind) {
case CXType_Void: res = Type::Void; return true;
case CXType_Bool: res = Type::Bool; return true;
case CXType_Char_S:
case CXType_Char_U: res = Type::Char; return true;
case CXType_SChar:
case CXType_Short:
case CXType_Int:
case CXType_Long:
case CXType_LongLong: return sized(true);
case CXType_UChar:
case CXType_UShort:
case CXType_UInt:
case CXType_ULong:
case CXType_ULongLong: return sized(false);
case CXType_Enum: return sized(true);
case CXType_Float: res = Type::F32; return true;
case CXType_Double: res = Type::F64; return true;
case CXType_Pointer: {
CXType pointee = clang_getCanonicalType(clang_getPointeeType(t));
if ((pointee.kind == CXType_Char_S || pointee.kind == CXType_Char_U) && ctx == Ctx::Signature) {
res = Type::Str; // char* in a signature is a C string
return true;
}
Type inner;
std::string ignored;
if (pointee.kind == CXType_Void || pointee.kind == CXType_FunctionProto ||
pointee.kind == CXType_FunctionNoProto || !mapType(pointee, inner, ignored, Ctx::Data) ||
inner.kind == Type::Void || inner.kind == Type::Fixed)
res = Type::Ptr; // an opaque `ptr` you can pass around
else
res = Type::ptrTo(inner);
return true;
}
case CXType_Record: {
if (clang_getCursorKind(clang_getTypeDeclaration(t)) == CXCursor_UnionDecl) {
why = "uses a C union by value";
return false;
}
std::string name = defineRecord(t);
if (name.empty()) {
why = "uses an incomplete C struct by value";
return false;
}
res = Type(Type::Named);
res.name = name;
res.module = "C";
return true;
}
case CXType_ConstantArray: {
Type inner;
if (!mapType(clang_getArrayElementType(t), inner, why, Ctx::Data)) return false;
res = Type(Type::Fixed);
res.elem = std::make_shared<Type>(inner);
res.count = clang_getArraySize(t);
return true;
}
case CXType_LongDouble: why = "uses long double"; return false;
default: why = "uses the C type '" + str(clang_getTypeSpelling(t)) + "'"; return false;
}
}
// Simple #defines: a single number, maybe negative, maybe in parentheses.
bool macroValue(CXCursor c, CConst &res) {
CXToken *toks;
unsigned n;
clang_tokenize(tu, clang_getCursorExtent(c), &toks, &n);
std::vector<std::string> parts;
for (unsigned k = 1; k < n; k++) { // token 0 is the macro's name
if (clang_getTokenKind(toks[k]) == CXToken_Comment) continue;
parts.push_back(str(clang_getTokenSpelling(tu, toks[k])));
}
clang_disposeTokens(tu, toks, n);
while (parts.size() >= 2 && parts.front() == "(" && parts.back() == ")") {
parts.erase(parts.begin());
parts.pop_back();
}
bool neg = false;
if (parts.size() == 2 && parts[0] == "-") {
neg = true;
parts.erase(parts.begin());
}
if (parts.size() != 1) return false;
const std::string &lit = parts[0];
if (lit.empty() || !(std::isdigit((unsigned char)lit[0]) || lit[0] == '.')) return false;
bool isFloat = lit.find_first_of(".eE") != std::string::npos && lit.rfind("0x", 0) != 0;
char *end;
if (isFloat) {
res.type = Type::F64;
res.f = std::strtod(lit.c_str(), &end);
if (neg) res.f = -res.f;
} else {
res.type = Type::I64;
res.i = (long long)std::strtoull(lit.c_str(), &end, 0); // C rules: 0x.., 0755
if (neg) res.i = -res.i;
}
for (; *end; end++) // allow suffixes: 10UL, 1.0f
if (!std::strchr("uUlLfF", *end)) return false;
return true;
}
void visitTop(CXCursor c) {
std::string name = str(clang_getCursorSpelling(c));
switch (clang_getCursorKind(c)) {
case CXCursor_TypedefDecl: {
// typedef struct {...} Name; -> the struct is called Name in Finch
CXType u = clang_getCanonicalType(clang_getTypedefDeclUnderlyingType(c));
if (u.kind == CXType_Record) {
std::string usr = str(clang_getCursorUSR(clang_getTypeDeclaration(u)));
// prefer the typedef name, unless the struct is already known under its tag
auto it = recordNames.find(usr);
bool used = it != recordNames.end() && out.structs.count(it->second);
if (!used && !out.structs.count(name)) recordNames[usr] = name;
}
break;
}
case CXCursor_FunctionDecl: {
if (out.fns.count(name)) break;
CFunc f;
f.name = name;
f.header = header;
if (clang_Cursor_getStorageClass(c) == CX_SC_Static) {
f.unsupported = "is a static inline C function (it has no code to link against)";
out.fns[name] = f;
break;
}
CXType ft = clang_getCanonicalType(clang_getCursorType(c));
if (ft.kind == CXType_FunctionNoProto) {
f.unsupported = "has no parameter list in its C declaration";
out.fns[name] = f;
break;
}
std::string why;
if (!mapType(clang_getResultType(ft), f.ret, why, Ctx::Signature)) f.unsupported = why;
int n = clang_getNumArgTypes(ft);
for (int k = 0; k < n && f.unsupported.empty(); k++) {
Type t;
if (!mapType(clang_getArgType(ft, k), t, why, Ctx::Signature)) f.unsupported = why;
f.params.push_back(t);
}
f.variadic = clang_isFunctionTypeVariadic(ft);
out.fns[name] = f;
break;
}
case CXCursor_VarDecl: {
if (clang_Cursor_getStorageClass(c) != CX_SC_Extern || out.globals.count(name)) break;
CGlobal g;
std::string why;
if (mapType(clang_getCursorType(c), g.type, why, Ctx::Data)) out.globals[name] = g;
break;
}
case CXCursor_StructDecl:
if (clang_isCursorDefinition(c) && !name.empty() && name.find(' ') == std::string::npos) {
std::string usr = str(clang_getCursorUSR(c));
if (!recordNames.count(usr)) recordNames[usr] = name;
}
break;
case CXCursor_EnumDecl:
clang_visitChildren(c, [](CXCursor k, CXCursor, CXClientData d) {
static_cast<Importer *>(d)->visitTop(k);
return CXChildVisit_Continue;
}, this);
break;
case CXCursor_EnumConstantDecl: {
CConst k;
k.type = Type::I64;
k.i = clang_getEnumConstantDeclValue(c);
out.consts.emplace(name, k);
break;
}
case CXCursor_MacroDefinition: {
if (clang_Cursor_isMacroBuiltin(c) || clang_Cursor_isMacroFunctionLike(c)) break;
CConst k;
if (macroValue(c, k)) out.consts.emplace(name, k);
break;
}
default: break;
}
}
};
} // namespace
CImports importHeaders(const std::vector<Import> &imports, const std::vector<std::string> &dirs) {
CImports out;
bool any = false;
for (const Import &im : imports) any |= im.isC;
if (!any) return out;
std::vector<std::string> args = {"-x", "c", "-std=gnu11"};
for (const std::string &d : dirs) args.push_back("-I" + d);
for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d);
std::vector<const char *> argv;
for (const std::string &a : args) argv.push_back(a.c_str());
Importer imp(out);
CXIndex index = clang_createIndex(0, 0);
for (const Import &im : imports) {
if (!im.isC) continue;
std::string src = "#include \"" + im.path + "\"\n";
CXUnsavedFile file = {"finch_import.c", src.c_str(), (unsigned long)src.size()};
CXTranslationUnit tu = nullptr;
CXErrorCode err = clang_parseTranslationUnit2(
index, "finch_import.c", argv.data(), (int)argv.size(), &file, 1,
CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies, &tu);
if (err != CXError_Success || !tu) failAt(im.pos.file, im.pos.line, im.pos.col, "couldn't read C header '" + im.path + "'");
for (unsigned k = 0; k < clang_getNumDiagnostics(tu); k++) {
CXDiagnostic d = clang_getDiagnostic(tu, k);
if (clang_getDiagnosticSeverity(d) >= CXDiagnostic_Error) {
std::string msg = str(clang_getDiagnosticSpelling(d));
clang_disposeDiagnostic(d);
if (msg.find("file not found") != std::string::npos) {
// "'GL/gl.h' file not found": it may be a header that this one includes
std::string missing = msg.substr(1, msg.find('\'', 1) - 1);
if (missing == im.path)
failAt(im.pos.file, im.pos.line, im.pos.col, "can't find the C header '" + im.path + "'");
failAt(im.pos.file, im.pos.line, im.pos.col, "can't find the C header '" + missing + "', which '" + im.path +
"' includes (is its library's development package installed?)");
}
failAt(im.pos.file, im.pos.line, im.pos.col, "C header '" + im.path + "' has an error: " + msg);
}
clang_disposeDiagnostic(d);
}
imp.tu = tu;
imp.header = im.path;
clang_visitChildren(clang_getTranslationUnitCursor(tu), [](CXCursor c, CXCursor, CXClientData d) {
static_cast<Importer *>(d)->visitTop(c);
return CXChildVisit_Continue;
}, &imp);
clang_disposeTranslationUnit(tu);
}
clang_disposeIndex(index);
return out;
}

View file

@ -1,792 +0,0 @@
// C headers, read through libclang, written in Finch.
//
// What `import "stdio.h"` teaches the compiler: the C functions, the constants (enum values and
// plain #define numbers), the extern variables (stdout) and the structs. It calls libclang's C API
// through Finch's own C import -- so to compile this file, the compiler reads libclang's headers
// with this very code.
//
// C structs by value appear in types as the named type `module "C"`.
import ast
import host
import "clang-c/Index.h"
link "clang"
struct CFunc {
str name
str header // the import it came from
ast.Type ret
[]ast.Type params
bool variadic
bool inline // static inline in the header: called through a pointer a small C file keeps (finch_inl_name)
str unsupported // non-empty: why Finch can't call it (yet)
}
struct CConst { // enum values and #define constants
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 {
str name
ast.Type ty
int offset // bytes
int size
int align
bool hidden // unions, bit fields, unmappable types: kept for layout only
}
struct CStruct {
str name
[]CField fields
int size
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 {
map[str]CFunc fns
map[str]CConst consts
map[str]ast.Type globals
map[str]CStruct structs
[]str structOrder // the order they were defined in
}
// A header to read: the path as written, and where it was written, for error messages.
struct Header {
str path
str where
}
// Where a C type is used decides how `char *` looks in Finch: in function signatures it is a
// `str`; inside structs, globals and behind pointers it stays a raw `ptr[char]`.
struct Mapped {
ast.Type ty
str why
bool ok
}
struct Importer {
CImports out
ptr tu
str header
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 {
out := clang_getCString(s)
clang_disposeString(s)
return out
}
fn ok(ast.Type t) -> Mapped {
return Mapped(ty: t, ok: true)
}
fn no(str why) -> Mapped {
return Mapped(why: why, ok: false)
}
fn fixed(ast.Type elem, int count) -> ast.Type {
t := ast.Type(kind: "fixed", count: count)
t.elem.push(elem)
return t
}
// ---------- the system's header folders ----------
// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would.
fn systemIncludeArgs(str cc) -> []str {
[]str dirs
tmp := host.tempDir() + "/finch_cc_dirs_" + str(host.pid()) + ".txt"
empty := host.tempDir() + "/finch_empty_" + str(host.pid()) + ".c"
write_file(empty, "")
host.run(cc + " -E -v -x c " + host.quote(empty) + " 2> " + host.quote(tmp) + " > " + host.nullDevice())
host.remove(empty)
if !file_exists(tmp) {
return dirs
}
inList := false
for line in read_file(tmp).split("\n") {
l := line.trim()
if l.starts_with("#include <...> search starts here:") {
inList = true
continue
}
if l.starts_with("End of search list.") {
break
}
if !inList || l.len == 0 {
continue
}
// macOS: "/.../Frameworks (framework directory)"
if l.ends_with(" (framework directory)") {
dirs.push("-iframework" + l.sub(0, l.len - 22))
} else {
dirs.push("-isystem" + l)
}
}
host.remove(tmp)
return dirs
}
// ---------- records ----------
// The Finch name of a C record: its typedef name, its tag, or a made-up one.
fn recordName(ptr[Importer] imp, CXType t) -> str {
decl := clang_getTypeDeclaration(t)
usr := text(clang_getCursorUSR(decl))
if imp.recordNames.has(usr) {
return imp.recordNames[usr]
}
name := text(clang_getCursorSpelling(decl))
if name.len == 0 || name.contains(" ") {
imp.anon += 1
name = "anon_struct_" + str(imp.anon)
}
imp.recordNames[usr] = name
return name
}
// Fill out.structs[name] for a complete record; returns its name, or "" if it can't be one.
fn defineRecord(ptr[Importer] imp, CXType type) -> str {
t := clang_getCanonicalType(type)
decl := clang_getTypeDeclaration(t)
usr := text(clang_getCursorUSR(decl))
name := recordName(imp, t)
if imp.out.structs.has(name) || imp.defining.has(usr) {
return name
}
size := int(clang_Type_getSizeOf(t))
if size < 0 {
return "" // incomplete (opaque) struct
}
imp.defining[usr] = true
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))
clang_Type_visitFields(t, addr(visitField), addr(ctx))
imp.defining.remove(usr)
imp.out.structs[name] = s
imp.out.structOrder.push(name)
return name
}
struct FieldCtx {
ptr[Importer] imp
ptr[CStruct] s
}
fn visitField(CXCursor field, ptr data) -> i32 {
ptr[FieldCtx] c = data
f := CField(name: text(clang_getCursorSpelling(field)))
ft := clang_getCanonicalType(clang_getCursorType(field))
f.offset = int(clang_Cursor_getOffsetOfField(field)) / 8
f.size = int(clang_Type_getSizeOf(ft))
f.align = int(clang_Type_getAlignOf(ft))
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 {
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
} else {
f.hidden = true
}
}
if f.hidden {
c.s.hasHidden = true
}
if f.name.len == 0 {
f.hidden = true
}
c.s.fields.push(f)
return CXVisit_Continue
}
// ---------- types ----------
fn sized(int size, bool signed) -> Mapped {
if size == 1 {
if signed {
return ok(ast.simple("i8"))
}
return ok(ast.simple("u8"))
}
if size == 2 {
if signed {
return ok(ast.simple("i16"))
}
return ok(ast.simple("u16"))
}
if size == 4 {
if signed {
return ok(ast.simple("i32"))
}
return ok(ast.simple("u32"))
}
if size == 8 {
if signed {
return ok(ast.simple("int"))
}
return ok(ast.simple("u64"))
}
return no("uses a " + str(size * 8) + "-bit number")
}
// C type -> Finch type. Not ok (with a reason) if Finch can't express it.
// `signature`: a char * is a str (a function's parameters and result); anywhere else it stays a ptr[char].
// A parameter that is a plain `char *` (not const) is a buffer C writes into, and stays a ptr[char];
// only `const char *` is text going in. A result that is a char * is always text coming out.
fn mapType(ptr[Importer] imp, CXType type, bool signature, bool param) -> Mapped {
t := clang_getCanonicalType(type)
size := int(clang_Type_getSizeOf(t))
k := t.kind
if k == CXType_Void {
return ok(ast.simple("void"))
}
if k == CXType_Bool {
return ok(ast.simple("bool"))
}
if k == CXType_Char_S || k == CXType_Char_U {
return ok(ast.simple("char"))
}
if k == CXType_SChar || k == CXType_Short || k == CXType_Int || k == CXType_Long || k == CXType_LongLong {
return sized(size, true)
}
if k == CXType_UChar || k == CXType_UShort || k == CXType_UInt || k == CXType_ULong || k == CXType_ULongLong {
return sized(size, false)
}
if k == CXType_Enum {
return sized(size, true)
}
if k == CXType_Float {
return ok(ast.simple("f32"))
}
if k == CXType_Double {
return ok(ast.simple("float"))
}
if k == CXType_Pointer {
pointee := clang_getCanonicalType(clang_getPointeeType(t))
if (pointee.kind == CXType_Char_S || pointee.kind == CXType_Char_U) && signature {
if !param || clang_isConstQualifiedType(pointee) != 0 {
return ok(ast.simple("str")) // a C string
}
}
if pointee.kind == CXType_Void || pointee.kind == CXType_FunctionProto || pointee.kind == CXType_FunctionNoProto {
return ok(ast.simple("ptr")) // an opaque `ptr` you can pass around
}
inner := mapType(imp, pointee, false, false)
if !inner.ok || inner.ty.kind == "void" || inner.ty.kind == "fixed" {
return ok(ast.simple("ptr"))
}
return ok(ast.ptrTo(inner.ty))
}
if k == CXType_Record {
name := defineRecord(imp, t)
if name.len == 0 {
return no("uses an incomplete C struct by value")
}
return ok(ast.Type(kind: "named", name: name, module: "C"))
}
if k == CXType_ConstantArray {
inner := mapType(imp, clang_getArrayElementType(t), false, false)
if !inner.ok {
return inner
}
return ok(fixed(inner.ty, int(clang_getArraySize(t))))
}
if k == CXType_LongDouble {
return no("uses long double")
}
return no("uses the C type '" + text(clang_getTypeSpelling(t)) + "'")
}
// ---------- #define numbers ----------
struct Number {
CConst c
bool ok
}
// "0x1F", "010", "10UL" -> the value, and whether the text was a whole number
fn parseInt(str lit) -> Number {
s := lit
base := 10
if s.starts_with("0x") || s.starts_with("0X") {
base = 16
s = s.sub(2, s.len)
} else if s.len > 1 && s[0] == '0' {
base = 8
s = s.sub(1, s.len)
}
n := 0
seen := false
i := 0
while i < s.len {
c := s[i]
d := 99
if c >= '0' && c <= '9' {
d = int(c) - int('0')
} else if c >= 'a' && c <= 'f' {
d = int(c) - int('a') + 10
} else if c >= 'A' && c <= 'F' {
d = int(c) - int('A') + 10
}
if d >= base {
break
}
n = n * base + d
seen = true
i += 1
}
// what is left may only be suffixes: 10UL
while i < s.len {
if !"uUlL".contains(str(s[i])) {
return Number()
}
i += 1
}
if !seen && base != 8 {
return Number()
}
return Number(c: CConst(i: n), ok: true)
}
// The tokens after a #define's name; `safe` is false if any of them could not be part of a
// constant expression (braces, semicolons, statements), so it is never handed to clang.
struct MacroTokens {
[]str parts
bool safe
}
fn macroTokens(ptr[Importer] imp, CXCursor c) -> MacroTokens {
ptr[CXToken] tokens = null
u32 n = 0
clang_tokenize(imp.tu, clang_getCursorExtent(c), addr(tokens), addr(n))
out := MacroTokens(safe: true)
allowed := ["(", ")", "+", "-", "*", "/", "%", "<<", ">>", "|", "&", "^", "~", "!", "<", ">", "<=", ">=", "==", "!=", "&&", "||", "?", ":", ",", "{", "}"]
words := ["sizeof", "unsigned", "signed", "int", "long", "short", "char", "float", "double", "const", "_Bool"]
depth := 0
for k in 1..int(n) { // token 0 is the macro's name
kind := clang_getTokenKind(tokens[k])
if kind == CXToken_Comment {
continue
}
t := text(clang_getTokenSpelling(imp.tu, tokens[k]))
out.parts.push(t)
if kind == CXToken_Punctuation {
if !allowed.contains(t) {
out.safe = false
}
if t == "(" {
depth += 1
} else if t == ")" {
depth -= 1
}
} else if kind == CXToken_Keyword && !words.contains(t) {
out.safe = false
}
}
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)
parts.pop()
}
neg := false
if parts.len == 2 && parts[0] == "-" {
neg = true
parts.remove(0)
}
if parts.len != 1 {
return Number()
}
lit := parts[0]
if lit.len == 0 || !((lit[0] >= '0' && lit[0] <= '9') || lit[0] == '.') {
return Number()
}
isFloat := (lit.contains(".") || lit.contains("e") || lit.contains("E")) && !lit.starts_with("0x")
if isFloat {
body := lit
while body.len > 0 && "fFlL".contains(str(body[body.len - 1])) {
body = body.sub(0, body.len - 1)
}
v := float(body) or {
return Number()
}
if neg {
v = 0.0 - v
}
return Number(c: CConst(isFloat: true, f: v, text: body), ok: true)
}
r := parseInt(lit)
if r.ok && neg {
r.c.i = 0 - r.c.i
}
return r
}
// ---------- the top level ----------
fn visitTop(CXCursor c, CXCursor parent, ptr data) -> i32 {
ptr[Importer] imp = data
visit(imp, c)
return CXChildVisit_Continue
}
fn visit(ptr[Importer] imp, CXCursor c) {
name := text(clang_getCursorSpelling(c))
kind := clang_getCursorKind(c)
if kind == CXCursor_TypedefDecl {
// typedef struct {...} Name; -> the struct is called Name in Finch
u := clang_getCanonicalType(clang_getTypedefDeclUnderlyingType(c))
if u.kind == CXType_Record {
usr := text(clang_getCursorUSR(clang_getTypeDeclaration(u)))
// prefer the typedef name, unless the struct is already known under its tag
used := imp.recordNames.has(usr) && imp.out.structs.has(imp.recordNames[usr])
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) {
return
}
f := CFunc(name: name, header: imp.header)
if clang_Cursor_getStorageClass(c) == CX_SC_Static {
f.inline = true // mingw's printf and time, header-only helpers
}
ft := clang_getCanonicalType(clang_getCursorType(c))
if ft.kind == CXType_FunctionNoProto {
f.unsupported = "has no parameter list in its C declaration"
imp.out.fns[name] = f
return
}
r := mapType(imp, clang_getResultType(ft), true, false)
if r.ok {
f.ret = r.ty
} else {
f.unsupported = r.why
}
n := int(clang_getNumArgTypes(ft))
for k in 0..n {
if f.unsupported.len > 0 {
break
}
p := mapType(imp, clang_getArgType(ft, u32(k)), true, true)
if p.ok {
f.params.push(p.ty)
} else {
f.unsupported = p.why
}
}
f.variadic = clang_isFunctionTypeVariadic(ft) != 0
imp.out.fns[name] = f
} else if kind == CXCursor_VarDecl {
if clang_Cursor_getStorageClass(c) != CX_SC_Extern || imp.out.globals.has(name) {
return
}
g := mapType(imp, clang_getCursorType(c), false, false)
if g.ok {
imp.out.globals[name] = g.ty
}
} 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) {
imp.recordNames[usr] = name
}
}
} else if kind == CXCursor_EnumDecl {
clang_visitChildren(c, addr(visitTop), imp)
} else if kind == CXCursor_EnumConstantDecl {
if !imp.out.consts.has(name) {
imp.out.consts[name] = CConst(i: int(clang_getEnumConstantDeclValue(c)))
}
} else if kind == CXCursor_MacroDefinition {
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[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
}
}
}
// Read every header in `headers`; `dirs` are searched first for "local.h" headers.
// `target` is the LLVM triple the program is built for ("" = this system): sizes and headers depend on it.
fn importHeaders([]Header headers, []str dirs, str target, bool windows, str ccFor) -> CImports! {
imp := Importer()
if headers.len == 0 {
return imp.out
}
[]str args = ["-x", "c", "-std=gnu11"]
if target.len > 0 {
args.push("--target=" + target)
}
if windows {
args.push("-D_USE_MATH_DEFINES") // M_PI and friends, as on Linux
}
for d in dirs {
args.push("-I" + d)
}
for d in systemIncludeArgs(ccFor) {
args.push(d)
}
[]ptr[char] argv
for i in 0..args.len {
argv.push(args[i].ptr)
}
index := clang_createIndex(0, 0)
for h in headers {
src := "#include \"" + h.path + "\"\n"
file := host.unsavedFile("finch_import.c", src)
ptr tu = null
flags := CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies
err := clang_parseTranslationUnit2(index, "finch_import.c", argv.ptr, i32(argv.len), addr(file), 1, u32(flags), addr(tu))
if err != CXError_Success || tu == null {
return error(h.where + ": error: couldn't read C header '" + h.path + "'")
}
nd := int(clang_getNumDiagnostics(tu))
for k in 0..nd {
d := clang_getDiagnostic(tu, u32(k))
if clang_getDiagnosticSeverity(d) >= CXDiagnostic_Error {
msg := text(clang_getDiagnosticSpelling(d))
clang_disposeDiagnostic(d)
if msg.contains("file not found") {
// "'GL/gl.h' file not found": it may be a header that this one includes
missing := msg.sub(1, msg.sub(1, msg.len).find("'") + 1)
if missing == h.path {
return error(h.where + ": error: can't find the C header '" + h.path + "'")
}
return error(h.where + ": error: can't find the C header '" + missing + "', which '" + h.path + "' includes (is its library's development package installed?)")
}
return error(h.where + ": error: C header '" + h.path + "' has an error: " + msg)
}
clang_disposeDiagnostic(d)
}
imp.tu = tu
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
}

56
src/cimport.h Normal file
View file

@ -0,0 +1,56 @@
#pragma once
#include "ast.h"
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
// What Finch learned from `import "something.h"`.
// C structs by value appear in types as Named{name, module "C"}.
struct CFunc {
std::string name;
std::string header; // the import it came from
Type ret;
std::vector<Type> params;
bool variadic = false;
std::string unsupported; // non-empty: why Finch can't call it (yet)
};
struct CConst { // enum values and simple #define numbers
Type type;
long long i = 0;
double f = 0;
};
struct CGlobal { // extern variables like stdout
Type type;
};
struct CField {
std::string name;
Type type;
long long offset = 0; // bytes
long long size = 0, align = 1;
bool hidden = false; // unions, bitfields, unmappable types: kept for layout only
};
struct CStruct {
std::string name;
std::vector<CField> fields;
long long size = 0, align = 1;
std::string unsupported; // why it can't be used by value (pointers to it still work)
bool hasHidden = false;
};
struct CImports {
std::unordered_map<std::string, CFunc> fns;
std::unordered_map<std::string, CConst> consts;
std::unordered_map<std::string, CGlobal> globals;
std::map<std::string, CStruct> structs;
};
// `dirs`: folders searched first for "local.h" headers (the .fch files' folders).
CImports importHeaders(const std::vector<Import> &imports, const std::vector<std::string> &dirs);

1092
src/codegen.cpp Normal file

File diff suppressed because it is too large Load diff

15
src/codegen.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include "ast.h"
#include "cimport.h"
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/Target/TargetMachine.h>
#include <memory>
// All files of the program (the main file first, then its modules) become one LLVM module.
// `tm` describes the machine we compile for (sizes, alignment), which shapes the IR.
std::unique_ptr<llvm::Module> generate(const std::vector<Program> &progs, const CImports &c, llvm::LLVMContext &ctx,
llvm::TargetMachine &tm, bool debug);

715
src/codegen_builtins.cpp Normal file
View file

@ -0,0 +1,715 @@
// Code generation, part 3: calls, built-in functions, conversions and methods.
#include "codegen_impl.h"
using namespace llvm;
namespace finch {
bool Codegen::isBuiltin(const std::string &n) {
static const std::set<std::string> names = {"print", "addr", "input", "new", "free", "read_file", "write_file",
"file_exists", "exit", "shell"};
FType t;
return names.count(n) || typeFromName(n, t);
}
// Help people who forgot an import.
std::string Codegen::headerHint(const std::string &name) {
static const std::unordered_map<std::string, const char *> known = {
{"printf", "stdio.h"}, {"puts", "stdio.h"}, {"putchar", "stdio.h"}, {"scanf", "stdio.h"},
{"fopen", "stdio.h"}, {"getchar", "stdio.h"}, {"sqrt", "math.h"}, {"pow", "math.h"},
{"sin", "math.h"}, {"cos", "math.h"}, {"floor", "math.h"}, {"fabs", "math.h"},
{"malloc", "stdlib.h"}, {"rand", "stdlib.h"}, {"srand", "stdlib.h"}, {"abs", "stdlib.h"},
{"atoi", "stdlib.h"}, {"strlen", "string.h"}, {"strcpy", "string.h"}, {"memcpy", "string.h"},
{"time", "time.h"}, {"sleep", "unistd.h"}, {"usleep", "unistd.h"},
};
auto it = known.find(name);
return it == known.end() ? "" : std::string(" (it's from C: add import \"") + it->second + "\" at the top)";
}
void Codegen::checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,
const std::string &name, bool variadic) {
for (size_t i = 0; i < names.size(); i++)
if (!names[i].empty())
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "named arguments (" + names[i] + ": ...) are only for creating structs");
if (args.size() == want || (variadic && args.size() > want)) return;
failAt(p.file, p.line, p.col, "'" + name + "' takes " + (variadic ? "at least " : "") + std::to_string(want) +
" argument(s), but got " + std::to_string(args.size()));
}
Function *Codegen::libc(const char *name, llvm::Type *ret, std::vector<llvm::Type *> params, bool vararg) {
FunctionCallee c = mod->getOrInsertFunction(name, FunctionType::get(ret, params, vararg));
return cast<Function>(c.getCallee());
}
// The runtime (runtime/finch_rt.c), declared on first use.
Function *Codegen::rt(const std::string &name) {
if (Function *f = mod->getFunction(name)) return f;
llvm::Type *P = b.getPtrTy(), *I = b.getInt64Ty(), *I32 = b.getInt32Ty(), *D = b.getDoubleTy(), *V = b.getVoidTy();
struct Sig { llvm::Type *ret; std::vector<llvm::Type *> params; };
std::map<std::string, Sig> sigs = {
{"finch_panic", {V, {P, I, P}}},
{"finch_panic_index", {V, {P, I, I, I}}},
{"finch_alloc", {P, {I}}},
{"finch_zalloc", {P, {I}}},
{"finch_str_copy", {V, {P, P}}},
{"finch_str_own", {V, {P}}},
{"finch_str_drop", {V, {P}}},
{"finch_str_from_c", {V, {P, P}}},
{"finch_str_concat", {V, {P, P, P}}},
{"finch_str_eq", {I32, {P, P}}},
{"finch_str_cmp", {I, {P, P}}},
{"finch_str_from_int", {V, {P, I}}},
{"finch_str_from_uint", {V, {P, I}}},
{"finch_str_from_float", {V, {P, D}}},
{"finch_str_from_char", {V, {P, b.getInt8Ty()}}},
{"finch_str_from_bool", {V, {P, I32}}},
{"finch_str_to_int", {I, {P, P, I}}},
{"finch_str_to_float", {D, {P, P, I}}},
{"finch_str_sub", {V, {P, P, I, I, P, I}}},
{"finch_str_find", {I, {P, P}}},
{"finch_str_starts", {I32, {P, P}}},
{"finch_str_ends", {I32, {P, P}}},
{"finch_str_trim", {V, {P, P}}},
{"finch_str_upper", {V, {P, P}}},
{"finch_str_lower", {V, {P, P}}},
{"finch_str_replace", {V, {P, P, P, P}}},
{"finch_str_split", {V, {P, P, P}}},
{"finch_str_join", {V, {P, P, P}}},
{"finch_str_repeat", {V, {P, P, I}}},
{"finch_str_from_bytes", {V, {P, P}}},
{"finch_arr_reserve", {V, {P, I, I}}},
{"finch_arr_make", {V, {P, I, I}}},
{"finch_arr_resize", {V, {P, I, I}}},
{"finch_arr_clone_raw", {V, {P, P, I}}},
{"finch_arr_free", {V, {P}}},
{"finch_arr_insert_gap", {V, {P, I, I, P, I}}},
{"finch_arr_remove_gap", {V, {P, I, I}}},
{"finch_bytes_from_str", {V, {P, P}}},
{"finch_args", {V, {P, I32, P}}},
{"finch_input", {V, {P, P}}},
{"finch_file_exists", {I32, {P}}},
{"finch_read_file", {V, {P, P, P, I}}},
{"finch_write_file", {I32, {P, P}}},
{"finch_shell", {I, {P}}},
};
auto it = sigs.find(name);
if (it == sigs.end()) {
std::fprintf(stderr, "internal compiler error: unknown runtime function %s\n", name.c_str());
std::exit(2);
}
Function *f = Function::Create(FunctionType::get(it->second.ret, it->second.params, false), Function::ExternalLinkage,
name, mod.get());
if (name == "finch_panic" || name == "finch_panic_index") {
f->addFnAttr(Attribute::NoReturn);
f->addFnAttr(Attribute::Cold);
}
if (name == "finch_str_from_char") f->addParamAttr(1, Attribute::SExt);
return f;
}
// ---------- runtime errors ----------
Value *Codegen::fileName(Pos p) {
std::string key = "file." + std::to_string(p.file);
if (GlobalVariable *g = mod->getNamedGlobal("finch." + key)) return g;
GlobalVariable *g = b.CreateGlobalString(g_files[p.file].path, "finch." + key);
return g;
}
void Codegen::panicIf(Value *cond, const std::string &msg, Pos p) {
BasicBlock *bad = newBlock("panic");
BasicBlock *ok = newBlock("ok");
b.CreateCondBr(cond, bad, ok);
b.SetInsertPoint(bad);
b.CreateCall(rt("finch_panic"), {fileName(p), b.getInt64(p.line), b.CreateGlobalString(msg, "panic.msg")});
b.CreateUnreachable();
b.SetInsertPoint(ok);
}
void Codegen::boundsCheck(Value *i, Value *len, Pos p) {
BasicBlock *bad = newBlock("out.of.range");
BasicBlock *ok = newBlock("in.range");
b.CreateCondBr(b.CreateICmpUGE(i, len), bad, ok); // unsigned: catches negative indexes too
b.SetInsertPoint(bad);
b.CreateCall(rt("finch_panic_index"), {fileName(p), b.getInt64(p.line), i, len});
b.CreateUnreachable();
b.SetInsertPoint(ok);
}
// Dividing by zero is undefined in C; in Finch it is a clear error.
void Codegen::checkDivisor(const Value_ &l, const Value_ &r, Pos p) {
auto *c = dyn_cast<ConstantInt>(r.v);
if (c && c->isZero()) failAt(p.file, p.line, p.col, "division by zero");
if (!c) panicIf(b.CreateICmpEQ(r.v, ConstantInt::get(r.v->getType(), 0)), "division by zero", p);
// the smallest signed number divided by -1 doesn't fit anywhere
if (l.type.isSigned() && (!c || c->isMinusOne())) {
Value *min = ConstantInt::get(l.v->getType(), APInt::getSignedMinValue(l.type.bits()));
Value *minusOne = ConstantInt::get(r.v->getType(), -1, true);
panicIf(b.CreateAnd(b.CreateICmpEQ(l.v, min), b.CreateICmpEQ(r.v, minusOne)),
"division overflows (the smallest number divided by -1)", p);
}
}
Value *Codegen::cGlobal(const std::string &name) {
if (GlobalVariable *gv = mod->getNamedGlobal(name)) return gv;
FType t = resolve(cimports.globals.at(name).type, Pos{});
return new GlobalVariable(*mod, ty(t), false, GlobalValue::ExternalLinkage, nullptr, name);
}
// ---------- calls ----------
Value_ Codegen::call(const CallExpr &c) {
const std::string &n = c.callee;
Pos p = c.pos;
auto one = [&](const char *what) -> const Expr & {
checkArgs(c.args, c.argNames, 1, p, n);
(void)what;
return *c.args[0];
};
if (n == "print") {
checkArgs(c.args, c.argNames, 0, p, n, true);
return print(c.args, p);
}
if (n == "addr") {
checkArgs(c.args, c.argNames, 1, p, n);
return addrOf(c.args, p);
}
if (n == "input") {
if (c.args.size() > 1) checkArgs(c.args, c.argNames, 1, p, n);
Value_ prompt = c.args.empty() ? Value_{strConst(""), FType::Str, false, true}
: coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the question");
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_input"), {out, tmp(prompt.v)});
release(prompt);
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
if (n == "new") {
const Expr &a = one("value");
Value_ v = expr(a);
if (v.type.kind == FType::Void || v.type.kind == FType::Null)
failAt(a.pos.file, a.pos.line, a.pos.col, "new(...) needs a value to put in memory, like new(Point(x: 1, y: 2))");
Value *mem = b.CreateCall(rt("finch_alloc"), {elemSize(v.type)});
b.CreateStore(own(v), mem);
return {mem, FType::ptrTo(v.type)};
}
if (n == "free") {
const Expr &a = one("pointer");
Value_ v = expr(a);
if (!v.type.isPtr())
failAt(a.pos.file, a.pos.line, a.pos.col, "free(...) is only for pointers from new(...) or C; arrays, str and structs are freed by Finch at the end of their block");
if (v.type.elem && owning(*v.type.elem)) {
BasicBlock *drop = newBlock("free.drop"), *done = newBlock("free.done");
b.CreateCondBr(b.CreateIsNull(v.v), done, drop);
b.SetInsertPoint(drop);
dropAt(v.v, *v.type.elem);
b.CreateBr(done);
b.SetInsertPoint(done);
}
b.CreateCall(libc("free", b.getVoidTy(), {b.getPtrTy()}), {v.v});
return {nullptr, FType::Void};
}
if (n == "exit") {
Value *code = coerce(expr(one("code")), FType::I32, c.args[0]->pos, "the exit code").v;
b.CreateCall(libc("fflush", b.getInt32Ty(), {b.getPtrTy()}), {ConstantPointerNull::get(b.getPtrTy())});
b.CreateCall(libc("exit", b.getVoidTy(), {b.getInt32Ty()}), {code});
return {nullptr, FType::Void};
}
if (n == "read_file" || n == "file_exists") {
Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name");
Value *pp = tmp(path.v);
Value_ res;
if (n == "file_exists") {
res = {b.CreateICmpNE(b.CreateCall(rt("finch_file_exists"), {pp}), b.getInt32(0)), FType::Bool};
} else {
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_read_file"), {out, pp, fileName(p), b.getInt64(p.line)});
res = {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
release(path);
return res;
}
if (n == "shell") {
Value_ cmd = coerce(expr(one("command")), FType::Str, c.args[0]->pos, "the command");
Value *ss = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_copy"), {ss, tmp(cmd.v)}); // a heap copy is always NUL-terminated
Value *r = b.CreateCall(rt("finch_shell"), {ss});
dropAt(ss, FType::Str);
release(cmd);
return {r, FType::I64};
}
if (n == "write_file") {
checkArgs(c.args, c.argNames, 2, p, n);
Value_ path = coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the file name");
Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text");
Value *ok = b.CreateCall(rt("finch_write_file"), {tmp(path.v), tmp(text.v)});
release(path);
release(text);
return {b.CreateICmpNE(ok, b.getInt32(0)), FType::Bool};
}
FType target;
if (typeFromName(n, target)) {
checkArgs(c.args, c.argNames, 1, p, n);
return convert(target, c.args, p, n);
}
ModuleScope &m = modules[curModule];
if (auto it = m.fns.find(n); it != m.fns.end()) return callFinch(it->second, c.args, c.argNames, p, n);
if (StructInfo *s = findStruct("", n, false)) return construct(s, c.args, c.argNames, p);
if (auto cf = cimports.fns.find(n); cf != cimports.fns.end()) {
checkArgs(c.args, c.argNames, cf->second.params.size(), p, n, cf->second.variadic);
return callC(cf->second, c.args, p);
}
if (lookup(n)) failAt(p.file, p.line, p.col, "'" + n + "' is a variable, not a function");
for (auto &[mname, ms] : modules)
if (ms.fns.count(n) && mname != curModule && !mname.empty())
failAt(p.file, p.line, p.col, "there is no function named '" + n + "' here; it is in the module " + mname +
" (write " + mname + "." + n + "(...)" + (m.imports.count(mname) ? "" : " and add: import " + mname) + ")");
failAt(p.file, p.line, p.col, "there is no function named '" + n + "'" + headerHint(n));
}
Value_ Codegen::callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shown) {
const FnDecl &d = *fn.decl;
if (fn.module.empty() && d.name == "main") failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts");
checkArgs(args, names, fn.params.size(), p, shown);
std::vector<Value *> vals;
std::vector<Value_> fresh;
for (size_t i = 0; i < args.size(); i++) {
Value_ v = coerce(exprWant(*args[i], fn.params[i]), fn.params[i], args[i]->pos, "argument '" + d.params[i].name + "'");
vals.push_back(v.v); // owning values are lent: the function never drops them
if (v.fresh) fresh.push_back(v);
}
setLoc(p);
Value *r = b.CreateCall(fn.llvm, vals);
for (auto &v : fresh) release(v);
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
return {r, fn.ret, false, owning(fn.ret)};
}
Value_ Codegen::construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p) {
resolveStruct(s);
FType t(FType::Struct);
t.info = s;
t.name = s->name;
t.module = s->module == "C" ? "" : s->module;
std::vector<const StructInfo::F *> fields;
for (auto &f : s->fields) fields.push_back(&f);
bool named = !names.empty() && !names[0].empty();
std::vector<const Expr *> given(fields.size(), nullptr);
for (size_t i = 0; i < args.size(); i++) {
Pos ap = args[i]->pos;
if (names[i].empty() == named)
failAt(ap.file, ap.line, ap.col, "give either all fields by name (x: 1, y: 2) or none (1, 2), not a mix");
if (!named) {
if (args.size() != fields.size())
failAt(p.file, p.line, p.col, s->name + " has " + std::to_string(fields.size()) + " fields, but got " +
std::to_string(args.size()) + " values (or name them: " + s->name + "(field: value))");
given[i] = args[i].get();
continue;
}
size_t k = 0;
while (k < fields.size() && fields[k]->name != names[i]) k++;
if (k == fields.size()) failAt(ap.file, ap.line, ap.col, s->name + " has no field '" + names[i] + "'");
if (given[k]) failAt(ap.file, ap.line, ap.col, "the field '" + names[i] + "' is given twice");
given[k] = args[i].get();
}
Value *v = zero(t);
for (size_t k = 0; k < fields.size(); k++) {
const StructInfo::F &f = *fields[k];
Value_ fv;
if (given[k]) {
fv = coerce(exprWant(*given[k], f.type), f.type, given[k]->pos, "the field '" + f.name + "'");
} else if (f.init) {
// a default value is evaluated where the struct is defined, without the caller's variables
std::vector<Scope> saved;
std::swap(saved, scopes);
scopes.reserve(256);
pushScope();
std::string savedModule = curModule;
curModule = s->module;
fv = coerce(exprWant(*f.init, f.type), f.type, f.init->pos, "the default of '" + f.name + "'");
curModule = savedModule;
scopes.pop_back();
std::swap(saved, scopes);
} else {
continue; // zero
}
v = b.CreateInsertValue(v, own(fv), f.llvmIndex);
}
return {v, t, false, owning(t)};
}
Value_ Codegen::callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p) {
if (!f.unsupported.empty())
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported);
std::vector<Value_> vals;
for (size_t i = 0; i < args.size(); i++) {
if (i < f.params.size()) {
FType pt = resolve(f.params[i], p);
vals.push_back(coerce(exprWant(*args[i], pt), pt, args[i]->pos, "argument " + std::to_string(i + 1) + " of " + f.name));
} else {
Value_ v = expr(*args[i]);
if (v.type.kind == FType::Void) failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "this gives back nothing");
if (v.type.kind == FType::Struct || v.type.kind == FType::Array)
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "structs and arrays can't be passed to the '...' part of a C function");
vals.push_back(v);
}
}
setLoc(p);
Value_ r = emitCCall(f, declareC(f), vals, p);
for (auto &v : vals) release(v);
return r;
}
// ---------- built-ins ----------
// print(a, b, c) prints the values separated by spaces, then a newline.
Value_ Codegen::print(const std::vector<ExprPtr> &args, Pos) {
for (size_t i = 0; i < args.size(); i++) {
Value_ v = expr(*args[i]);
if (v.type.kind == FType::Void)
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "this gives back nothing, so there is nothing to print");
if (i) printf_(" ", {});
emitPrint(v.v, v.type, false);
release(v);
}
printf_("\n", {});
return {nullptr, FType::Void};
}
// addr(x) gives a pointer to x; addr(function) gives a C-callable function pointer.
Value_ Codegen::addrOf(const std::vector<ExprPtr> &args, Pos p) {
const Expr &e = *args[0];
if (e.kind == ExprKind::Var) {
const std::string &n = static_cast<const VarExpr &>(e).name;
Var *v = lookup(n);
if (!v) {
auto &fns = modules[curModule].fns;
if (auto it = fns.find(n); it != fns.end()) return {cThunk(it->second, p), FType::Ptr};
}
if (v && v->readonly) failAt(e.pos.file, e.pos.line, e.pos.col, "can't take the address of a loop variable");
}
if (e.kind != ExprKind::Var && e.kind != ExprKind::Member && e.kind != ExprKind::Index)
failAt(e.pos.file, e.pos.line, e.pos.col, "addr(...) needs a variable, a field or an element, like addr(x)");
Place pl = place(e, true);
return {pl.addr, FType::ptrTo(pl.type)};
}
// Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), str(x), ptr(x) ...
Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name) {
Value_ v = expr(*args[0]);
const FType &from = v.type;
Pos p = args[0]->pos;
if (to.kind == FType::Str) {
Value *out = tmpOf(ty(FType::Str));
if (from.kind == FType::Str) return {copyValue(v.v, FType::Str), FType::Str, false, true};
if (from.isPtr()) return strFromC(v.v);
if (from.isSigned()) b.CreateCall(rt("finch_str_from_int"), {out, b.CreateSExt(v.v, b.getInt64Ty())});
else if (from.isUnsigned()) b.CreateCall(rt("finch_str_from_uint"), {out, b.CreateZExt(v.v, b.getInt64Ty())});
else if (from.isFloat()) b.CreateCall(rt("finch_str_from_float"), {out, b.CreateFPExt(v.v, b.getDoubleTy())});
else if (from.kind == FType::Char) b.CreateCall(rt("finch_str_from_char"), {out, v.v});
else if (from.kind == FType::Bool) b.CreateCall(rt("finch_str_from_bool"), {out, b.CreateZExt(v.v, b.getInt32Ty())});
else if (from.kind == FType::Array && (from.elem->kind == FType::U8 || from.elem->kind == FType::Char)) {
b.CreateCall(rt("finch_str_from_bytes"), {out, tmp(v.v)});
release(v);
} else {
failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into str");
}
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
if (from == to) return v;
if (from.kind == FType::Str && (to.isInt() || to.isFloat())) {
Value *sp = tmp(v.v);
Value_ r;
if (to.isInt()) r = {intCast(b.CreateCall(rt("finch_str_to_int"), {sp, fileName(p), b.getInt64(p.line)}), FType::I64, to), to};
else r = {b.CreateFPCast(b.CreateCall(rt("finch_str_to_float"), {sp, fileName(p), b.getInt64(p.line)}), ty(to)), to};
release(v);
return r;
}
if (to.isInt() || to.kind == FType::Char) {
if (from.isInt() || from.kind == FType::Char) return {b.CreateIntCast(v.v, ty(to), from.isSigned()), to};
if (from.kind == FType::Bool) return {b.CreateZExt(v.v, ty(to)), to};
if (from.isFloat() && to.isInt()) return {to.isSigned() ? b.CreateFPToSI(v.v, ty(to)) : b.CreateFPToUI(v.v, ty(to)), to};
}
if (to.isFloat()) {
if (from.isInt()) return {intToFloat(v.v, from, to), to};
if (from.isFloat()) return {b.CreateFPCast(v.v, ty(to)), to};
}
if (to.kind == FType::Bool && from.isInt()) return {b.CreateICmpNE(v.v, ConstantInt::get(ty(from), 0)), to};
if (to.isPtr() && (from.isPtr() || from.kind == FType::Null)) return {v.v, to};
if (to.isPtr() && from.isInt()) return {b.CreateIntToPtr(intCast(v.v, from, FType::U64), b.getPtrTy()), to}; // an address or offset for C
if (to.isInt() && from.isPtr()) return {b.CreatePtrToInt(v.v, ty(to)), to};
if (to.isPtr() && from.kind == FType::Str) failAt(p.file, p.line, p.col, "use .ptr to get a str's address (it is only valid while the str lives)");
failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into " + name);
}
// ---------- methods ----------
Value_ Codegen::method(const MethodExpr &m) {
Pos p = m.pos;
// math.sqrt(x), math.Vec(1, 2)
if (m.obj->kind == ExprKind::Var) {
const std::string &mn = static_cast<const VarExpr &>(*m.obj).name;
if (!lookup(mn) && modules[curModule].imports.count(mn)) {
auto mi = modules.find(mn);
if (mi == modules.end()) failAt(p.file, p.line, p.col, "the module '" + mn + "' was not found");
if (auto it = mi->second.fns.find(m.name); it != mi->second.fns.end())
return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name);
if (auto it = mi->second.structs.find(m.name); it != mi->second.structs.end())
return construct(it->second, m.args, m.argNames, p);
failAt(p.file, p.line, p.col, "the module '" + mn + "' has no function or struct named '" + m.name + "'");
}
if (!lookup(mn) && !cimports.globals.count(mn) && !cimports.consts.count(mn))
failAt(m.obj->pos.file, m.obj->pos.line, m.obj->pos.col,
"there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)");
}
static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"};
LRef r = ref(*m.obj, changing.count(m.name) > 0);
FType t = r.isPlace ? r.pl.type : r.val.type;
if (t.kind == FType::Array) {
Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : r.val;
if (!r.isPlace) {
if (changing.count(m.name)) failAt(p.file, p.line, p.col, m.name + "() would change a temporary array; store it in a variable first");
recv = {tmp(r.val.v), t, false, r.val.fresh};
}
Value_ out = arrayMethod(m, &recv);
if (!r.isPlace && r.val.fresh) dropAt(recv.v, t);
return out;
}
if (t.kind == FType::Str) {
Value_ sv = r.isPlace ? Value_{b.CreateLoad(ty(t), r.pl.addr), t} : r.val;
Value *addr = tmp(sv.v); // the runtime takes strings by address
auto argStr = [&](size_t i, const char *what) {
return coerce(expr(*m.args[i]), FType::Str, m.args[i]->pos, what);
};
auto argInt = [&](size_t i, const char *what) {
return coerce(expr(*m.args[i]), FType::I64, m.args[i]->pos, what).v;
};
auto need = [&](size_t n) { checkArgs(m.args, m.argNames, n, p, m.name); };
Value *out = tmpOf(ty(FType::Str));
Value_ res;
const std::string &n = m.name;
auto strRes = [&]() { return Value_{b.CreateLoad(ty(FType::Str), out), FType::Str, false, true}; };
if (n == "sub") {
need(2);
Value *a = argInt(0, "the start"), *e = argInt(1, "the end");
b.CreateCall(rt("finch_str_sub"), {out, addr, a, e, fileName(p), b.getInt64(p.line)});
res = strRes();
} else if (n == "find" || n == "contains" || n == "starts_with" || n == "ends_with") {
need(1);
Value_ t2 = argStr(0, "the text to look for");
Value *tp = tmp(t2.v);
if (n == "find") res = {b.CreateCall(rt("finch_str_find"), {addr, tp}), FType::I64};
else if (n == "contains") res = {b.CreateICmpSGE(b.CreateCall(rt("finch_str_find"), {addr, tp}), b.getInt64(0)), FType::Bool};
else res = {b.CreateICmpNE(b.CreateCall(rt(n == "starts_with" ? "finch_str_starts" : "finch_str_ends"), {addr, tp}), b.getInt32(0)), FType::Bool};
release(t2);
} else if (n == "trim" || n == "upper" || n == "lower") {
need(0);
b.CreateCall(rt("finch_str_" + n), {out, addr});
res = strRes();
} else if (n == "replace") {
need(2);
Value_ a = argStr(0, "the text to replace"), c = argStr(1, "the new text");
b.CreateCall(rt("finch_str_replace"), {out, addr, tmp(a.v), tmp(c.v)});
release(a);
release(c);
res = strRes();
} else if (n == "split") {
need(1);
Value_ sep = argStr(0, "the separator");
FType at = FType::arrayOf(FType::Str);
Value *ao = tmpOf(ty(at));
b.CreateCall(rt("finch_str_split"), {ao, addr, tmp(sep.v)});
release(sep);
res = {b.CreateLoad(ty(at), ao), at, false, true};
} else if (n == "repeat") {
need(1);
b.CreateCall(rt("finch_str_repeat"), {out, addr, argInt(0, "the count")});
res = strRes();
} else if (n == "bytes") {
need(0);
FType at = FType::arrayOf(FType::U8);
Value *ao = tmpOf(ty(at));
b.CreateCall(rt("finch_bytes_from_str"), {ao, addr});
res = {b.CreateLoad(ty(at), ao), at, false, true};
} else {
failAt(p.file, p.line, p.col, "a str has no method '" + n + "' (it has: sub, find, contains, starts_with, ends_with, split, trim, upper, lower, replace, repeat, bytes)");
}
if (!r.isPlace) release(sv);
return res;
}
if (t.kind == FType::Struct)
failAt(p.file, p.line, p.col, "structs don't have methods; write a function and call it: " + m.name + "(value, ...)");
failAt(p.file, p.line, p.col, t.show() + " has no methods");
}
// An element compare for find/contains/sort: -1, 0, 1 (only numbers, chars, bools and str)
static bool comparable(const FType &t) {
return t.isNumber() || t.kind == FType::Char || t.kind == FType::Bool || t.kind == FType::Str;
}
Value_ Codegen::arrayMethod(const MethodExpr &m, Value_ *recv) {
Pos p = m.pos;
const std::string &n = m.name;
FType at = recv->type, et = *at.elem;
llvm::Type *AT = ty(at), *ET = ty(et);
Value *a = recv->v; // the array's address
auto need = [&](size_t k) { checkArgs(m.args, m.argNames, k, p, n); };
auto lenAddr = [&]() { return b.CreateStructGEP(AT, a, 1); };
auto len = [&]() { return b.CreateLoad(b.getInt64Ty(), lenAddr()); };
auto data = [&]() { return b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(AT, a, 0)); };
auto elemValue = [&](size_t i) { return own(coerce(exprWant(*m.args[i], et), et, m.args[i]->pos, "the element")); };
if (n == "push") {
need(1);
Value *v = elemValue(0);
Value *l = len();
b.CreateCall(rt("finch_arr_reserve"), {a, b.CreateAdd(l, b.getInt64(1)), elemSize(et)});
b.CreateStore(v, b.CreateInBoundsGEP(ET, data(), l));
b.CreateStore(b.CreateAdd(l, b.getInt64(1)), lenAddr());
return {nullptr, FType::Void};
}
if (n == "pop") {
need(0);
Value *l = len();
panicIf(b.CreateICmpEQ(l, b.getInt64(0)), "pop() on an empty array", p);
Value *last = b.CreateSub(l, b.getInt64(1));
Value *v = b.CreateLoad(ET, b.CreateInBoundsGEP(ET, data(), last));
b.CreateStore(last, lenAddr());
return {v, et, false, owning(et)};
}
if (n == "insert") {
need(2);
Value *i = coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the position").v;
Value *v = elemValue(1);
b.CreateCall(rt("finch_arr_insert_gap"), {a, i, elemSize(et), fileName(p), b.getInt64(p.line)});
b.CreateStore(v, b.CreateInBoundsGEP(ET, data(), i));
return {nullptr, FType::Void};
}
if (n == "remove") {
need(1);
Value *i = coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the position").v;
boundsCheck(i, len(), p);
Value *v = b.CreateLoad(ET, b.CreateInBoundsGEP(ET, data(), i));
b.CreateCall(rt("finch_arr_remove_gap"), {a, i, elemSize(et)});
return {v, et, false, owning(et)};
}
if (n == "clear" || n == "resize") {
need(n == "clear" ? 0 : 1);
Value *nl = n == "clear" ? (Value *)b.getInt64(0) : coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the new length").v;
if (owning(et)) { // drop the elements that go away
Value *l = len(), *d = data();
Value *from = b.CreateSelect(b.CreateICmpSLT(nl, l), b.CreateSelect(b.CreateICmpSLT(nl, b.getInt64(0)), b.getInt64(0), nl), l);
forN(b.CreateSub(l, from), [&](Value *k) { dropAt(b.CreateInBoundsGEP(ET, d, b.CreateAdd(from, k)), et); });
}
b.CreateCall(rt("finch_arr_resize"), {a, nl, elemSize(et)});
return {nullptr, FType::Void};
}
if (n == "find" || n == "contains") {
need(1);
if (!comparable(et)) failAt(p.file, p.line, p.col, n + "() works on arrays of numbers, chars, bools or str");
Value_ x = coerce(exprWant(*m.args[0], et), et, m.args[0]->pos, "the value to look for");
Value *xv = x.v;
Value *res = tmpOf(b.getInt64Ty());
b.CreateStore(b.getInt64(-1), res);
Value *l = len(), *d = data();
Value *i = tmpOf(b.getInt64Ty());
b.CreateStore(b.getInt64(0), i);
BasicBlock *cond = newBlock("find.cond"), *body = newBlock("find.body"), *hit = newBlock("find.hit"),
*next = newBlock("find.next"), *end = newBlock("find.end");
b.CreateBr(cond);
b.SetInsertPoint(cond);
Value *iv = b.CreateLoad(b.getInt64Ty(), i);
b.CreateCondBr(b.CreateICmpSLT(iv, l), body, end);
b.SetInsertPoint(body);
Value_ ev{b.CreateLoad(ET, b.CreateInBoundsGEP(ET, d, iv)), et};
Value *eq = compare(BinOp::Eq, ev, {xv, et}, p).v;
b.CreateCondBr(eq, hit, next);
b.SetInsertPoint(hit);
b.CreateStore(iv, res);
b.CreateBr(end);
b.SetInsertPoint(next);
b.CreateStore(b.CreateAdd(iv, b.getInt64(1)), i);
b.CreateBr(cond);
b.SetInsertPoint(end);
release(x);
Value *r = b.CreateLoad(b.getInt64Ty(), res);
if (n == "find") return {r, FType::I64};
return {b.CreateICmpSGE(r, b.getInt64(0)), FType::Bool};
}
if (n == "join") {
need(1);
if (et.kind != FType::Str) failAt(p.file, p.line, p.col, "join() works on an array of str");
Value_ sep = coerce(expr(*m.args[0]), FType::Str, m.args[0]->pos, "the separator");
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_join"), {out, a, tmp(sep.v)});
release(sep);
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
if (n == "slice") {
need(2);
Value *s = coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the start").v;
Value *e = coerce(expr(*m.args[1]), FType::I64, m.args[1]->pos, "the end").v;
Value *l = len();
panicIf(b.CreateOr(b.CreateOr(b.CreateICmpSLT(s, b.getInt64(0)), b.CreateICmpSGT(e, l)), b.CreateICmpSGT(s, e)),
"slice(start, end) is out of range", p);
Value *cnt = b.CreateSub(e, s);
Value *out = tmpOf(AT);
b.CreateCall(rt("finch_arr_make"), {out, cnt, elemSize(et)});
Value *src = data();
Value *dst = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(AT, out, 0));
forN(cnt, [&](Value *k) {
copyAt(b.CreateInBoundsGEP(ET, dst, k), b.CreateInBoundsGEP(ET, src, b.CreateAdd(s, k)), et);
});
return {b.CreateLoad(AT, out), at, false, true};
}
if (n == "reverse") {
need(0);
Value *l = len(), *d = data();
forN(b.CreateSDiv(l, b.getInt64(2)), [&](Value *k) {
Value *x = b.CreateInBoundsGEP(ET, d, k);
Value *y = b.CreateInBoundsGEP(ET, d, b.CreateSub(b.CreateSub(l, b.getInt64(1)), k));
Value *xv = b.CreateLoad(ET, x), *yv = b.CreateLoad(ET, y);
b.CreateStore(yv, x);
b.CreateStore(xv, y);
});
return {nullptr, FType::Void};
}
if (n == "sort") {
need(0);
if (!comparable(et) || et.kind == FType::Bool) failAt(p.file, p.line, p.col, "sort() works on arrays of numbers, chars or str");
std::string key = "cmp." + typeKey(et);
Function *cmp = helpers[key];
if (!cmp) {
cmp = Function::Create(FunctionType::get(b.getInt32Ty(), {b.getPtrTy(), b.getPtrTy()}, false),
Function::InternalLinkage, "finch." + key, mod.get());
helpers[key] = cmp;
IRBuilderBase::InsertPointGuard g(b);
DISubprogram *savedFn = diFn;
diFn = nullptr;
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", cmp));
b.SetCurrentDebugLocation(DebugLoc());
Value *x = cmp->getArg(0), *y = cmp->getArg(1);
Value *r;
if (et.kind == FType::Str) {
r = b.CreateTrunc(b.CreateCall(rt("finch_str_cmp"), {x, y}), b.getInt32Ty());
} else {
Value_ xv{b.CreateLoad(ET, x), et}, yv{b.CreateLoad(ET, y), et};
Value *lt = compare(BinOp::Less, xv, yv, p).v, *gt = compare(BinOp::Greater, xv, yv, p).v;
r = b.CreateSub(b.CreateZExt(gt, b.getInt32Ty()), b.CreateZExt(lt, b.getInt32Ty()));
}
b.CreateRet(r);
diFn = savedFn;
}
b.CreateCall(libc("qsort", b.getVoidTy(), {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty(), b.getPtrTy()}),
{data(), len(), elemSize(et), cmp});
return {nullptr, FType::Void};
}
failAt(p.file, p.line, p.col, "an array has no method '" + n + "' (it has: push, pop, insert, remove, clear, resize, find, contains, join, slice, reverse, sort)");
}
} // namespace finch

265
src/codegen_impl.h Normal file
View file

@ -0,0 +1,265 @@
#pragma once
// Internal to the code generator: the Codegen class, shared by codegen*.cpp and abi.cpp.
#include "codegen.h"
#include "error.h"
#include <llvm/IR/Constants.h>
#include <llvm/IR/DIBuilder.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/Verifier.h>
#include <llvm/Support/raw_ostream.h>
#include <functional>
#include <map>
#include <set>
#include <unordered_map>
using FType = ::Type; // Finch types (llvm::Type is the LLVM one)
struct StructInfo {
std::string name;
std::string module; // "" main file, a module name, or "C" for imported C structs
bool isC = false;
const StructDecl *decl = nullptr; // Finch structs
const CStruct *cdecl = nullptr; // C structs
struct F {
std::string name;
FType type;
const Expr *init = nullptr; // default value
bool hidden = false; // C union/bitfield padding: has no name in Finch
unsigned llvmIndex = 0; // C structs carry explicit padding between fields
};
std::vector<F> fields;
llvm::StructType *llvm = nullptr;
int state = 0; // 0 = not resolved, 1 = resolving (cycle check), 2 = done
bool owning = false;
std::string unsupported; // C structs Finch can't lay out
Pos pos;
};
namespace finch {
struct Value_ {
llvm::Value *v;
FType type;
bool literal = false; // a number written in the code (or a C constant): adapts to the type around it
bool fresh = false; // an owning value nobody holds yet: it must be stored (moved) or released
};
struct Var {
llvm::Value *slot;
FType type;
bool readonly = false; // loop variables
std::string readonlyWhy;
bool owned = false; // dropped when its scope ends
int order = 0; // declaration order, for defer
bool moved = false; // returned out of the function: not dropped on that path
};
struct Cleanup {
bool isDefer;
std::string var; // Drop: the variable
const Stmt *body = nullptr; // Defer: the statement
int order = 0; // Defer: variables declared after it are not visible
};
struct Scope {
std::unordered_map<std::string, Var> vars;
std::vector<Cleanup> cleanups;
};
struct Fn {
llvm::Function *llvm = nullptr;
const FnDecl *decl = nullptr;
std::string module;
std::vector<bool> borrowParam; // owning params the function never changes: no copy
llvm::Function *cThunk = nullptr;
FType ret; // resolved types
std::vector<FType> params;
};
struct Loop {
llvm::BasicBlock *continueTo, *breakTo;
size_t scopeDepth; // scopes opened inside the loop are cleaned up by break/continue
};
struct Place { // something that can be assigned to
llvm::Value *addr;
FType type;
};
struct LRef {
bool isPlace;
Place pl;
Value_ val;
};
struct ModuleScope {
std::map<std::string, Fn> fns;
std::map<std::string, StructInfo *> structs;
std::set<std::string> imports; // Finch modules visible here
};
// How a C struct travels through registers (System V x86-64).
struct AbiArg {
enum Kind { Direct, Expand, Memory } kind = Direct;
std::vector<llvm::Type *> parts; // Expand: one value per eightbyte
llvm::Type *coerced = nullptr; // Expand: { parts... } for returns
};
class Codegen {
public:
Codegen(llvm::LLVMContext &c, const std::vector<Program> &progs, const CImports &ci, llvm::TargetMachine &tm,
bool debug);
std::unique_ptr<llvm::Module> run();
// ---------- state ----------
llvm::LLVMContext &ctx;
llvm::IRBuilder<> b;
std::unique_ptr<llvm::Module> mod;
const std::vector<Program> &progs;
const CImports &cimports;
const llvm::DataLayout *dl;
std::map<std::string, ModuleScope> modules;
std::map<std::string, StructInfo *> cStructs;
std::vector<std::unique_ptr<StructInfo>> structStore;
std::vector<Scope> scopes;
std::vector<Loop> loops;
const Fn *curFn = nullptr;
std::string curModule;
int varOrder = 0;
int deferLimit = -1; // while emitting a defer: hide variables declared after it
int inDefer = 0;
std::map<std::string, llvm::Function *> helpers; // drop/copy/print per type
// debug info (-g)
bool debug;
std::unique_ptr<llvm::DIBuilder> di;
std::vector<llvm::DIFile *> diFiles;
llvm::DICompileUnit *diUnit = nullptr;
llvm::DISubprogram *diFn = nullptr;
std::map<std::string, llvm::DIType *> diTypes;
// ---------- codegen.cpp: program, statements, expressions ----------
void declareStructs();
void declareFns();
void declare(const Program &p, const FnDecl &fn);
void define(Fn &fn);
void defineMainWrapper();
void block(const BlockStmt &blk);
void blockBody(const BlockStmt &blk);
void stmt(const Stmt &s);
void varDecl(const VarDeclStmt &s);
void assign(const AssignStmt &s);
void ifStmt(const IfStmt &s);
void whileStmt(const WhileStmt &s);
void forStmt(const ForStmt &s);
void forEachStmt(const ForEachStmt &s);
void returnStmt(const ReturnStmt &s);
void jump(bool isBreak, Pos p);
llvm::Value *condition(const Expr &e);
void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd);
bool terminated();
llvm::BasicBlock *newBlock(const char *name);
void pushScope();
void popScope(); // emits the scope's cleanups if reachable
void emitCleanups(size_t downTo); // run cleanups of scopes [downTo, end) without popping
void emitScopeCleanups(size_t idx);
llvm::AllocaInst *slot(const FType &t, const std::string &name);
Var *lookup(const std::string &name);
Var &addVar(Pos p, const std::string &name, const FType &t, llvm::Value *init, bool owned, bool readonly = false,
unsigned argNo = 0);
Value_ expr(const Expr &e);
Value_ exprWant(const Expr &e, const FType &want);
Value_ binary(const BinaryExpr &e);
Value_ unary(const UnaryExpr &e);
Value_ logic(const BinaryExpr &e);
Value_ arith(BinOp op, Value_ l, Value_ r, Pos p);
Value_ arithOp(BinOp op, Value_ l, Value_ r, Pos p);
Value_ compare(BinOp op, Value_ l, Value_ r, Pos p);
bool unify(Value_ &l, Value_ &r);
[[noreturn]] void badOperands(BinOp op, const Value_ &l, const Value_ &r, Pos p);
LRef ref(const Expr &e, bool forWrite = false); // an address if `e` has one, else a value
Place place(const Expr &e, bool forWrite = false);
LRef member(const MemberExpr &m, bool forWrite);
LRef index(const IndexExpr &e, bool forWrite);
Value_ arrayLit(const ArrayLitExpr &a, const FType *want);
std::string rootVar(const Expr &e);
bool mutates(const std::string &name, const Stmt &s);
bool mutatesExpr(const std::string &name, const Expr &e);
// ---------- codegen_types.cpp: types, conversions, ownership, printing ----------
llvm::Type *ty(const FType &t);
FType resolve(const FType &t, Pos p);
FType resolveT(const FType &t, Pos p, bool deep); // deep: structs by value must be complete
void forN(llvm::Value *n, const std::function<void(llvm::Value *)> &body); // for i in 0..n
llvm::Value *tmpOf(llvm::Type *t); // an uninitialised stack slot
void resolveStruct(StructInfo *s);
StructInfo *findStruct(const std::string &module, const std::string &name, bool qualified);
bool owning(const FType &t);
llvm::Constant *zero(const FType &t);
Value_ defaultValue(const FType &t, Pos p);
static bool widens(const FType &from, const FType &to);
static bool fits(llvm::ConstantInt *c, const FType &from, const FType &to);
llvm::Value *intCast(llvm::Value *v, const FType &from, const FType &to);
llvm::Value *intToFloat(llvm::Value *v, const FType &from, const FType &to);
Value_ coerce(Value_ v, const FType &want, Pos p, const std::string &what);
llvm::Value *own(const Value_ &v); // something that can be stored: moved if fresh, else copied
void release(const Value_ &v); // drop a fresh value nobody kept
llvm::Value *copyValue(llvm::Value *v, const FType &t);
void dropValue(llvm::Value *v, const FType &t);
void dropAt(llvm::Value *addr, const FType &t);
void copyAt(llvm::Value *dst, llvm::Value *src, const FType &t);
llvm::Function *dropFn(const FType &t);
llvm::Function *copyFn(const FType &t);
std::string typeKey(const FType &t);
llvm::Value *tmp(llvm::Value *v); // a stack copy of v, to pass by address
void emitPrint(llvm::Value *v, const FType &t, bool quoted);
llvm::Function *printFn(const FType &t);
void printf_(const std::string &fmt, std::vector<llvm::Value *> args);
llvm::Value *strConst(const std::string &s);
llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C
Value_ strFromC(llvm::Value *p); // char* -> borrowed str
llvm::Value *elemSize(const FType &t);
// ---------- codegen_builtins.cpp: calls, built-ins, methods ----------
Value_ call(const CallExpr &c);
Value_ method(const MethodExpr &m);
Value_ callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shownName);
Value_ construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p);
Value_ callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p);
Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name);
Value_ print(const std::vector<ExprPtr> &args, Pos p);
Value_ addrOf(const std::vector<ExprPtr> &args, Pos p);
Value_ arrayMethod(const MethodExpr &m, Value_ *recv);
bool isBuiltin(const std::string &n);
std::string headerHint(const std::string &name);
void checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,
const std::string &name, bool variadic = false);
llvm::Value *cGlobal(const std::string &name);
llvm::Function *rt(const std::string &name); // a runtime function by name
// runtime errors
llvm::Value *fileName(Pos p);
void panicIf(llvm::Value *cond, const std::string &msg, Pos p);
void boundsCheck(llvm::Value *i, llvm::Value *len, Pos p);
void checkDivisor(const Value_ &l, const Value_ &r, Pos p);
llvm::Function *libc(const char *name, llvm::Type *ret, std::vector<llvm::Type *> params, bool vararg = false);
// ---------- abi.cpp: C calling convention for structs by value ----------
AbiArg classify(const FType &t);
llvm::Function *declareC(const CFunc &f);
Value_ emitCCall(const CFunc &f, llvm::Function *fn, std::vector<Value_> args, Pos p);
llvm::Function *cThunk(Fn &fn, Pos p);
// ---------- debug info ----------
void setLoc(Pos p);
llvm::DIType *diType(const FType &t);
};
} // namespace finch

501
src/codegen_types.cpp Normal file
View file

@ -0,0 +1,501 @@
// Code generation, part 2: types, conversions, ownership (copy/drop) and printing.
#include "codegen_impl.h"
using namespace llvm;
namespace finch {
// ---------- types ----------
llvm::Type *Codegen::ty(const FType &t) {
switch (t.kind) {
case FType::Void: return b.getVoidTy();
case FType::Bool: return b.getInt1Ty();
case FType::Char:
case FType::I8:
case FType::U8: return b.getInt8Ty();
case FType::I16:
case FType::U16: return b.getInt16Ty();
case FType::I32:
case FType::U32: return b.getInt32Ty();
case FType::I64:
case FType::U64: return b.getInt64Ty();
case FType::F32: return b.getFloatTy();
case FType::F64: return b.getDoubleTy();
case FType::Str:
case FType::Array: return StructType::get(ctx, {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty()});
case FType::Ptr:
case FType::Null: return b.getPtrTy();
case FType::Fixed: return ArrayType::get(ty(*t.elem), t.count);
case FType::Struct: return t.info->llvm;
case FType::Named: break;
}
std::fprintf(stderr, "internal compiler error: unresolved type %s\n", t.show().c_str());
std::exit(2);
}
FType Codegen::resolve(const FType &t, Pos p) { return resolveT(t, p, true); }
StructInfo *Codegen::findStruct(const std::string &module, const std::string &name, bool qualified) {
if (qualified) {
if (module == "C") {
auto it = cStructs.find(name);
return it == cStructs.end() ? nullptr : it->second;
}
auto m = modules.find(module);
if (m == modules.end()) return nullptr;
auto it = m->second.structs.find(name);
return it == m->second.structs.end() ? nullptr : it->second;
}
auto &here = modules[curModule].structs;
if (auto it = here.find(name); it != here.end()) return it->second;
if (auto it = cStructs.find(name); it != cStructs.end()) return it->second;
return nullptr;
}
FType Codegen::resolveT(const FType &t, Pos p, bool deep) {
switch (t.kind) {
case FType::Ptr:
if (!t.elem) return t;
return FType::ptrTo(resolveT(*t.elem, p, false));
case FType::Array: return FType::arrayOf(resolveT(*t.elem, p, false));
case FType::Fixed: {
FType f = t;
f.elem = std::make_shared<FType>(resolveT(*t.elem, p, deep));
return f;
}
case FType::Named: {
bool qualified = !t.module.empty();
if (qualified && t.module != "C" && !modules[curModule].imports.count(t.module))
failAt(p.file, p.line, p.col, "'" + t.module + "' is not imported here (add: import " + t.module + ")");
StructInfo *s = findStruct(t.module, t.name, qualified);
if (!s) failAt(p.file, p.line, p.col, "there is no type named '" + t.show() + "'");
FType r(FType::Struct);
r.info = s;
r.name = s->name;
r.module = s->module == "C" ? "" : s->module; // shown as math.Vec; C and main-file structs by name
if (deep) resolveStruct(s);
return r;
}
default: return t;
}
}
void Codegen::resolveStruct(StructInfo *s) {
if (s->state == 2) return;
if (s->state == 1)
failAt(s->pos.file, s->pos.line, s->pos.col,
"the struct " + s->name + " contains itself; use ptr[" + s->name + "] or []" + s->name + " for that field");
s->state = 1;
std::string savedModule = curModule;
curModule = s->isC ? "" : s->module;
std::vector<llvm::Type *> elems;
if (!s->isC) {
for (const Field &f : s->decl->fields) {
StructInfo::F nf;
nf.name = f.name;
nf.type = resolveT(f.type, f.pos, true);
if (nf.type.kind == FType::Void) failAt(f.pos.file, f.pos.line, f.pos.col, "a field can't have the type 'nothing'");
nf.init = f.init.get();
nf.llvmIndex = elems.size();
elems.push_back(ty(nf.type));
s->owning |= owning(nf.type);
s->fields.push_back(nf);
}
s->llvm->setBody(elems);
} else {
// Lay the C struct out byte for byte: explicit padding, packed, exactly C's offsets.
long long off = 0;
Pos none;
for (const CField &cf : s->cdecl->fields) {
if (cf.hidden || cf.offset < off) continue;
FType ft = resolveT(cf.type, none, true);
llvm::Type *lt = ty(ft);
long long size = dl->getTypeAllocSize(lt);
if (size != cf.size) continue; // something Finch can't mirror; the padding covers it
if (cf.offset > off) elems.push_back(ArrayType::get(b.getInt8Ty(), cf.offset - off));
StructInfo::F nf;
nf.name = cf.name;
nf.type = ft;
nf.llvmIndex = elems.size();
elems.push_back(lt);
s->fields.push_back(nf);
off = cf.offset + size;
}
if (s->cdecl->size > off) elems.push_back(ArrayType::get(b.getInt8Ty(), s->cdecl->size - off));
s->llvm->setBody(elems, true);
if (s->cdecl->hasHidden && s->unsupported.empty()) s->unsupported = "has fields Finch can't see (unions or bit fields)";
}
curModule = savedModule;
s->state = 2;
}
bool Codegen::owning(const FType &t) {
switch (t.kind) {
case FType::Str:
case FType::Array: return true;
case FType::Struct:
resolveStruct(t.info);
return t.info->owning;
default: return false;
}
}
Constant *Codegen::zero(const FType &t) { return Constant::getNullValue(ty(t)); }
Value_ Codegen::defaultValue(const FType &t, Pos p) {
if (t.kind == FType::Struct && !t.info->isC) {
for (auto &f : t.info->fields)
if (f.init) return construct(t.info, {}, {}, p);
}
return {zero(t), t, false, owning(t)};
}
// Can every value of `from` be stored in `to`? (i8 -> i32 yes, i32 -> u32 no)
bool Codegen::widens(const FType &from, const FType &to) {
if (!from.isInt() || !to.isInt()) return false;
if (from.isSigned() && to.isUnsigned()) return false;
return to.bits() > from.bits();
}
// Does the constant number `c` (of type `from`) fit into `to`?
bool Codegen::fits(ConstantInt *c, const FType &from, const FType &to) {
if (!to.isInt()) return false;
bool negative = from.isSigned() && c->isNegative();
if (negative && to.isUnsigned()) return false;
if (negative) return c->getSExtValue() >= -(1LL << (to.bits() - 1));
unsigned long long v = c->getZExtValue();
if (to.bits() == 64) return to.isUnsigned() || v <= (unsigned long long)INT64_MAX;
unsigned long long max = to.isUnsigned() ? (1ULL << to.bits()) - 1 : (1ULL << (to.bits() - 1)) - 1;
return v <= max;
}
Value *Codegen::intCast(Value *v, const FType &from, const FType &to) { return b.CreateIntCast(v, ty(to), from.isSigned()); }
Value *Codegen::intToFloat(Value *v, const FType &from, const FType &to) {
return from.isSigned() ? b.CreateSIToFP(v, ty(to)) : b.CreateUIToFP(v, ty(to));
}
// Implicit conversions: only the ones that can't surprise you.
Value_ Codegen::coerce(Value_ v, const FType &want, Pos p, const std::string &what) {
const FType &from = v.type;
if (from == want) return v;
if (from.kind == FType::Null && want.isPtr()) return {ConstantPointerNull::get(b.getPtrTy()), want};
// any pointer fits an untyped `ptr`; an untyped ptr fits any pointer
if (want.isPtr() && from.isPtr() && (!want.elem || !from.elem)) return {v.v, want};
if (from.isInt() && want.isInt()) {
auto *c = v.literal ? dyn_cast<ConstantInt>(v.v) : nullptr;
if ((c && fits(c, from, want)) || widens(from, want)) return {intCast(v.v, from, want), want, (bool)c};
if (c)
failAt(p.file, p.line, p.col, "the number " + std::to_string(from.isSigned() ? c->getSExtValue() : (long long)c->getZExtValue()) +
" doesn't fit in " + want.show());
}
if (from.isInt() && want.isFloat()) return {intToFloat(v.v, from, want), want, v.literal};
if (from.kind == FType::F32 && want.kind == FType::F64) return {b.CreateFPExt(v.v, ty(want)), want};
if (from.kind == FType::F64 && want.kind == FType::F32 && v.literal) return {b.CreateFPTrunc(v.v, ty(want)), want, true};
std::string msg = what + " must be " + want.show() + ", but this is " + from.show();
if (from.isNumber() && want.isNumber())
msg += " (use " + want.show() + "(...) to convert" + (from.isFloat() && want.isInt() ? ", it cuts off the fraction)" : ")");
else if (want.kind == FType::Str && (from.isNumber() || from.kind == FType::Bool || from.kind == FType::Char))
msg += " (turn it into text with str(...))";
else if (want.isPtr() && from.kind == FType::Str)
msg += " (use .ptr to get the text's address)";
if (from.kind == FType::Null) msg = what + " can't be null (only pointers can)";
failAt(p.file, p.line, p.col, msg);
}
// ---------- ownership ----------
Value *Codegen::tmpOf(llvm::Type *t) {
Function *f = b.GetInsertBlock()->getParent();
IRBuilder<> entry(&f->getEntryBlock(), f->getEntryBlock().begin());
AllocaInst *a = entry.CreateAlloca(t);
if (t->isStructTy()) a->setAlignment(Align(16));
return a;
}
Value *Codegen::tmp(Value *v) {
Value *a = tmpOf(v->getType());
b.CreateStore(v, a);
return a;
}
Value *Codegen::own(const Value_ &v) {
if (!owning(v.type) || v.fresh) return v.v;
return copyValue(v.v, v.type);
}
void Codegen::release(const Value_ &v) {
// constants (string literals, empty arrays) own no memory: nothing to free
if (v.fresh && v.v && !isa<Constant>(v.v) && owning(v.type)) dropValue(v.v, v.type);
}
Value *Codegen::copyValue(Value *v, const FType &t) {
if (!owning(t)) return v;
Value *out = tmpOf(ty(t));
copyAt(out, tmp(v), t);
return b.CreateLoad(ty(t), out);
}
void Codegen::dropValue(Value *v, const FType &t) {
if (owning(t)) dropAt(tmp(v), t);
}
void Codegen::dropAt(Value *addr, const FType &t) {
if (!owning(t)) return;
if (t.kind == FType::Str) b.CreateCall(rt("finch_str_drop"), {addr});
else b.CreateCall(dropFn(t), {addr});
}
void Codegen::copyAt(Value *dst, Value *src, const FType &t) {
if (!owning(t)) {
b.CreateStore(b.CreateLoad(ty(t), src), dst);
return;
}
if (t.kind == FType::Str) b.CreateCall(rt("finch_str_copy"), {dst, src});
else b.CreateCall(copyFn(t), {dst, src});
}
void Codegen::forN(Value *n, const std::function<void(Value *)> &body) {
Value *i = tmpOf(b.getInt64Ty());
b.CreateStore(b.getInt64(0), i);
BasicBlock *cond = newBlock("n.cond"), *loop = newBlock("n.body"), *end = newBlock("n.end");
b.CreateBr(cond);
b.SetInsertPoint(cond);
Value *iv = b.CreateLoad(b.getInt64Ty(), i);
b.CreateCondBr(b.CreateICmpSLT(iv, n), loop, end);
b.SetInsertPoint(loop);
body(iv);
b.CreateStore(b.CreateAdd(iv, b.getInt64(1)), i);
b.CreateBr(cond);
b.SetInsertPoint(end);
}
std::string Codegen::typeKey(const FType &t) {
switch (t.kind) {
case FType::Array: return "arr_" + typeKey(*t.elem);
case FType::Fixed: return "fix" + std::to_string(t.count) + "_" + typeKey(*t.elem);
case FType::Ptr: return "ptr";
case FType::Struct: return "S_" + (t.info->module.empty() ? std::string("main") : t.info->module) + "_" + t.info->name;
default: return t.show();
}
}
// Helper functions are generated once per type and shared; they have no source position.
struct HelperScope {
Codegen &cg;
IRBuilderBase::InsertPointGuard guard;
DISubprogram *savedFn;
HelperScope(Codegen &c, Function *f) : cg(c), guard(c.b), savedFn(c.diFn) {
c.diFn = nullptr;
c.b.SetInsertPoint(BasicBlock::Create(c.ctx, "entry", f));
c.b.SetCurrentDebugLocation(DebugLoc());
}
~HelperScope() { cg.diFn = savedFn; }
};
Function *Codegen::dropFn(const FType &t) {
std::string key = "drop." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage,
"finch." + key, mod.get());
helpers[key] = f;
HelperScope hs(*this, f);
Value *a = f->getArg(0);
if (t.kind == FType::Array) {
FType et = *t.elem;
if (owning(et)) {
Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1));
forN(len, [&](Value *i) { dropAt(b.CreateInBoundsGEP(ty(et), data, i), et); });
}
b.CreateCall(rt("finch_arr_free"), {a});
} else if (t.kind == FType::Struct) {
for (auto &fl : t.info->fields)
if (owning(fl.type)) dropAt(b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex), fl.type);
}
b.CreateRetVoid();
return f;
}
Function *Codegen::copyFn(const FType &t) {
std::string key = "copy." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getPtrTy()}, false),
Function::InternalLinkage, "finch." + key, mod.get());
helpers[key] = f;
HelperScope hs(*this, f);
Value *dst = f->getArg(0), *src = f->getArg(1);
if (t.kind == FType::Array) {
FType et = *t.elem;
b.CreateCall(rt("finch_arr_clone_raw"), {dst, src, elemSize(et)});
if (owning(et)) {
Value *d = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), dst, 0));
Value *s = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), src, 0));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), src, 1));
forN(len, [&](Value *i) {
copyAt(b.CreateInBoundsGEP(ty(et), d, i), b.CreateInBoundsGEP(ty(et), s, i), et);
});
}
} else if (t.kind == FType::Struct) {
b.CreateStore(b.CreateLoad(ty(t), src), dst);
for (auto &fl : t.info->fields)
if (owning(fl.type))
copyAt(b.CreateStructGEP(t.info->llvm, dst, fl.llvmIndex), b.CreateStructGEP(t.info->llvm, src, fl.llvmIndex), fl.type);
}
b.CreateRetVoid();
return f;
}
// ---------- strings ----------
Value *Codegen::strConst(const std::string &s) {
Constant *g = b.CreateGlobalString(s, "str");
return ConstantStruct::get(cast<StructType>(ty(FType::Str)), {g, b.getInt64(s.size()), b.getInt64(0)});
}
// str -> char* for C (an empty str may have no memory at all; C gets "" then)
Value *Codegen::cstr(Value *s) {
Value *p = b.CreateExtractValue(s, 0);
return b.CreateSelect(b.CreateIsNull(p), b.CreateGlobalString("", "empty"), p);
}
Value_ Codegen::strFromC(Value *p) {
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_from_c"), {out, p});
return {b.CreateLoad(ty(FType::Str), out), FType::Str};
}
Value *Codegen::elemSize(const FType &t) { return b.getInt64(dl->getTypeAllocSize(ty(t))); }
// ---------- printing ----------
void Codegen::printf_(const std::string &fmt, std::vector<Value *> args) {
args.insert(args.begin(), b.CreateGlobalString(fmt, "fmt"));
b.CreateCall(libc("printf", b.getInt32Ty(), {b.getPtrTy()}, true), args);
}
void Codegen::emitPrint(Value *v, const FType &t, bool quoted) {
if (t.isSigned()) printf_("%lld", {b.CreateSExt(v, b.getInt64Ty())});
else if (t.isUnsigned()) printf_("%llu", {b.CreateZExt(v, b.getInt64Ty())});
else if (t.isFloat()) printf_("%g", {b.CreateFPExt(v, b.getDoubleTy())});
else if (t.kind == FType::Char) printf_(quoted ? "'%c'" : "%c", {b.CreateZExt(v, b.getInt32Ty())});
else if (t.kind == FType::Bool) printf_("%s", {b.CreateSelect(v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))});
else if (t.kind == FType::Str)
printf_(quoted ? "\"%.*s\"" : "%.*s", {b.CreateTrunc(b.CreateExtractValue(v, 1), b.getInt32Ty()), cstr(v)});
else if (t.isPtr()) printf_("%p", {v});
else if (t.kind == FType::Null) printf_("null", {});
else b.CreateCall(printFn(t), {tmp(v)});
}
Function *Codegen::printFn(const FType &t) {
std::string key = "print." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage,
"finch." + key, mod.get());
helpers[key] = f;
HelperScope hs(*this, f);
Value *a = f->getArg(0);
if (t.kind == FType::Array || t.kind == FType::Fixed) {
FType et = *t.elem;
Value *data, *len;
if (t.kind == FType::Fixed) {
data = a;
len = b.getInt64(t.count);
} else {
data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0));
len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1));
}
printf_("[", {});
forN(len, [&](Value *i) {
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item);
b.SetInsertPoint(sep);
printf_(", ", {});
b.CreateBr(item);
b.SetInsertPoint(item);
emitPrint(b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true);
});
printf_("]", {});
} else if (t.kind == FType::Struct) {
printf_(t.info->name + "(", {});
bool first = true;
for (auto &fl : t.info->fields) {
printf_((first ? "" : ", ") + fl.name + ": ", {});
first = false;
emitPrint(b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true);
}
printf_(")", {});
}
b.CreateRetVoid();
return f;
}
// ---------- debug info ----------
DIType *Codegen::diType(const FType &t) {
std::string key = typeKey(t);
if (auto it = diTypes.find(key); it != diTypes.end()) return it->second;
DIType *d = nullptr;
auto basic = [&](const char *n, unsigned bits, unsigned enc) { return di->createBasicType(n, bits, enc); };
switch (t.kind) {
case FType::Void: return nullptr;
case FType::Bool: d = basic("bool", 8, dwarf::DW_ATE_boolean); break;
case FType::Char: d = basic("char", 8, dwarf::DW_ATE_signed_char); break;
case FType::I8: d = basic("i8", 8, dwarf::DW_ATE_signed); break;
case FType::I16: d = basic("i16", 16, dwarf::DW_ATE_signed); break;
case FType::I32: d = basic("i32", 32, dwarf::DW_ATE_signed); break;
case FType::I64: d = basic("int", 64, dwarf::DW_ATE_signed); break;
case FType::U8: d = basic("u8", 8, dwarf::DW_ATE_unsigned); break;
case FType::U16: d = basic("u16", 16, dwarf::DW_ATE_unsigned); break;
case FType::U32: d = basic("u32", 32, dwarf::DW_ATE_unsigned); break;
case FType::U64: d = basic("u64", 64, dwarf::DW_ATE_unsigned); break;
case FType::F32: d = basic("f32", 32, dwarf::DW_ATE_float); break;
case FType::F64: d = basic("float", 64, dwarf::DW_ATE_float); break;
case FType::Ptr:
case FType::Null: d = di->createPointerType(t.elem ? diType(*t.elem) : nullptr, 64, 0, std::nullopt, t.show()); break;
case FType::Fixed:
d = di->createArrayType(t.count * (uint64_t)dl->getTypeAllocSizeInBits(ty(*t.elem)), 0, diType(*t.elem),
di->getOrCreateArray({di->getOrCreateSubrange(0, t.count)}));
break;
case FType::Str:
case FType::Array: {
DIType *et = t.kind == FType::Str ? diType(FType::Char) : diType(*t.elem);
DIType *pt = di->createPointerType(et, 64);
DIType *i64 = diType(FType::I64);
DIFile *f = diFiles[0];
Metadata *els[] = {di->createMemberType(diUnit, "ptr", f, 0, 64, 64, 0, DINode::FlagZero, pt),
di->createMemberType(diUnit, "len", f, 0, 64, 64, 64, DINode::FlagZero, i64),
di->createMemberType(diUnit, "cap", f, 0, 64, 64, 128, DINode::FlagZero, i64)};
d = di->createStructType(diUnit, t.show(), f, 0, 192, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els));
break;
}
case FType::Struct: {
StructInfo *s = t.info;
DIFile *f = s->isC ? diFiles[0] : diFiles[s->pos.file];
auto *fwd = di->createReplaceableCompositeType(dwarf::DW_TAG_structure_type, s->name, diUnit, f, s->pos.line);
diTypes[key] = fwd;
const StructLayout *lay = dl->getStructLayout(s->llvm);
std::vector<Metadata *> els;
for (auto &fl : s->fields) {
uint64_t bits = dl->getTypeAllocSizeInBits(ty(fl.type));
els.push_back(di->createMemberType(diUnit, fl.name, f, s->pos.line, bits, 8, lay->getElementOffsetInBits(fl.llvmIndex),
DINode::FlagZero, diType(fl.type)));
}
DICompositeType *real = di->createStructType(diUnit, s->name, f, s->pos.line, lay->getSizeInBits(), 8,
DINode::FlagZero, nullptr, di->getOrCreateArray(els));
di->replaceTemporary(TempDIType(fwd), real);
d = real;
break;
}
case FType::Named: return nullptr;
}
diTypes[key] = d;
return d;
}
} // namespace finch

View file

@ -1,162 +0,0 @@
// One description of every built-in function and method, shared by the compiler (the index the language
// server asks for) and the language server (hover, completion, signature help).
struct Doc {
str name
str signature
str text
}
fn doc(str name, str signature, str text) -> Doc {
return Doc(name: name, signature: signature, text: text)
}
fn builtins() -> []Doc {
return [
doc("print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."),
doc("input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."),
doc("read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read, unless handled: read_file(p) or { ... }, try read_file(p)."),
doc("write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked; with or / try, the reason it didn't."),
doc("file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"),
doc("delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."),
doc("error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."),
doc("shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."),
doc("exit", "exit(int code)", "Ends the program right away with this exit code (it also ends an or { } block)."),
doc("min", "min(a, b, ...) -> number", "The smallest of the numbers, in the type they all fit."),
doc("max", "max(a, b, ...) -> number", "The biggest of the numbers, in the type they all fit."),
doc("abs", "abs(x) -> number", "x without its minus sign (any number type)."),
doc("clamp", "clamp(x, lo, hi) -> number", "x, but at least lo and at most hi."),
doc("cpu_has", "cpu_has(str feature) -> bool", "Does the processor running the program have this feature (\"avx2\", \"avx512f\", \"avxvnni\", \"dotprod\", \"i8mm\" ...)? Check before calling a @cpu(...) function."),
doc("concat", "concat(V a, V b) -> V2", "One vector with a's lanes, then b's (twice as many)."),
doc("select", "select(mask, a, b) -> V", "Lane by lane: a where the mask is true, b where it is false. The mask comes from comparing vectors (v > 0.0)."),
doc("fma", "fma(a, b, c) -> float", "a * b + c with one rounding, always (numbers or vectors). a * b + c written out is never fused: two roundings."),
doc("muladd", "muladd(a, b, c) -> float", "a * b + c the fastest way here: one fused instruction where the processor has one, else a multiply and an add."),
doc("f32_bits", "f32_bits(f32 x) -> u32", "The bits of x, unchanged (also f32xN -> u32xN). f64_bits gives a u64."),
doc("f32_from_bits", "f32_from_bits(u32 bits) -> f32", "The f32 with these bits (also u32xN -> f32xN). f64_from_bits takes a u64."),
doc("f64_bits", "f64_bits(float x) -> u64", "The bits of x, unchanged (also f64xN -> u64xN)."),
doc("f64_from_bits", "f64_from_bits(u64 bits) -> float", "The float with these bits (also u64xN -> f64xN)."),
doc("f16_to_f32", "f16_to_f32(u16 half) -> f32", "A half-precision number, kept in a u16, as an f32 (also u16xN -> f32xN)."),
doc("f32_to_f16", "f32_to_f16(f32 x) -> u16", "x as a half-precision number in a u16, rounded to the nearest (also f32xN -> u16xN)."),
doc("bf16_to_f32", "bf16_to_f32(u16 b) -> f32", "A bfloat16, kept in a u16, as an f32 (also u16xN -> f32xN)."),
doc("f32_to_bf16", "f32_to_bf16(f32 x) -> u16", "x as a bfloat16 in a u16, rounded to the nearest, ties to even (also f32xN -> u16xN)."),
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. 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."),
doc("bool", "bool(int n) -> bool", "true for any number but 0."),
doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."),
doc("f32x8", "f32x8(x) / f32x8(a, b, ...) / f32x8(list, i) / f32x8(v)", "A vector: 8 f32 side by side, worked on all at once (SIMD). Every lane x; one value per lane; 8 numbers from list[i]; or another 8-lane vector converted. The same goes for i8x16, i32x8, f64x4 and the rest.")
]
}
fn arrayMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of elements."),
doc("ptr", ".ptr -> ptr[T]", "The address of the first element, for C."),
doc("push", "push(T x)", "Adds x at the end."),
doc("pop", "pop() -> T", "Removes the last element and gives it back."),
doc("insert", "insert(int i, T x)", "Puts x at position i, moving the rest."),
doc("remove", "remove(int i) -> T", "Removes position i and gives it back."),
doc("clear", "clear()", "Removes everything."),
doc("resize", "resize(int n)", "Makes the array n long; new elements are zero."),
doc("find", "find(T x) -> int", "The index of x, or -1."),
doc("contains", "contains(T x) -> bool", "Is x in the array?"),
doc("sort", "sort()", "Sorts in place (numbers, chars, str)."),
doc("reverse", "reverse()", "Reverses in place."),
doc("slice", "slice(int start, int end) -> []T", "A new array with the elements start to end-1."),
doc("join", "join(str sep) -> str", "For []str: one text with sep between the parts.")
]
}
fn strMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of bytes."),
doc("ptr", ".ptr -> ptr[char]", "The address of the text, for C (valid while the str lives)."),
doc("sub", "sub(int start, int end) -> str", "The part from start to end-1."),
doc("find", "find(str t) -> int", "Where t starts, or -1."),
doc("contains", "contains(str t) -> bool", "Does the text contain t?"),
doc("starts_with", "starts_with(str t) -> bool", "Does the text start with t?"),
doc("ends_with", "ends_with(str t) -> bool", "Does the text end with t?"),
doc("split", "split(str sep) -> []str", "The parts between each sep; split(\"\") gives single characters."),
doc("trim", "trim() -> str", "Without spaces and newlines at both ends."),
doc("upper", "upper() -> str", "With a-z changed to A-Z."),
doc("lower", "lower() -> str", "With A-Z changed to a-z."),
doc("replace", "replace(str from, str to) -> str", "Every from replaced by to."),
doc("repeat", "repeat(int n) -> str", "The text n times."),
doc("bytes", "bytes() -> []u8", "The bytes of the text.")
]
}
fn mapMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of keys."),
doc("has", "has(K key) -> bool", "Is the key in the map?"),
doc("get", "get(K key, V default) -> V", "The value for key, or default if the key isn't there."),
doc("remove", "remove(K key) -> bool", "Removes the key and its value; true if it was there."),
doc("clear", "clear()", "Removes everything."),
doc("keys", "keys() -> []K", "The keys, in the order they were added."),
doc("values", "values() -> []V", "The values, in the order their keys were added.")
]
}
fn vecMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of lanes."),
doc("sum", "sum() -> T", "All the lanes added up (floats: in whatever order is fastest)."),
doc("min", "min() -> T", "The smallest lane."),
doc("max", "max() -> T", "The biggest lane."),
doc("dot", "dot(V w) -> T", "Lane times lane, added up: (v * w).sum()."),
doc("sqrt", "sqrt() -> V", "The square root of every lane (f32/f64 lanes)."),
doc("floor", "floor() -> V", "Every lane rounded down (f32/f64 lanes)."),
doc("ceil", "ceil() -> V", "Every lane rounded up (f32/f64 lanes)."),
doc("round", "round() -> V", "Every lane rounded to the nearest whole number, halves away from 0 (f32/f64 lanes)."),
doc("abs", "abs() -> V", "Every lane without its minus sign."),
doc("fma", "fma(V b, V c) -> V", "v * b + c with one rounding, always (f32/f64 lanes). One instruction with FMA hardware, slower without."),
doc("muladd", "muladd(V b, V c) -> V", "v * b + c the fastest way here: fused into one instruction where the processor can (f32/f64 lanes)."),
doc("store", "store([]T list, int i)", "Writes the lanes into list[i], list[i+1], ... (or through a ptr[T])."),
doc("store_part", "store_part([]T list, int i)", "Writes the lanes that fit in the list (for its last, shorter piece)."),
doc("shuffle", "shuffle(int a, int b, ...) -> V", "A vector of the lanes with these numbers (written in the code): v.shuffle(3, 2, 1, 0) reverses an f32x4."),
doc("lo", "lo() -> V", "The lower half of the lanes."),
doc("hi", "hi() -> V", "The upper half of the lanes."),
doc("lookup", "lookup(I indices) -> V", "For every lane of indices: this vector's lane (index % len). A byte-table lookup (pshufb, tbl)."),
doc("dot_add", "dot_add(a, b) -> i32xN", "On an i32xN of sums: each lane gains the sum of 4 byte products of a and b (u8/i8, 4N lanes). One instruction with AVX-VNNI, AVX-512 VNNI or ARM's dot product."),
doc("any", "any() -> bool", "A mask: is any lane true?"),
doc("all", "all() -> bool", "A mask: are all lanes true?"),
doc("count", "count() -> int", "A mask: how many lanes are true.")
]
}
fn keywords() -> []str {
return ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", "null", "import",
"link", "struct", "enum", "match", "export", "defer", "or", "try", "self", "const"]
}
fn typeNames() -> []str {
return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64",
"f32x4", "f32x8", "f32x16", "f64x2", "f64x4", "f64x8", "i8x16", "i8x32", "i8x64", "u8x16", "u8x32", "u8x64",
"i16x8", "i16x16", "i16x32", "i32x4", "i32x8", "i32x16", "u32x4", "u32x8", "u32x16", "i64x2", "i64x4", "i64x8"]
}
// the entry called `name`, or an empty Doc
fn find([]Doc list, str name) -> Doc {
for d in list {
if d.name == name {
return d
}
}
return Doc()
}
// the text of a hover for a documented built-in
fn hover(Doc d) -> str {
return "```finch\n" + d.signature + "\n```\n" + d.text
}

File diff suppressed because one or more lines are too long

40
src/error.h Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
// Every source file the compiler has read; positions point into this list.
struct SourceFile {
std::string path;
std::string text;
};
inline std::vector<SourceFile> g_files;
inline int g_curFile = 0; // the file the lexer/parser is working on
// Finch stops at the first error and shows exactly where it is.
[[noreturn]] inline void failAt(int file, int line, int col, const std::string &msg) {
const SourceFile &f = g_files.at(file);
std::fprintf(stderr, "%s:%d:%d: error: %s\n", f.path.c_str(), line, col, msg.c_str());
size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) {
start = f.text.find('\n', start);
if (start != std::string::npos) start++;
}
if (line > 0 && start != std::string::npos && start < f.text.size()) {
std::string text = f.text.substr(start, f.text.find('\n', start) - start);
std::string pad;
int c = 1; // columns count characters; skip UTF-8 continuation bytes
for (size_t k = 0; k < text.size() && c < col; k++) {
if (((unsigned char)text[k] & 0xC0) == 0x80) continue;
pad += text[k] == '\t' ? '\t' : ' ';
c++;
}
std::fprintf(stderr, "%5d | %s\n | %s^\n", line, text.c_str(), pad.c_str());
}
std::exit(1);
}
[[noreturn]] inline void fail(int line, int col, const std::string &msg) { failAt(g_curFile, line, col, msg); }

View file

@ -1,224 +0,0 @@
// finch fmt: the indentation by { }, 4 spaces a level; no spaces at line ends; at most one empty line
// in a row; one line end at the end of the file. Nothing else is moved: lines that go on inside ( or [,
// """ text and /* */ comments keep their shape, shifted along with the line they belong to.
struct Scan {
int braces // { } open at the start of the line
int parens // ( [ open
bool inLong // inside """ text
bool inBlock // inside /* */
char last // the last thing of code on the line ('"' for text): + or , means the next line goes on
}
// how deep a line's leading spaces go (a tab is 4)
fn indentOf(str line) -> int {
n := 0
for c in line {
if c == ' ' {
n += 1
} else if c == '\t' {
n += 4
} else {
break
}
}
return n
}
// how many spaces and tabs a line starts with
fn leading(str line) -> int {
i := 0
while i < line.len && (line[i] == ' ' || line[i] == '\t') {
i += 1
}
return i
}
fn trimEnd(str line) -> str {
n := line.len
while n > 0 && (line[n - 1] == ' ' || line[n - 1] == '\t' || line[n - 1] == '\r') {
n -= 1
}
return line.sub(0, n)
}
fn spaces(int n) -> str {
[]str out
for i in 0..n {
out.push(" ")
}
return out.join("")
}
fn startsWith(str line, int i, str what) -> bool {
return i + what.len <= line.len && line.sub(i, i + what.len) == what
}
// past one line: what is open at its end
fn scanLine(str line, ptr[Scan] s) {
s.last = ' '
i := 0
while i < line.len {
c := line[i]
if s.inLong {
if startsWith(line, i, "\"\"\"") {
s.inLong = false
i += 3
} else {
i += 1
}
continue
}
if s.inBlock {
if startsWith(line, i, "*/") {
s.inBlock = false
i += 2
} else {
i += 1
}
continue
}
if startsWith(line, i, "//") {
return
}
if startsWith(line, i, "/*") {
s.inBlock = true
i += 2
continue
}
if startsWith(line, i, "\"\"\"") {
s.inLong = true
i += 3
continue
}
if c == '"' {
i = pastString(line, i + 1)
s.last = '"'
continue
}
if c == '\'' {
i += 1
while i < line.len && line[i] != '\'' {
if line[i] == '\\' {
i += 1
}
i += 1
}
i += 1
s.last = '"'
continue
}
if c != ' ' && c != '\t' && c != '\r' {
s.last = c
}
if c == '{' {
s.braces += 1
} else if c == '}' {
s.braces -= 1
} else if c == '(' || c == '[' {
s.parens += 1
} else if c == ')' || c == ']' {
s.parens -= 1
}
i += 1
}
}
// past "text" (i is just after the opening quote), with \(code) that may have its own "text" in it
fn pastString(str line, int start) -> int {
i := start
while i < line.len {
c := line[i]
if c == '"' {
return i + 1
}
if c == '\\' && i + 1 < line.len && line[i + 1] == '(' {
depth := 1
i += 2
while i < line.len && depth > 0 {
if line[i] == '"' {
i = pastString(line, i + 1)
continue
}
if line[i] == '(' {
depth += 1
} else if line[i] == ')' {
depth -= 1
}
i += 1
}
continue
}
if c == '\\' {
i += 1
}
i += 1
}
return i
}
fn format(str text) -> str {
lines := text.replace("\r\n", "\n").split("\n")
[]str out
s := Scan()
stmtWas := 0 // where the line that started the statement was, and where it is now
stmtNow := 0
blank := 0
for raw in lines {
goesOn := s.last == '+' || s.last == '-' || s.last == '*' || s.last == '/' || s.last == ',' || s.last == '=' || s.last == '|' || s.last == '&' || s.last == '%'
own := !s.inLong && !s.inBlock && s.parens <= 0 && !goesOn // the line starts something of its own
if s.inLong {
// """ text as it is, moved with its statement (its closing line says how much is cut off)
out.push(shifted(raw, stmtNow - stmtWas))
scanLine(raw, addr(s))
continue
}
line := trimEnd(raw)
if line.len == 0 {
if s.inBlock {
out.push("")
} else {
blank += 1
}
continue
}
if blank > 0 && out.len > 0 {
out.push("")
}
blank = 0
body := line.sub(leading(line), line.len)
if own {
depth := s.braces
if body.starts_with("}") {
depth -= 1
}
if depth < 0 {
depth = 0
}
stmtWas = indentOf(line)
stmtNow = depth * 4
out.push(spaces(stmtNow) + body)
} else {
out.push(shifted(line, stmtNow - stmtWas))
}
scanLine(line, addr(s))
}
while out.len > 0 && out[out.len - 1].len == 0 {
out.pop()
}
return out.join("\n") + "\n"
}
// the line moved right (or left) by `by`, keeping what it has after its indentation
fn shifted(str line, int by) -> str {
n := indentOf(line)
body := line.sub(leading(line), line.len)
if body.len == 0 {
return line
}
to := n + by
if to < 0 {
to = 0
}
return spaces(to) + body
}

File diff suppressed because it is too large Load diff

View file

@ -1,98 +0,0 @@
// What differs between the systems the compiler itself runs on: how a command line is quoted and run,
// where temporary files go, making and removing files. This one is for Linux and macOS (name_windows.fch has
// the Windows one).
import "stdlib.h"
import "unistd.h"
import "clang-c/Index.h"
// libclang's description of a file that isn't on disk: its Length is C's `unsigned long`, which is 64 bits here
fn unsavedFile(str name, str contents) -> CXUnsavedFile {
return CXUnsavedFile(Filename: name.ptr, Contents: contents.ptr, Length: u64(contents.len))
}
fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'"
}
// run a command line through the shell; its exit code
fn run(str cmd) -> int {
return shell(cmd)
}
fn nullDevice() -> str {
return "/dev/null"
}
// the folder for temporary files
fn tempDir() -> str {
t := getenv("TMPDIR")
if t.len > 0 {
if t.ends_with("/") {
return t.sub(0, t.len - 1)
}
return t
}
return "/tmp"
}
fn pathSeparator() -> str {
return ":"
}
fn pid() -> int {
return int(getpid())
}
// the folder the compiler was started in
fn cwd() -> str {
return getenv("PWD")
}
// where files that are made once and kept go
fn cacheDir() -> str {
x := getenv("XDG_CACHE_HOME")
if x.len > 0 {
return x + "/finch"
}
h := getenv("HOME")
if h.len > 0 {
return h + "/.cache/finch"
}
return tempDir() + "/finch-cache"
}
// the editor protocol is bytes, not text lines
fn binaryStdio() {
}
fn makeDirs(str path) {
shell("mkdir -p " + quote(path))
}
fn remove(str path) {
delete_file(path)
}
fn removeTree(str path) {
shell("rm -rf " + quote(path))
}
// move a file over another
fn move(str from, str to) {
shell("mv " + quote(from) + " " + quote(to))
}
// the files under dir (and its folders) whose names end with ext, like ".fch"
fn listFiles(str dir, str ext) -> []str {
tmp := tempDir() + "/finch_files_" + str(pid()) + ".txt"
run("find " + quote(dir) + " -type f -name " + quote("*" + ext) + " > " + quote(tmp) + " 2> " + nullDevice())
text := read_file(tmp) or ""
remove(tmp)
[]str out
for line in text.replace("\r", "").split("\n") {
if line.ends_with(ext) {
out.push(line)
}
}
return out
}

View file

@ -1,119 +0,0 @@
// What differs between the systems the compiler itself runs on: how a command line is quoted and run,
// where temporary files go, making and removing files. This one is for Windows (host.fch is for Linux and macOS).
import "stdlib.h"
import "stdio.h"
import "process.h"
import "direct.h"
import "io.h"
import "clang-c/Index.h"
// a Windows argument: in double quotes, with the quotes inside doubled up the way CreateProcess reads them
// libclang's description of a file that isn't on disk: its Length is C's `unsigned long`, which is 32 bits on Windows
fn unsavedFile(str name, str contents) -> CXUnsavedFile {
return CXUnsavedFile(Filename: name.ptr, Contents: contents.ptr, Length: u32(contents.len))
}
fn quote(str a) -> str {
return "\"" + a.replace("\"", "\\\"") + "\""
}
// Paths use / inside the compiler; cmd.exe wants \
fn native(str path) -> str {
return path.replace("/", "\\")
}
// run a command line through cmd.exe; its exit code.
// system() runs `cmd /c <line>`, and cmd drops the first and last quote of a line that starts with one: so the line
// gets one more pair around it.
fn run(str cmd) -> int {
return shell("\"" + cmd + "\"")
}
fn nullDevice() -> str {
return "NUL"
}
fn tempDir() -> str {
for name in ["TEMP", "TMP"] {
t := getenv(name)
if t.len > 0 {
return t.replace("\\", "/")
}
}
return "."
}
fn pathSeparator() -> str {
return ";"
}
fn pid() -> int {
return int(getpid())
}
fn cwd() -> str {
here := _getcwd(null, 0) // C allocates the buffer
if here.len == 0 {
return "."
}
return here.replace("\\", "/")
}
fn cacheDir() -> str {
x := getenv("LOCALAPPDATA")
if x.len > 0 {
return x.replace("\\", "/") + "/finch"
}
return tempDir() + "/finch-cache"
}
// the editor protocol is bytes, not text lines: no \r\n translation on stdin and stdout
fn binaryStdio() {
_setmode(0, 32768) // _O_BINARY
_setmode(1, 32768)
}
fn makeDirs(str path) {
p := path.replace("\\", "/")
at := 0
for part in p.split("/") {
if at > 0 {
at += 1
}
at += part.len
if part.len == 0 || part.ends_with(":") {
continue // "" before a leading / and the drive letter
}
sofar := p.sub(0, at)
_mkdir(sofar) // fails when it is there already, which is fine
}
}
fn remove(str path) {
delete_file(path)
}
fn removeTree(str path) {
shell("\"rmdir /s /q " + quote(native(path)) + " 2> NUL\"")
}
// move a file over another
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
}

212
src/lexer.cpp Normal file
View file

@ -0,0 +1,212 @@
#include "lexer.h"
#include "error.h"
#include <cctype>
#include <unordered_map>
static const std::unordered_map<std::string, Tok> keywords = {
{"fn", Tok::Fn}, {"return", Tok::Return}, {"if", Tok::If},
{"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For},
{"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue},
{"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null},
{"import", Tok::Import}, {"link", Tok::Link}, {"struct", Tok::Struct},
{"defer", Tok::Defer},
};
std::vector<Token> lex(int file) {
g_curFile = file;
const std::string &src = g_files[file].text;
std::vector<Token> out;
size_t i = 0;
int line = 1, col = 1;
bool newline = true;
auto advance = [&]() {
if (src[i] == '\n') { line++; col = 1; newline = true; }
else if (((unsigned char)src[i] & 0xC0) != 0x80) col++; // count characters, not UTF-8 bytes
i++;
};
auto peek = [&](size_t off = 0) -> char {
return i + off < src.size() ? src[i + off] : '\0';
};
while (i < src.size()) {
char c = src[i];
if (std::isspace((unsigned char)c)) { advance(); continue; }
if (c == '/' && peek(1) == '/') {
while (i < src.size() && src[i] != '\n') advance();
continue;
}
if (c == '/' && peek(1) == '*') {
int l = line, cl = col;
advance(); advance();
while (i < src.size() && !(src[i] == '*' && peek(1) == '/')) advance();
if (i >= src.size()) fail(l, cl, "comment is never closed (missing */)");
advance(); advance();
continue;
}
Token t{Tok::End, "", line, col, newline};
newline = false;
if (std::isalpha((unsigned char)c) || c == '_') {
size_t start = i;
while (std::isalnum((unsigned char)peek()) || peek() == '_') advance();
t.text = src.substr(start, i - start);
auto kw = keywords.find(t.text);
t.kind = kw != keywords.end() ? kw->second : Tok::Ident;
} else if (std::isdigit((unsigned char)c)) {
size_t start = i;
t.kind = Tok::Int;
if (c == '0' && (peek(1) == 'x' || peek(1) == 'X')) { // 0xFF
advance(); advance();
while (std::isxdigit((unsigned char)peek()) || peek() == '_') advance();
if (i - start == 2) fail(t.line, t.col, "'0x' needs hex digits after it, like 0xFF");
} else {
while (std::isdigit((unsigned char)peek()) || peek() == '_') advance();
// "1..5" is a range, not the float "1."
if (peek() == '.' && std::isdigit((unsigned char)peek(1))) {
t.kind = Tok::Float;
advance();
while (std::isdigit((unsigned char)peek()) || peek() == '_') advance();
}
}
for (size_t k = start; k < i; k++)
if (src[k] != '_') t.text += src[k];
} else if (c == '"' || c == '\'') {
char quote = c;
t.kind = quote == '"' ? Tok::String : Tok::Char;
advance();
while (peek() != quote) {
if (peek() == '\0' || peek() == '\n')
fail(t.line, t.col, quote == '"' ? "string is never closed (missing \")"
: "char is never closed (missing ')");
char ch = peek();
advance();
if (ch == '\\') {
char e = peek();
advance();
switch (e) {
case 'n': ch = '\n'; break;
case 't': ch = '\t'; break;
case 'r': ch = '\r'; break;
case '0': ch = '\0'; break;
case '\\': ch = '\\'; break;
case '"': ch = '"'; break;
case '\'': ch = '\''; break;
default: fail(line, col - 1, std::string("unknown escape \\") + e);
}
}
t.text += ch;
}
advance();
if (t.kind == Tok::Char && t.text.size() != 1)
fail(t.line, t.col, "a char holds exactly one character, like 'a'");
} else {
char n = peek(1);
auto two = [&](Tok k) { t.kind = k; advance(); advance(); };
auto one = [&](Tok k) { t.kind = k; advance(); };
switch (c) {
case '(': one(Tok::LParen); break;
case ')': one(Tok::RParen); break;
case '{': one(Tok::LBrace); break;
case '}': one(Tok::RBrace); break;
case '[': one(Tok::LBracket); break;
case ']': one(Tok::RBracket); break;
case ',': one(Tok::Comma); break;
case '%': n == '=' ? two(Tok::PercentEq) : one(Tok::Percent); break;
case '^': one(Tok::Caret); break;
case '~': one(Tok::Tilde); break;
case '.': n == '.' ? two(Tok::DotDot) : one(Tok::Dot); break;
case '+': n == '=' ? two(Tok::PlusEq) : one(Tok::Plus); break;
case '-': n == '=' ? two(Tok::MinusEq) : n == '>' ? two(Tok::Arrow) : one(Tok::Minus); break;
case '*': n == '=' ? two(Tok::StarEq) : one(Tok::Star); break;
case '/': n == '=' ? two(Tok::SlashEq) : one(Tok::Slash); break;
case '=': n == '=' ? two(Tok::Eq) : one(Tok::Assign); break;
case '!': n == '=' ? two(Tok::NotEq) : one(Tok::Not); break;
case '<': n == '=' ? two(Tok::LessEq) : n == '<' ? two(Tok::Shl) : one(Tok::Less); break;
case '>': n == '=' ? two(Tok::GreaterEq) : n == '>' ? two(Tok::Shr) : one(Tok::Greater); break;
case ':': n == '=' ? two(Tok::Declare) : one(Tok::Colon); break;
case '&': n == '&' ? two(Tok::And) : one(Tok::Amp); break;
case '|': n == '|' ? two(Tok::Or) : one(Tok::Pipe); break;
default: {
// show the whole UTF-8 character, not just its first byte
size_t len = (c & 0xE0) == 0xC0 ? 2 : (c & 0xF0) == 0xE0 ? 3 : (c & 0xF8) == 0xF0 ? 4 : 1;
std::string ch = src.substr(i, len);
std::string hint = (unsigned char)c >= 0x80 ? " (names use only a-z, A-Z, 0-9 and _; other letters only go inside \"text\")" : "";
fail(line, col, "unexpected character '" + ch + "'" + hint);
}
}
}
out.push_back(t);
}
out.push_back({Tok::End, "", line, col, true});
return out;
}
const char *tokName(Tok t) {
switch (t) {
case Tok::End: return "end of file";
case Tok::Ident: return "a name";
case Tok::Int: return "a number";
case Tok::Float: return "a number";
case Tok::String: return "a string";
case Tok::Char: return "a char";
case Tok::Fn: return "'fn'";
case Tok::Return: return "'return'";
case Tok::If: return "'if'";
case Tok::Else: return "'else'";
case Tok::While: return "'while'";
case Tok::For: return "'for'";
case Tok::In: return "'in'";
case Tok::Break: return "'break'";
case Tok::Continue: return "'continue'";
case Tok::True: return "'true'";
case Tok::False: return "'false'";
case Tok::Null: return "'null'";
case Tok::Import: return "'import'";
case Tok::Link: return "'link'";
case Tok::Struct: return "'struct'";
case Tok::Defer: return "'defer'";
case Tok::Colon: return "':'";
case Tok::LParen: return "'('";
case Tok::RParen: return "')'";
case Tok::LBrace: return "'{'";
case Tok::RBrace: return "'}'";
case Tok::LBracket: return "'['";
case Tok::RBracket: return "']'";
case Tok::Comma: return "','";
case Tok::Dot: return "'.'";
case Tok::Arrow: return "'->'";
case Tok::DotDot: return "'..'";
case Tok::Plus: return "'+'";
case Tok::Minus: return "'-'";
case Tok::Star: return "'*'";
case Tok::Slash: return "'/'";
case Tok::Percent: return "'%'";
case Tok::Amp: return "'&'";
case Tok::Pipe: return "'|'";
case Tok::Caret: return "'^'";
case Tok::Tilde: return "'~'";
case Tok::Shl: return "'<<'";
case Tok::Shr: return "'>>'";
case Tok::PercentEq: return "'%='";
case Tok::Assign: return "'='";
case Tok::Declare: return "':='";
case Tok::PlusEq: return "'+='";
case Tok::MinusEq: return "'-='";
case Tok::StarEq: return "'*='";
case Tok::SlashEq: return "'/='";
case Tok::Eq: return "'=='";
case Tok::NotEq: return "'!='";
case Tok::Less: return "'<'";
case Tok::LessEq: return "'<='";
case Tok::Greater: return "'>'";
case Tok::GreaterEq: return "'>='";
case Tok::And: return "'&&'";
case Tok::Or: return "'||'";
case Tok::Not: return "'!'";
}
return "?";
}

View file

@ -1,470 +0,0 @@
// The Finch lexer, written in Finch.
// A token's kind is a word: "ident", "int", "float", "string", "char", "eof",
// or the keyword / operator itself ("fn", "{", "+=", ...).
struct Token {
str kind
str text
int line
int col
bool nl // a newline came before this token
}
fn isAlpha(char c) -> bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}
fn isDigit(char c) -> bool {
return c >= '0' && c <= '9'
}
fn isHex(char c) -> bool {
return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
fn isKeyword(str w) -> bool {
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false",
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export", "const"]
return keywords.contains(w)
}
struct Lexer {
str src
str path
int i
int line
int col
bool nl
}
fn fail(ptr[Lexer] lx, int line, int col, str msg) {
print(lx.path + ":" + str(line) + ":" + str(col) + ": error: " + msg)
exit(1)
}
fn peek(ptr[Lexer] lx, int off) -> char {
if lx.i + off < lx.src.len {
return lx.src[lx.i + off]
}
return char(0)
}
fn advance(ptr[Lexer] lx) {
c := lx.src[lx.i]
if c == '\n' {
lx.line += 1
lx.col = 1
lx.nl = true
} else if int(c) & 0xC0 != 0x80 {
lx.col += 1
}
lx.i += 1
}
// operators, longest first
fn operator(ptr[Lexer] lx) -> str {
twos := ["->", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "..", ":=", "+=", "-=", "*=", "/=", "%="]
if lx.i + 1 < lx.src.len {
two := lx.src.sub(lx.i, lx.i + 2)
if twos.contains(two) {
return two
}
}
ones := "(){}[],.:+-*/%&|^~<>=!@"
c := lx.src.sub(lx.i, lx.i + 1)
if ones.contains(c) {
return c
}
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'
}
if e == 't' {
return '\t'
}
if e == 'r' {
return '\r'
}
if e == '0' {
return char(0)
}
if e == '\\' || e == '"' || e == '\'' {
return e
}
fail(lx, lx.line, lx.col - 1, "unknown escape \\" + str(e))
return e
}
fn lex(str src, str path) -> []Token {
return lexFrom(src, path, 1, 1)
}
// `src` starts at this line and column of the file (the code inside a "\(...)")
fn lexFrom(str src, str path, int firstLine, int firstCol) -> []Token {
lexer := Lexer(src: src, path: path, i: 0, line: firstLine, col: firstCol, nl: true)
lx := addr(lexer)
[]Token out
while lx.i < lx.src.len {
c := lx.src[lx.i]
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
advance(lx)
continue
}
if c == '/' && peek(lx, 1) == '/' {
while lx.i < lx.src.len && lx.src[lx.i] != '\n' {
advance(lx)
}
continue
}
if c == '/' && peek(lx, 1) == '*' {
line := lx.line
col := lx.col
advance(lx)
advance(lx)
while lx.i < lx.src.len && !(lx.src[lx.i] == '*' && peek(lx, 1) == '/') {
advance(lx)
}
if lx.i >= lx.src.len {
fail(lx, line, col, "comment is never closed (missing */)")
}
advance(lx)
advance(lx)
continue
}
t := Token(kind: "", text: "", line: lx.line, col: lx.col, nl: lx.nl)
lx.nl = false
start := lx.i
if isAlpha(c) {
while lx.i < lx.src.len && (isAlpha(lx.src[lx.i]) || isDigit(lx.src[lx.i])) {
advance(lx)
}
t.text = lx.src.sub(start, lx.i)
t.kind = "ident"
if isKeyword(t.text) {
t.kind = t.text
}
} 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)
while lx.i < lx.src.len && (isHex(lx.src[lx.i]) || lx.src[lx.i] == '_') {
advance(lx)
}
} else {
while lx.i < lx.src.len && (isDigit(lx.src[lx.i]) || lx.src[lx.i] == '_') {
advance(lx)
}
if peek(lx, 0) == '.' && isDigit(peek(lx, 1)) {
t.kind = "float"
advance(lx)
while lx.i < lx.src.len && (isDigit(lx.src[lx.i]) || lx.src[lx.i] == '_') {
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"
if quote == '\'' {
t.kind = "char"
}
advance(lx)
[]str parts
while peek(lx, 0) != quote {
if peek(lx, 0) == char(0) || peek(lx, 0) == '\n' {
if quote == '"' {
fail(lx, t.line, t.col, "string is never closed (missing \")")
}
fail(lx, t.line, t.col, "char is never closed (missing ')")
}
ch := peek(lx, 0)
advance(lx)
if ch == '\\' {
e := peek(lx, 0)
advance(lx)
ch = escape(lx, e)
}
parts.push(str(ch))
}
advance(lx)
t.text = parts.join("")
if quote == '\'' && t.text.len != 1 {
fail(lx, t.line, t.col, "a char holds exactly one character, like 'a'")
}
} else {
op := operator(lx)
if op.len == 0 {
// show the whole UTF-8 character, not just its first byte
n := 1
if (int(c) & 0xE0) == 0xC0 {
n = 2
} else if (int(c) & 0xF0) == 0xE0 {
n = 3
} else if (int(c) & 0xF8) == 0xF0 {
n = 4
}
hint := ""
if int(c) >= 0x80 {
hint = " (names use only a-z, A-Z, 0-9 and _; other letters only go inside \"text\")"
}
fail(lx, t.line, t.col, "unexpected character '" + lx.src.sub(lx.i, lx.i + n) + "'" + hint)
}
for k in 0..op.len {
advance(lx)
}
t.kind = op
t.text = op
}
out.push(t)
}
out.push(Token(kind: "eof", text: "", line: lx.line, col: lx.col, nl: true))
return out
}

35
src/lexer.h Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <vector>
enum class Tok {
End,
Ident, Int, Float, String, Char,
// keywords
Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer,
// punctuation
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow,
// operators
Plus, Minus, Star, Slash, Percent,
Amp, Pipe, Caret, Tilde, Shl, Shr, // & | ^ ~ << >>
Assign, Declare, // = :=
PlusEq, MinusEq, StarEq, SlashEq, PercentEq, // += -= *= /= %=
Eq, NotEq, Less, LessEq, Greater, GreaterEq,
And, Or, Not,
};
struct Token {
Tok kind;
std::string text;
int line;
int col;
bool newlineBefore; // token starts a new line: ends the previous statement
};
// Lexes g_files[file].text.
std::vector<Token> lex(int file);
const char *tokName(Tok t);

View file

@ -1,548 +0,0 @@
// From LLVM IR to a program: compile the IR, add the runtime (compiled once and kept), link the C libraries
// and C files the program asks for, and turn the linker's complaints into something a person can act on.
import platform
import cimport
import host
// 32-bit FNV-1a, as hex: names the cached runtime object after what it was built from
fn fingerprint(str text) -> str {
u32 h = 2166136261
for c in text {
h = (h ^ u32(c)) * 16777619
}
digits := "0123456789abcdef"
[]str parts
for k in 0..8 {
parts.push(str(digits[int((h >> u32(28 - 4 * k)) & 15)]))
}
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") {
return "-framework " + quote(name.sub(0, name.len - 10)) // macOS: link "Cocoa.framework"
}
if !tg.cross {
tmp := host.tempDir() + "/finch_pkg_flags_" + str(host.pid()) + ".txt"
code := host.run("pkg-config --libs " + quote(name) + " > " + quote(tmp) + " 2> " + host.nullDevice())
if code == 0 && file_exists(tmp) {
flags := (read_file(tmp) or "").trim()
host.remove(tmp)
if flags.len > 0 {
return flags
}
}
}
return quote("-l" + name)
}
// "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name
fn guessLib(str header) -> str {
name := header
slash := name.find("/")
while slash >= 0 {
name = name.sub(slash + 1, name.len)
slash = name.find("/")
}
dot := name.len
for i in 0..name.len {
if name[i] == '.' {
dot = i
}
}
name = name.sub(0, dot)
while name.len > 0 && name[name.len - 1] >= '0' && name[name.len - 1] <= '9' {
name = name.sub(0, name.len - 1)
}
return name.lower()
}
struct Names {
[]str items
}
fn Names.add(str x) {
if x.len > 0 && !self.items.contains(x) {
self.items.push(x)
}
}
// the text after `marker` up to the next one of `stops`
fn between(str line, str marker, str stops) -> str {
a := line.find(marker)
if a < 0 {
return ""
}
rest := line.sub(a + marker.len, line.len)
n := 0
while n < rest.len && !stops.contains(str(rest[n])) {
n += 1
}
return rest.sub(0, n)
}
// The linker speaks in riddles; say what is missing and what to write.
fn linkFailed(str output, cimport.CImports ci, platform.Target tg) -> int {
missing := Names()
libs := Names()
for line in output.split("\n") {
l := line.trim()
if l.contains("undefined reference to `") {
missing.add(between(l, "undefined reference to `", "'"))
} else if l.contains("undefined symbol: ") {
missing.add(between(l, "undefined symbol: ", " \n"))
} else if l.contains("unresolved external symbol ") {
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 '") {
libs.add(between(l, "ld: library '", "'"))
} else if l.contains("framework '") && l.contains("not found") {
libs.add(between(l, "framework '", "'") + ".framework")
} else if l.starts_with("\"") && l.contains("\", referenced from:") {
// Apple ld: "_name", referenced from:
name := l.sub(1, l.find("\", referenced from:"))
if tg.darwin && name.starts_with("_") {
name = name.sub(1, name.len)
}
missing.add(name)
}
}
if missing.items.len == 0 && libs.items.len == 0 {
print(output + "error: linking failed")
return 1
}
for lib in libs.items {
print("error: the library '" + lib + "' wasn't found. Is it installed? (on Nix: add it to shell.nix)")
}
// the missing functions, grouped by the header they came from
[]str headers
[]Names byHeader
for name in missing.items {
header := ""
if ci.fns.has(name) {
header = ci.fns[name].header
}
k := headers.find(header)
if k < 0 {
headers.push(header)
byHeader.push(Names())
k = headers.len - 1
}
byHeader[k].add(name)
}
for k in 0..headers.len {
names := byHeader[k].items
[]str shown
for i in 0..names.len {
if i < 3 {
shown.push(names[i])
}
}
list := shown.join(", ")
if names.len > 3 {
list = list + " and " + str(names.len - 3) + " more"
}
if headers[k].len == 0 {
print("error: no code found for " + list)
continue
}
verb := "come"
if names.len == 1 {
verb = "comes"
}
print("error: " + list + " " + verb + " from \"" + headers[k] + "\", but its library isn't linked")
print(" the header only says the functions exist; their code lives in a library.")
print(" add this at the top of your file (with the library's real name):")
print("")
print(" link \"" + guessLib(headers[k]) + "\"")
print("")
}
return 1
}
// The runtime, compiled once per system and compiler and kept in the cache folder.
fn runtimeObject(str rt, platform.Target tg, str cc) -> str! {
source := try read_file(rt)
dir := host.cacheDir()
obj := dir + "/rt-" + fingerprint(source + cc + tg.triple + "2.5") + ".o"
if file_exists(obj) {
return obj
}
host.makeDirs(dir)
part := obj + ".part" + str(host.pid())
pic := " -fPIC"
if tg.windows {
pic = ""
}
log := part + ".log"
code := host.run(cc + " -O2" + pic + " -c " + quote(rt) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
if code != 0 {
text := read_file(log) or ""
host.remove(part)
host.remove(log)
return error(text + "error: couldn't build the Finch runtime with '" + cc + "' (is a C compiler installed?)")
}
host.remove(log)
host.move(part, obj)
return obj
}
struct Build {
str out // the program
str ir // its LLVM IR, as text
str rt // the runtime's source file
str opt // -O2 or -O0
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.
fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
clang := clangCmd()
// the C compiler of the runtime and the linker: the target's own when building for another system
cc := clang
if tg.cross {
cc = tg.cc
} else if getenv("FINCH_CC").len > 0 {
cc = getenv("FINCH_CC")
}
rtobj := runtimeObject(b.rt, tg, cc) or {
print(err)
return 1
}
target := ""
if tg.cross || tg.windows {
target = " --target=" + tg.triple
}
log := b.out + ".log"
// -ffp-contract=on: only what asks to be fused is (muladd); a * b + c stays two roundings, whatever clang's default
obj := irObject(clang, target + " " + b.opt + " -ffp-contract=on" + b.extra, b.ir, b.name + "\n" + tg.triple, log) or {
print(err)
host.remove(log)
return 1
}
// with C++ files the C++ compiler links, so its library comes along
linkWith := cc
for l in b.links {
if isCpp(l) {
linkWith = cxxFor(cc)
}
}
cmd := linkWith + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
// static inline C functions the program calls: a C file that keeps a pointer to each
inl := inlineFile(b.ir, ci)
if inl.len > 0 {
pic := " -fPIC"
if tg.windows {
pic = ""
}
o := cObject(inl, cc, " -O2" + pic + b.cflags, tg, log) or {
print(err)
host.remove(log)
return 1
}
cmd = cmd + " " + quote(o)
}
if !tg.msvc {
cmd = cmd + " -lm"
}
if tg.android {
cmd = cmd + " -llog" // bionic has threads built in
} else if !tg.windows && !tg.darwin {
cmd = cmd + " -lpthread" // std/thread (in glibc since 2.34, needed before)
}
if b.ldflags.len > 0 {
cmd = cmd + b.ldflags // finch.toml: libdirs and ldflags
}
if getenv("FINCH_LDFLAGS").len > 0 {
cmd = cmd + " " + getenv("FINCH_LDFLAGS") // more flags for the linker, like -L/where/libclang/is
}
if b.shared {
if tg.darwin {
cmd = cmd + " -dynamiclib -undefined dynamic_lookup" // like a .so: the host's functions are found when it is loaded
} else {
cmd = cmd + " -shared"
}
}
for l in b.links {
if l.ends_with(".c") || isCpp(l) {
// the program's own C or C++ file, built along with it (and kept in the cache until it changes)
pic := " -fPIC"
if tg.windows {
pic = ""
}
compiler := cc
if isCpp(l) {
compiler = cxxFor(cc)
}
o := cObject(l, compiler, " -O2" + pic + b.cflags, tg, log) or {
print(err)
host.remove(log)
return 1
}
cmd = cmd + " " + quote(o)
} else if l.ends_with(".o") || l.ends_with(".a") || l.ends_with(".lib") {
cmd = cmd + " " + quote(l)
} else {
cmd = cmd + " " + libFlags(l, tg)
}
}
status := host.run(cmd + " > " + quote(log) + " 2>&1")
output := read_file(log) or ""
host.remove(log)
if status == 127 {
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")
return 1
}
if status != 0 {
return linkFailed(output, ci, tg)
}
if output.len > 0 {
print(output) // warnings, if any
}
return 0
}
// clang, as the program is compiled with it. Nix's clang wrapper removes -march=native when NIX_ENFORCE_NO_NATIVE
// is 1, as it is in nix-shell; --cpu native asks for it on purpose, so the wrapper is told to keep it.
fn clangCmd() -> str {
clang := getenv("FINCH_CLANG")
if clang.len == 0 {
clang = "clang"
}
if host.pathSeparator() != ";" {
return "NIX_ENFORCE_NO_NATIVE=0 " + clang
}
return clang
}
// -march=x86-64-v3 or -mcpu=apple-m1: how clang is told the processor
fn cpuFlag(platform.Target tg, str cpu) -> str {
if cpu.len == 0 {
return ""
}
if tg.arm64 {
return " -mcpu=" + quote(cpu)
}
return " -march=" + quote(cpu)
}
// What the processor the program is for can do, as Finch's feature names (avx2, avxvnni, i8mm ...): clang says it in
// the macros it predefines for that target and --cpu. Kept in the cache, one file per clang, target and cpu.
fn cpuFeatures(platform.Target tg, str cpu) -> []str {
clang := clangCmd()
flags := cpuFlag(tg, cpu)
if tg.cross || tg.windows {
flags = " --target=" + tg.triple + flags
}
dir := host.cacheDir() + "/cpu"
host.makeDirs(dir)
slot := dir + "/" + fingerprint2(clang + flags)
text := read_file(slot + ".txt") or ""
if text.len == 0 {
empty := slot + ".part" + str(host.pid()) + ".c"
out := slot + ".part" + str(host.pid()) + ".txt"
write_file(empty, "")
host.run(clang + flags + " -dM -E " + quote(empty) + " > " + quote(out) + " 2> " + host.nullDevice())
host.remove(empty)
text = read_file(out) or ""
if text.len == 0 {
host.remove(out)
[]str none
return none
}
host.move(out, slot + ".txt")
}
macros := ["__SSE3__", "__SSSE3__", "__SSE4_1__", "__SSE4_2__", "__POPCNT__", "__AVX__", "__AVX2__", "__FMA__", "__F16C__",
"__BMI__", "__BMI2__", "__AVX512F__", "__AVX512BW__", "__AVX512DQ__", "__AVX512VL__", "__AVX512VNNI__",
"__AVX512BF16__", "__AVX512FP16__", "__AVXVNNI__", "__ARM_NEON", "__ARM_FEATURE_FP16_VECTOR_ARITHMETIC",
"__ARM_FEATURE_DOTPROD", "__ARM_FEATURE_MATMUL_INT8", "__ARM_FEATURE_BF16_VECTOR_ARITHMETIC", "__ARM_FEATURE_SVE",
"__ARM_FEATURE_SVE2"]
names := ["sse3", "ssse3", "sse4.1", "sse4.2", "popcnt", "avx", "avx2", "fma", "f16c", "bmi1", "bmi2", "avx512f", "avx512bw",
"avx512dq", "avx512vl", "avx512vnni", "avx512bf16", "avx512fp16", "avxvnni", "neon", "fp16", "dotprod", "i8mm", "bf16",
"sve", "sve2"]
[]str have
for line in text.split("\n") {
parts := line.split(" ")
if parts.len >= 2 && parts[0] == "#define" {
k := macros.find(parts[1])
if k >= 0 && !have.contains(names[k]) {
have.push(names[k])
}
}
}
return have
}

View file

@ -1,948 +0,0 @@
// The language server: `finch lsp`. An editor talks to it over stdin and stdout (JSON-RPC, as the Language Server
// Protocol says) and gets errors while it types, hover, go to definition, completion, an outline and signature
// help.
//
// It asks the compiler instead of being one: for every change it runs `finch index` on the text the editor has
// (see the index records in gen.fch), and answers from what comes back.
import json
import host
import docs
import ast
import fmt
import "stdio.h"
import "stdlib.h"
// ---------- what one run of the compiler told ----------
struct Ref {
str file
int line
int col
int len
str defFile
int defLine
int defCol
str hover
}
struct VarRec {
str name
str type
str file
int line
int col
str fnFile
int fnLine
int fnEnd
}
struct FnRec {
str module
str name
str recv
str file
int line
int col
int endLine
str signature
[]str params
}
struct StructRec {
str module
str name
str file
int line
int col
[]str fields
[]str types
}
struct EnumRec {
str module
str name
str file
int line
int col
[]str variants
}
struct Diag {
bool has
str file
int line
int col
str message
}
struct Analysis {
[]Ref refs
[]VarRec vars
[]FnRec fns
[]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 {
str self // the compiler's file: what to run for each check
str tmp // a folder for the copies of the editor's text
map[str]str docs // path -> the text the editor has
map[str]Analysis results // path -> the last check
map[str]Analysis good // path -> the last check that found no error
[]str diagnosed // the files an error was shown for
bool stopping
}
// ---------- paths and text ----------
fn hexval(char c) -> int {
if c >= '0' && c <= '9' {
return int(c) - int('0')
}
if c >= 'a' && c <= 'f' {
return int(c) - int('a') + 10
}
if c >= 'A' && c <= 'F' {
return int(c) - int('A') + 10
}
return -1
}
fn uriToPath(str uri) -> str {
s := uri
if s.starts_with("file://") {
s = s.sub(7, s.len)
}
[]u8 out
i := 0
while i < s.len {
if s[i] == '%' && i + 2 < s.len + 0 && hexval(s[i + 1]) >= 0 && hexval(s[i + 2]) >= 0 {
out.push(u8(hexval(s[i + 1]) * 16 + hexval(s[i + 2])))
i += 3
} else {
out.push(u8(s[i]))
i += 1
}
}
p := str(out)
if p.len > 2 && p[0] == '/' && p[2] == ':' {
p = p.sub(1, p.len) // Windows: file:///C:/x -> C:/x
}
return p
}
fn pathToUri(str path) -> str {
digits := "0123456789ABCDEF"
[]str parts = ["file://"]
p := path.replace("\\", "/")
if p.len > 1 && p[1] == ':' {
parts.push("/" + p.sub(0, 2)) // Windows: C:/x -> file:///C:/x
p = p.sub(2, p.len)
}
for c in p {
n := int(c)
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "/-_.~".contains(str(c)) {
parts.push(str(c))
} else {
parts.push("%" + str(digits[(n / 16) % 16]) + str(digits[n % 16]))
}
}
return parts.join("")
}
// "\n" written as two characters, back to a line break
fn unesc(str s) -> str {
[]str parts
i := 0
while i < s.len {
if s[i] == '\\' && i + 1 < s.len {
if s[i + 1] == 'n' {
parts.push("\n")
} else {
parts.push(str(s[i + 1]))
}
i += 2
} else {
parts.push(str(s[i]))
i += 1
}
}
return parts.join("")
}
fn lineOf(str text, int line) -> str {
lines := text.split("\n")
if line >= 1 && line <= lines.len {
return lines[line - 1]
}
return ""
}
// ---------- the protocol ----------
fn readLine() -> str {
[]u8 bytes
while true {
c := getchar()
if c < 0 {
return ""
}
if c == 10 {
break
}
bytes.push(u8(c))
}
line := str(bytes)
if line.ends_with("\r") {
line = line.sub(0, line.len - 1)
}
return line
}
// the next message; an error at the end of the input
fn readMessage() -> json.Json! {
length := 0
while true {
header := readLine()
if header.len == 0 {
if length > 0 {
break
}
if feof(stdin) != 0 {
return error("the editor closed the connection")
}
continue
}
if header.starts_with("Content-Length:") {
length = int(header.sub(15, header.len).trim()) or 0
}
}
[]u8 body
for i in 0..length {
c := getchar()
if c < 0 {
return error("the message was cut short")
}
body.push(u8(c))
}
return try json.parse(str(body))
}
fn send(json.Json v) {
body := json.stringify(v)
head := "Content-Length: " + str(body.len) + "\r\n\r\n"
text := head + body
fwrite(text.ptr, 1, u64(text.len), stdout)
fflush(stdout)
}
fn reply(json.Json id, json.Json result) {
m := json.new_object()
m.set("jsonrpc", json.new_text("2.0"))
m.set("id", id)
m.set("result", result)
send(m)
}
fn notify(str method, json.Json params) {
m := json.new_object()
m.set("jsonrpc", json.new_text("2.0"))
m.set("method", json.new_text(method))
m.set("params", params)
send(m)
}
fn num(int n) -> json.Json {
return json.new_number(float(n))
}
fn position(int line, int col) -> json.Json {
p := json.new_object()
p.set("line", num(line))
p.set("character", num(col))
return p
}
fn range(int line, int col, int endCol) -> json.Json {
r := json.new_object()
r.set("start", position(line, col))
r.set("end", position(line, endCol))
return r
}
// ---------- asking the compiler ----------
fn splitTabs(str line) -> []str {
return line.split("\t")
}
fn intOf(str s) -> int {
return int(s) or 0
}
// "path:line:col: error: message" -> a diagnostic
fn parseDiag(str line) -> Diag {
at := line.find(": error: ")
if at < 0 {
return Diag()
}
where := line.sub(0, at)
message := line.sub(at + 9, line.len)
// the last two colon-separated numbers are the line and column
parts := where.split(":")
if parts.len < 3 {
return Diag()
}
col := intOf(parts[parts.len - 1])
ln := intOf(parts[parts.len - 2])
[]str pathParts
for i in 0..parts.len - 2 {
pathParts.push(parts[i])
}
return Diag(has: true, file: pathParts.join(":"), line: ln, col: col, message: message)
}
fn parseOutput(str output) -> Analysis {
a := Analysis()
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]
if tag == "@R" && f.len >= 9 {
a.refs.push(Ref(file: f[1], line: intOf(f[2]), col: intOf(f[3]), len: intOf(f[4]), defFile: f[5], defLine: intOf(f[6]), defCol: intOf(f[7]), hover: unesc(f[8])))
} else if tag == "@V" && f.len >= 9 {
a.vars.push(VarRec(name: f[1], type: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5]), fnFile: f[6], fnLine: intOf(f[7]), fnEnd: intOf(f[8])))
} else if tag == "@F" && f.len >= 10 {
a.fns.push(FnRec(module: f[1], name: f[2], recv: f[3], file: f[4], line: intOf(f[5]), col: intOf(f[6]), endLine: intOf(f[7]), signature: unesc(f[8]), params: f[9].split("|")))
} else if tag == "@S" && f.len >= 6 {
a.structs.push(StructRec(module: f[1], name: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5])))
} else if tag == "@SF" && f.len >= 5 && a.structs.len > 0 {
k := a.structs.len - 1
a.structs[k].fields.push(f[3])
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)
}
}
return a
}
fn Server.analyse(str path) {
// the editor's text, as files the compiler can read: it asks for the file by its real name
host.makeDirs(self.tmp)
overlays := ""
n := 0
for p in self.docs.keys() {
copy := self.tmp + "/doc" + str(n) + ".fch"
write_file(copy, self.docs[p])
overlays = overlays + " --overlay " + host.quote(p + "=" + copy)
n += 1
}
out := self.tmp + "/out.txt"
host.run(host.quote(self.self) + " index " + host.quote(path) + overlays + " > " + host.quote(out) + " 2>&1")
a := parseOutput(read_file(out) or "")
if !a.diag.has {
self.good[path] = a
} else if self.good.has(path) {
// half-typed code doesn't compile: completion and signatures keep using what the last good version knew
g := self.good[path]
a.fns = g.fns
a.structs = g.structs
a.enums = g.enums
a.vars = g.vars
if a.imports.len == 0 {
a.imports = g.imports
a.importKeys = g.importKeys
}
}
self.results[path] = a
self.publish(path, a)
}
fn Server.publish(str path, Analysis a) {
// the files to say something about: the one checked, and any that had an error that is gone
[]str files = [path]
for d in self.diagnosed {
if !files.contains(d) {
files.push(d)
}
}
[]str now
for f in files {
list := json.new_list()
if a.diag.has && a.diag.file == f {
line := a.diag.line - 1
if line < 0 {
line = 0
}
col := a.diag.col - 1
if col < 0 {
col = 0
}
d := json.new_object()
d.set("range", range(line, col, col + 1))
d.set("severity", num(1))
d.set("source", json.new_text("finch"))
d.set("message", json.new_text(a.diag.message))
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)
notify("textDocument/publishDiagnostics", p)
}
self.diagnosed = now
}
// ---------- the questions ----------
fn refAt(Analysis a, str path, int line, int col) -> int {
for i in 0..a.refs.len {
r := a.refs[i]
if r.line == line && col >= r.col && col <= r.col + r.len && r.file == path {
return i
}
}
return -1
}
fn hoverAt(Analysis a, str path, int line, int col) -> json.Json {
i := refAt(a, path, line, col)
if i < 0 {
return json.new_null()
}
contents := json.new_object()
contents.set("kind", json.new_text("markdown"))
contents.set("value", json.new_text(a.refs[i].hover))
out := json.new_object()
out.set("contents", contents)
return out
}
fn definitionAt(Analysis a, str path, int line, int col) -> json.Json {
i := refAt(a, path, line, col)
if i < 0 || a.refs[i].defFile.len == 0 {
return json.new_null()
}
r := a.refs[i]
out := json.new_object()
out.set("uri", json.new_text(pathToUri(r.defFile)))
out.set("range", range(r.defLine - 1, r.defCol - 1, r.defCol - 1))
return out
}
fn symbolsOf(Analysis a, str path) -> json.Json {
list := json.new_list()
for f in a.fns {
if f.file != path {
continue
}
kind := 12 // Function
name := f.name
if f.recv.len > 0 {
kind = 6 // Method
name = f.recv + "." + f.name
}
list.push(symbol(name, kind, f.line, f.col, f.endLine))
}
for s in a.structs {
if s.file == path {
list.push(symbol(s.name, 23, s.line, s.col, s.line + s.fields.len + 1))
}
}
for e in a.enums {
if e.file == path {
list.push(symbol(e.name, 10, e.line, e.col, e.line + e.variants.len + 1))
}
}
return list
}
fn symbol(str name, int kind, int line, int col, int endLine) -> json.Json {
s := json.new_object()
s.set("name", json.new_text(name))
s.set("kind", num(kind))
full := json.new_object()
full.set("start", position(line - 1, 0))
full.set("end", position(endLine, 0))
s.set("range", full)
s.set("selectionRange", range(line - 1, col - 1, col - 1))
return s
}
// ---------- completion ----------
fn item(str label, int kind, str detail, str text) -> json.Json {
o := json.new_object()
o.set("label", json.new_text(label))
o.set("kind", num(kind))
if detail.len > 0 {
o.set("detail", json.new_text(detail))
}
if text.len > 0 {
d := json.new_object()
d.set("kind", json.new_text("markdown"))
d.set("value", json.new_text(text))
o.set("documentation", d)
}
return o
}
fn isWordChar(char c) -> bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}
fn methodItems(ptr[json.Json] items, []docs.Doc list) {
for d in list {
kind := 2 // Method
if d.signature.starts_with(".") {
kind = 5 // Field
}
items.push(item(d.name, kind, d.signature, d.text))
}
}
// the variable called `name` that is in scope at `line` (the innermost one declared before it)
fn varNamed(Analysis a, str path, str name, int line) -> VarRec {
found := VarRec()
for v in a.vars {
if v.name == name && v.line <= line && line <= v.fnEnd + 1 && v.fnFile == path {
found = v
}
}
return found
}
fn completionAt(Analysis a, str text, str path, int line, int col) -> json.Json {
items := json.new_list()
lineText := lineOf(text, line)
end := col - 1
if end > lineText.len {
end = lineText.len
}
before := lineText.sub(0, end)
// the word being typed, and whether a dot comes right before it
wordStart := before.len
while wordStart > 0 && isWordChar(before[wordStart - 1]) {
wordStart -= 1
}
afterDot := wordStart > 0 && before[wordStart - 1] == '.'
if afterDot {
s := wordStart - 1
while s > 0 && (isWordChar(before[s - 1]) || before[s - 1] == '[' || before[s - 1] == ']') {
s -= 1
}
owner := before.sub(s, wordStart - 1)
element := false
br := owner.find("[")
if br >= 0 {
owner = owner.sub(0, br)
element = true
}
// 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, ""))
}
}
for st in a.structs {
if st.module == owner {
items.push(item(st.name, 22, "struct " + st.name, ""))
}
}
for en in a.enums {
if en.module == owner {
items.push(item(en.name, 13, "enum " + en.name, ""))
}
}
return completionList(items)
}
// an enum: its variants
for en in a.enums {
if en.name == owner && varNamed(a, path, owner, line).name.len == 0 {
for v in en.variants {
items.push(item(v, 20, en.name + "." + v, ""))
}
return completionList(items)
}
}
// a variable: by its type
type := varNamed(a, path, owner, line).type
if element && type.starts_with("[]") {
type = type.sub(2, type.len)
} else if element && type == "str" {
type = "char"
}
if type.starts_with("ptr[") && type.ends_with("]") {
type = type.sub(4, type.len - 1)
}
if type.starts_with("[]") {
methodItems(addr(items), docs.arrayMethods())
} else if type == "str" {
methodItems(addr(items), docs.strMethods())
} else if type.starts_with("map[") {
methodItems(addr(items), docs.mapMethods())
} else if ast.vecType(type).kind.len > 0 {
methodItems(addr(items), docs.vecMethods()) // f32x8, i8x32, ...
} else {
base := type
for st in a.structs {
if st.name == base {
for k in 0..st.fields.len {
items.push(item(st.fields[k], 5, st.types[k], ""))
}
}
}
for f in a.fns {
if f.recv == base {
items.push(item(f.name, 2, f.signature, ""))
}
}
}
return completionList(items)
}
// not after a dot: what is in scope
seen := Names()
for v in a.vars {
if v.line <= line && line <= v.fnEnd + 1 && v.fnFile == path && seen.add(v.name) {
items.push(item(v.name, 6, v.type, ""))
}
}
for f in a.fns {
if f.recv.len == 0 && f.module == "" && seen.add(f.name) {
items.push(item(f.name, 3, f.signature, ""))
}
}
for st in a.structs {
if st.module == "" && seen.add(st.name) {
items.push(item(st.name, 22, "struct " + st.name, ""))
}
}
for en in a.enums {
if en.module == "" && seen.add(en.name) {
items.push(item(en.name, 13, "enum " + en.name, ""))
}
}
for m in a.imports {
if seen.add(m) {
items.push(item(m, 9, "module", ""))
}
}
for d in docs.builtins() {
if seen.add(d.name) {
items.push(item(d.name, 3, d.signature, d.text))
}
}
for k in docs.keywords() {
if seen.add(k) {
items.push(item(k, 14, "", ""))
}
}
for t in docs.typeNames() {
if seen.add(t) {
items.push(item(t, 25, "", ""))
}
}
return completionList(items)
}
struct Names {
[]str items
}
fn Names.add(str x) -> bool {
if self.items.contains(x) {
return false
}
self.items.push(x)
return true
}
fn completionList(json.Json items) -> json.Json {
o := json.new_object()
o.set("isIncomplete", json.new_bool(false))
o.set("items", items)
return o
}
// ---------- signature help ----------
fn signatureAt(Analysis a, str text, str path, int line, int col) -> json.Json {
lineText := lineOf(text, line)
end := col - 1
if end > lineText.len {
end = lineText.len
}
before := lineText.sub(0, end)
// back to the `(` that is still open, counting the commas at its own level
depth := 0
commas := 0
open := -1
i := before.len - 1
while i >= 0 {
c := before[i]
if c == ')' || c == ']' {
depth += 1
} else if c == '(' || c == '[' {
if depth == 0 {
if c == '(' {
open = i
break
}
} else {
depth -= 1
}
} else if c == ',' && depth == 0 {
commas += 1
}
i -= 1
}
if open < 0 {
return json.new_null()
}
nameEnd := open
nameStart := nameEnd
while nameStart > 0 && isWordChar(before[nameStart - 1]) {
nameStart -= 1
}
name := before.sub(nameStart, nameEnd)
if name.len == 0 {
return json.new_null()
}
recv := ""
if nameStart > 0 && before[nameStart - 1] == '.' {
// a method: its struct from the type of what comes before the dot
s := nameStart - 1
while s > 0 && isWordChar(before[s - 1]) {
s -= 1
}
owner := before.sub(s, nameStart - 1)
type := varNamed(a, path, owner, line).type
if type.starts_with("ptr[") && type.ends_with("]") {
type = type.sub(4, type.len - 1)
}
recv = type
}
for f in a.fns {
if f.name == name && f.recv == recv {
sig := json.new_object()
sig.set("label", json.new_text(f.signature))
params := json.new_list()
for p in f.params {
if p.len > 0 {
one := json.new_object()
one.set("label", json.new_text(p))
params.push(one)
}
}
sig.set("parameters", params)
sigs := json.new_list()
sigs.push(sig)
out := json.new_object()
out.set("signatures", sigs)
out.set("activeSignature", num(0))
out.set("activeParameter", num(commas))
return out
}
}
d := docs.find(docs.builtins(), name)
if d.name.len > 0 {
sig := json.new_object()
sig.set("label", json.new_text(d.signature))
sig.set("documentation", json.new_text(d.text))
sigs := json.new_list()
sigs.push(sig)
out := json.new_object()
out.set("signatures", sigs)
out.set("activeSignature", num(0))
out.set("activeParameter", num(commas))
return out
}
return json.new_null()
}
// ---------- the messages ----------
fn textOf(json.Json params) -> str {
return params.get("textDocument").get("text").str_or("")
}
fn pathOf(json.Json params) -> str {
return uriToPath(params.get("textDocument").get("uri").str_or(""))
}
fn Server.answer(json.Json msg) {
method := msg.get("method").str_or("")
params := msg.get("params")
id := msg.get("id")
match method {
"initialize" -> {
caps := json.new_object()
caps.set("textDocumentSync", num(1))
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("."))
comp.set("triggerCharacters", dot)
caps.set("completionProvider", comp)
sigp := json.new_object()
trig := json.new_list()
trig.push(json.new_text("("))
trig.push(json.new_text(","))
sigp.set("triggerCharacters", trig)
caps.set("signatureHelpProvider", sigp)
result := json.new_object()
result.set("capabilities", caps)
info := json.new_object()
info.set("name", json.new_text("finch"))
result.set("serverInfo", info)
reply(id, result)
}
"shutdown" -> {
self.stopping = true
reply(id, json.new_null())
}
"exit" -> {
exit(0)
}
"textDocument/didOpen" -> {
path := pathOf(params)
self.docs[path] = textOf(params)
self.analyse(path)
}
"textDocument/didChange" -> {
path := pathOf(params)
changes := params.get("contentChanges")
if changes.len() > 0 {
self.docs[path] = changes.at(changes.len() - 1).get("text").str_or("")
self.analyse(path)
}
}
"textDocument/didSave" -> {
path := pathOf(params)
if self.docs.has(path) {
self.analyse(path)
}
}
"textDocument/didClose" -> {
path := pathOf(params)
self.docs.remove(path)
self.results.remove(path)
self.good.remove(path)
empty := json.new_object()
empty.set("uri", json.new_text(pathToUri(path)))
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
col := params.get("position").get("character").int_or(0) + 1
if !self.results.has(path) {
reply(id, json.new_null())
return
}
a := self.results[path]
text := ""
if self.docs.has(path) {
text = self.docs[path]
}
match method {
"textDocument/hover" -> reply(id, hoverAt(a, path, line, col))
"textDocument/definition" -> reply(id, definitionAt(a, path, line, col))
"textDocument/completion" -> reply(id, completionAt(a, text, path, line, col))
"textDocument/documentSymbol" -> reply(id, symbolsOf(a, path))
else -> reply(id, signatureAt(a, text, path, line, col))
}
}
else -> {
// a request we don't know gets an answer so the editor does not wait; notifications get none
if id.kind != json.Kind.Null {
reply(id, json.new_null())
}
}
}
}
// finch lsp
fn run(str self) -> int {
host.binaryStdio()
s := Server(self: self, tmp: host.tempDir() + "/finch_lsp_" + str(host.pid()))
while true {
msg := readMessage() or {
break
}
s.answer(msg)
}
host.removeTree(s.tmp)
return 0
}

387
src/main.cpp Normal file
View file

@ -0,0 +1,387 @@
#include "cimport.h"
#include "codegen.h"
#include "error.h"
#include "lexer.h"
#include "parser.h"
#include <llvm/Config/llvm-config.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Passes/PassBuilder.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Target/TargetMachine.h>
#include <llvm/TargetParser/Host.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <sstream>
#include <string>
#include <sys/wait.h>
#include <vector>
#include "rt_source.inc" // kRuntimeSource: runtime/finch_rt.c, embedded at build time
using namespace llvm;
static void usage() {
std::fprintf(stderr,
"Finch " FINCH_VERSION " - small, quick, sharp.\n\n"
"usage:\n"
" finch run <file.fch> [args...] compile and run\n"
" finch build <file.fch> [-o name] compile to a program\n"
" finch ir <file.fch> show the generated LLVM IR\n"
" finch version show the version\n"
"\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n"
" -g add debug info (for gdb / lldb)\n"
" -O0 skip optimizations (to read the raw IR)\n");
std::exit(1);
}
static bool readFile(const std::string &path, std::string &out) {
std::ifstream in(path);
if (!in) return false;
std::stringstream ss;
ss << in.rdbuf();
out = ss.str();
return true;
}
static std::string dirOf(const std::string &path) {
size_t slash = path.find_last_of('/');
return slash == std::string::npos ? "." : path.substr(0, slash);
}
// ---------- loading the program and its modules ----------
struct Loader {
std::vector<Program> progs;
std::map<std::string, bool> loaded; // module name -> seen
void load(const std::string &path, const std::string &module, Pos from) {
std::string text;
if (!readFile(path, text)) {
if (from.line) failAt(from.file, from.line, from.col, "can't find the module '" + module + "' (looked for " + path + ")");
std::fprintf(stderr, "error: can't open '%s'\n", path.c_str());
std::exit(1);
}
int idx = (int)g_files.size();
g_files.push_back({path, text});
Program p = parse(lex(idx), idx);
p.module = module;
p.path = path;
std::vector<Import> mods;
for (const Import &im : p.imports)
if (!im.isC) mods.push_back(im);
progs.push_back(std::move(p));
for (const Import &im : mods) {
if (im.path == "main") failAt(im.pos.file, im.pos.line, im.pos.col, "'main' can't be imported");
if (loaded[im.path]) continue;
loaded[im.path] = true;
load(findModule(dirOf(path), im.path), im.path, im.pos);
}
}
// next to the importing file, then in $FINCH_PATH folders
static std::string findModule(const std::string &dir, const std::string &name) {
std::string here = dir + "/" + name + ".fch";
if (sys::fs::exists(here)) return here;
if (const char *fp = std::getenv("FINCH_PATH")) {
std::stringstream ss(fp);
for (std::string d; std::getline(ss, d, ':');)
if (!d.empty() && sys::fs::exists(d + "/" + name + ".fch")) return d + "/" + name + ".fch";
}
return here;
}
};
// ---------- LLVM ----------
static void optimize(Module &mod, TargetMachine *tm) {
LoopAnalysisManager lam;
FunctionAnalysisManager fam;
CGSCCAnalysisManager cgam;
ModuleAnalysisManager mam;
PassBuilder pb(tm);
pb.registerModuleAnalyses(mam);
pb.registerCGSCCAnalyses(cgam);
pb.registerFunctionAnalyses(fam);
pb.registerLoopAnalyses(lam);
pb.crossRegisterProxies(lam, fam, cgam, mam);
pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam);
}
static TargetMachine *hostMachine() {
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
Triple triple(sys::getDefaultTargetTriple());
std::string err;
const Target *target = TargetRegistry::lookupTarget(triple, err);
if (!target) {
std::fprintf(stderr, "error: %s\n", err.c_str());
std::exit(1);
}
return target->createTargetMachine(triple, "generic", "", TargetOptions(), Reloc::PIC_);
}
static void emitObject(Module &mod, TargetMachine *tm, const std::string &path) {
std::error_code ec;
raw_fd_ostream out(path, ec, sys::fs::OF_None);
if (ec) {
std::fprintf(stderr, "error: can't write '%s': %s\n", path.c_str(), ec.message().c_str());
std::exit(1);
}
legacy::PassManager pm;
if (tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)) {
std::fprintf(stderr, "error: this machine can't produce object files\n");
std::exit(1);
}
pm.run(mod);
}
// ---------- linking ----------
// Run a shell command; returns its exit status, everything it printed goes to `output`.
static int capture(const std::string &cmd, std::string &output) {
FILE *p = popen((cmd + " 2>&1").c_str(), "r");
if (!p) return -1;
char buf[4096];
size_t n;
while ((n = std::fread(buf, 1, sizeof buf, p)) > 0) output.append(buf, n);
int status = pclose(p);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
static std::string ccName() {
const char *cc = std::getenv("CC");
return cc ? cc : "cc";
}
// The runtime is compiled once per version and kept in ~/.cache/finch.
static std::string runtimeObject() {
std::string dir;
if (const char *x = std::getenv("XDG_CACHE_HOME")) dir = std::string(x) + "/finch";
else if (const char *h = std::getenv("HOME")) dir = std::string(h) + "/.cache/finch";
else dir = "/tmp/finch-cache";
size_t hash = std::hash<std::string>()(std::string(kRuntimeSource) + FINCH_VERSION + ccName());
std::string obj = dir + "/rt-" + std::to_string(hash) + ".o";
if (sys::fs::exists(obj)) return obj;
sys::fs::create_directories(dir);
std::string src = obj + ".c", part = obj + ".part";
{
std::ofstream out(src);
out << kRuntimeSource;
}
std::string output;
int status = capture(ccName() + " -O2 -fPIC -c '" + src + "' -o '" + part + "'", output);
std::remove(src.c_str());
if (status != 0) {
std::fprintf(stderr, "%serror: couldn't build the Finch runtime (is a C compiler installed?)\n", output.c_str());
std::exit(1);
}
sys::fs::rename(part, obj);
return obj;
}
// pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>.
static std::string libFlags(const std::string &lib) {
std::string out;
if (capture("pkg-config --libs '" + lib + "'", out) == 0) {
while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back();
return " " + out;
}
return " '-l" + lib + "'";
}
// "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name.
static std::string guessLib(const std::string &header) {
std::string name = header.substr(header.find_last_of('/') + 1);
name = name.substr(0, name.rfind('.'));
while (!name.empty() && std::isdigit((unsigned char)name.back())) name.pop_back();
for (char &ch : name) ch = std::tolower((unsigned char)ch);
return name;
}
// The linker speaks in riddles; turn its complaints into something you can act on.
[[noreturn]] static void linkFailed(const std::string &output, const CImports &c) {
std::vector<std::string> missing, missingLibs;
auto addOnce = [](std::vector<std::string> &v, const std::string &x) {
if (std::find(v.begin(), v.end(), x) == v.end()) v.push_back(x);
};
std::istringstream lines(output);
for (std::string l; std::getline(lines, l);) {
size_t a;
if ((a = l.find("undefined reference to `")) != std::string::npos) {
a += 24;
addOnce(missing, l.substr(a, l.find('\'', a) - a));
} else if ((a = l.find("undefined symbol: ")) != std::string::npos) {
a += 18;
addOnce(missing, l.substr(a, l.find_first_of(" \n", a) - a));
} else if ((a = l.find("cannot find -l")) != std::string::npos) {
a += 14;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n", a) - a));
}
}
if (missing.empty() && missingLibs.empty()) {
std::fprintf(stderr, "%serror: linking failed\n", output.c_str());
std::exit(1);
}
for (const std::string &lib : missingLibs)
std::fprintf(stderr, "error: the library '%s' wasn't found. Is it installed? (on Nix: add it to shell.nix)\n", lib.c_str());
std::vector<std::pair<std::string, std::vector<std::string>>> byHeader;
for (const std::string &name : missing) {
auto f = c.fns.find(name);
std::string header = f == c.fns.end() ? "" : f->second.header;
auto it = std::find_if(byHeader.begin(), byHeader.end(), [&](auto &g) { return g.first == header; });
if (it == byHeader.end()) byHeader.push_back({header, {name}});
else it->second.push_back(name);
}
for (auto &[header, names] : byHeader) {
std::string list;
for (size_t k = 0; k < names.size() && k < 3; k++) list += (k ? ", " : "") + names[k];
if (names.size() > 3) list += " and " + std::to_string(names.size() - 3) + " more";
if (header.empty()) {
std::fprintf(stderr, "error: no code found for %s\n", list.c_str());
continue;
}
std::fprintf(stderr,
"error: %s come%s from \"%s\", but its library isn't linked\n"
" the header only says the functions exist; their code lives in a library.\n"
" add this at the top of your file (with the library's real name):\n\n"
" link \"%s\"\n\n",
list.c_str(), names.size() == 1 ? "s" : "", header.c_str(), guessLib(header).c_str());
}
std::exit(1);
}
static bool endsWith(const std::string &s, const char *x) {
size_t n = std::strlen(x);
return s.size() > n && s.compare(s.size() - n, n, x) == 0;
}
static void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &libs,
const CImports &c) {
std::string cmd = ccName() + " '" + obj + "' '" + runtimeObject() + "' -o '" + exe + "' -lm";
std::vector<std::string> temps;
for (const std::string &l : libs) {
if (endsWith(l, ".c")) { // compile the user's C file
std::string o = obj + "." + std::to_string(temps.size()) + ".o", output;
if (capture(ccName() + " -O2 -fPIC -c '" + l + "' -o '" + o + "'", output) != 0) {
std::fprintf(stderr, "%serror: couldn't compile %s\n", output.c_str(), l.c_str());
std::exit(1);
}
temps.push_back(o);
cmd += " '" + o + "'";
} else if (endsWith(l, ".o") || endsWith(l, ".a")) {
cmd += " '" + l + "'";
} else {
cmd += libFlags(l);
}
}
std::string output;
int status = capture(cmd, output);
std::remove(obj.c_str());
for (const std::string &t : temps) std::remove(t.c_str());
if (status == 127) {
std::fprintf(stderr, "error: no C compiler found to link with (install gcc or clang, or set CC)\n");
std::exit(1);
}
if (status != 0) linkFailed(output, c);
std::fputs(output.c_str(), stderr); // warnings, if any
}
static std::string stem(const std::string &path) {
size_t slash = path.find_last_of('/');
std::string name = slash == std::string::npos ? path : path.substr(slash + 1);
size_t dot = name.rfind('.');
return dot == std::string::npos ? name : name.substr(0, dot);
}
static std::string shellQuote(const std::string &s) {
std::string out = "'";
for (char ch : s) out += ch == '\'' ? std::string("'\\''") : std::string(1, ch);
return out + "'";
}
int main(int argc, char **argv) {
if (argc == 2 && (std::string(argv[1]) == "version" || std::string(argv[1]) == "--version")) {
std::printf("finch %s (LLVM %s)\n", FINCH_VERSION, LLVM_VERSION_STRING);
return 0;
}
if (argc < 3) usage();
std::string cmd = argv[1], file = argv[2];
if (cmd != "run" && cmd != "build" && cmd != "ir") usage();
std::string out = stem(file);
std::vector<std::string> libs, programArgs;
bool optimizeCode = true, debug = false;
for (int i = 3; i < argc; i++) {
std::string a = argv[i];
if (cmd == "run" && !programArgs.empty()) programArgs.push_back(a);
else if (a == "-o" && i + 1 < argc && cmd == "build") out = argv[++i];
else if (a == "-O0") optimizeCode = false;
else if (a == "-g") debug = true;
else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]);
else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2));
else if (cmd == "run") programArgs.push_back(a);
else usage();
}
Loader loader;
loader.load(file, "", Pos{});
std::vector<Import> allImports;
std::vector<std::string> dirs;
for (const Program &p : loader.progs) {
allImports.insert(allImports.end(), p.imports.begin(), p.imports.end());
for (const Link &l : p.links) {
bool file = endsWith(l.lib, ".c") || endsWith(l.lib, ".o") || endsWith(l.lib, ".a");
std::string path = file && l.lib[0] != '/' ? dirOf(p.path) + "/" + l.lib : l.lib;
if (file && !sys::fs::exists(path)) failAt(l.pos.file, l.pos.line, l.pos.col, "can't find the file '" + path + "'");
libs.push_back(path);
}
if (std::find(dirs.begin(), dirs.end(), dirOf(p.path)) == dirs.end()) dirs.push_back(dirOf(p.path));
}
CImports c = importHeaders(allImports, dirs);
LLVMContext ctx;
TargetMachine *tm = hostMachine();
std::unique_ptr<Module> mod = generate(loader.progs, c, ctx, *tm, debug);
if (optimizeCode) optimize(*mod, tm);
if (cmd == "ir") {
mod->print(outs(), nullptr);
return 0;
}
std::string obj = out + ".o";
if (cmd == "run") {
SmallString<128> tmp;
sys::fs::createTemporaryFile("finch", "", tmp);
out = tmp.str().str();
obj = out + ".o";
}
emitObject(*mod, tm, obj);
link(obj, out, libs, c);
if (cmd == "run") {
std::string line = shellQuote(out);
for (const std::string &a : programArgs) line += " " + shellQuote(a);
int status = std::system(line.c_str());
std::remove(out.c_str());
if (WIFSIGNALED(status)) {
std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status)));
return 128 + WTERMSIG(status);
}
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
return 0;
}

View file

@ -1,531 +0,0 @@
// finch: the Finch compiler, written in Finch (`finch` with no arguments lists the commands).
//
// It reads the program and the modules it imports, checks it, writes LLVM IR as text
// and lets clang turn that into a program, linked with Finch's C runtime.
import lexer
import ast
import parser
import gen
import cimport
import platform
import linker
import lsp
import host
import embedded
import manifest
import fmt
import "stdlib.h"
fn dirOf(str path) -> str {
i := path.len - 1
while i >= 0 && path[i] != '/' {
i -= 1
}
if i < 0 {
return "."
}
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("./") {
name = path
}
dot := name.find(".")
if dot > 0 {
return name.sub(0, dot)
}
return name
}
struct Loader {
[]ast.Program progs
[]str files
[]str loaded
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
}
fn textOfFile(ptr[Loader] l, str path) -> str {
k := l.overlayPaths.find(path)
if k >= 0 {
return read_file(l.overlayFiles[k])
}
return read_file(path)
}
// import json: the file next to the one that imports it, else the standard library's.
// A program's own json.fch wins over the library's.
fn findModule(ptr[Loader] l, str from, str name) -> str {
// in each folder, the version for this system first: time_windows.fch, then time.fch
local := dirOf(from) + "/" + name + ".fch"
if file_exists(dirOf(from) + "/" + name + "_" + l.os + ".fch") {
return dirOf(from) + "/" + name + "_" + l.os + ".fch"
}
if file_exists(local) {
return local
}
for d in l.paths {
if file_exists(d + "/" + name + "_" + l.os + ".fch") {
return d + "/" + name + "_" + l.os + ".fch"
}
if file_exists(d + "/" + name + ".fch") {
return d + "/" + name + ".fch"
}
}
if l.std.len > 0 {
if file_exists(l.std + "/" + name + "_" + l.os + ".fch") {
return l.std + "/" + name + "_" + l.os + ".fch"
}
if file_exists(l.std + "/" + name + ".fch") {
return l.std + "/" + name + ".fch"
}
}
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 {
print("error: can't find the module '" + module + "' (looked for " + path + ")")
} else {
print("error: can't open '" + path + "'")
}
exit(1)
}
src := textOfFile(l, path)
file := l.files.len
l.files.push(path)
prog := parser.parse(lexer.lex(src, path), file, path)
prog.module = module
prog.path = path
if module.len == 0 {
l.root = dirOf(path)
}
[]str files
for i in 0..prog.modules.len {
f := findModule(l, path, prog.modules[i])
files.push(f)
prog.modules[i] = moduleKey(l, f, prog.modules[i])
}
modules := prog.modules
l.progs.push(prog)
for i in 0..modules.len {
if !l.loaded.contains(modules[i]) {
l.loaded.push(modules[i])
load(l, files[i], modules[i])
}
}
}
fn usage() {
print("Finch -- the compiler is written in Finch")
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:")
print(" -O0 no optimization (faster to build)")
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")
print(" --cpu <name> use all of this processor's instructions (AVX2, AVX-512, NEON...): native is the one")
print(" building it; or x86-64-v3 (AVX2), x86-64-v4 (AVX-512), apple-m1, ... The program then")
print(" needs such a processor. Default: any processor of the target system")
exit(1)
}
// The runtime and the standard library are carried inside the compiler (embedded.fch). When they aren't found as
// files next to it, they are written to the cache folder, once, and used from there.
fn unpack() -> str {
names := embedded.names()
dir := host.cacheDir() + "/files-" + linker.fingerprint(embedded.texts().join("\n"))
if file_exists(dir + "/done") {
return dir
}
texts := embedded.texts()
for i in 0..names.len {
host.makeDirs(dirOf(dir + "/" + names[i]))
write_file(dir + "/" + names[i], texts[i])
}
write_file(dir + "/done", "")
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.6.0 (the compiler is written in Finch)")
return 0
}
if args.len >= 2 && args[1] == "fmt" {
return format(args)
}
if args.len == 2 && args[1] == "lsp" {
return lsp.run(args[0]) // the language server, for editors
}
if args.len < 2 {
usage()
}
cmd := args[1]
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 := ""
cpu := ""
opt := "-O2"
[]str extraLibs
[]str extraDirs
[]str overlayPaths
[]str overlayFiles
[]str progArgs
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
progArgs.push(a)
} else if a == "-o" && ai + 1 < args.len && cmd == "build" {
out = args[ai + 1]
outGiven = true
ai += 1
} else if a == "--shared" && cmd == "build" {
shared = true
} else if a == "-O0" {
opt = "-O0"
} else if a == "-l" && ai + 1 < args.len {
extraLibs.push(args[ai + 1])
ai += 1
} else if a.starts_with("-l") && a.len > 2 {
extraLibs.push(a.sub(2, a.len))
} else if a == "-I" && ai + 1 < args.len {
extraDirs.push(args[ai + 1])
ai += 1
} else if a == "--overlay" && ai + 1 < args.len {
both := args[ai + 1].split("=")
overlayPaths.push(both[0])
overlayFiles.push(both[1])
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
} else if a == "--cpu" && ai + 1 < args.len {
cpu = args[ai + 1]
ai += 1
} else if cmd == "run" {
progArgs.push(a)
} else {
usage()
}
ai += 1
}
if !traceGiven && (debug || opt == "-O0") {
trace = true
}
tg := platform.select(targetName)
if cmd == "run" || cmd == "test" {
out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe
} else if shared && !outGiven {
if tg.windows {
out = out + ".dll"
} else if tg.darwin {
out = "lib" + out + ".dylib"
} else {
out = "lib" + out + ".so"
}
} else if !outGiven {
out = out + tg.exe
}
loader := Loader()
loader.os = "linux"
if tg.windows {
loader.os = "windows"
} else if tg.darwin {
loader.os = "macos"
}
for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", dirOf(args[0]) + "/../share/finch/std", "std"] {
if loader.std.len == 0 && file_exists(cand + "/path.fch") {
loader.std = cand
}
}
if loader.std.len == 0 {
loader.std = unpack() + "/std"
}
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)
}
if cpu.len == 0 {
cpu = man.value("cpu", "")
}
}
for d in getenv("FINCH_PATH").split(host.pathSeparator()) {
if d.len > 0 {
loader.paths.push(d)
}
}
load(addr(loader), file, "")
// C headers, from every file; their folders are searched first for "local.h"
[]cimport.Header headers
[]str seen
[]str dirs = extraDirs
[]str links = extraLibs
for prog in loader.progs {
d := dirOf(prog.path)
if !dirs.contains(d) {
dirs.push(d)
}
for i in 0..prog.cimports.len {
if !seen.contains(prog.cimports[i]) {
seen.push(prog.cimports[i])
headers.push(cimport.Header(path: prog.cimports[i], where: prog.cwhere[i]))
}
}
for l in prog.links {
if linker.isOwnCode(l) {
links.push(d + "/" + l) // the program's own C code, next to the .fch file
} else {
links.push(l)
}
}
}
triple := ""
if tg.cross || tg.windows {
triple = tg.triple // on Windows libclang.dll would otherwise assume MSVC, while clang may be MinGW
}
ci := cimport.importHeaders(headers, dirs, triple, tg.windows, tg.cc) or {
print(err)
return 1
}
[]str testModules
for prog in loader.progs {
if !prog.path.starts_with(loader.std + "/") {
testModules.push(prog.module) // your own files' tests, not the standard library's
}
}
// what the processor can do (dot_add and the like pick their instructions by it); not needed for the editor's index
[]str features
if cmd != "index" {
features = linker.cpuFeatures(tg, cpu)
if cpu == "native" && !tg.arm64 && !features.contains("sse4.2") {
print("warning: --cpu native made no difference to clang (a clang wrapper may remove -march=native; try --cpu with a name, like x86-64-v3)")
}
}
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, features: features))
if cmd == "ir" || cmd == "index" {
print(ir)
return 0
}
// the runtime is Finch's C file; FINCH_RT can point somewhere else
rt := getenv("FINCH_RT")
if rt.len == 0 {
rt = dirOf(args[0]) + "/../runtime/finch_rt.c"
}
if !file_exists(rt) {
rt = dirOf(args[0]) + "/runtime/finch_rt.c"
}
if !file_exists(rt) {
rt = dirOf(args[0]) + "/../share/finch/runtime/finch_rt.c"
}
if !file_exists(rt) && file_exists("runtime/finch_rt.c") {
rt = "runtime/finch_rt.c"
}
if !file_exists(rt) {
rt = unpack() + "/runtime/finch_rt.c"
}
extra := ""
if shared {
extra = " -fPIC"
}
if debug {
extra = extra + " -g" // or clang drops the debug info that is in the IR
}
extra = extra + linker.cpuFlag(tg, cpu) // x86-64 names a processor with -march, ARM with -mcpu
[]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" || cmd == "test" {
[]str quoted
for a in progArgs {
quoted.push(linker.quote(a))
}
status := host.run(tg.runner + linker.quote(out) + " " + quoted.join(" "))
host.remove(out)
return status
}
return 0
}

View file

@ -1,243 +0,0 @@
// finch.toml: what a project needs to build, in one file next to its code (instead of FINCH_PATH,
// C_INCLUDE_PATH, -I, -l and linker flags on the command line).
//
// name = "game" the program finch build makes
// entry = "src/main.fch" what finch run / build / test use when no file is given
// paths = ["src", "lib"] folders with modules
// include = ["vendor/SDL3/include"] folders with C headers (also for link "x.c" files)
// links = ["SDL3"] libraries
// libdirs = ["vendor/SDL3/lib"] folders with libraries
// cflags = ["-DFAST"] for the program's own C files
// ldflags = ["-Wl,-rpath,."] for the linker
//
// [target.windows] added only when building for that system:
// links = ["ws2_32"] windows, linux, macos, android, or with the CPU: linux-arm64
//
// Folders are relative to finch.toml. Only this part of TOML is read: [sections], key = "text",
// key = ["a", "b"] (over several lines too), numbers, true/false and # comments.
struct Manifest {
bool found
str path
str dir
map[str]str values // "section.key" (top level: "key") -> a single value
map[str][]str lists // "section.key" -> a list
}
fn dirOf(str path) -> str {
i := path.len - 1
while i >= 0 && path[i] != '/' && path[i] != '\\' {
i -= 1
}
if i < 0 {
return "."
}
if i == 0 {
return "/"
}
return path.sub(0, i)
}
// finch.toml in `dir` or the folders above it
fn find(str dir) -> Manifest! {
d := dir
while true {
p := d + "/finch.toml"
if d == "/" {
p = "/finch.toml"
}
if file_exists(p) {
return try parse(p, try read_file(p))
}
up := dirOf(d)
if up == d || d == "." || d.len == 0 {
break
}
d = up
}
return Manifest()
}
struct Reader {
str text
int i
int line
str path
}
fn Reader.fail(str msg) -> str {
return self.path + ":" + str(self.line) + ": error: " + msg
}
fn Reader.at() -> char {
if self.i < self.text.len {
return self.text[self.i]
}
return char(0)
}
// past spaces, and past line ends and comments too when `lines`
fn Reader.skip(bool lines) {
while self.i < self.text.len {
c := self.text[self.i]
if c == ' ' || c == '\t' || c == '\r' {
self.i += 1
} else if c == '#' {
while self.i < self.text.len && self.text[self.i] != '\n' {
self.i += 1
}
} else if c == '\n' && lines {
self.i += 1
self.line += 1
} else {
return
}
}
}
fn isKeyChar(char c) -> bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.'
}
fn Reader.word() -> str {
start := self.i
while self.i < self.text.len && isKeyChar(self.text[self.i]) {
self.i += 1
}
return self.text.sub(start, self.i)
}
// "text" (with \" \\ \n \t) or 'text' (as it is)
fn Reader.quoted() -> str! {
q := self.at()
self.i += 1
[]str out
while self.at() != q {
c := self.at()
if c == char(0) || c == '\n' {
return error(self.fail("this text is never closed"))
}
if c == '\\' && q == '"' {
self.i += 1
e := self.at()
if e == 'n' {
c = '\n'
} else if e == 't' {
c = '\t'
} else {
c = e
}
}
out.push(str(c))
self.i += 1
}
self.i += 1
return out.join("")
}
// a single value: text, a number, true or false (all kept as text)
fn Reader.scalar() -> str! {
c := self.at()
if c == '"' || c == '\'' {
return try self.quoted()
}
w := self.word()
if w.len == 0 {
return error(self.fail("expected a value: \"text\", a number, true, false or a [list]"))
}
return w
}
fn parse(str path, str text) -> Manifest! {
m := Manifest(found: true, path: path, dir: dirOf(path))
r := Reader(text: text, i: 0, line: 1, path: path)
section := ""
while true {
r.skip(true)
if r.i >= r.text.len {
break
}
if r.at() == '[' {
r.i += 1
r.skip(false)
section = r.word()
r.skip(false)
if r.at() != ']' || section.len == 0 {
return error(r.fail("a section looks like [name] or [target.windows]"))
}
r.i += 1
continue
}
key := r.word()
if key.len == 0 {
return error(r.fail("expected a key, like name = \"game\""))
}
r.skip(false)
if r.at() != '=' {
return error(r.fail("expected '=' after '" + key + "'"))
}
r.i += 1
r.skip(false)
full := key
if section.len > 0 {
full = section + "." + key
}
if r.at() == '[' {
r.i += 1
[]str items
while true {
r.skip(true)
if r.at() == ']' {
r.i += 1
break
}
items.push(try r.scalar())
r.skip(true)
if r.at() == ',' {
r.i += 1
} else if r.at() != ']' {
return error(r.fail("expected ',' or ']' in the list of '" + key + "'"))
}
}
m.lists[full] = items
} else {
m.values[full] = try r.scalar()
}
r.skip(false)
if r.i < r.text.len && r.at() != '\n' {
return error(r.fail("one key per line"))
}
}
return m
}
fn Manifest.value(str key, str fallback) -> str {
return self.values.get(key, fallback)
}
// the list `key` from the top level and from the sections for this system, in that order
fn Manifest.list(str key, []str targets) -> []str {
[]str out
if self.lists.has(key) {
for x in self.lists[key] {
out.push(x)
}
}
for t in targets {
k := "target." + t + "." + key
if self.lists.has(k) {
for x in self.lists[k] {
out.push(x)
}
}
}
return out
}
// a folder or file of the manifest, from where finch.toml is
fn Manifest.resolve(str p) -> str {
if p.starts_with("/") || (p.len > 2 && p[1] == ':') {
return p
}
return self.dir + "/" + p
}

531
src/parser.cpp Normal file
View file

@ -0,0 +1,531 @@
#include "parser.h"
#include "error.h"
#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <cstring>
#include <unordered_map>
bool typeFromName(const std::string &name, Type &out) {
static const std::unordered_map<std::string, Type::Kind> names = {
{"int", Type::I64}, {"float", Type::F64}, {"bool", Type::Bool}, {"char", Type::Char},
{"str", Type::Str}, {"ptr", Type::Ptr}, {"i8", Type::I8}, {"i16", Type::I16},
{"i32", Type::I32}, {"i64", Type::I64}, {"u8", Type::U8}, {"u16", Type::U16},
{"u32", Type::U32}, {"u64", Type::U64}, {"f32", Type::F32}, {"f64", Type::F64},
};
auto it = names.find(name);
if (it == names.end()) return false;
out = Type(it->second);
return true;
}
namespace {
bool isTypeName(const std::string &name) {
Type t;
return typeFromName(name, t);
}
class Parser {
public:
Parser(const std::vector<Token> &t, int f) : toks(t), file(f) {}
Program program() {
Program p;
while (!at(Tok::End)) {
switch (cur().kind) {
case Tok::Import: p.imports.push_back(import()); break;
case Tok::Link: p.links.push_back(link()); break;
case Tok::Struct: p.structs.push_back(structDecl()); break;
default: p.fns.push_back(function()); break;
}
}
return p;
}
private:
const std::vector<Token> &toks;
int file;
size_t i = 0;
int parenDepth = 0; // inside ( ) and [ ] newlines don't end anything
const Token &cur() const { return toks[i]; }
const Token &peekTok(size_t off = 1) const { return toks[std::min(i + off, toks.size() - 1)]; }
const Token &next() { return toks[i++]; }
bool at(Tok k) const { return cur().kind == k; }
Pos pos() const { return {cur().line, cur().col, file}; }
std::string describe(const Token &t) const {
return t.kind == Tok::Ident ? "'" + t.text + "'" : tokName(t.kind);
}
bool accept(Tok k) {
if (!at(k)) return false;
i++;
return true;
}
const Token &expect(Tok k, const char *what = nullptr) {
if (!at(k))
fail(cur().line, cur().col,
std::string("expected ") + (what ? what : tokName(k)) + ", found " + describe(cur()));
return next();
}
[[noreturn]] void unexpected(const char *ctx) {
fail(cur().line, cur().col, "unexpected " + describe(cur()) + " " + ctx);
}
// A token can continue an expression only if it is on the same line (or we're inside brackets).
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
// int, u8, ptr, ptr[int], []str, Point, math.Vec ...
Type type() {
if (accept(Tok::LBracket)) {
expect(Tok::RBracket, "']' (an array type looks like []int)");
return Type::arrayOf(type());
}
if (!at(Tok::Ident)) unexpected("(expected a type like int, str, []int, ptr[int] or a struct name)");
std::string name = next().text;
Type t;
if (typeFromName(name, t)) {
if (t.kind == Type::Ptr && accept(Tok::LBracket)) {
t = Type::ptrTo(type());
expect(Tok::RBracket);
}
return t;
}
t = Type(Type::Named);
t.name = name;
if (at(Tok::Dot) && peekTok().kind == Tok::Ident) { // module.Struct
next();
t.module = name;
t.name = next().text;
}
return t;
}
// Does a declaration like `int x`, `ptr[int] p`, `[]int a`, `Point p` or `math.Vec v` start here?
bool atDeclaration() const {
if (at(Tok::LBracket)) return peekTok().kind == Tok::RBracket;
if (!at(Tok::Ident)) return false;
Tok n = peekTok().kind;
if (n == Tok::Ident) return !peekTok().newlineBefore;
if (cur().text == "ptr" && n == Tok::LBracket) return true;
return n == Tok::Dot && peekTok(2).kind == Tok::Ident && peekTok(3).kind == Tok::Ident &&
!peekTok(3).newlineBefore;
}
// ---------- top level ----------
Import import() {
Import im;
im.pos = pos();
expect(Tok::Import);
if (at(Tok::Ident)) {
im.isC = false;
im.path = next().text;
} else {
im.path = expect(Tok::String, "a module name (import math) or a C header in quotes (import \"stdio.h\")").text;
}
return im;
}
Link link() {
Link l;
l.pos = pos();
expect(Tok::Link);
l.lib = expect(Tok::String, "a library name in quotes, like link \"glfw\"").text;
auto ends = [&](const char *x) { size_t n = std::strlen(x); return l.lib.size() > n && l.lib.compare(l.lib.size() - n, n, x) == 0; };
if (ends(".c") || ends(".o") || ends(".a")) return l; // your own C code, next to the .fch file
if (l.lib.rfind("lib", 0) == 0 || l.lib.find(".so") != std::string::npos || l.lib.find('/') != std::string::npos)
fail(l.pos.line, l.pos.col, "write just the library's name: for libglfw.so that's link \"glfw\"");
return l;
}
StructDecl structDecl() {
StructDecl s;
s.pos = pos();
expect(Tok::Struct);
s.name = expect(Tok::Ident, "a struct name").text;
if (isTypeName(s.name)) fail(s.pos.line, s.pos.col, "'" + s.name + "' is a built-in type name, pick another");
Pos open = pos();
expect(Tok::LBrace);
while (!at(Tok::RBrace)) {
if (at(Tok::End)) fail(open.line, open.col, "this '{' is never closed (missing '}')");
Field f;
f.pos = pos();
if (!atDeclaration()) unexpected("(a field looks like: int x or str name = \"default\")");
f.type = type();
f.name = expect(Tok::Ident, "a field name").text;
for (const Field &other : s.fields)
if (other.name == f.name) fail(f.pos.line, f.pos.col, "the field '" + f.name + "' is listed twice");
if (accept(Tok::Assign)) f.init = expr();
s.fields.push_back(std::move(f));
endOfStatement();
}
next();
return s;
}
FnDecl function() {
FnDecl fn;
fn.pos = pos();
if (at(Tok::Ident) && isTypeName(cur().text))
fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }");
expect(Tok::Fn, "'fn', 'struct', 'import' or 'link' (every function starts with fn)");
fn.name = expect(Tok::Ident, "a function name").text;
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
Param p;
p.pos = pos();
p.type = type();
p.name = expect(Tok::Ident, "a parameter name").text;
fn.params.push_back(p);
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
fn.ret = accept(Tok::Arrow) ? type() : Type(Type::Void);
fn.body = block();
return fn;
}
// ---------- statements ----------
BlockPtr block() {
auto b = std::make_unique<BlockStmt>(pos());
expect(Tok::LBrace);
int saved = parenDepth;
parenDepth = 0;
while (!at(Tok::RBrace)) {
if (at(Tok::End)) fail(b->pos.line, b->pos.col, "this '{' is never closed (missing '}')");
b->body.push_back(statement());
endOfStatement();
}
parenDepth = saved;
next();
return b;
}
void endOfStatement() {
if (at(Tok::RBrace) || at(Tok::End) || cur().newlineBefore) return;
unexpected("(put each statement on its own line)");
}
StmtPtr statement() {
Pos p = pos();
switch (cur().kind) {
case Tok::If: return ifStmt();
case Tok::While: {
next();
auto s = std::make_unique<WhileStmt>(p);
s->cond = expr();
s->body = block();
return s;
}
case Tok::For: {
next();
std::string var = expect(Tok::Ident, "a loop variable name").text;
expect(Tok::In);
ExprPtr first = expr();
if (accept(Tok::DotDot)) { // for i in 0..10
auto s = std::make_unique<ForStmt>(p);
s->var = var;
s->from = std::move(first);
s->to = expr();
s->body = block();
return s;
}
auto s = std::make_unique<ForEachStmt>(p); // for x in list
s->var = var;
s->list = std::move(first);
s->body = block();
return s;
}
case Tok::Return: {
next();
auto s = std::make_unique<ReturnStmt>(p);
if (!at(Tok::RBrace) && !cur().newlineBefore) s->value = expr();
return s;
}
case Tok::Break: next(); return std::make_unique<BreakStmt>(p);
case Tok::Continue: next(); return std::make_unique<ContinueStmt>(p);
case Tok::Defer: {
next();
if (at(Tok::Defer)) fail(p.line, p.col, "defer inside defer is not allowed");
return std::make_unique<DeferStmt>(p, statement());
}
case Tok::LBrace: return block();
case Tok::Fn: fail(p.line, p.col, "functions can't be inside other functions (move it out)");
case Tok::Struct: fail(p.line, p.col, "structs go outside functions, at the top level of the file");
case Tok::Import:
case Tok::Link: fail(p.line, p.col, "import and link go at the top of the file, outside functions");
default: break;
}
// int x = 5, Point p, []int a
if (atDeclaration()) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = true;
s->type = type();
s->name = expect(Tok::Ident, "a variable name").text;
if (accept(Tok::Assign)) s->init = expr();
return s;
}
// x := 5
if (at(Tok::Ident) && peekTok().kind == Tok::Declare) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = false;
s->name = next().text;
next();
s->init = expr();
return s;
}
ExprPtr e = expr();
// x = 5, p.value += 1, a[i] = 2, ...
char op = 0;
switch (cur().kind) {
case Tok::Assign: op = '='; break;
case Tok::PlusEq: op = '+'; break;
case Tok::MinusEq: op = '-'; break;
case Tok::StarEq: op = '*'; break;
case Tok::SlashEq: op = '/'; break;
case Tok::PercentEq: op = '%'; break;
case Tok::Declare:
fail(cur().line, cur().col, "':=' creates a new variable, so the left side must be a plain name");
default: break;
}
if (op && sameLine()) {
if (e->kind != ExprKind::Var && e->kind != ExprKind::Member && e->kind != ExprKind::Index)
fail(p.line, p.col, "only a variable, a field, an element or p.value can be assigned to");
auto s = std::make_unique<AssignStmt>(p);
s->target = std::move(e);
s->op = op;
next();
s->value = expr();
return s;
}
if (e->kind != ExprKind::Call && e->kind != ExprKind::Method)
fail(p.line, p.col, "this value is computed but never used");
return std::make_unique<ExprStmt>(p, std::move(e));
}
StmtPtr ifStmt() {
auto s = std::make_unique<IfStmt>(pos());
expect(Tok::If);
s->cond = expr();
s->then = block();
if (accept(Tok::Else)) {
if (at(Tok::If)) s->otherwise = ifStmt();
else s->otherwise = block();
}
return s;
}
// ---------- expressions (lowest to highest precedence) ----------
ExprPtr expr() { return orExpr(); }
ExprPtr orExpr() {
ExprPtr l = andExpr();
while (sameLine() && at(Tok::Or)) {
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, BinOp::Or, std::move(l), andExpr());
}
return l;
}
ExprPtr andExpr() {
ExprPtr l = compare();
while (sameLine() && at(Tok::And)) {
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, BinOp::And, std::move(l), compare());
}
return l;
}
ExprPtr compare() {
ExprPtr l = additive();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Eq: op = BinOp::Eq; break;
case Tok::NotEq: op = BinOp::NotEq; break;
case Tok::Less: op = BinOp::Less; break;
case Tok::LessEq: op = BinOp::LessEq; break;
case Tok::Greater: op = BinOp::Greater; break;
case Tok::GreaterEq: op = BinOp::GreaterEq; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), additive());
}
return l;
}
// Precedence is Go's, not C's: bit operators bind tighter than comparisons,
// so `x & 1 == 0` means `(x & 1) == 0`.
ExprPtr additive() { // + - | ^
ExprPtr l = multiplicative();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Plus: op = BinOp::Add; break;
case Tok::Minus: op = BinOp::Sub; break;
case Tok::Pipe: op = BinOp::BitOr; break;
case Tok::Caret: op = BinOp::BitXor; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), multiplicative());
}
return l;
}
ExprPtr multiplicative() { // * / % << >> &
ExprPtr l = unary();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Star: op = BinOp::Mul; break;
case Tok::Slash: op = BinOp::Div; break;
case Tok::Percent: op = BinOp::Mod; break;
case Tok::Shl: op = BinOp::Shl; break;
case Tok::Shr: op = BinOp::Shr; break;
case Tok::Amp: op = BinOp::BitAnd; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), unary());
}
return l;
}
ExprPtr unary() {
Pos p = pos();
if (accept(Tok::Minus)) return std::make_unique<UnaryExpr>(p, '-', unary());
if (accept(Tok::Not)) return std::make_unique<UnaryExpr>(p, '!', unary());
if (accept(Tok::Tilde)) return std::make_unique<UnaryExpr>(p, '~', unary());
return postfix();
}
// p.value, a.len, a[i], a.push(x), math.sqrt(2)
ExprPtr postfix() {
ExprPtr e = primary();
while (sameLine()) {
Pos p = pos();
if (accept(Tok::Dot)) {
std::string name = expect(Tok::Ident, "a name after '.'").text;
if (at(Tok::LParen) && !cur().newlineBefore) {
auto m = std::make_unique<MethodExpr>(p, std::move(e), name);
args(m->args, m->argNames);
e = std::move(m);
} else {
e = std::make_unique<MemberExpr>(p, std::move(e), name);
}
} else if (at(Tok::LBracket)) {
next();
parenDepth++;
ExprPtr index = expr();
parenDepth--;
expect(Tok::RBracket);
e = std::make_unique<IndexExpr>(p, std::move(e), std::move(index));
} else {
return e;
}
}
return e;
}
// ( arg, name: arg, ... ) -- we are at the '('
void args(std::vector<ExprPtr> &out, std::vector<std::string> &names) {
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
if (at(Tok::RParen)) break; // a trailing comma is fine
std::string name;
if (at(Tok::Ident) && peekTok().kind == Tok::Colon) {
name = next().text;
next();
}
names.push_back(name);
out.push_back(expr());
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
}
ExprPtr primary() {
Pos p = pos();
const Token &t = cur();
switch (t.kind) {
case Tok::Int: {
next();
errno = 0;
unsigned long long v = std::strtoull(t.text.c_str(), nullptr, t.text.size() > 1 && (t.text[1] == 'x' || t.text[1] == 'X') ? 16 : 10);
if (errno == ERANGE || v > (unsigned long long)LLONG_MAX)
fail(p.line, p.col, "number is too big for int");
return std::make_unique<IntExpr>(p, (long long)v);
}
case Tok::Float: next(); return std::make_unique<FloatExpr>(p, std::strtod(t.text.c_str(), nullptr));
case Tok::True: next(); return std::make_unique<BoolExpr>(p, true);
case Tok::False: next(); return std::make_unique<BoolExpr>(p, false);
case Tok::Null: next(); return std::make_unique<NullExpr>(p);
case Tok::Char: next(); return std::make_unique<CharExpr>(p, t.text[0]);
case Tok::String: next(); return std::make_unique<StrExpr>(p, t.text);
case Tok::LParen: {
next();
parenDepth++;
ExprPtr e = expr();
parenDepth--;
expect(Tok::RParen);
return e;
}
case Tok::LBracket: { // [1, 2, 3]
next();
auto a = std::make_unique<ArrayLitExpr>(p);
parenDepth++;
while (!at(Tok::RBracket)) {
a->elems.push_back(expr());
if (!accept(Tok::Comma)) break; // a trailing comma is fine
}
parenDepth--;
expect(Tok::RBracket);
return a;
}
case Tok::Ident: {
next();
if (at(Tok::LParen) && !cur().newlineBefore) {
auto call = std::make_unique<CallExpr>(p, t.text);
args(call->args, call->argNames);
return call;
}
if (isTypeName(t.text))
fail(p.line, p.col, "a type name alone is not a value (did you mean " + t.text + "(...)?)");
return std::make_unique<VarExpr>(p, t.text);
}
default: unexpected("(expected a value)");
}
}
};
} // namespace
Program parse(const std::vector<Token> &tokens, int file) {
return Parser(tokens, file).program();
}

View file

@ -1,929 +0,0 @@
// The Finch parser, written in Finch: tokens -> ast.Program.
import lexer
import ast
struct Parser {
[]lexer.Token toks
int i
int depth // inside ( ) and [ ] newlines don't end anything
int file
str path
}
fn fail(ptr[Parser] p, lexer.Token t, str msg) {
print(p.path + ":" + str(t.line) + ":" + str(t.col) + ": error: " + msg)
exit(1)
}
fn cur(ptr[Parser] p) -> lexer.Token {
return p.toks[p.i]
}
fn curKind(ptr[Parser] p) -> str {
return p.toks[p.i].kind
}
fn peekKind(ptr[Parser] p, int off) -> str {
k := p.i + off
if k >= p.toks.len {
k = p.toks.len - 1
}
return p.toks[k].kind
}
fn at(ptr[Parser] p, str k) -> bool {
return p.toks[p.i].kind == k
}
fn next(ptr[Parser] p) -> lexer.Token {
t := p.toks[p.i]
p.i += 1
return t
}
fn accept(ptr[Parser] p, str k) -> bool {
if at(p, k) {
p.i += 1
return true
}
return false
}
fn describe(lexer.Token t) -> str {
if t.kind == "ident" {
return "'" + t.text + "'"
}
if t.kind == "eof" {
return "end of file"
}
if t.kind == "int" || t.kind == "float" {
return "a number"
}
if t.kind == "string" {
return "a string"
}
if t.kind == "char" {
return "a char"
}
return "'" + t.kind + "'"
}
fn expect(ptr[Parser] p, str k, str what) -> lexer.Token {
if !at(p, k) {
fail(p, cur(p), "expected " + what + ", found " + describe(cur(p)))
}
return next(p)
}
fn sameLine(ptr[Parser] p) -> bool {
return p.depth > 0 || !p.toks[p.i].nl
}
fn node(ptr[Parser] p, str kind) -> ast.Node {
t := p.toks[p.i]
return ast.Node(kind: kind, line: t.line, col: t.col, file: p.file)
}
fn isTypeName(str n) -> bool {
names := ["int", "float", "bool", "char", "str", "ptr", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"]
return names.contains(n) || ast.vecType(n).kind.len > 0
}
// int, str, ptr[T], []T, Name, module.Name
fn parseType(ptr[Parser] p) -> ast.Type {
if accept(p, "[") {
expect(p, "]", "']' (an array type looks like []int)")
return ast.arrayOf(parseType(p))
}
if !at(p, "ident") {
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (expected a type like int, str, []int or a struct name)")
}
name := next(p).text
if name == "map" && at(p, "[") {
next(p)
k := parseType(p)
expect(p, "]", "']' (a map type looks like map[str]int)")
return ast.mapOf(k, parseType(p))
}
if isTypeName(name) {
if name == "ptr" && accept(p, "[") {
t := ast.ptrTo(parseType(p))
expect(p, "]", "']'")
return t
}
if name == "i64" {
return ast.simple("int")
}
if name == "f64" {
return ast.simple("float")
}
vt := ast.vecType(name)
if vt.kind.len > 0 {
return vt
}
return ast.simple(name)
}
t := ast.Type(kind: "named", name: name)
if at(p, ".") && peekKind(p, 1) == "ident" {
next(p)
t.module = name
t.name = next(p).text
}
// Stack[int], Pair[str, int]: the type arguments of a generic struct
if at(p, "[") && !cur(p).nl && peekKind(p, 1) != "]" {
next(p)
while true {
t.elem.push(parseType(p))
if !accept(p, ",") {
break
}
}
expect(p, "]", "']' after the type arguments")
}
return t
}
// Stack[int] name: a generic type, then a name on the same line. (a[i] by itself is an index.)
fn genericDeclAhead(ptr[Parser] p) -> bool {
if !at(p, "ident") || peekKind(p, 1) != "[" || peekKind(p, 2) == "]" {
return false
}
depth := 0
i := p.i + 1
while i < p.toks.len {
k := p.toks[i].kind
if k == "[" {
depth += 1
} else if k == "]" {
depth -= 1
if depth == 0 {
return i + 1 < p.toks.len && p.toks[i + 1].kind == "ident" && !p.toks[i + 1].nl
}
} else if k == "eof" || p.toks[i].nl && depth == 0 {
return false
}
i += 1
}
return false
}
// The same, when the name was just taken and the parser stands on the `[`.
fn genericCallAheadFrom(ptr[Parser] p) -> bool {
p.i -= 1
r := genericCallAhead(p)
p.i += 1
return r
}
// Is the `[` after this name followed, once it closes, by `(`? Then it is max[int](...) and not an index.
fn genericCallAhead(ptr[Parser] p) -> bool {
if peekKind(p, 1) != "[" || p.toks[p.i + 1].nl || peekKind(p, 2) == "]" {
return false
}
depth := 0
i := p.i + 1
while i < p.toks.len {
k := p.toks[i].kind
if k == "[" {
depth += 1
} else if k == "]" {
depth -= 1
if depth == 0 {
return i + 1 < p.toks.len && p.toks[i + 1].kind == "(" && !p.toks[i + 1].nl
}
} else if k == "eof" || p.toks[i].nl {
return false
}
i += 1
}
return false
}
fn atDeclaration(ptr[Parser] p) -> bool {
if genericDeclAhead(p) {
return true
}
if at(p, "[") {
return peekKind(p, 1) == "]"
}
if !at(p, "ident") {
return false
}
n := peekKind(p, 1)
if n == "ident" {
return !p.toks[p.i + 1].nl
}
if (cur(p).text == "ptr" || cur(p).text == "map") && n == "[" {
return true
}
return n == "." && peekKind(p, 2) == "ident" && peekKind(p, 3) == "ident" && !p.toks[p.i + 3].nl
}
fn endOfStatement(ptr[Parser] p) {
if at(p, "}") || at(p, "eof") || cur(p).nl {
return
}
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (put each statement on its own line)")
}
// ---------- top level ----------
fn parseProgram(ptr[Parser] p) -> ast.Program {
prog := ast.Program()
while !at(p, "eof") {
if at(p, "import") {
t := next(p)
if at(p, "string") {
prog.cwhere.push(p.path + ":" + str(t.line) + ":" + str(t.col))
prog.cimports.push(next(p).text)
} else {
if !at(p, "ident") {
fail(p, t, "expected a module name or a C header after 'import'")
}
// import ui/button (a folder), import path as p (another name here)
key := next(p).text
name := key
while at(p, "/") && !cur(p).nl {
next(p)
name = expect(p, "ident", "a module name after '/'").text
key = key + "/" + name
}
if at(p, "ident") && cur(p).text == "as" && !cur(p).nl {
next(p)
name = expect(p, "ident", "the name to use for the module, like import path as p").text
}
if prog.imports.contains(name) {
fail(p, t, "the name '" + name + "' is already used by another import (pick one with: import " + key + " as other)")
}
prog.imports.push(name)
prog.modules.push(key)
}
} else if at(p, "link") {
lt := next(p)
lib := expect(p, "string", "a library name in quotes, like link \"glfw\"").text
ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") || lib.ends_with(".cpp") || lib.ends_with(".cc") || lib.ends_with(".cxx") || lib.ends_with(".lib") // your own C or C++ code, next to the .fch file
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)
if e.flag {
prog.structs.push(sumStruct(e)) // an enum with values inside is stored as a struct (see sumStruct)
}
} else if at(p, "@") {
// @cpu("avx2,fma") fn ...: a function that may use these processor features (check with cpu_has first)
a := next(p)
name := expect(p, "ident", "an attribute name, like @cpu(\"avx2\")").text
if name != "cpu" {
fail(p, a, "the attribute here is @cpu(\"...\"): the processor features a function may use")
}
expect(p, "(", "'('")
feats := expect(p, "string", "the features in quotes, like @cpu(\"avx2,fma\")").text
expect(p, ")", "')'")
if !at(p, "fn") && !at(p, "export") {
fail(p, cur(p), "@cpu(...) goes right before a function")
}
f := parseFn(p)
f.cpu = feats
prog.fns.push(f)
} else {
prog.fns.push(parseFn(p))
}
}
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).
fn parseEnum(ptr[Parser] p) -> ast.Node {
e := node(p, "enum")
expect(p, "enum", "'enum'")
e.name = expect(p, "ident", "an enum name").text
expect(p, "{", "'{'")
while !at(p, "}") {
if at(p, "eof") {
fail(p, cur(p), "an enum is never closed (missing '}')")
}
v := node(p, "variant")
v.name = expect(p, "ident", "a variant name, like Red").text
if accept(p, "(") {
e.flag = true
while !at(p, ")") {
f := node(p, "field")
f.ty = parseType(p)
v.kids.push(f)
if !accept(p, ",") {
break
}
}
expect(p, ")", "')' after the values of the variant")
if v.kids.len == 0 {
fail(p, cur(p), "the variant " + v.name + " has empty () : write just " + v.name + " if it holds nothing")
}
}
if accept(p, "=") {
if e.flag {
fail(p, cur(p), "a variant can't have both values inside and a number")
}
sign := ""
if accept(p, "-") {
sign = "-"
}
v.text = sign + expect(p, "int", "a whole number, like Ok = 200").text
}
e.kids.push(v)
if !accept(p, ",") {
endOfStatement(p)
}
}
next(p)
if e.kids.len == 0 {
fail(p, cur(p), "the enum " + e.name + " has no variants")
}
if e.flag {
for v in e.kids {
if v.text.len > 0 {
fail(p, cur(p), "an enum whose variants hold values can't number them (" + v.name + " = " + v.text + ")")
}
}
}
return e
}
// An enum whose variants hold values is a struct underneath: a `tag` (which variant it is) and, one after the other,
// the values of every variant (Rect's are Rect_0, Rect_1). Only the tag's variant has values; the rest stay
// empty. It's simple and it makes copying, dropping and methods the struct's own.
fn sumStruct(ast.Node e) -> ast.Node {
s := ast.Node(kind: "struct", name: e.name, line: e.line, col: e.col, file: e.file)
tag := ast.Node(kind: "field", name: "tag", line: e.line, col: e.col, file: e.file)
tag.ty = ast.simple("int")
s.kids.push(tag)
for v in e.kids {
for j in 0..v.kids.len {
f := ast.Node(kind: "field", name: v.name + "_" + str(j), line: v.line, col: v.col, file: v.file)
f.ty = v.kids[j].ty
s.kids.push(f)
}
}
return s
}
// match value { A, B -> ... C -> ... else -> ... }
// As a statement the arms are statements or { blocks }; as an expression they are expressions.
fn parseMatch(ptr[Parser] p, bool asExpr) -> ast.Node {
m := node(p, "match")
m.flag = asExpr
expect(p, "match", "'match'")
m.kids.push(parseExpr(p))
expect(p, "{", "'{'")
saved := p.depth
p.depth = 0
while !at(p, "}") {
if at(p, "eof") {
fail(p, cur(p), "a match is never closed (missing '}')")
}
arm := node(p, "arm")
pats := node(p, "patterns")
if accept(p, "else") {
arm.flag = true
} else {
while true {
pats.kids.push(parseExpr(p))
if !accept(p, ",") {
break
}
}
}
arm.kids.push(pats)
expect(p, "->", "'->' after the value to match, like Red -> print(\"red\")")
if at(p, "{") && !asExpr {
arm.kids.push(parseBlock(p))
} else if asExpr {
arm.kids.push(parseExpr(p))
} else {
arm.kids.push(parseStatement(p))
}
m.kids.push(arm)
if !accept(p, ",") {
endOfStatement(p)
}
}
p.depth = saved
next(p)
if m.kids.len == 1 {
fail(p, cur(p), "a match needs at least one arm")
}
return m
}
// [T, U]
fn parseTParams(ptr[Parser] p) -> []str {
[]str names
expect(p, "[", "'['")
while true {
names.push(expect(p, "ident", "a type parameter name, like T").text)
if !accept(p, ",") {
break
}
}
expect(p, "]", "']' after the type parameters")
return names
}
fn parseStruct(ptr[Parser] p) -> ast.Node {
s := node(p, "struct")
expect(p, "struct", "'struct'")
s.name = expect(p, "ident", "a struct name").text
if at(p, "[") {
s.tparams = parseTParams(p)
}
expect(p, "{", "'{'")
while !at(p, "}") {
if at(p, "eof") {
fail(p, cur(p), "a struct is never closed (missing '}')")
}
f := node(p, "field")
if !atDeclaration(p) {
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (a field looks like: int x)")
}
f.ty = parseType(p)
f.name = expect(p, "ident", "a field name").text
if accept(p, "=") {
f.kids.push(parseExpr(p))
}
s.kids.push(f)
endOfStatement(p)
}
next(p)
return s
}
fn parseFn(ptr[Parser] p) -> ast.Node {
f := node(p, "fn")
f.exported = accept(p, "export")
if at(p, "ident") && isTypeName(cur(p).text) {
fail(p, cur(p), "functions start with 'fn', like: fn add(int a, int b) -> int { ... }")
}
expect(p, "fn", "'fn', 'struct', 'enum', 'const' or 'import'")
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(...)
}
if accept(p, ".") {
f.recv = f.name
f.name = expect(p, "ident", "a method name").text
}
expect(p, "(", "'('")
p.depth += 1
if !at(p, ")") {
while true {
prm := node(p, "param")
prm.ty = parseType(p)
prm.name = expect(p, "ident", "a parameter name").text
f.kids.push(prm)
if !accept(p, ",") {
break
}
}
}
p.depth -= 1
expect(p, ")", "')'")
f.ty = ast.simple("void")
if accept(p, "->") {
if !at(p, "!") {
f.ty = parseType(p)
}
f.fallible = accept(p, "!")
if !f.fallible && f.ty.kind == "void" {
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (after '->' comes a type, or ! for a function that can fail)")
}
}
f.kids.push(parseBlock(p))
f.endLine = f.kids[f.kids.len - 1].endLine
return f
}
// ---------- statements ----------
fn parseBlock(ptr[Parser] p) -> ast.Node {
blk := node(p, "block")
expect(p, "{", "'{'")
saved := p.depth
p.depth = 0
while !at(p, "}") {
if at(p, "eof") {
fail(p, cur(p), "a '{' is never closed (missing '}')")
}
blk.kids.push(parseStatement(p))
endOfStatement(p)
}
p.depth = saved
blk.endLine = cur(p).line
next(p)
return blk
}
fn parseStatement(ptr[Parser] p) -> ast.Node {
k := curKind(p)
if k == "if" {
return parseIf(p)
}
if k == "while" {
s := node(p, "while")
next(p)
s.kids.push(parseExpr(p))
s.kids.push(parseBlock(p))
return s
}
if k == "for" {
s := node(p, "for")
next(p)
s.name = expect(p, "ident", "a loop variable name").text
if accept(p, ",") {
s.text = expect(p, "ident", "a second loop variable name").text
}
expect(p, "in", "'in'")
s.kids.push(parseExpr(p))
if accept(p, "..") {
if s.text.len > 0 {
fail(p, cur(p), "a range of numbers has one loop variable: for i in 0..n")
}
s.kids.push(parseExpr(p))
} else {
s.kind = "each"
}
s.kids.push(parseBlock(p))
return s
}
if k == "match" {
return parseMatch(p, false)
}
if k == "return" {
s := node(p, "return")
next(p)
if !at(p, "}") && !cur(p).nl {
s.kids.push(parseExpr(p))
}
return s
}
if k == "break" || k == "continue" {
s := node(p, k)
next(p)
return s
}
if k == "{" {
return parseBlock(p)
}
if k == "defer" {
s := node(p, "defer")
next(p)
if at(p, "defer") {
fail(p, cur(p), "defer inside defer is not allowed")
}
s.kids.push(parseStatement(p))
return s
}
if k == "fn" || k == "struct" || k == "import" || k == "link" {
fail(p, cur(p), "'" + k + "' can't be used here in the self-hosted compiler")
}
if atDeclaration(p) {
s := node(p, "decl")
s.flag = true
s.ty = parseType(p)
s.name = expect(p, "ident", "a variable name").text
if accept(p, "=") {
s.kids.push(parseExpr(p))
}
return s
}
if at(p, "ident") && peekKind(p, 1) == ":=" {
s := node(p, "decl")
s.name = next(p).text
next(p)
s.kids.push(parseExpr(p))
return s
}
start := cur(p)
e := parseExpr(p)
ops := ["=", "+=", "-=", "*=", "/=", "%="]
if ops.contains(curKind(p)) && sameLine(p) {
if e.kind != "var" && e.kind != "member" && e.kind != "index" {
fail(p, start, "only a variable, a field, an element or p.value can be assigned to")
}
s := ast.Node(kind: "assign", line: start.line, col: start.col, file: p.file)
s.name = next(p).kind
s.kids.push(e)
s.kids.push(parseExpr(p))
return s
}
if e.kind != "call" && e.kind != "method" && e.kind != "try" && e.kind != "or" {
fail(p, start, "this value is computed but never used")
}
s := ast.Node(kind: "expr", line: start.line, col: start.col, file: p.file)
s.kids.push(e)
return s
}
fn parseIf(ptr[Parser] p) -> ast.Node {
s := node(p, "if")
expect(p, "if", "'if'")
s.kids.push(parseExpr(p))
s.kids.push(parseBlock(p))
if accept(p, "else") {
if at(p, "if") {
s.kids.push(parseIf(p))
} else {
s.kids.push(parseBlock(p))
}
}
return s
}
// ---------- 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
}
if op == "&&" {
return 2
}
if op == "==" || op == "!=" || op == "<" || op == "<=" || op == ">" || op == ">=" {
return 3
}
if op == "+" || op == "-" || op == "|" || op == "^" {
return 4
}
if op == "*" || op == "/" || op == "%" || op == "<<" || op == ">>" || op == "&" {
return 5
}
return 0
}
// f(x) or fallback / f(x) or { ... }: the lowest precedence, right to left
fn parseExpr(ptr[Parser] p) -> ast.Node {
l := parseBinary(p, 1)
if !sameLine(p) || !at(p, "or") {
return l
}
e := node(p, "or")
next(p)
e.kids.push(l)
if at(p, "{") {
e.flag = true
e.kids.push(parseBlock(p))
} else {
e.kids.push(parseExpr(p))
}
return e
}
// precedence climbing; all binary operators are left-associative
fn parseBinary(ptr[Parser] p, int level) -> ast.Node {
if level > 5 {
return parseUnary(p)
}
left := parseBinary(p, level + 1)
while sameLine(p) && binaryLevel(curKind(p)) == level {
e := node(p, "binary")
e.name = next(p).kind
e.kids.push(left)
e.kids.push(parseBinary(p, level + 1))
left = e
}
return left
}
fn parseUnary(ptr[Parser] p) -> ast.Node {
if at(p, "try") {
e := node(p, "try")
next(p)
e.kids.push(parsePostfix(p))
return e
}
if at(p, "-") || at(p, "!") || at(p, "~") {
e := node(p, "unary")
e.name = next(p).kind
e.kids.push(parseUnary(p))
return e
}
return parsePostfix(p)
}
fn parseArgs(ptr[Parser] p, ptr[ast.Node] call) {
expect(p, "(", "'('")
p.depth += 1
if !at(p, ")") {
while !at(p, ")") {
name := ""
if at(p, "ident") && peekKind(p, 1) == ":" {
name = next(p).text
next(p)
}
call.names.push(name)
call.kids.push(parseExpr(p))
if !accept(p, ",") {
break
}
}
}
p.depth -= 1
expect(p, ")", "')'")
}
fn parsePostfix(ptr[Parser] p) -> ast.Node {
e := parsePrimary(p)
while sameLine(p) {
if at(p, ".") {
m := node(p, "member")
next(p)
m.name = expect(p, "ident", "a name after '.'").text
m.kids.push(e)
if at(p, "(") && !cur(p).nl {
m.kind = "method"
parseArgs(p, addr(m))
}
e = m
} else if at(p, "[") {
ix := node(p, "index")
next(p)
p.depth += 1
ix.kids.push(e)
ix.kids.push(parseExpr(p))
p.depth -= 1
expect(p, "]", "']'")
e = ix
} else {
return e
}
}
return e
}
fn parsePrimary(ptr[Parser] p) -> ast.Node {
t := cur(p)
e := node(p, "")
k := t.kind
if k == "int" || k == "float" || k == "char" {
next(p)
e.kind = k
e.text = t.text
return e
}
if k == "string" {
next(p)
e.kind = "str"
e.text = t.text
return e
}
if k == "true" || k == "false" {
next(p)
e.kind = "bool"
e.text = k
return e
}
if k == "null" {
next(p)
e.kind = "null"
return e
}
if k == "(" {
next(p)
p.depth += 1
inner := parseExpr(p)
p.depth -= 1
expect(p, ")", "')'")
return inner
}
if k == "match" {
return parseMatch(p, true)
}
if k == "if" {
return parseIfValue(p)
}
if k == "[" {
next(p)
e.kind = "array"
p.depth += 1
if at(p, ":") && peekKind(p, 1) == "]" { // [:], an empty map
next(p)
e.kind = "map"
}
while !at(p, "]") {
e.kids.push(parseExpr(p))
if e.kids.len == 1 && at(p, ":") { // ["a": 1, ...]
e.kind = "map"
}
if e.kind == "map" {
expect(p, ":", "':' (a map entry looks like \"key\": value)")
e.kids.push(parseExpr(p))
}
if !accept(p, ",") {
break
}
}
p.depth -= 1
expect(p, "]", "']'")
return e
}
if k == "ident" {
next(p)
e.name = t.text
if at(p, "[") && p.toks[p.i - 1].kind == "ident" && genericCallAheadFrom(p) {
// max[int](3, 4), Stack[int]()
next(p)
e.ty = ast.Type(kind: "targs")
while true {
e.ty.elem.push(parseType(p))
if !accept(p, ",") {
break
}
}
expect(p, "]", "']' after the type arguments")
e.kind = "call"
parseArgs(p, addr(e))
return e
}
if at(p, "(") && !cur(p).nl {
e.kind = "call"
parseArgs(p, addr(e))
return e
}
e.kind = "var"
return e
}
fail(p, t, "unexpected " + describe(t) + " (expected a value)")
return e
}
fn parse([]lexer.Token toks, int file, str path) -> ast.Program {
parser := Parser(toks: toks, i: 0, depth: 0, file: file, path: path)
return parseProgram(addr(parser))
}

7
src/parser.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
#include "ast.h"
#include "lexer.h"
// Parses one file's tokens; `file` is its index in g_files.
Program parse(const std::vector<Token> &tokens, int file);

View file

@ -1,120 +0,0 @@
// The system a program is built for, and what depends on it: the C compiler that links, the file
// name of the program, how it is run here, and (through the flags) the C calling convention.
import host
import "stdlib.h"
struct Target {
str triple // "" = the system the compiler runs on
bool windows // Microsoft x64 calling convention, .exe
bool msvc
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 "
}
fn firstLine(str text) -> str {
parts := text.trim().split("\n")
return parts[0].trim()
}
// what clang calls the system it runs on, like x86_64-pc-linux-gnu
fn hostTriple() -> str {
tmp := host.tempDir() + "/finch_triple_" + str(host.pid()) + ".txt"
host.run("clang -dumpmachine > " + host.quote(tmp) + " 2> " + host.nullDevice())
out := read_file(tmp) or ""
host.remove(tmp)
if out.trim().len == 0 {
return "x86_64-unknown-linux-gnu"
}
return firstLine(out)
}
// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = this system
fn select(str name) -> Target {
hostName := hostTriple()
t := name
if name.len == 0 {
t = hostName
} else if name == "windows" {
t = "x86_64-w64-mingw32"
} else if name == "linux" {
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"
} else {
t = "x86_64-apple-macosx14.0.0"
}
}
if t == "x86_64-w64-windows-gnu" {
t = "x86_64-w64-mingw32" // what clang calls the same system when it runs on Windows
}
tg := Target(triple: t)
tg.windows = t.contains("windows") || t.contains("mingw") || t.contains("msvc") || t.contains("win32")
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")
tg.cross = tg.windows != hostWin || tg.darwin != hostDarwin || tg.arm64 != hostArm
tg.exe = ""
if tg.windows {
tg.exe = ".exe"
}
// FINCH_CC chooses the C compiler for any 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 {
tg.cc = "aarch64-linux-gnu-gcc"
} else if tg.cross {
tg.cc = "clang --target=" + t
} else if host.pathSeparator() == ";" {
tg.cc = "clang" // Windows has no cc
} else {
tg.cc = "cc"
}
if tg.cross && tg.windows {
tg.runner = "wine "
} 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
}

View file

@ -1,9 +0,0 @@
/* The runtime's folder functions, for std/fs.fch (they are in runtime/finch_rt.c, linked into every program). */
int finch_fs_is_dir(const char *path);
int finch_fs_exists(const char *path);
int finch_fs_mkdir(const char *path);
int finch_fs_remove_dir(const char *path);
void *finch_fs_list_open(const char *path);
const char *finch_fs_list_next(void *list);
void finch_fs_list_close(void *list);
const char *finch_fs_data_home(void);

View file

@ -1,8 +0,0 @@
/* The runtime's thread functions, for std/thread.fch (they are in runtime/finch_rt.c, linked into every program). */
void *finch_thread_spawn(void (*fn)(void *), void *arg);
int finch_thread_join(void *thread);
void *finch_mutex_new(void);
void finch_mutex_lock(void *mutex);
void finch_mutex_unlock(void *mutex);
void finch_mutex_free(void *mutex);
int finch_cpu_count(void);

View file

@ -1,6 +0,0 @@
/* The Windows functions std/time_windows.fch needs. Declared here, not by including <windows.h>:
libclang can't parse that header with MinGW's intrinsics. Both are in kernel32. */
unsigned long long GetTickCount64(void);
void Sleep(unsigned long milliseconds);
int QueryPerformanceCounter(long long *count);
int QueryPerformanceFrequency(long long *frequency);

View file

@ -1,70 +0,0 @@
// Files and folders: bytes, folders, listing, and a place for a program's own files.
//
// import fs
// data := try fs.read_bytes("atlas.png") the whole file as []u8 (any bytes, NUL included)
// try fs.write_bytes("out.bin", data)
// try fs.mkdir("saves/slot1") with the folders above it
// names := try fs.list("saves") the names in a folder, sorted
// fs.exists(p) fs.is_dir(p) try fs.remove_dir(p)
// dir := try fs.data_dir("MyGame") a folder for this user's saves, made if needed
import "finch_fs.h"
fn read_bytes(str path) -> []u8! {
text := try read_file(path)
return text.bytes()
}
fn write_bytes(str path, []u8 data) -> ! {
try write_file(path, str(data))
}
fn exists(str path) -> bool {
return finch_fs_exists(path) != 0
}
fn is_dir(str path) -> bool {
return finch_fs_is_dir(path) != 0
}
fn mkdir(str path) -> ! {
if finch_fs_mkdir(path) == 0 {
return error("can't make the folder \"" + path + "\"")
}
}
// only an empty folder
fn remove_dir(str path) -> ! {
if finch_fs_remove_dir(path) == 0 {
return error("can't remove the folder \"" + path + "\" (is it empty?)")
}
}
fn list(str path) -> []str! {
l := finch_fs_list_open(path)
if l == null {
return error("can't list the folder \"" + path + "\"")
}
[]str names
while true {
name := finch_fs_list_next(l)
if name.len == 0 {
break
}
names.push(name)
}
finch_fs_list_close(l)
names.sort()
return names
}
// The folder where a program keeps this user's files (saves, settings), made if it isn't there:
// ~/.local/share/<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
}

View file

@ -1,518 +0,0 @@
// JSON: read it, build it, write it.
//
// import json
//
// doc := try json.parse("{\"name\": \"Finch\", \"tags\": [\"fast\", \"small\"], \"stars\": 5}")
// print(doc.get("name").str_or("")) Finch
// print(doc.get("tags").at(1).str_or("")) small
// print(doc.get("stars").int_or(0)) 5
// print(doc.get("missing").is_null()) true (a missing key is null, so chains never fail)
//
// out := json.new_object()
// out.set("ok", json.new_bool(true))
// list := json.new_list()
// list.push(json.new_number(1.5))
// out.set("values", list)
// print(json.stringify(out)) {"ok":true,"values":[1.5]}
// print(json.pretty(out)) the same, over several lines
enum Kind { Null, Bool, Number, Text, List, Object }
// One JSON value. `kind` says which of the other fields it uses: flag for Bool, num for Number,
// text for Text, items for List, and items with keys (the same length, in the same order) for Object.
struct Json {
Kind kind
bool flag
float num
str text
[]Json items
[]str keys
}
fn new_null() -> Json {
return Json(kind: Kind.Null)
}
fn new_bool(bool b) -> Json {
return Json(kind: Kind.Bool, flag: b)
}
fn new_number(float n) -> Json {
return Json(kind: Kind.Number, num: n)
}
fn new_text(str s) -> Json {
return Json(kind: Kind.Text, text: s)
}
fn new_list() -> Json {
return Json(kind: Kind.List)
}
fn new_object() -> Json {
return Json(kind: Kind.Object)
}
// ---------- looking inside ----------
fn Json.is_null() -> bool {
return self.kind == Kind.Null
}
// the number of elements of a list, of keys of an object, of bytes of a text; 0 for the rest
fn Json.len() -> int {
return match self.kind {
List, Object -> self.items.len
Text -> self.text.len
else -> 0
}
}
fn Json.has(str key) -> bool {
return self.kind == Kind.Object && self.keys.contains(key)
}
// the value of a key of an object; null if there is none (or this is not an object)
fn Json.get(str key) -> Json {
if self.kind != Kind.Object {
return new_null()
}
i := self.keys.find(key)
if i < 0 {
return new_null()
}
return self.items[i]
}
// the element of a list; null if there is none
fn Json.at(int i) -> Json {
if self.kind != Kind.List || i < 0 || i >= self.items.len {
return new_null()
}
return self.items[i]
}
// the value if it is of that kind, else what was given
fn Json.str_or(str fallback) -> str {
if self.kind == Kind.Text {
return self.text
}
return fallback
}
fn Json.float_or(float fallback) -> float {
if self.kind == Kind.Number {
return self.num
}
return fallback
}
fn Json.int_or(int fallback) -> int {
if self.kind == Kind.Number {
return int(self.num)
}
return fallback
}
fn Json.bool_or(bool fallback) -> bool {
if self.kind == Kind.Bool {
return self.flag
}
return fallback
}
// the keys of an object, in the order they were written
fn Json.names() -> []str {
return self.keys
}
// ---------- building ----------
// add to the end of a list
fn Json.push(Json v) {
self.items.push(v)
}
// give a key a value in an object; a key that is already there is replaced
fn Json.set(str key, Json v) {
i := self.keys.find(key)
if i >= 0 {
self.items[i] = v
return
}
self.keys.push(key)
self.items.push(v)
}
// ---------- writing ----------
fn quote(str s) -> str {
[]str parts = ["\""]
for c in s {
n := int(c)
if c == '"' {
parts.push("\\\"")
} else if c == '\\' {
parts.push("\\\\")
} else if c == '\n' {
parts.push("\\n")
} else if c == '\r' {
parts.push("\\r")
} else if c == '\t' {
parts.push("\\t")
} else if n < 32 {
parts.push("\\u00" + hexdigit(n / 16) + hexdigit(n % 16))
} else {
parts.push(str(c))
}
}
parts.push("\"")
return parts.join("")
}
fn hexdigit(int n) -> str {
digits := "0123456789abcdef"
return str(digits[n])
}
fn number_text(float n) -> str {
if n == float(int(n)) && n > -1000000000000000.0 && n < 1000000000000000.0 {
return str(int(n))
}
return str(n)
}
struct Writer {
[]str out
}
fn write(Json j, str indent, str pad, ptr[Writer] w) {
nl := ""
inner := ""
sep := ","
colon := ":"
if indent.len > 0 {
nl = "\n"
inner = pad + indent
colon = ": "
}
match j.kind {
Null -> w.out.push("null")
Bool -> {
if j.flag {
w.out.push("true")
} else {
w.out.push("false")
}
}
Number -> w.out.push(number_text(j.num))
Text -> w.out.push(quote(j.text))
List -> {
if j.items.len == 0 {
w.out.push("[]")
return
}
w.out.push("[" + nl)
for i, item in j.items {
if i > 0 {
w.out.push(sep + nl)
}
w.out.push(inner)
write(item, indent, inner, w)
}
w.out.push(nl + pad + "]")
}
Object -> {
if j.items.len == 0 {
w.out.push("{}")
return
}
w.out.push("{" + nl)
for i, item in j.items {
if i > 0 {
w.out.push(sep + nl)
}
w.out.push(inner + quote(j.keys[i]) + colon)
write(item, indent, inner, w)
}
w.out.push(nl + pad + "}")
}
}
}
// the value as text on one line
fn stringify(Json j) -> str {
w := Writer()
write(j, "", "", addr(w))
return w.out.join("")
}
// the value as text over several lines, indented by two spaces
fn pretty(Json j) -> str {
w := Writer()
write(j, " ", "", addr(w))
return w.out.join("")
}
// ---------- reading ----------
struct Reader {
str src
int i
}
// the line and column of a position, for a message
fn where(ptr[Reader] r) -> str {
line := 1
col := 1
for k in 0..r.i {
if k < r.src.len && r.src[k] == '\n' {
line += 1
col = 1
} else {
col += 1
}
}
return "line " + str(line) + ", column " + str(col)
}
fn peek(ptr[Reader] r) -> char {
if r.i < r.src.len {
return r.src[r.i]
}
return char(0)
}
fn skip_space(ptr[Reader] r) {
while r.i < r.src.len && (r.src[r.i] == ' ' || r.src[r.i] == '\n' || r.src[r.i] == '\t' || r.src[r.i] == '\r') {
r.i += 1
}
}
fn unexpected(ptr[Reader] r, str wanted) -> str {
if r.i >= r.src.len {
return "json: the text ends where " + wanted + " should be (" + where(r) + ")"
}
return "json: found '" + str(r.src[r.i]) + "' where " + wanted + " should be (" + where(r) + ")"
}
// does `word` come next?
fn comes(ptr[Reader] r, str word) -> bool {
return r.src.len - r.i >= word.len && r.src.sub(r.i, r.i + word.len) == word
}
fn expect_word(ptr[Reader] r, str word) -> ! {
if !comes(r, word) {
return error(unexpected(r, "'" + word + "'"))
}
r.i += word.len
}
fn hex_value(char c) -> int {
if c >= '0' && c <= '9' {
return int(c) - int('0')
}
if c >= 'a' && c <= 'f' {
return int(c) - int('a') + 10
}
if c >= 'A' && c <= 'F' {
return int(c) - int('A') + 10
}
return -1
}
// a code point as UTF-8
fn utf8(int cp) -> str {
[]u8 b
if cp < 128 {
b.push(u8(cp))
} else if cp < 2048 {
b.push(u8(192 + cp / 64))
b.push(u8(128 + cp % 64))
} else if cp < 65536 {
b.push(u8(224 + cp / 4096))
b.push(u8(128 + (cp / 64) % 64))
b.push(u8(128 + cp % 64))
} else {
b.push(u8(240 + cp / 262144))
b.push(u8(128 + (cp / 4096) % 64))
b.push(u8(128 + (cp / 64) % 64))
b.push(u8(128 + cp % 64))
}
return str(b)
}
fn read_hex4(ptr[Reader] r) -> int! {
n := 0
for k in 0..4 {
if r.i >= r.src.len || hex_value(r.src[r.i]) < 0 {
return error(unexpected(r, "a hexadecimal digit"))
}
n = n * 16 + hex_value(r.src[r.i])
r.i += 1
}
return n
}
fn read_string(ptr[Reader] r) -> str! {
r.i += 1 // the opening quote
[]str parts
start := r.i
while true {
if r.i >= r.src.len {
return error("json: a text is never closed (" + where(r) + ")")
}
c := r.src[r.i]
if c == '"' {
parts.push(r.src.sub(start, r.i))
r.i += 1
return parts.join("")
}
if int(c) < 32 {
return error("json: a line break or control character inside a text (" + where(r) + ")")
}
if c != '\\' {
r.i += 1
continue
}
parts.push(r.src.sub(start, r.i))
r.i += 1
e := peek(r)
r.i += 1
if e == 'n' {
parts.push("\n")
} else if e == 't' {
parts.push("\t")
} else if e == 'r' {
parts.push("\r")
} else if e == 'b' {
parts.push(utf8(8))
} else if e == 'f' {
parts.push(utf8(12))
} else if e == '"' || e == '\\' || e == '/' {
parts.push(str(e))
} else if e == 'u' {
cp := try read_hex4(r)
// a pair of 😀 stands for one character above U+FFFF
if cp >= 55296 && cp < 56320 && comes(r, "\\u") {
r.i += 2
low := try read_hex4(r)
cp = 65536 + (cp - 55296) * 1024 + (low - 56320)
}
parts.push(utf8(cp))
} else {
return error("json: unknown escape \\" + str(e) + " in a text (" + where(r) + ")")
}
start = r.i
}
}
fn read_number(ptr[Reader] r) -> Json! {
start := r.i
if peek(r) == '-' {
r.i += 1
}
while r.i < r.src.len && ((r.src[r.i] >= '0' && r.src[r.i] <= '9') || r.src[r.i] == '.' || r.src[r.i] == 'e' || r.src[r.i] == 'E' || r.src[r.i] == '+' || r.src[r.i] == '-') {
r.i += 1
}
n := float(r.src.sub(start, r.i)) or {
r.i = start
return error(unexpected(r, "a number"))
}
return new_number(n)
}
fn read_value(ptr[Reader] r) -> Json! {
skip_space(r)
c := peek(r)
if r.i >= r.src.len {
return error(unexpected(r, "a value"))
}
if c == '{' {
obj := new_object()
r.i += 1
skip_space(r)
if peek(r) == '}' {
r.i += 1
return obj
}
while true {
skip_space(r)
if peek(r) != '"' {
return error(unexpected(r, "a key in quotes"))
}
key := try read_string(r)
skip_space(r)
if peek(r) != ':' {
return error(unexpected(r, "':'"))
}
r.i += 1
v := try read_value(r)
obj.set(key, v)
skip_space(r)
if peek(r) == ',' {
r.i += 1
continue
}
if peek(r) == '}' {
r.i += 1
return obj
}
return error(unexpected(r, "',' or '}'"))
}
}
if c == '[' {
list := new_list()
r.i += 1
skip_space(r)
if peek(r) == ']' {
r.i += 1
return list
}
while true {
v := try read_value(r)
list.push(v)
skip_space(r)
if peek(r) == ',' {
r.i += 1
continue
}
if peek(r) == ']' {
r.i += 1
return list
}
return error(unexpected(r, "',' or ']'"))
}
}
if c == '"' {
s := try read_string(r)
return new_text(s)
}
if c == 't' {
try expect_word(r, "true")
return new_bool(true)
}
if c == 'f' {
try expect_word(r, "false")
return new_bool(false)
}
if c == 'n' {
try expect_word(r, "null")
return new_null()
}
if c == '-' || (c >= '0' && c <= '9') {
return try read_number(r)
}
return error(unexpected(r, "a value"))
}
// the JSON in `text`; an error with the line and column where it went wrong
fn parse(str text) -> Json! {
r := Reader(src: text)
v := try read_value(addr(r))
skip_space(addr(r))
if r.i < r.src.len {
return error(unexpected(addr(r), "the end of the text"))
}
return v
}

View file

@ -1,128 +0,0 @@
// Paths, as text. Elements are separated by "/".
//
// import path
// path.join("a", "b.txt") a/b.txt
// path.base("/tmp/x/a.tar.gz") a.tar.gz path.stem(...) a.tar path.ext(...) .gz
// path.dir("/tmp/x/a.txt") /tmp/x
// path.clean("a//b/../c/./d") a/c/d
// a and b with one separator between; an absolute b replaces a
fn join(str a, str b) -> str {
if a.len == 0 {
return b
}
if b.len == 0 {
return a
}
if b[0] == '/' {
return b
}
if a[a.len - 1] == '/' {
return a + b
}
return a + "/" + b
}
fn is_abs(str p) -> bool {
return p.len > 0 && p[0] == '/'
}
// the last element: "a/b.txt" -> "b.txt", "a/b/" -> "b", "" -> ".", "/" -> "/"
fn base(str p) -> str {
if p.len == 0 {
return "."
}
end := p.len
while end > 1 && p[end - 1] == '/' {
end -= 1
}
if end == 1 && p[0] == '/' {
return "/"
}
start := end
while start > 0 && p[start - 1] != '/' {
start -= 1
}
return p.sub(start, end)
}
// everything before the last element: "a/b.txt" -> "a", "b.txt" -> ".", "/b" -> "/"
fn dir(str p) -> str {
end := p.len
while end > 1 && p[end - 1] == '/' {
end -= 1
}
i := end
while i > 0 && p[i - 1] != '/' {
i -= 1
}
if i == 0 {
return "."
}
while i > 1 && p[i - 1] == '/' {
i -= 1
}
return p.sub(0, i)
}
// the part of the last element from its last dot: "a.tar.gz" -> ".gz"; "" when there is none
fn ext(str p) -> str {
b := base(p)
i := b.len
while i > 0 && b[i - 1] != '.' {
i -= 1
}
if i <= 1 { // no dot, or a name that only starts with one (.bashrc)
return ""
}
return b.sub(i - 1, b.len)
}
// the last element without its extension
fn stem(str p) -> str {
b := base(p)
e := ext(p)
return b.sub(0, b.len - e.len)
}
// the elements of a path: "/a/b/c" -> ["a", "b", "c"]
fn parts(str p) -> []str {
[]str out
for piece in p.split("/") {
if piece.len > 0 {
out.push(piece)
}
}
return out
}
// the shortest path with the same meaning: no "//", no ".", and ".." resolved where it can be
fn clean(str p) -> str {
if p.len == 0 {
return "."
}
abs := is_abs(p)
[]str out
for piece in parts(p) {
if piece == "." {
continue
}
if piece == ".." {
if out.len > 0 && out[out.len - 1] != ".." {
out.pop()
} else if !abs {
out.push("..")
}
continue
}
out.push(piece)
}
joined := out.join("/")
if abs {
return "/" + joined
}
if joined.len == 0 {
return "."
}
return joined
}

View file

@ -1,48 +0,0 @@
// Programs: run a command, read what it printed, look at the environment.
//
// import proc
// code := proc.run("make") the exit code; what the command prints goes to the screen
// text := try proc.output("git describe") what the command printed; an error if it failed
// proc.env("HOME", "/") an environment variable, or the second value if it is not set
import "stdlib.h"
import "unistd.h"
import "time.h"
// run a command; its output goes to the screen. Gives its exit code.
fn run(str command) -> int {
return shell(command)
}
// run a command and give what it printed. Fails, with its output as the message, if it exits with an error.
fn output(str command) -> str! {
tmp := "/tmp/finch_out_" + str(getpid()) + "_" + str(int(time(null)))
code := shell("(" + command + ") > '" + tmp + "' 2>&1")
text := read_file(tmp) or ""
delete_file(tmp)
if code != 0 {
return error("'" + command + "' exited with code " + str(code) + ": " + text.trim())
}
return text
}
fn env(str name, str fallback) -> str {
v := getenv(name)
if v.len == 0 {
return fallback
}
return v
}
fn pid() -> int {
return int(getpid())
}
// the folder the program is running in
fn cwd() -> str {
[]char buf
for i in 0..4096 {
buf.push(char(0))
}
getcwd(buf.ptr, 4096)
return str(buf.ptr)
}

View file

@ -1,45 +0,0 @@
// Programs, for Windows. The same functions as proc.fch (that file is for Linux and macOS).
import "stdlib.h"
import "process.h"
import "direct.h"
import "time.h"
fn run(str command) -> int {
return shell(command)
}
fn output(str command) -> str! {
tmp := proc_env("TEMP", ".") + "\\finch_out_" + str(getpid()) + "_" + str(int(time(null)))
code := shell(command + " > \"" + tmp + "\" 2>&1")
text := read_file(tmp) or ""
delete_file(tmp)
if code != 0 {
return error("'" + command + "' exited with code " + str(code) + ": " + text.trim())
}
return text
}
fn proc_env(str name, str fallback) -> str {
v := getenv(name)
if v.len == 0 {
return fallback
}
return v
}
fn env(str name, str fallback) -> str {
return proc_env(name, fallback)
}
fn pid() -> int {
return int(getpid())
}
fn cwd() -> str {
[]char buf
for i in 0..4096 {
buf.push(char(0))
}
getcwd(buf.ptr, 4096)
return str(buf.ptr)
}

View file

@ -1,65 +0,0 @@
// Threads: run a function next to the rest of the program, and a lock for what they share.
//
// import thread
// fn work(ptr arg) { a thread's function takes one ptr (to whatever it needs)
// ptr[Job] job = arg
// ...
// }
// t := try thread.spawn(addr(work), addr(job))
// t.join() wait for it; `job` must live until then
//
// m := thread.mutex()
// defer m.close()
// m.lock() ... m.unlock() one thread at a time in between
// atomic_add(addr(count), 1) (built in) numbers changed by many threads at once
//
// What a thread is given is shared, not copied: don't let it outlive the block it comes from, and
// change it only under a lock (or with atomic_...).
import "finch_thread.h"
struct Thread {
ptr handle
}
struct Mutex {
ptr handle
}
// start fn(arg) on a new thread; fn is addr(f) of a fn f(ptr arg)
fn spawn(ptr fn_, ptr arg) -> Thread! {
h := finch_thread_spawn(fn_, arg)
if h == null {
return error("can't start a thread")
}
return Thread(handle: h)
}
// wait until the thread has finished
fn Thread.join() {
if self.handle != null {
finch_thread_join(self.handle)
self.handle = null
}
}
fn mutex() -> Mutex {
return Mutex(handle: finch_mutex_new())
}
fn Mutex.lock() {
finch_mutex_lock(self.handle)
}
fn Mutex.unlock() {
finch_mutex_unlock(self.handle)
}
fn Mutex.close() {
finch_mutex_free(self.handle)
self.handle = null
}
// how many threads the computer runs at the same time
fn cpus() -> int {
return int(finch_cpu_count())
}

View file

@ -1,88 +0,0 @@
// Time: the clock, sleeping, and dates.
//
// import time
// start := time.millis() milliseconds from a clock that only goes forward (to measure with)
// time.sleep(250) wait a quarter of a second
// time.now() seconds since 1970-01-01 00:00 UTC
// d := time.date(time.now()) the parts: d.year, d.month, d.day, d.hour, d.minute, d.second
// time.format(time.now(), "%Y-%m-%d %H:%M") as text, in the computer's time zone
import "time.h"
struct Date {
int year
int month // 1 to 12
int day // 1 to 31
int hour
int minute
int second
int weekday // 0 is Sunday
int yearday // 1 to 366
}
// seconds since 1970-01-01 00:00 UTC
fn now() -> int {
return int(time(null))
}
// milliseconds on a clock that only goes forward: for measuring how long something took
fn millis() -> int {
timespec ts
clock_gettime(CLOCK_MONOTONIC, addr(ts))
return int(ts.tv_sec) * 1000 + int(ts.tv_nsec) / 1000000
}
fn sleep(int ms) {
timespec ts
ts.tv_sec = ms / 1000
ts.tv_nsec = (ms % 1000) * 1000000
nanosleep(addr(ts), null)
}
fn parts(tm t) -> Date {
return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour),
minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1)
}
// the date and time of a moment, in UTC
fn date(int unix) -> Date {
tm t
i64 stamp = unix
gmtime_r(addr(stamp), addr(t))
return parts(t)
}
// the date and time of a moment, in the computer's time zone
fn local(int unix) -> Date {
tm t
i64 stamp = unix
localtime_r(addr(stamp), addr(t))
return parts(t)
}
// a moment as text, in the computer's time zone: %Y year, %m month, %d day, %H hour, %M minute, %S second
// (the rest of C's strftime works too: %A weekday name, %B month name, ...)
fn format(int unix, str pattern) -> str {
tm t
i64 stamp = unix
localtime_r(addr(stamp), addr(t))
[]char buf
for i in 0..256 {
buf.push(char(0))
}
strftime(buf.ptr, 256, pattern, addr(t))
return str(buf.ptr)
}
// the number of days in a month (month is 1 to 12)
fn days_in_month(int year, int month) -> int {
if month == 2 {
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
return 29
}
return 28
}
if month == 4 || month == 6 || month == 9 || month == 11 {
return 30
}
return 31
}

View file

@ -1,75 +0,0 @@
// Time, for Windows. The same functions as time.fch (that file is for Linux and macOS).
import "time.h"
import "finch_win.h"
struct Date {
int year
int month // 1 to 12
int day // 1 to 31
int hour
int minute
int second
int weekday // 0 is Sunday
int yearday // 1 to 366
}
fn now() -> int {
return int(time(null))
}
// milliseconds on a clock that only goes forward (the performance counter: GetTickCount64 ticks every 15 ms)
fn millis() -> int {
i64 count = 0
i64 freq = 1
QueryPerformanceFrequency(addr(freq))
QueryPerformanceCounter(addr(count))
return int(count / freq) * 1000 + int(count % freq) * 1000 / int(freq)
}
fn sleep(int ms) {
Sleep(u32(ms))
}
fn parts(tm t) -> Date {
return Date(year: int(t.tm_year) + 1900, month: int(t.tm_mon) + 1, day: int(t.tm_mday), hour: int(t.tm_hour),
minute: int(t.tm_min), second: int(t.tm_sec), weekday: int(t.tm_wday), yearday: int(t.tm_yday) + 1)
}
fn date(int unix) -> Date {
tm t
i64 stamp = unix
_gmtime64_s(addr(t), addr(stamp))
return parts(t)
}
fn local(int unix) -> Date {
tm t
i64 stamp = unix
_localtime64_s(addr(t), addr(stamp))
return parts(t)
}
fn format(int unix, str pattern) -> str {
tm t
i64 stamp = unix
_localtime64_s(addr(t), addr(stamp))
[]char buf
for i in 0..256 {
buf.push(char(0))
}
strftime(buf.ptr, 256, pattern, addr(t))
return str(buf.ptr)
}
fn days_in_month(int year, int month) -> int {
if month == 2 {
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
return 29
}
return 28
}
if month == 4 || month == 6 || month == 9 || month == 11 {
return 30
}
return 31
}

50
tests/boot.sh Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# The self-hosted compiler (boot/) must
# 1. build itself: stage1 (built by build/finch) -> stage2 -> stage3, with identical IR
# 2. give the same output as build/finch on every test in tests/run it supports
set -u
cd "$(dirname "$0")/.."
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
build/finch build boot/main.fch -o "$tmp/boot1" || exit 1
"$tmp/boot1" build boot/main.fch -o "$tmp/boot2" || exit 1
"$tmp/boot1" ir boot/main.fch > "$tmp/stage2.ll"
"$tmp/boot2" ir boot/main.fch > "$tmp/stage3.ll"
if cmp -s "$tmp/stage2.ll" "$tmp/stage3.ll"; then
echo "bootstrap: stage 2 and stage 3 are identical ($(wc -l < "$tmp/stage3.ll") lines of IR)"
else
echo "bootstrap: FAILED, stage 2 and stage 3 differ"
exit 1
fi
pass=0; failed=0; skipped=0
for f in tests/run/*.fch; do
grep -q "^fn main" "$f" || continue
# the bootstrap compiler doesn't do C imports, sized numbers or defer
# (checked in the test and in the modules it imports)
files="$f"
for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done
if grep -qE '^import "|^link |\b(u8|u16|u32|u64|i8|i16|i32|f32|f64)\b|defer |0x' $files; then
skipped=$((skipped + 1))
continue
fi
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
if ! "$tmp/boot2" build "$f" -o "$tmp/prog" > "$tmp/err" 2>&1; then
failed=$((failed + 1))
echo "FAIL (compile) $f"
head -3 "$tmp/err"
continue
fi
got=$("$tmp/prog" < "$input" 2>&1)
if [ "$got" == "$(cat "${f%.fch}.out")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL (output) $f"
diff <(echo "$got") "${f%.fch}.out" | head -10
fi
done
echo "self-hosted compiler: $pass passed, $failed failed, $skipped skipped (features it doesn't have)"
[ $failed -eq 0 ]

View file

@ -1,60 +0,0 @@
#!/usr/bin/env bash
# Builds every test for another system and runs it there through an emulator:
# tests/cross.sh windows x86_64-w64-mingw32, run with Wine
# tests/cross.sh arm64 aarch64 Linux, run with qemu-aarch64
# e.g. nix-shell -p pkgsCross.mingwW64.buildPackages.gcc wine64 --run 'tests/cross.sh windows'
# nix-shell -p pkgsCross.aarch64-multiplatform.buildPackages.gcc qemu \
# --run 'FINCH_CC=aarch64-unknown-linux-gnu-gcc QEMU_LD_PREFIX=<the aarch64 glibc> tests/cross.sh arm64'
# (On a real Windows/ARM64/macOS machine, use tests/run.sh with that system's finch.)
set -u
cd "$(dirname "$0")"
target=${1:?usage: tests/cross.sh windows|arm64}
FINCH=${FINCH:-../build/finch}
export WINEDEBUG=-all
case $target in
windows) runner=wine; ext=.exe
wine cmd /c exit > /dev/null 2>&1 || true ;; # the first run sets Wine up and says so: not in a test's output
arm64) runner=qemu-aarch64; ext= ;;
*) echo "unknown target $target"; exit 2 ;;
esac
pass=0; failed=0
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
run_exe() { # program, stdin -> output without Windows' \r
$runner "$1" < "$2" 2>&1 | tr -d '\r'
}
for f in run/*.fch new/*.fch; do
grep -q "^fn main" "$f" || continue
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
exe="$tmp/$(basename "${f%.fch}")$ext"
if ! "$FINCH" build "$f" --target "$target" -o "$exe" > "$tmp/err" 2>&1; then
failed=$((failed + 1)); echo "FAIL (build) $f"; head -5 "$tmp/err"; continue
fi
got=$(run_exe "$exe" "$input")
# (warnings come from the build, which is not part of what the program prints)
if [ "$got" == "$(tr -d '\r' < "${f%.fch}.out" | grep -v ': warning: ')" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1)); echo "FAIL $f"; diff <(echo "$got") "${f%.fch}.out" | head -10
fi
done
for f in fail/*.fch new/fail/*.fch; do
expect=$(head -1 "$f" | sed -n 's|^// expect: ||p')
exe="$tmp/$(basename "${f%.fch}")$ext"
if "$FINCH" build "$f" --target "$target" -o "$exe" > "$tmp/out" 2>&1; then
got=$(run_exe "$exe" /dev/null)
else
got=$(cat "$tmp/out")
fi
if [[ "$got" == *"$expect"* ]]; then
pass=$((pass + 1))
else
failed=$((failed + 1)); echo "FAIL $f (wanted: $expect)"; echo "$got" | head -5
fi
done
echo "$target ($runner): $pass passed, $failed failed"
[ $failed -eq 0 ]

View file

@ -1,7 +0,0 @@
// expect: @cpu: 'avx3' isn't a feature Finch knows
@cpu("avx2,avx3")
fn f() {
}
fn main() {
f()
}

View file

@ -1,4 +0,0 @@
// expect: cpu_has: 'avx-512' isn't a feature Finch knows
fn main() {
print(cpu_has("avx-512"))
}

Some files were not shown because too many files have changed in this diff Show more