mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 08:54:25 +00:00
Compare commits
229 changed files with 9966 additions and 615491 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
|
@ -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
|
||||
187
.github/workflows/ci.yml
vendored
187
.github/workflows/ci.yml
vendored
|
|
@ -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
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
|||
build/
|
||||
*.o
|
||||
core.*
|
||||
|
|
|
|||
47
CMakeLists.txt
Normal file
47
CMakeLists.txt
Normal 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})
|
||||
84
README.md
84
README.md
|
|
@ -38,10 +38,6 @@ fn main() {
|
|||
- **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.
|
||||
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:**
|
||||
```
|
||||
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
|
||||
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
|
||||
seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical.
|
||||
- **Written in itself, too.** `boot/` is a Finch compiler written in Finch that compiles itself to a
|
||||
byte-identical result.
|
||||
|
||||
## Quick start
|
||||
|
||||
**Ready builds** for Linux (x64, ARM64), macOS (Apple Silicon, Intel) and Windows (x64) are in the
|
||||
[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):
|
||||
Linux x86-64, LLVM 21.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git
|
||||
cd Finch
|
||||
nix-shell # or install clang + libclang (LLVM 21) + pkg-config yourself
|
||||
bootstrap/build.sh # the compiler is build/finch: built from the seed, then from itself
|
||||
nix-shell # or install LLVM 21 + libclang + cmake + ninja yourself
|
||||
cmake -S . -B build -G Ninja
|
||||
ninja -C build
|
||||
|
||||
./build/finch run examples/hello.fch
|
||||
./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
|
||||
|
||||
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) |
|
||||
| **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) |
|
||||
| **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
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Variables | `x := 5` (inferred) or `int x = 5`; `const W = 640` |
|
||||
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, arrays `[]T`, maps `map[K]V`, pointers `ptr[T]`, vectors `f32x8` `i8x32`… |
|
||||
| Variables | `x := 5` (inferred) or `int x = 5` |
|
||||
| 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 }` |
|
||||
| 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 i, n in nums { }` |
|
||||
| Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` |
|
||||
| Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` |
|
||||
| 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` |
|
||||
| Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for n in nums { }` |
|
||||
| Text | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
|
||||
| Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
|
||||
| Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures |
|
||||
| Enums, match | `enum Color { Red, Green }`, `match c { Red -> ... else -> ... }` (also as a value) |
|
||||
| Generics | `fn max[T](T a, T b) -> T`, `struct Stack[T]`, `Stack[int]()` |
|
||||
| Modules | `import shapes` → `shapes.area(b)`, `import ui/button as btn`; the standard library: `json`, `path`, `time`, `proc`, `fs`, `thread` |
|
||||
| Threads | `thread.spawn(addr(work), addr(data))`, `t.join()`, `thread.mutex()`, `atomic_add(addr(n), 1)` |
|
||||
| Tests | `test "name" { assert(x == 3) }` next to the code, run with `finch test` |
|
||||
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"` / `"glue.cpp"`, unions, `#define`s (also `(Color){…}`), `addr(fn)` for callbacks |
|
||||
| Modules | `import shapes` → `shapes.area(b)` |
|
||||
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks |
|
||||
| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
|
||||
| Tools | `finch run/build/test/fmt/ir/lsp`, `finch.toml` for a project, a build cache, `--target windows/arm64/macos/android`, `--cpu native`, `-g` for gdb/lldb, `-O0`, the chain of calls on runtime errors |
|
||||
| Tools | `finch run/build/ir`, `-g` for gdb, `-O0` |
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -125,17 +101,15 @@ Pick the guide that fits you:
|
|||
| [`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/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/types.fch`](examples/types.fch) | sized numbers and pointers |
|
||||
| [`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/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/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 |
|
||||
| [`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
|
||||
|
||||
|
|
@ -151,30 +125,18 @@ Pick the guide that fits you:
|
|||
- [x] Finch modules (`import name`)
|
||||
- [x] Debug info (`-g`)
|
||||
- [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`)
|
||||
- [x] Language server (`finch lsp`) and a VS Code extension
|
||||
- [x] macOS and ARM64 (Linux and Apple Silicon), with their C calling conventions; Kate support
|
||||
- [x] Methods on structs, maps, errors as values (`!`, `or`, `try`)
|
||||
- [x] Generics (`fn max[T]`, `struct Stack[T]`), `match`, enums
|
||||
- [x] A standard library in Finch: `json`, `path`, `time`, `proc`, `fs`
|
||||
- [x] `const`, `if` as a value, `"\(x)"` in text, `"""` text, C unions and `#define` expressions
|
||||
- [x] `test` blocks and `finch test`, `finch fmt`, `finch.toml`, a build cache, the chain of calls on runtime errors
|
||||
- [x] Threads (`import thread`) and atomics; C++ files; `--target android`
|
||||
- [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
|
||||
- [ ] Methods on structs, maps, `match`
|
||||
- [ ] Error values instead of stopping the program
|
||||
- [ ] More platforms (ARM64, macOS)
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/ the compiler, in Finch: lexer, parser, AST, code generation (to LLVM IR as text), C import (libclang) and
|
||||
the C calling conventions, the language server, the driver
|
||||
std/ the standard library, in Finch: json, path, time, proc, fs, thread
|
||||
src/ compiler: lexer, parser, AST, codegen, C import + ABI, driver (C++17, ~4.5k lines)
|
||||
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
|
||||
editors/ VS Code extension, Kate highlighting and LSP setup
|
||||
tests/ run/ new/ (golden output), fail/ new/fail/ (expected errors), run.sh, cross.sh, shared.sh, lsp_test.py
|
||||
tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh
|
||||
docs/ en/ and pl/ guides for three audiences
|
||||
```
|
||||
|
||||
|
|
|
|||
172
README.pl.md
172
README.pl.md
|
|
@ -5,8 +5,8 @@
|
|||
</picture>
|
||||
</p>
|
||||
|
||||
**Mały, szybki, celny.** Język programowania oparty na LLVM, ze składnią w stylu C. Nauczysz się go w godzinę,
|
||||
a programy działają tak szybko jak w C.
|
||||
**Mały, zwinny, skuteczny.** Język programowania na LLVM ze składnią podobną do C,
|
||||
na tyle prosty, że nauczysz się go w godzinę, i tak szybki jak C.
|
||||
|
||||
> 🇬🇧 [English version](README.md)
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ fn przeciwprostokatna(float a, float b) -> float {
|
|||
|
||||
fn main() {
|
||||
imie := "Finch"
|
||||
print("Cześć, tu", imie, "!")
|
||||
print("Cześć od", imie, "!")
|
||||
|
||||
for i in 1..4 {
|
||||
print(i, "→", przeciwprostokatna(float(i), 1.0))
|
||||
|
|
@ -29,155 +29,115 @@ fn main() {
|
|||
|
||||
## Dlaczego Finch
|
||||
|
||||
- **Prosty.** Nie ma średników, plików nagłówkowych ani preprocesora. Na każdą rzecz jest jeden sposób.
|
||||
- **Szybki.** Kod przechodzi przez optymalizacje LLVM (`-O2`) i staje się zwykłym programem, 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
|
||||
ze sprawdzaniem zakresu są tak samo szybkie jak w C.
|
||||
- **Pamięć bez kłopotów.** Listy, teksty i struktury są wartościami: przypisanie robi kopię, a koniec bloku sprząta.
|
||||
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.
|
||||
- **Bezpieczniejszy niż C tam, gdzie nic to nie kosztuje.** Liczby nie zmieniają po cichu typu, a liczb ze znakiem
|
||||
i bez znaku nie da się przypadkiem pomieszać. Indeks poza tablicą, dzielenie przez zero czy `null` kończą program
|
||||
jasnym komunikatem, a nie niezdefiniowanym zachowaniem. Kolejność działań jest taka, jakiej się spodziewasz
|
||||
(`x & 1 == 0` znaczy dokładnie to, co widać).
|
||||
- **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ć:**
|
||||
- **Prosty.** Bez średników, bez plików nagłówkowych, bez preprocesora. Każdą rzecz robi się na jeden sposób.
|
||||
- **Szybki.** Kompiluje się przez potok `-O2` z LLVM do kodu natywnego, bez garbage collectora.
|
||||
Rekurencyjne `fib(40)` trwa ok. 0,19 s (`clang -O2` na tym samym kodzie C: ok. 0,23 s), a kod na tablicach
|
||||
z kontrolą zakresu dorównuje C.
|
||||
- **Pamięć bez bólu.** Listy, teksty i struktury są wartościami: przypisanie kopiuje, a koniec bloku je zwalnia.
|
||||
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 to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem
|
||||
i bez znaku. Indeks poza zakresem, dzielenie przez zero i null zatrzymują program czytelnym błędem, zamiast
|
||||
powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać).
|
||||
- **Błędy mówią po ludzku:**
|
||||
```
|
||||
gra.fch:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
|
||||
3 | i32 y = x
|
||||
| ^
|
||||
```
|
||||
- **Biblioteki C bez pośredników.** Wystarczy `import "raylib.h"` i `link "raylib"`, żeby z niej korzystać, łącznie
|
||||
ze strukturami przekazywanymi przez wartość i funkcjami zwrotnymi. Nagłówki czyta libclang, więc niczego nie trzeba
|
||||
deklarować ręcznie.
|
||||
- **Napisany w samym sobie.** Kompilator to wyłącznie kod w Finchu: `src/*.fch`, ok. 12 tys. linii. Buduje się go
|
||||
z pliku startowego (jego własnego kodu LLVM IR, w `bootstrap/`), a potem jeszcze raz nim samym. Oba wyniki są identyczne.
|
||||
- **Każda biblioteka C, bezpośrednio.** Piszesz `import "raylib.h"` i `link "raylib"`, i możesz z niej korzystać,
|
||||
także ze struktur przekazywanych przez wartość i z callbacków. Nagłówki czyta libclang, więc nie trzeba niczego deklarować ręcznie.
|
||||
- **Napisany także w sobie.** `boot/` to kompilator Fincha napisany w Finchu, który kompiluje sam siebie
|
||||
do identycznego co do bajtu wyniku.
|
||||
|
||||
## Na start
|
||||
## Szybki start
|
||||
|
||||
**Gotowe kompilatory** na Linuksa (x64, ARM64), macOS (Apple Silicon, Intel) i Windowsa (x64) znajdziesz w
|
||||
[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):
|
||||
Linux x86-64, LLVM 21.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git
|
||||
cd Finch
|
||||
nix-shell # albo zainstaluj clang, libclang (LLVM 21) i pkg-config po swojemu
|
||||
bootstrap/build.sh # gotowy kompilator to build/finch
|
||||
nix-shell # albo zainstaluj samodzielnie LLVM 21 + libclang + cmake + ninja
|
||||
cmake -S . -B build -G Ninja
|
||||
ninja -C build
|
||||
|
||||
./build/finch run examples/hello.fch
|
||||
./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
|
||||
|
||||
Wybierz przewodnik dla siebie:
|
||||
|
||||
| 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) |
|
||||
| **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) |
|
||||
| **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) |
|
||||
| **Ściąga** | Wszystkie funkcje, metody, operatory i polecenia na jednej stronie | [sciaga.md](docs/pl/sciaga.md) | [cheatsheet.md](docs/en/cheatsheet.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) |
|
||||
| **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) |
|
||||
| **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) |
|
||||
|
||||
## Język w pigułce
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Zmienne | `x := 5` (typ wynika z wartości) albo `int x = 5`; `const W = 640` |
|
||||
| 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`… |
|
||||
| Funkcje | `fn dodaj(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)` |
|
||||
| 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 i, n in liczby { }` |
|
||||
| Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cyryl")`, `ile[slowo] += 1`, `for k, v in wiek { }` |
|
||||
| Błędy | `fn parsuj(str s) -> int! { … return error("zła liczba") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` |
|
||||
| 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)` |
|
||||
| Procesory | `--cpu native`; `@cpu("avx2") fn …` razem z `if cpu_has("avx2")`; liczby zmiennoprzecinkowe liczone tak, jak zapisane (`fma`/`muladd` do łączenia) |
|
||||
| Tekst | `"hp: \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
|
||||
| Sterowanie | `if` / `else if` / `else` (również jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` |
|
||||
| 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 |
|
||||
| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` |
|
||||
| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, tablice `[]T`, wskaźniki `ptr[T]` |
|
||||
| Funkcje | `fn add(int a, int b) -> int { return a + b }` |
|
||||
| Struktury | `struct Punkt { … }` z jednym polem na linijkę (`int x`, `int y = 0`); tworzenie przez `Punkt(1, 2)` albo `Punkt(x: 1)` |
|
||||
| Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for n in liczby { }` |
|
||||
| Tekst | `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` |
|
||||
| Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue`, `defer` |
|
||||
| Pamięć | automatyczna dla tablic, tekstów i struktur; `new(...)` / `free(...)` dla własnych struktur na stercie |
|
||||
| Moduły | `import ksztalty` → `ksztalty.pole(p)` |
|
||||
| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków |
|
||||
| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
|
||||
| Narzędzia | `finch run/build/ir`, `-g` dla gdb, `-O0` |
|
||||
|
||||
## 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/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/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 jednokierunkowa z `new`/`free` |
|
||||
| [`examples/types.fch`](examples/types.fch) | liczby o podanym rozmiarze i wskaźniki |
|
||||
| [`examples/structs.fch`](examples/structs.fch) | struktury, tablice struktur, lista wiązana z `new`/`free` |
|
||||
| [`examples/types.fch`](examples/types.fch) | liczby z rozmiarem i wskaźniki |
|
||||
| [`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 z OpenGL |
|
||||
| [`examples/window.fch`](examples/window.fch) | okno GLFW + OpenGL |
|
||||
| [`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/raylib.fch`](examples/raylib.fch) | raylib i struktury C przekazywane przez wartość |
|
||||
| [`examples/llvm.fch`](examples/llvm.fch) | Finch, który buduje LLVM IR przez API LLVM-C |
|
||||
| [`src/`](src/) | kompilator napisany w Finchu (plik startowy do jego zbudowania leży w [`bootstrap/`](bootstrap/)) |
|
||||
| [`examples/vulkan.fch`](examples/vulkan.fch) | Vulkan: lista kart graficznych, ich pamięć i kolejki, tworzenie urządzenia |
|
||||
| [`examples/raylib.fch`](examples/raylib.fch) | raylib ze strukturami C przez wartość |
|
||||
| [`examples/llvm.fch`](examples/llvm.fch) | Finch budujący LLVM IR przez API LLVM-C |
|
||||
| [`boot/`](boot/) | kompilator Fincha napisany w Finchu |
|
||||
|
||||
## Plan rozwoju
|
||||
|
||||
- [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] Czytelne błędy kompilacji; sprawdzanie w trakcie działania (dzielenie, `null`, zakres tablicy)
|
||||
- [x] Operatory bitowe z rozsądną kolejnością działań
|
||||
- [x] Nagłówki C przez libclang; `link` z pkg-config; podpowiedzi przy błędach linkera
|
||||
- [x] `struct`, w tym struktury C przekazywane przez wartość (ABI System V) i funkcje zwrotne
|
||||
- [x] Tablice z `.len`, sprawdzaniem zakresu i metodami; `for x in lista`
|
||||
- [x] Pamięć: wartości zwalniane na końcu bloku, `defer`, `new`/`free`
|
||||
- [x] Typy, w tym liczby z rozmiarem, wskaźniki, konwersje; funkcje; `if`/`while`/`for`
|
||||
- [x] Czytelne błędy kompilacji; kontrole w czasie działania (dzielenie, null, zakres)
|
||||
- [x] Operatory bitowe z rozsądnymi priorytetami
|
||||
- [x] Nagłówki C przez libclang; `link` z pkg-config; porady przy błędach linkowania
|
||||
- [x] `struct`, także struktury C przez wartość (ABI System V) i callbacki
|
||||
- [x] Tablice z `.len`, kontrolą zakresu i metodami; `for x in lista`
|
||||
- [x] Pamięć: zwalnianie na końcu bloku, `defer`, `new`/`free`
|
||||
- [x] Teksty: `+`, `.len`, indeksowanie, metody, `str(x)`, `int(s)`; `input()`; pliki
|
||||
- [x] Moduły Fincha (`import nazwa`)
|
||||
- [x] Informacje dla debuggera (`-g`)
|
||||
- [x] Bootstrap: kompilator Fincha napisany w Finchu, który kompiluje sam siebie
|
||||
- [x] Windows: `finch.exe`, programy `.exe`, ABI Microsoft x64; budowanie na Windowsa z Linuksa (`--target windows`)
|
||||
- [x] Serwer języka (`finch lsp`) i rozszerzenie do VS Code
|
||||
- [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate
|
||||
- [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
|
||||
- [x] Bootstrap: kompilator Fincha napisany w Finchu, który buduje sam siebie
|
||||
- [ ] Metody w strukturach, mapy, `match`
|
||||
- [ ] Błędy jako wartości zamiast zatrzymywania programu
|
||||
- [ ] Więcej platform (ARM64, macOS)
|
||||
|
||||
## Co gdzie leży
|
||||
## Struktura
|
||||
|
||||
```
|
||||
src/ kompilator w Finchu: lekser, parser, AST, generowanie kodu (LLVM IR jako tekst), import C (libclang)
|
||||
i konwencje wywołań C, serwer języka, polecenie finch
|
||||
std/ biblioteka standardowa w Finchu: json, path, time, proc, fs, thread
|
||||
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
|
||||
src/ kompilator: lekser, parser, AST, codegen, import C + ABI, sterownik (C++17, ok. 4,5 tys. linii)
|
||||
runtime/ mały runtime w C dołączany do każdego programu
|
||||
boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii)
|
||||
examples/ przykładowe programy
|
||||
editors/ rozszerzenie do VS Code, podświetlanie składni i konfiguracja LSP dla Kate
|
||||
tests/ run/ new/ (wzorcowe wyniki), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py
|
||||
docs/ przewodniki po angielsku (en/) i po polsku (pl/) dla trzech grup czytelników
|
||||
tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh
|
||||
docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
|
||||
```
|
||||
|
||||
## Licencja
|
||||
|
|
|
|||
94
boot/ast.fch
Normal file
94
boot/ast.fch
Normal 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
2161
boot/gen.fch
Normal file
File diff suppressed because it is too large
Load diff
210
boot/lexer.fch
Normal file
210
boot/lexer.fch
Normal 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
116
boot/main.fch
Normal 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
501
boot/parser.fch
Normal 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))
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
|
@ -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))"
|
||||
117510
bootstrap/linux-aarch64.ll
117510
bootstrap/linux-aarch64.ll
File diff suppressed because one or more lines are too long
117512
bootstrap/linux-x86_64.ll
117512
bootstrap/linux-x86_64.ll
File diff suppressed because one or more lines are too long
117504
bootstrap/macos-arm64.ll
117504
bootstrap/macos-arm64.ll
File diff suppressed because one or more lines are too long
117509
bootstrap/macos-x86_64.ll
117509
bootstrap/macos-x86_64.ll
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
117654
bootstrap/windows-x86_64.ll
117654
bootstrap/windows-x86_64.ll
File diff suppressed because one or more lines are too long
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
|
|
@ -53,9 +53,8 @@ The computer reads your program from top to bottom, line by line.
|
|||
|
||||
## 2. Getting Finch ready
|
||||
|
||||
Finch runs on **Linux**, **macOS** and **Windows** 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
|
||||
[guide for technicians](for-technicians.md#2-installing): it has a ready-made Finch for both.)
|
||||
Finch runs on **Linux** computers. If a teacher, parent or friend has already set it up
|
||||
for you, skip to the next section.
|
||||
|
||||
Otherwise, open the **Terminal** program (a window where you type commands) and type
|
||||
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
|
||||
cd Finch
|
||||
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.
|
||||
|
|
@ -77,7 +77,7 @@ To check it works, type:
|
|||
./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`.
|
||||
The [guide for technicians](for-technicians.md#10-arrays-and-maps) 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}
|
||||
}
|
||||
```
|
||||
The [guide for technicians](for-technicians.md#arrays) lists them all.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -678,36 +618,6 @@ fn main() {
|
|||
|
||||
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
|
||||
|
|
@ -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` |
|
||||
| `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 |
|
||||
| `'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
|
||||
}
|
||||
|
||||
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { } for i, n in nums { }
|
||||
// maps: ages := ["Ola": 9] ages["Jan"] = 10 ages["Ola"] ages.has("Tom") for name, age in ages { }
|
||||
// lists: nums := [1, 2, 3] nums.push(4) nums[0] nums.len for n in nums { }
|
||||
// text: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
|
||||
// ask: name := input("Name? ")
|
||||
// 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` |
|
||||
| **list / array** | Many values kept in order, like `[1, 2, 3]` |
|
||||
| **index** | The position of a value in a list, starting at 0 |
|
||||
| **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 |
|
||||
| **struct** | Your own kind of value, made of named parts |
|
||||
| **input** | What the person types into the program |
|
||||
| **error** | A message saying something is wrong, and where |
|
||||
| **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
|
|
@ -1,8 +1,8 @@
|
|||
# Finch dla początkujących
|
||||
|
||||
**Programowanie to dla ciebie coś zupełnie nowego? Ten przewodnik jest dla ciebie.**
|
||||
Nie musisz nic umieć: wystarczy matematyka ze szkoły podstawowej.
|
||||
Czytaj spokojnie, rozdział po rozdziale, i każdy przykład przepisuj sam: tak najlepiej zostaje w głowie.
|
||||
**Ten przewodnik jest dla ciebie, jeśli jeszcze nigdy nie pisałeś programu.**
|
||||
Nie potrzebujesz żadnego doświadczenia ani matematyki ponad to, czego uczy się w podstawówce.
|
||||
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)
|
||||
|
||||
|
|
@ -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)
|
||||
2. [Przygotowanie Fincha](#2-przygotowanie-fincha)
|
||||
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)
|
||||
6. [Rodzaje wartości](#6-rodzaje-wartości)
|
||||
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)
|
||||
17. [Małe projekty do zrobienia](#17-małe-projekty-do-zrobienia)
|
||||
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?
|
||||
|
||||
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.
|
||||
> 2. Wbij je do miski.
|
||||
> 3. Wymieszaj.
|
||||
|
||||
**Język programowania** to sposób zapisania takiego przepisu tak, żeby komputer go zrozumiał.
|
||||
**Finch** to jeden z takich języków. Powstał po to, żeby był mały i prosty, więc szybko się go nauczysz.
|
||||
**Język programowania** to sposób zapisania takiego przepisu, który komputer rozumie.
|
||||
**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
|
||||
|
||||
Finch działa na komputerach z **Linuksem**, **macOS** i **Windowsem**. Jeśli nauczyciel, rodzic albo znajomy
|
||||
już go dla ciebie przygotował, przejdź do następnego rozdziału. (Na Macu i na Windowsie poproś kogoś o skorzystanie
|
||||
z [przewodnika dla techników](dla-technikow.md#2-instalacja): są tam gotowe wersje Fincha na oba systemy.)
|
||||
Finch działa na komputerach z **Linuksem**. Jeśli nauczyciel, rodzic albo znajomy już go
|
||||
dla ciebie zainstalował, przejdź od razu do następnego rozdziału.
|
||||
|
||||
W przeciwnym razie otwórz program **Terminal** (okno, w którym wpisuje się polecenia) i wpisz po kolei te linie,
|
||||
naciskając po każdej **Enter**:
|
||||
W przeciwnym razie otwórz program **Terminal** (okienko, w którym wpisuje się polecenia)
|
||||
i wpisz te linijki po kolei, naciskając **Enter** po każdej:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Nexoniarz/Finch.git
|
||||
cd Finch
|
||||
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 na twoim komputerze nie ma `nix-shell`, poproś kogoś o skorzystanie z
|
||||
[przewodnika dla techników](dla-technikow.md#2-instalacja). Jest tam opisany inny sposób.)
|
||||
Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.
|
||||
(Jeśli komputer nie zna polecenia `nix-shell`, poproś kogoś o skorzystanie z
|
||||
[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
|
||||
./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
|
||||
|
||||
1. Otwórz dowolny edytor tekstu (np. *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*).
|
||||
2. Przepisz dokładnie to:
|
||||
1. Otwórz dowolny edytor tekstu (na przykład *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*).
|
||||
2. Wpisz dokładnie to:
|
||||
|
||||
```c
|
||||
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:
|
||||
|
||||
```sh
|
||||
./build/finch run hello.fch
|
||||
./build/finch run czesc.fch
|
||||
```
|
||||
|
||||
Zobaczysz:
|
||||
|
|
@ -103,7 +105,7 @@ Zobaczysz:
|
|||
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?
|
||||
|
||||
|
|
@ -113,26 +115,26 @@ fn main() { ← „tu zaczyna się główna część mojego programu
|
|||
} ← „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.
|
||||
- `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.
|
||||
Zawsze je dodawaj. To dobry nawyk.
|
||||
Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku
|
||||
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
|
||||
fn main() {
|
||||
print("Mam", 10, "lat")
|
||||
print("2 + 2 =", 2 + 2)
|
||||
print()
|
||||
print("Linia wyżej jest pusta")
|
||||
print("Linijka wyżej jest pusta")
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -142,10 +144,10 @@ Pokaże:
|
|||
Mam 10 lat
|
||||
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
|
||||
|
||||
|
|
@ -162,8 +164,8 @@ fn main() {
|
|||
|
||||
## 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,
|
||||
czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**.
|
||||
Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a potem sprawdzić,
|
||||
co jest w środku, czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**.
|
||||
|
||||
```c
|
||||
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
|
||||
fn main() {
|
||||
|
|
@ -195,39 +197,42 @@ Punkty: 5
|
|||
Punkty: 6
|
||||
```
|
||||
|
||||
`punkty = punkty + 1` czyta się 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`.
|
||||
`punkty = punkty + 1` czytamy tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”*.
|
||||
To samo można zapisać krócej: `punkty += 1`.
|
||||
|
||||
**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**.
|
||||
|
||||
### 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:
|
||||
`wiek`, `suma_punktow`, `gracz2` są w porządku, a `2gracz` nie.
|
||||
Polskich liter (`ą`, `ę`, `ż`…) nie wolno używać w nazwach, tylko w tekstach w cudzysłowie: `print("Łódź")` jest w porządku.
|
||||
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. `2gracz` już nie.
|
||||
|
||||
⚠️ **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`.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
| Typ | Co przechowuje | Przykłady |
|
||||
|---------|-----------------------------|--------------------------|
|
||||
| `int` | liczby całkowite | `0`, `42`, `-7` |
|
||||
| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` |
|
||||
| `str` | tekst | `"Cześć"`, `"Ola"` |
|
||||
| `bool` | tak albo nie | `true`, `false` |
|
||||
| `char` | jedną literę albo znak | `'A'`, `'z'`, `'?'` |
|
||||
| Typ | Co przechowuje | Przykłady |
|
||||
|---------|---------------------------|--------------------------|
|
||||
| `int` | liczby całkowite | `0`, `42`, `-7` |
|
||||
| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` |
|
||||
| `str` | tekst | `"Cześć"`, `"Ola"` |
|
||||
| `bool` | tak albo nie | `true` (prawda), `false` (fałsz) |
|
||||
| `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
|
||||
imie := "Ola" // str
|
||||
|
|
@ -236,7 +241,7 @@ wzrost := 1.42 // float
|
|||
lubi_koty := true // bool
|
||||
```
|
||||
|
||||
Typ można też napisać samemu, przed nazwą:
|
||||
Typ możesz też napisać sam, przed nazwą:
|
||||
|
||||
```c
|
||||
int wiek = 10
|
||||
|
|
@ -245,23 +250,24 @@ str imie = "Ola"
|
|||
|
||||
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.
|
||||
Finch się zatrzyma i ci o tym powie, a to pomaga wcześnie wyłapywać pomyłki.
|
||||
**Pudełko nie zmienia typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu.
|
||||
Finch zatrzyma się i powie o tym, co pomaga wcześnie wyłapywać pomyłki.
|
||||
|
||||
---
|
||||
|
||||
## 7. Liczenie
|
||||
|
||||
| Znak | Znaczenie | Przykład | Wynik |
|
||||
|------|--------------------------------|----------|--------|
|
||||
| `+` | dodawanie | `7 + 2` | `9` |
|
||||
| `-` | odejmowanie | `7 - 2` | `5` |
|
||||
| `*` | mnożenie | `7 * 2` | `14` |
|
||||
| `/` | dzielenie | `7 / 2` | `3` |
|
||||
| `%` | reszta z dzielenia | `7 % 2` | `1` |
|
||||
| Znak | Co robi | Przykład | Wynik |
|
||||
|------|-----------------------------|----------|-------|
|
||||
| `+` | dodawanie | `7 + 2` | `9` |
|
||||
| `-` | odejmowanie | `7 - 2` | `5` |
|
||||
| `*` | mnożenie | `7 * 2` | `14` |
|
||||
| `/` | dzielenie | `7 / 2` | `3` |
|
||||
| `%` | 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
|
||||
też jest całkowity (część po kropce znika). Jeśli chcesz `3.5`, użyj liczby z kropką: `7.0 / 2`.
|
||||
**Uwaga na dzielenie liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`. Obie liczby są
|
||||
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.
|
||||
Nawiasy zmieniają kolejność:
|
||||
|
|
@ -271,28 +277,28 @@ print(2 + 3 * 4) // 14
|
|||
print((2 + 3) * 4) // 20
|
||||
```
|
||||
|
||||
`%` przydaje się do sprawdzania, czy liczba jest parzysta: liczba parzysta dzielona przez 2 nie daje reszty,
|
||||
więc `n % 2` wynosi `0`.
|
||||
`%` przydaje się do sprawdzania, czy liczba jest parzysta. Liczba parzysta dzielona przez 2
|
||||
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
|
||||
|
||||
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
|
||||
fn main() {
|
||||
temperatura := 25
|
||||
|
||||
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
|
||||
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
|
||||
if temperatura > 25 {
|
||||
print("Upał!")
|
||||
print("Gorąco!")
|
||||
} else if temperatura > 15 {
|
||||
print("Przyjemnie")
|
||||
} else {
|
||||
|
|
@ -322,18 +329,18 @@ if temperatura > 25 {
|
|||
| `!=` | nie jest równe |
|
||||
| `<` | jest mniejsze niż |
|
||||
| `>` | jest większe niż |
|
||||
| `<=` | jest mniejsze albo równe |
|
||||
| `>=` | jest większe albo równe |
|
||||
| `<=` | jest mniejsze lub równe |
|
||||
| `>=` | jest większe lub 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ń
|
||||
|
||||
- `&&` 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 **nie**: zamienia prawdę w fałsz i odwrotnie. `if !pada { ... }`
|
||||
- `||` znaczy **lub**: wystarczy, że jeden jest prawdziwy. `if dzien == "sob" || dzien == "nd" { ... }`
|
||||
- `!` znaczy **nie**: odwraca prawdę i fałsz. `if !pada { ... }`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -359,7 +366,7 @@ Liczba 4
|
|||
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.
|
||||
|
||||
### Powtarzanie, dopóki coś jest prawdą: `while`
|
||||
|
|
@ -382,8 +389,8 @@ fn main() {
|
|||
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.
|
||||
Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać.
|
||||
⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się
|
||||
w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać.
|
||||
|
||||
### 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
|
||||
for i in 1..100 {
|
||||
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
|
||||
|
||||
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`.
|
||||
|
||||
```c
|
||||
|
|
@ -422,12 +429,12 @@ Cześć, Ola
|
|||
Cześć, Tomek
|
||||
```
|
||||
|
||||
`str imie` w nawiasach znaczy: *„ta funkcja potrzebuje jednego tekstu i w środku będziemy go nazywać `imie`”.*
|
||||
To, co wpiszesz w nawiasach przy jej użyciu, trafia do środka.
|
||||
`str imie` w nawiasie znaczy: *„ta funkcja potrzebuje jednego tekstu, a w środku nazywamy go `imie`”*.
|
||||
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
|
||||
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
|
||||
|
||||
Czasem jedno pudełko nie wystarcza. Na liście zakupów jest wiele rzeczy.
|
||||
**Lista** (programiści mówią też **tablica**) przechowuje wiele wartości po kolei:
|
||||
Czasem jedno pudełko nie wystarczy. Na liście zakupów jest wiele rzeczy.
|
||||
**Lista** (programiści mówią też **tablica**) trzyma wiele wartości po kolei:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
|
|
@ -464,34 +471,34 @@ Mam 3 owoce
|
|||
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.
|
||||
- `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:
|
||||
`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
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
[]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(7)
|
||||
print(wyniki) // [10, 25, 7]
|
||||
|
||||
ostatni := wyniki.pop() // zdejmij ostatni
|
||||
ostatni := wyniki.pop() // zabierz ostatni
|
||||
print(ostatni, wyniki) // 7 [10, 25]
|
||||
|
||||
wyniki.sort()
|
||||
wyniki.sort() // posortuj
|
||||
print(wyniki)
|
||||
}
|
||||
```
|
||||
|
||||
### Przechodzenie po liście
|
||||
|
||||
`for ... in` odwiedza po kolei każdą wartość:
|
||||
`for ... in` odwiedza każdą wartość po kolei:
|
||||
|
||||
```c
|
||||
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…,
|
||||
a druga to wartość:
|
||||
|
||||
```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}
|
||||
}
|
||||
```
|
||||
Listy umieją więcej: `contains` (czy zawiera), `find` (znajdź), `insert` (wstaw), `remove` (usuń),
|
||||
`sort` (sortuj), `reverse` (odwróć), `join` (połącz). Wszystkie opisuje [przewodnik dla techników](dla-technikow.md#tablice).
|
||||
|
||||
---
|
||||
|
||||
## 12. Zabawa tekstem
|
||||
|
||||
Teksty można sklejać przez `+`:
|
||||
Teksty sklejasz znakiem `+`:
|
||||
|
||||
```c
|
||||
fn main() {
|
||||
|
|
@ -593,16 +541,16 @@ fn main() {
|
|||
slowo := "Finch"
|
||||
print(slowo.len) // 5 liter
|
||||
print(slowo[0]) // F (pierwsza litera)
|
||||
print(slowo.upper()) // FINCH
|
||||
print(slowo.lower()) // finch
|
||||
print(slowo.contains("in")) // true
|
||||
print(slowo.upper()) // FINCH (wielkimi literami)
|
||||
print(slowo.lower()) // finch (małymi literami)
|
||||
print(slowo.contains("in")) // true (czy zawiera "in")
|
||||
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
|
||||
w rodzaju `can't turn "abc" into int` (nie da się zamienić "abc" na liczbę).
|
||||
Jeśli ktoś wpisze coś, co nie jest liczbą, program zatrzyma się z komunikatem
|
||||
w stylu `can't turn "abc" into int` („nie da się zamienić "abc" na int”).
|
||||
|
||||
---
|
||||
|
||||
## 14. Własne rodzaje wartości: struktury
|
||||
|
||||
Pomyśl o karcie w bibliotece: każda ma tytuł, autora i rok wydania.
|
||||
**Struktura** pozwala stworzyć własny rodzaj wartości z nazwanymi częściami:
|
||||
Wyobraź sobie kartę w bibliotece: każda karta ma tytuł, autora i rok.
|
||||
**Struktura** (`struct`) pozwala zrobić własny rodzaj wartości z nazwanymi częściami:
|
||||
|
||||
```c
|
||||
struct Ksiazka {
|
||||
|
|
@ -657,11 +605,11 @@ Hobbit napisał Tolkien
|
|||
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ę.
|
||||
- `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
|
||||
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)`.
|
||||
|
||||
### 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.
|
||||
Możesz też trzymać wiele struktur na liście: `[]Ksiazka polka`, a potem `polka.push(k)`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -724,14 +642,14 @@ fn main() {
|
|||
- `read_file(nazwa)` oddaje tekst z pliku.
|
||||
- `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
|
||||
|
||||
Każdy, kto programuje, robi błędy, nawet eksperci, codziennie.
|
||||
Finch bardzo się stara wyjaśnić prostymi słowami, co poszło nie tak. Przykład:
|
||||
Każdy się myli przy programowaniu, nawet eksperci, codziennie.
|
||||
Finch stara się tłumaczyć, co poszło nie tak, prostymi słowami (na razie po angielsku). Przykład:
|
||||
|
||||
```c
|
||||
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)
|
||||
| ^
|
||||
```
|
||||
|
||||
Jak to czytać:
|
||||
|
||||
- `hello.fch` to plik,
|
||||
- `3` to numer **linii**,
|
||||
- `11` mówi, jak daleko w linii (to **kolumna**),
|
||||
- dalej jest wyjaśnienie, a pod nim ta linia ze znakiem `^`, który wskazuje problem.
|
||||
- `czesc.fch` to plik,
|
||||
- `3` to numer **linijki**,
|
||||
- `11` to który znak w linijce (**kolumna**),
|
||||
- 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ć |
|
||||
|---|---|---|
|
||||
| `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 := ...` |
|
||||
| `a variable named 'x' already exists here` | Użyto `:=` dwa razy dla tej samej nazwy | Za drugim razem napisz `=` |
|
||||
| `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ż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 |
|
||||
| `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 `}` |
|
||||
| `a condition must be bool` | `if` potrzebuje pytania typu 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 |
|
||||
| `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` |
|
||||
| `can't turn "abc" into int` | `int(...)` dostało tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
|
||||
| `can't use '+' on str and int` | Tekst plus liczba | Użyj `str(...)` na liczbie |
|
||||
| `'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) |
|
||||
|
||||
### 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).)
|
||||
| `a condition must be bool` | `if` potrzebuje pytania tak/nie | Porównaj coś: `if x > 0` |
|
||||
| `unexpected character 'ż'` | Polska litera w nazwie | Zmień na zwykłą literę: `zolw` |
|
||||
| `runtime error: division by zero` | Program podzielił przez 0 w trakcie działania | Sprawdź liczbę przed dzieleniem |
|
||||
| `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 turn "abc" into int` | `int(...)` dostał tekst, który nie jest liczbą | Sprawdź, co wpisał użytkownik |
|
||||
| `can't use '+' on str and int` | Tekst plus liczba | Zamień liczbę na tekst przez `str(...)` |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
|
|
@ -844,7 +712,7 @@ fn main() {
|
|||
|
||||
### 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>
|
||||
|
||||
|
|
@ -863,7 +731,7 @@ fn main() {
|
|||
|
||||
### 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>
|
||||
|
||||
|
|
@ -885,8 +753,8 @@ fn main() {
|
|||
|
||||
### Projekt 4: Fizz Buzz
|
||||
|
||||
Policz od 1 do 20. Dla liczb podzielnych przez 3 powiedz `Fizz`, przez 5 `Buzz`,
|
||||
przez obie `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
|
||||
Policz od 1 do 20. Dla liczb podzielnych przez 3 napisz `Fizz`, przez 5 napisz `Buzz`,
|
||||
przez obie naraz napisz `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
|
||||
|
||||
<details><summary>Rozwiązanie</summary>
|
||||
|
||||
|
|
@ -909,7 +777,7 @@ fn main() {
|
|||
|
||||
### 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>
|
||||
|
||||
|
|
@ -932,8 +800,7 @@ fn main() {
|
|||
|
||||
### Projekt 6: Zgadnij liczbę
|
||||
|
||||
Komputer wybiera tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem
|
||||
mówi „więcej” albo „mniej”.
|
||||
Komputer ma tajną liczbę. Gracz zgaduje, aż trafi, a komputer za każdym razem mówi „więcej” albo „mniej”.
|
||||
|
||||
<details><summary>Rozwiązanie</summary>
|
||||
|
||||
|
|
@ -949,7 +816,7 @@ fn main() {
|
|||
} else if strzal > tajna {
|
||||
print("Mniej!")
|
||||
} else {
|
||||
print("Tak! Liczba prób:", proby)
|
||||
print("Tak! Potrzebowałeś", proby, "prób")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -961,7 +828,7 @@ fn main() {
|
|||
|
||||
### 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>
|
||||
|
||||
|
|
@ -991,9 +858,9 @@ fn main() {
|
|||
fn main() { // tu zaczyna się program
|
||||
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)
|
||||
x = 7 // zmiana zawartości pudełka
|
||||
x = 7 // zmień zawartość
|
||||
x += 1 // dodaj 1 do x
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
struct Zwierzak { // twój własny rodzaj wartości
|
||||
struct Zwierzak { // własny rodzaj wartości
|
||||
str imie
|
||||
int wiek = 1
|
||||
}
|
||||
|
||||
// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { } for i, 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")
|
||||
// listy: liczby := [1, 2, 3] liczby.push(4) liczby[0] liczby.len for n in liczby { }
|
||||
// tekst: "a" + "b" str(42) int("42") s.len s.upper() s.contains("x")
|
||||
// pytanie: imie := input("Imię? ")
|
||||
// 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) ... }
|
||||
// pliki: write_file("p.txt", tekst) read_file("p.txt")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Słowa, które będziesz słyszeć
|
||||
## 19. Słowniczek
|
||||
|
||||
| Słowo | Znaczenie |
|
||||
|---|---|
|
||||
| **program** | Lista kroków dla komputera |
|
||||
| **kod** | Tekst programu |
|
||||
| **uruchomić** | Kazać komputerowi wykonać kroki |
|
||||
| **skompilować** | Przetłumaczyć kod na coś, co komputer potrafi uruchomić. `finch run` robi to za ciebie |
|
||||
| **zmienna** | Pudełko z nazwą, w którym leży wartość |
|
||||
| **kompilować** | Przetłumaczyć kod na coś, co komputer umie uruchomić. `finch run` robi to za ciebie |
|
||||
| **zmienna** | Pudełko z nazwą, w którym jest wartość |
|
||||
| **typ** | Rodzaj wartości: liczba, tekst, tak/nie… |
|
||||
| **funkcja** | Nazwana grupa kroków, której można używać wiele razy |
|
||||
| **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]` |
|
||||
| **indeks** | Miejsce wartości na liście, liczone od 0 |
|
||||
| **struktura** | Twój własny rodzaj wartości z nazwanych części |
|
||||
| **metoda** | Polecenie należące do struktury, np. `g.trafiony()` |
|
||||
| **mapa** | Wartości wyszukiwane po kluczu (nazwie), jak w słowniku |
|
||||
| **wejście** (ang. *input*) | To, co ktoś wpisuje do programu |
|
||||
| **błąd** (ang. *error*) | Komunikat, że coś jest nie tak, i gdzie |
|
||||
| **indeks** | Pozycja wartości na liście, liczona od 0 |
|
||||
| **struktura** (struct) | Własny rodzaj wartości złożony z nazwanych części |
|
||||
| **wejście** (input) | To, co użytkownik wpisuje do programu |
|
||||
| **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie |
|
||||
| **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.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
@ -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"`.
|
||||
|
|
@ -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=""""" attribute="String" context="LongString" beginRegion="LongString"/>
|
||||
<DetectChar char=""" attribute="String" context="String"/>
|
||||
<DetectChar char="'" attribute="Char" context="Char"/>
|
||||
|
||||
<WordDetect String="fn" attribute="Keyword" context="FunctionName"/>
|
||||
<RegExpr String="\btest(?=\s*")" attribute="Keyword"/>
|
||||
<RegExpr String="(?<=\s)as(?=\s+[A-Za-z_])" attribute="Import"/>
|
||||
<WordDetect String="struct" attribute="Keyword" context="StructName"/>
|
||||
<keyword String="control" attribute="Control Flow"/>
|
||||
<keyword String="imports" attribute="Import"/>
|
||||
<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=">" attribute="Operator"/>
|
||||
<Detect2Chars char="." char1="." attribute="Operator"/>
|
||||
<AnyChar String="+-*/%=<>!&|^~" 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\\"']" attribute="Escape"/>
|
||||
<RegExpr String="\\." attribute="Error"/>
|
||||
<DetectChar char=""" attribute="String" context="#pop"/>
|
||||
</context>
|
||||
|
||||
<context name="LongString" attribute="String" lineEndContext="#stay">
|
||||
<RegExpr String="\\\(" attribute="Escape" context="Interpolation"/>
|
||||
<RegExpr String="\\[ntr0\\"']" attribute="Escape"/>
|
||||
<StringDetect String=""""" attribute="String" context="#pop" endRegion="LongString"/>
|
||||
</context>
|
||||
|
||||
<!-- "hp \(hp + 1)": code inside text, up to the ) that closes \( -->
|
||||
<context name="Interpolation" attribute="Normal Text" lineEndContext="#stay">
|
||||
<DetectChar char=")" attribute="Escape" context="#pop"/>
|
||||
<DetectChar char="(" attribute="Symbol" context="InterpolationParen"/>
|
||||
<IncludeRules context="Normal"/>
|
||||
</context>
|
||||
<context name="InterpolationParen" attribute="Normal Text" lineEndContext="#stay">
|
||||
<DetectChar char=")" attribute="Symbol" context="#pop"/>
|
||||
<DetectChar char="(" attribute="Symbol" context="InterpolationParen"/>
|
||||
<IncludeRules context="Normal"/>
|
||||
</context>
|
||||
|
||||
<context name="Char" attribute="Char" lineEndContext="#pop">
|
||||
<RegExpr String="\\[ntr0\\"']" attribute="Escape"/>
|
||||
<DetectChar char="'" attribute="Char" context="#pop"/>
|
||||
</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>
|
||||
|
|
@ -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'"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"servers": {
|
||||
"finch": {
|
||||
"command": ["finch", "lsp"],
|
||||
"url": "https://github.com/Nexoniarz/Finch",
|
||||
"highlightingModeRegex": "^Finch$"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
editors/vscode/.gitignore
vendored
2
editors/vscode/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
node_modules/
|
||||
*.vsix
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.vscodeignore
|
||||
test/**
|
||||
node_modules/.package-lock.json
|
||||
node_modules/vscode-textmate/**
|
||||
node_modules/vscode-oniguruma/**
|
||||
node_modules/@vscode/**
|
||||
**/*.map
|
||||
.gitignore
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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" } }
|
||||
}
|
||||
3957
editors/vscode/package-lock.json
generated
3957
editors/vscode/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
1
notatka.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Kupić mleko
|
||||
1
note.txt
Normal file
1
note.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Buy milk
|
||||
|
|
@ -2,36 +2,20 @@
|
|||
// 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.
|
||||
//
|
||||
// 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
|
||||
// cap > 0 : heap memory owned by the value (cap bytes allocated)
|
||||
// cap == 0 : a string literal, valid forever, never written to
|
||||
// cap == -1: borrowed from C, must be copied before it is kept
|
||||
// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0
|
||||
// map[K]V: see "maps" below
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <pthread.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char *ptr;
|
||||
|
|
@ -49,52 +33,10 @@ static char empty[1] = "";
|
|||
|
||||
// ---------- 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) {
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s:%lld: runtime error: %s", file, (long long)line, msg);
|
||||
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) {
|
||||
|
|
@ -106,7 +48,8 @@ __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t
|
|||
static void *must(void *p) {
|
||||
if (!p) {
|
||||
fflush(stdout);
|
||||
fatal("runtime error: out of memory");
|
||||
fprintf(stderr, "runtime error: out of memory\n");
|
||||
exit(1);
|
||||
}
|
||||
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_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }
|
||||
|
||||
// ---------- assert and finch test ----------
|
||||
|
||||
__attribute__((noreturn, cold)) void finch_assert_failed(const char *file, int64_t line, const FStr *why) {
|
||||
char msg[600];
|
||||
if (why->len) snprintf(msg, sizeof msg, "assert failed: %.*s", (int)(why->len > 500 ? 500 : why->len), why->ptr);
|
||||
else snprintf(msg, sizeof msg, "assert failed");
|
||||
finch_panic(file, line, msg);
|
||||
}
|
||||
|
||||
// Each test runs on its own: a runtime error (a failed assert too) ends that test, and the next one runs.
|
||||
static jmp_buf test_jump;
|
||||
static char test_message[1100];
|
||||
static int64_t tests_passed, tests_failed;
|
||||
|
||||
static void test_fatal(const char *msg) {
|
||||
snprintf(test_message, sizeof test_message, "%s", msg);
|
||||
longjmp(test_jump, 1);
|
||||
}
|
||||
|
||||
void finch_test_run(const char *name, void (*fn)(void)) {
|
||||
void (*saved)(const char *) = fatal_hook;
|
||||
void *chain = trace_top ? trace_top() : NULL;
|
||||
test_message[0] = 0;
|
||||
fatal_hook = test_fatal;
|
||||
int failed = setjmp(test_jump);
|
||||
if (!failed) fn();
|
||||
fatal_hook = saved;
|
||||
if (trace_set) trace_set(chain); // a test that stopped early left its calls on the chain
|
||||
fflush(stdout);
|
||||
if (failed) {
|
||||
tests_failed++;
|
||||
printf("FAIL %s\n %s\n", name, test_message);
|
||||
} else {
|
||||
tests_passed++;
|
||||
printf("ok %s\n", name);
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int finch_test_summary(void) {
|
||||
printf("\n%lld passed, %lld failed\n", (long long)tests_passed, (long long)tests_failed);
|
||||
return tests_failed ? 1 : 0;
|
||||
}
|
||||
|
||||
// ---------- strings ----------
|
||||
|
||||
static void str_heap(FStr *out, const char *p, int64_t len) {
|
||||
|
|
@ -237,136 +136,43 @@ void finch_str_from_uint(FStr *out, uint64_t v) {
|
|||
str_heap(out, buf, n);
|
||||
}
|
||||
|
||||
// The shortest text that reads back as the same number (as an f32 when `single`):
|
||||
// 0.1 -> "0.1", 1.0/3 -> "0.3333333333333333", 1e8 -> "100000000", 1e20 -> "1e+20".
|
||||
static int fmt_float(char *buf, size_t size, double v, int single) {
|
||||
if (isnan(v)) return snprintf(buf, size, "nan");
|
||||
if (isinf(v)) return snprintf(buf, size, v < 0 ? "-inf" : "inf");
|
||||
int p = 1;
|
||||
for (; p < 17; p++) {
|
||||
snprintf(buf, size, "%.*e", p - 1, v);
|
||||
double back = strtod(buf, NULL);
|
||||
if (single ? (float)back == (float)v : back == v) break;
|
||||
}
|
||||
int exp = atoi(strchr(buf, 'e') + 1);
|
||||
if (exp < -5 || exp >= 17) return snprintf(buf, size, "%.*g", p, v);
|
||||
int decimals = p - 1 - exp;
|
||||
return snprintf(buf, size, "%.*f", decimals > 0 ? decimals : 0, v);
|
||||
}
|
||||
|
||||
void finch_str_from_float(FStr *out, double v) {
|
||||
char buf[64];
|
||||
int n = fmt_float(buf, sizeof buf, v, 0);
|
||||
int n = snprintf(buf, sizeof buf, "%g", v);
|
||||
str_heap(out, buf, n);
|
||||
}
|
||||
|
||||
void finch_str_from_f32(FStr *out, double v) {
|
||||
char buf[64];
|
||||
int n = fmt_float(buf, sizeof buf, v, 1);
|
||||
str_heap(out, buf, n);
|
||||
}
|
||||
|
||||
// str(x, 2): a fixed number of digits after the point
|
||||
void finch_str_fixed(FStr *out, double v, int64_t digits) {
|
||||
char buf[400];
|
||||
if (digits < 0) digits = 0;
|
||||
if (digits > 60) digits = 60;
|
||||
int n = snprintf(buf, sizeof buf, "%.*f", (int)digits, v);
|
||||
str_heap(out, buf, n);
|
||||
}
|
||||
|
||||
void finch_print_float(double v, int32_t single) {
|
||||
char buf[64];
|
||||
fmt_float(buf, sizeof buf, v, single);
|
||||
fputs(buf, stdout);
|
||||
}
|
||||
|
||||
void finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }
|
||||
|
||||
void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); }
|
||||
|
||||
static void number_error(char *msg, size_t size, const FStr *s, const char *kind) {
|
||||
snprintf(msg, size, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr ? s->ptr : "", kind);
|
||||
static void bad_number(const FStr *s, const char *kind, const char *file, int64_t line) {
|
||||
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) {
|
||||
const char *p = s->ptr ? s->ptr : "";
|
||||
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
|
||||
const char *p = s->ptr;
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
char *end;
|
||||
errno = 0;
|
||||
long long v = strtoll(p, &end, 10);
|
||||
while (isspace((unsigned char)*end)) end++;
|
||||
*out = v;
|
||||
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);
|
||||
}
|
||||
if (end == p || *end || errno == ERANGE) bad_number(s, "int", file, line);
|
||||
return v;
|
||||
}
|
||||
|
||||
double finch_str_to_float(const FStr *s, const char *file, int64_t line) {
|
||||
double v;
|
||||
if (!parse_float(s, &v)) {
|
||||
char msg[200];
|
||||
number_error(msg, sizeof msg, s, "float");
|
||||
finch_panic(file, line, msg);
|
||||
}
|
||||
const char *p = s->ptr;
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
char *end;
|
||||
double v = strtod(p, &end);
|
||||
while (isspace((unsigned char)*end)) end++;
|
||||
if (end == p || *end) bad_number(s, "float", file, line);
|
||||
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) {
|
||||
if (start < 0 || end > s->len || start > end) {
|
||||
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); }
|
||||
|
||||
// ---------- 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 ----------
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
// after main: the arguments go
|
||||
void finch_args_free(FArr *a) {
|
||||
for (int64_t i = 0; i < a->len; i++) finch_str_drop((FStr *)a->ptr + i);
|
||||
finch_arr_free(a);
|
||||
}
|
||||
|
||||
// input("Name? ") prints the prompt and reads one line, without the newline.
|
||||
void finch_input(FStr *out, const FStr *prompt) {
|
||||
if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);
|
||||
fflush(stdout);
|
||||
// read one line of any length (getline isn't available everywhere)
|
||||
size_t size = 128, n = 0;
|
||||
char *line = finch_alloc((int64_t)size);
|
||||
int c, got = 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) {
|
||||
char *line = NULL;
|
||||
size_t size = 0;
|
||||
ssize_t n = getline(&line, &size, stdin);
|
||||
if (n < 0) {
|
||||
free(line);
|
||||
str_static(out, empty);
|
||||
return;
|
||||
}
|
||||
while (n > 0 && line[n - 1] == '\r') n--;
|
||||
line[n] = 0;
|
||||
while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = 0;
|
||||
out->ptr = line;
|
||||
out->len = (int64_t)n;
|
||||
out->len = n;
|
||||
out->cap = (int64_t)size;
|
||||
}
|
||||
|
||||
|
|
@ -666,27 +399,8 @@ int finch_file_exists(const FStr *path) {
|
|||
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) {
|
||||
FILE *f = fopen(path->ptr ? path->ptr : "", "rb");
|
||||
FILE *f = fopen(path->ptr, "rb");
|
||||
if (!f) {
|
||||
char msg[300];
|
||||
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) {
|
||||
fflush(stdout);
|
||||
int status = system(cmd->ptr ? cmd->ptr : "");
|
||||
#ifdef _WIN32
|
||||
return status; // Windows gives the exit code directly
|
||||
#else
|
||||
if (status == -1) return -1;
|
||||
if (WIFEXITED(status)) return WEXITSTATUS(status);
|
||||
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) {
|
||||
FILE *f = fopen(path->ptr ? path->ptr : "", "wb");
|
||||
FILE *f = fopen(path->ptr, "wb");
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ pkgs.mkShell {
|
|||
packages = with pkgs; [
|
||||
# building Finch (the clang wrapper first, so `clang` finds the system headers)
|
||||
llvmPackages.clang
|
||||
llvmPackages.llvm
|
||||
llvmPackages.libclang # reads C headers for `import "stdio.h"`
|
||||
cmake
|
||||
ninja
|
||||
pkg-config # finds the right flags for `link "..."`
|
||||
python3 # tests/lsp_test.py
|
||||
|
||||
# for the graphics examples (window, opengl, raylib, vulkan)
|
||||
glfw
|
||||
|
|
@ -15,7 +17,4 @@ pkgs.mkShell {
|
|||
vulkan-headers
|
||||
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
309
src/abi.cpp
Normal 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> ¶ms, 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> ¶ms) {
|
||||
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
|
||||
172
src/ast.fch
172
src/ast.fch
|
|
@ -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
312
src/ast.h
Normal 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
367
src/cimport.cpp
Normal 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;
|
||||
}
|
||||
792
src/cimport.fch
792
src/cimport.fch
|
|
@ -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
56
src/cimport.h
Normal 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
1092
src/codegen.cpp
Normal file
File diff suppressed because it is too large
Load diff
15
src/codegen.h
Normal file
15
src/codegen.h
Normal 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
715
src/codegen_builtins.cpp
Normal 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
265
src/codegen_impl.h
Normal 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
501
src/codegen_types.cpp
Normal 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
|
||||
162
src/docs.fch
162
src/docs.fch
|
|
@ -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
40
src/error.h
Normal 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); }
|
||||
224
src/fmt.fch
224
src/fmt.fch
|
|
@ -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
|
||||
}
|
||||
7741
src/gen.fch
7741
src/gen.fch
File diff suppressed because it is too large
Load diff
98
src/host.fch
98
src/host.fch
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
212
src/lexer.cpp
Normal 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 "?";
|
||||
}
|
||||
470
src/lexer.fch
470
src/lexer.fch
|
|
@ -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
35
src/lexer.h
Normal 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);
|
||||
548
src/linker.fch
548
src/linker.fch
|
|
@ -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
|
||||
}
|
||||
948
src/lsp.fch
948
src/lsp.fch
|
|
@ -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
387
src/main.cpp
Normal 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;
|
||||
}
|
||||
531
src/main.fch
531
src/main.fch
|
|
@ -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
|
||||
}
|
||||
243
src/manifest.fch
243
src/manifest.fch
|
|
@ -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
531
src/parser.cpp
Normal 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();
|
||||
}
|
||||
929
src/parser.fch
929
src/parser.fch
|
|
@ -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
7
src/parser.h
Normal 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);
|
||||
120
src/platform.fch
120
src/platform.fch
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
70
std/fs.fch
70
std/fs.fch
|
|
@ -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
|
||||
}
|
||||
518
std/json.fch
518
std/json.fch
|
|
@ -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
|
||||
}
|
||||
128
std/path.fch
128
std/path.fch
|
|
@ -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
|
||||
}
|
||||
48
std/proc.fch
48
std/proc.fch
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
88
std/time.fch
88
std/time.fch
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
50
tests/boot.sh
Executable 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 ]
|
||||
|
|
@ -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 ]
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// expect: @cpu: 'avx3' isn't a feature Finch knows
|
||||
@cpu("avx2,avx3")
|
||||
fn f() {
|
||||
}
|
||||
fn main() {
|
||||
f()
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue