Compare commits

..
Author SHA1 Message Date
Nexoniarz
b24a7845ef Merge embed: export fn, --shared, -I and the two limit fixes were ported to the Finch-written compiler
The C++ sources those commits changed no longer exist on this branch; their tests (early_exit, map_add_while_looping,
shared) are in tests/ and pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-26 00:37:21 +01:00
Nexoniarz
b53e0910d8 The compiler runs on Windows (and has seeds for macOS)
Windows: the compiler made from bootstrap/windows-x86_64.ll runs under Wine, compiles itself to the same IR, builds an
exe of itself, and every test program passes through it. It needed: Windows-sized unsigned long for libclang's unsaved
file, no 'cross' when Windows builds for Windows, --target passed to libclang (libclang.dll assumes MSVC), the MinGW
triple always in the IR, FINCH_LDFLAGS, .exe names and FINCH_LLVM_INCLUDE in bootstrap/build.sh, winseed.sh/winbuild.sh.
macOS: seeds for arm64 and x86-64 made with the 11.3 SDK; IR is accepted by clang, nothing has run on a Mac yet.
CI gets a windows and a macos job. UNTESTED on real Windows/macOS runners.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-26 00:37:06 +01:00
Nexoniarz
3e45b62812 Enums whose variants hold values
enum Shape { Circle(float), Rect(float, float), Point }; Shape.Circle(2.0) makes one, match names the values
(Circle(r), _ to ignore), exhaustive like plain enums, printing shows Rect(2, 3.5). Underneath it is a struct (tag +
every variant's values), so copying, freeing, methods and valgrind-cleanliness come with it. Also: stray note files removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-26 00:18:53 +01:00
Nexoniarz
dc0334fa0e Programs built by the Finch compiler free their memory
gen.fch never dropped anything (it was the bootstrap compiler); now it does what the C++ one did: owned variables are
dropped when their scope ends (defers interleaved, newest first), return moves a local out, assignments drop the old
value, temporaries nobody keeps are released, values an expression holds are dropped when try/or leave early,
clear/resize/remove/map.clear drop what goes, free() drops the pointee. The compiler itself now frees as it goes.

MEMCHECK=1 tests/run.sh: 108 passed under valgrind with --leak-check=full.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 22:45:39 +01:00
Nexoniarz
837ad45177 The compiler carries its runtime and std inside itself; host module for the system it runs on
src/embedded.fch (made by tools/pack.fch, checked by tests/run.sh) holds runtime/finch_rt.c and std/; when they aren't
beside the binary they're written to the cache folder once. All the sh/cmd specifics of the driver (quoting, temp dir,
mkdir, rm, mv, null device, path separator, cache dir, binary stdio) moved into host.fch / host_windows.fch.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 22:32:02 +01:00
Nexoniarz
a0c54a1252 docs: the engineers' guide describes the compiler written in Finch
Rewritten (EN+PL): architecture, source tree, front end, code generation, C import and ABI, driver, bootstrapping and the
language server. The type, ownership and semantics sections are kept; the C++ function reference is gone (branch
before-bootstrap has that version).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 21:55:42 +01:00
Nexoniarz
81df9f8c3e Install, CI, seeds and docs for the compiler written in Finch
bootstrap/install.sh and a share/finch lookup for the runtime and std; an ARM64 Linux seed; bootstrap/README.md;
CI builds from the seed on x64 and ARM64 and runs every test on Windows (Wine) and ARM64 (qemu); README, install and
layout sections of the guides (EN+PL) no longer talk about CMake; editors know enum, match and export.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
2026-10-25 21:53:50 +01:00
Nexoniarz
636eaefe59 finch lsp: the language server, written in Finch
Hover, go to definition, completion (after a dot too: fields, methods, a module's names, an enum's variants,
array/text/map methods), signature help, the outline and errors while typing -- everything the C++ one did,
and the same tests/lsp_test.py passes against it.

It doesn't type-check by itself: for every change it runs  finch index  on the editor's text (--overlay says
which file the text stands in for) and reads what the compiler learned about every name, which is also what
the compiler's own error messages are made of. Half-typed code keeps using the last version that compiled.
src/docs.fch is the one description of the built-ins that hover and completion share.

tests/run.sh runs the LSP test too. 107 passed.
2026-10-25 21:48:11 +01:00
Nexoniarz
b9378050ab The compiler is written in Finch, and nothing else: src/*.fch, built from a seed
boot/ is src/ now. The C++ compiler and CMake are gone from this branch (they are on before-bootstrap).
bootstrap/build.sh builds the compiler from bootstrap/<system>.ll -- the compiler's own LLVM IR -- then builds
it with itself and checks the two come out identical. bootstrap/build.sh seed writes the seed anew.

The driver is complete: the runtime is compiled once and kept in ~/.cache/finch; a program's own C files,
.o and .a files and frameworks link along; a linker failure is read and answered with  link "lib"  or the
library that is missing. The old tests/fail diagnostics are all in again (a missing import, a str compared with
null, 'write just the library's name', a map key that is a float, non-ASCII names, ...).

tests/run.sh is the one test runner: programs, refusals, -g builds and the shared library. 106 passed.
2026-10-25 21:42:17 +01:00
Nexoniarz
750eca3e85 --target: windows, linux, arm64, macos or a triple, in the compiler written in Finch
Targets, the target's C compiler (FINCH_CC) for linking, C import for the target (--target to libclang, its
headers), the AAPCS64 and Microsoft x64 ways of passing structs by value (and the System V one),
dllexport for libraries on Windows, .exe names, and running the program through wine or qemu-aarch64.
std/ gets time_windows.fch and proc_windows.fch: a module has a version per system (name_windows.fch,
name_macos.fch, name_linux.fch) that wins over name.fch. stdin, stdout and stderr work where C makes them macros.

Run for real: all 27 programs under tests/run and tests/new pass on ARM64 Linux under qemu and on Windows
under Wine (MinGW), structs by value and callbacks included. macOS is only checked as far as clang accepts the
IR; there is no emulator for it here.
2026-10-25 21:33:19 +01:00
Nexoniarz
8c3b5b15b9 -g: debug info, in the compiler written in Finch
DWARF as LLVM metadata: a compile unit, a subprogram per function, a location on every call, a variable
for each local and parameter with types a debugger can open (structs with their fields, arrays and text
as ptr/len/cap, maps, enums with their variants, pointers, fixed arrays). Methods show self.

Use it with -O0, as the docs say: the optimizer inlines the program into main, which has no debug info.
boot.sh builds ten programs with -g -O0 and checks they still behave. 51 passed.
2026-10-25 21:08:13 +01:00
Nexoniarz
dd9cbe7611 export fn and finch build --shared, in the compiler written in Finch; FINCH_PATH, FINCH_RT, FINCH_CC
export fn name(...) is a C function of a library; build --shared makes lib<name>.so with no main and a
finch_module_on_fatal(handler) the host can take runtime errors over with. Ported from the embed work (the
runtime, its test and the docs are that work's; the compiler side is new).

The driver reads FINCH_PATH (more module folders), FINCH_RT (the runtime's file) and FINCH_CC (the clang).
tests/boot.sh also runs tests/shared.sh with the compiler under test. 41 passed.
2026-10-25 21:03:05 +01:00
Nexoniarz
94e41484dd docs: enums, match, generics and the standard library (English and Polish) 2026-10-25 20:59:38 +01:00
Nexoniarz
ea6d2a57e5 a standard library in Finch: path, time, proc, json
import json   import path   import time   import proc  -- found in std/ after the program's own files,
so a program's own json.fch always wins.

path: join, base, dir, ext, stem, parts, clean, is_abs.
time: now, millis, sleep, date, local, format, days_in_month (clock_gettime, strftime through C import).
proc: run, output (an error with the command's output when it fails), env, pid, cwd.
json: parse (errors with line and column), stringify, pretty, new_*, get/at/set/push/has/names and
str_or/int_or/float_or/bool_or; \u escapes with surrogate pairs.

The C importer now leaves a plain (non-const) char * parameter a ptr[char], a buffer C writes into;
only const char * is text going in. str(ptr) and str([]u8) are in the self-hosted compiler.

boot.sh: 40 passed, 0 failed.
2026-10-25 20:58:26 +01:00
Nexoniarz
04eb0cce55 generics (in the compiler written in Finch)
fn max[T](T a, T b) -> T      struct Stack[T] { []T items }      fn Stack[T].push(T x)

The type is worked out from the arguments (max(3, 4), first([1, 2])) and written out
where it cannot be: max[float](1, 2.5), Stack[int](), Pair[str, int](...). Each use makes
its own copy, so a generic costs nothing at run time. Several parameters
(Pair[A, B]), nesting (Stack[Pair[int, str]]), methods, and printing (Stack[int](items: ...))
work. An error inside a generic says which version it was building.

boot.sh: 37 passed, 0 failed (stage 2 and 3 identical).
2026-10-25 20:53:13 +01:00
Nexoniarz
609ceffe58 enums and match (in the compiler written in Finch)
enum Color { Red, Green, Blue }   enum Status { Ok = 200, NotFound = 404 }

Color.Red, Color(n) (checked), int(c), == and != only, printed by name, usable as
map keys and struct fields, with methods (fn Color.next()).

match value { Red, Green -> ... Blue -> ... else -> ... } as a statement, or as an
expression that gives a value. Works on enums, whole numbers, chars, text and bools;
a match on an enum has to handle every variant or end with else, and says which are
missing. A bare variant name (Red) is enough inside a match on an enum.

Only the self-hosted compiler has them; tests/new holds their tests, tests/run stays what
the C++ compiler also runs. boot.sh: 31 passed.
2026-10-25 20:49:04 +01:00
Nexoniarz
a5ba4bc8f0 boot: finch run, version, -O0, -l, -I; clear errors for -g and --target
The self-hosted driver now has the everyday commands. -g (debug info), --target
(Windows, ARM64, macOS) and lsp are still only in the C++ compiler, and say so.
2026-10-25 19:49:04 +01:00
Nexoniarz
04d5486477 boot: structs by value (System V), addr(fn) callbacks -- the compiler builds itself
C structs travel as C passes them: up to 16 bytes in INTEGER/SSE registers per eightbyte,
bigger ones byval and through sret, out-of-registers structs on the stack. addr(fn) makes a
C-callable thunk that rebuilds the arguments. With those, the self-hosted compiler can
compile boot/cimport.fch, which calls libclang with CXCursor and CXString by value and gives
it callbacks -- so it compiles itself.

boot.sh: stage 2 and stage 3 are identical (57904 lines of IR); 22 passed, 0 failed,
0 skipped. Only x86-64 System V so far; Windows and ARM64 conventions are still in C++.
2026-10-25 19:47:20 +01:00
Nexoniarz
13ef63a586 boot: C import (import "x.h", link, C functions, constants, globals, .ptr, ptr(x))
The self-hosted compiler reads headers with boot/cimport.fch and compiles calls to C
functions (variadics, char* as str, C's default promotions), enum and #define
constants, extern variables like stdout, C structs laid out as C laid them (padding
and fixed arrays, by pointer), .ptr of a str or array, ptr(x) and int(ptr). link
"lib" asks pkg-config, link "x.c" compiles the file along.

cimport and c_pointers pass. Not yet: structs by value (the calling convention) and
addr(fn) for C callbacks, which is also what stops it building itself.

boot.sh now says so when stage 2 can't be made and runs the tests on stage 1.
2026-10-25 19:44:58 +01:00
Nexoniarz
f71ae8b1ef boot: cimport.fch, the C header importer written in Finch on libclang
Reads functions (with variadics and why one is unsupported), enum and #define
constants, extern variables and structs with their layout, the same as
src/cimport.cpp. It calls libclang through Finch's own C import, so compiling
it is what the bootstrap will have to do. Checked with the C++ compiler:
stdio.h gives 90 functions and 347 constants; EOF is -1, stdout is ptr[C.FILE].
2026-10-25 19:40:50 +01:00
Nexoniarz
5f2ba99f2d -I <dir>: look for imported C headers there too (an SDK's headers)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:20:42 +02:00
Nexoniarz
68637a35a7 export fn and finch build --shared: libraries a C program loads (game scripts)
export fn name(...) is a C function by that name (numbers, bool, char, pointers in and out).
--shared builds lib<name>.so / <name>.dll with no main. finch_module_on_fatal(handler) lets the
host take runtime errors over (longjmp out) instead of the process exiting.
tests/shared.sh: a C host loads one, calls it and survives an index error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:14:09 +02:00
Nexoniarz
f9838dfe03 Fix the two known limits: adding map keys while looping over the map is a runtime error; temporaries held by an expression are freed when try / or-blocks leave early. err usable after or, inner err shadows outer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 01:26:53 +02:00
109 changed files with 595774 additions and 9410 deletions

View file

@ -1,14 +1,15 @@
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. Seeds: Linux x86-64 and ARM64 (tested here), Windows x86-64 (tested under Wine, and here),
# macOS ARM64 and x86-64 (made with Apple's headers; only checked as IR until the macos job below passes).
on:
push:
branches: [main, "ci/**"]
tags: ["v*"]
pull_request:
env:
LLVM_VERSION: 21.1.8
jobs:
linux:
strategy:
@ -21,75 +22,100 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install LLVM 21, libclang, tools
- 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 llvm-21-dev libclang-21-dev ninja-build valgrind
sudo apt-get install -y libclang-21-dev pkg-config valgrind
echo "/usr/lib/llvm-21/bin" >> $GITHUB_PATH
- name: Build
- name: Build the compiler (seed, then itself, and the two must agree)
run: |
cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm -DCMAKE_BUILD_TYPE=Release
ninja -C build
export FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21"
bootstrap/build.sh
build/finch version
- name: Tests
- 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: Self-hosting
run: tests/boot.sh
- name: Language server
run: python3 tests/lsp_test.py
- name: Package
run: |
mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/
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:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Install LLVM 21
- name: Install LLVM 21 (libclang)
run: brew install llvm@21 pkg-config
- name: Build the compiler
run: |
brew install llvm@21 ninja
echo "$(brew --prefix llvm@21)/bin" >> $GITHUB_PATH
- name: Build
run: |
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
ninja -C build
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: CC=/usr/bin/clang tests/run.sh
- name: Self-hosting
run: CC=/usr/bin/clang tests/boot.sh
- name: Language server
run: python3 tests/lsp_test.py
- name: Apple frameworks
run: |
cat > fw.fch <<'EOF'
import "CoreFoundation/CoreFoundation.h"
link "CoreFoundation.framework"
run: tests/run.sh
fn main() {
print(CFAbsoluteTimeGetCurrent() > 0.0)
}
EOF
out=$(CC=/usr/bin/clang build/finch run fw.fch)
echo "$out"
test "$out" = "true"
- name: Package
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: |
mkdir -p dist/finch && cp build/finch README.md LICENSE dist/finch/ && cp -r examples dist/finch/
tar -czf finch-macos-arm64.tar.gz -C dist finch
- uses: actions/upload-artifact@v4
with:
name: finch-macos-arm64
path: finch-macos-arm64.tar.gz
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
vscode:
runs-on: ubuntu-24.04
@ -110,51 +136,3 @@ jobs:
with:
name: finch-vscode
path: editors/vscode/finch-lang.vsix
windows:
runs-on: windows-2022
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
- name: Cache LLVM
id: cache-llvm
uses: actions/cache@v4
with:
path: C:/llvm
key: llvm-${{ env.LLVM_VERSION }}-x86_64-pc-windows-msvc
- name: Download LLVM ${{ env.LLVM_VERSION }}
if: steps.cache-llvm.outputs.cache-hit != 'true'
run: |
curl -sSL -o llvm.tar.xz "https://github.com/llvm/llvm-project/releases/download/llvmorg-$LLVM_VERSION/clang+llvm-$LLVM_VERSION-x86_64-pc-windows-msvc.tar.xz"
mkdir -p /c/llvm
tar -xf llvm.tar.xz -C /c/llvm --strip-components=1
rm llvm.tar.xz
- name: Build finch.exe
run: |
echo "C:/llvm/bin" >> $GITHUB_PATH
export PATH="/c/llvm/bin:$PATH"
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR=C:/llvm/lib/cmake/llvm \
-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl
ninja -C build
cp /c/llvm/bin/libclang.dll build/
build/finch.exe version
- name: Tests
run: |
export PATH="/c/llvm/bin:$PATH"
tests/run.sh
- name: Language server
run: python tests/lsp_test.py
- name: Package
run: |
mkdir -p dist/finch
cp build/finch.exe build/libclang.dll dist/finch/
cp README.md LICENSE dist/finch/
cp -r 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

1
.gitignore vendored
View file

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

View file

@ -1,91 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(finch VERSION 2.4.0 LANGUAGES C CXX) # C: LLVM's CMake config checks C headers on some systems
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/codegen_map.cpp
src/abi.cpp
src/cimport.cpp
src/target.cpp
src/loader.cpp
src/lsp.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)
# (written as several raw string pieces: MSVC limits one literal to 16 KB)
file(READ ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c FINCH_RT_SOURCE)
string(LENGTH "${FINCH_RT_SOURCE}" FINCH_RT_LENGTH)
set(FINCH_RT_INC "static const char *kRuntimeSource =\n")
set(FINCH_RT_POS 0)
while(FINCH_RT_POS LESS FINCH_RT_LENGTH)
string(SUBSTRING "${FINCH_RT_SOURCE}" ${FINCH_RT_POS} 8000 FINCH_RT_PIECE)
string(APPEND FINCH_RT_INC "R\"FINCHRT(${FINCH_RT_PIECE})FINCHRT\"\n")
math(EXPR FINCH_RT_POS "${FINCH_RT_POS} + 8000")
endwhile()
string(APPEND FINCH_RT_INC ";\n")
file(WRITE ${CMAKE_BINARY_DIR}/rt_source.inc "${FINCH_RT_INC}")
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}")
if(MSVC)
target_compile_options(finch PRIVATE /W3 /utf-8 /EHsc /wd4244 /wd4267 /wd4146 /wd4624)
else()
target_compile_options(finch PRIVATE -Wall -Wextra)
endif()
# The official Windows LLVM package names the DIA SDK library at the path of the machine it was
# built on (Visual Studio 2019); point it at the Visual Studio installed here.
if(MSVC AND TARGET LLVMDebugInfoPDB AND DEFINED ENV{VSINSTALLDIR})
get_target_property(FINCH_PDB_LIBS LLVMDebugInfoPDB INTERFACE_LINK_LIBRARIES)
if(FINCH_PDB_LIBS)
file(TO_CMAKE_PATH "$ENV{VSINSTALLDIR}DIA SDK/lib/amd64/diaguids.lib" FINCH_DIA_LIB)
list(TRANSFORM FINCH_PDB_LIBS REPLACE ".*diaguids\\.lib$" "${FINCH_DIA_LIB}")
set_target_properties(LLVMDebugInfoPDB PROPERTIES INTERFACE_LINK_LIBRARIES "${FINCH_PDB_LIBS}")
endif()
endif()
# Link the one shared libLLVM when available: libclang uses it too, and two copies
# of LLVM in one process crash on exit.
# Finch generates code for x86-64 and ARM64; use whichever of them this LLVM was built with.
set(FINCH_BACKENDS)
foreach(t X86 AArch64)
if(t IN_LIST LLVM_TARGETS_TO_BUILD)
list(APPEND FINCH_BACKENDS ${t})
string(TOUPPER ${t} T)
target_compile_definitions(finch PRIVATE FINCH_TARGET_${T})
endif()
endforeach()
message(STATUS "Finch backends: ${FINCH_BACKENDS}")
if(TARGET LLVM)
set(LLVM_LIBS LLVM)
else()
set(FINCH_COMPONENTS core support passes native)
foreach(t ${FINCH_BACKENDS})
list(APPEND FINCH_COMPONENTS ${t}codegen ${t}asmparser ${t}desc ${t}info)
endforeach()
llvm_map_components_to_libnames(LLVM_LIBS ${FINCH_COMPONENTS})
endif()
target_link_libraries(finch PRIVATE ${LLVM_LIBS} ${LIBCLANG})

View file

@ -48,38 +48,33 @@ 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, too.** `boot/` is a Finch compiler written in Finch that compiles itself to a
byte-identical result. It handles the whole language except C import and `link` (sized numbers,
methods, `defer`, errors as values, maps and `for k, v` all work); `tests/boot.sh` shows what is left.
- **Written in itself.** The compiler is Finch code, and nothing else: `src/*.fch`, about 8.6k lines. It is built from a
seed (its own LLVM IR, `bootstrap/`) and then from itself, and the two come out identical. Its first version, in C++,
is kept on the `before-bootstrap` branch.
## Quick start
Ready builds are in the [releases](https://github.com/Nexoniarz/Finch/releases):
| System | Download | Also needs |
|---|---|---|
| Windows x64 | `finch-windows-x64.zip` | LLVM 21, Visual Studio Build Tools (C++); [steps](docs/en/for-technicians.md#windows) |
| macOS (Apple Silicon) | `finch-macos-arm64.tar.gz` | `brew install llvm@21`; [steps](docs/en/for-technicians.md#macos) |
| Linux x64 / ARM64 | `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` | LLVM 21 libraries, a C compiler; [steps](docs/en/for-technicians.md#linux) |
Editors: **Kate** (`editors/kate/install.sh`), **VS Code** (the `finch-lang` `.vsix` from the release), or any
editor with LSP support (`finch lsp`).
**Building it yourself** (Linux or macOS, LLVM 21):
**Building it** (Linux x86-64 or ARM64; needs `clang` and libclang, LLVM 21):
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # or install LLVM 21 + libclang + cmake + ninja yourself
cmake -S . -B build -G Ninja
ninja -C build
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
./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour
tests/run.sh # 74 passed, 0 failed
tests/boot.sh # the compiler written in Finch builds itself
tests/run.sh # every program, every refusal, -g, a shared library, the language server
```
Ready builds of the compiler are in the [releases](https://github.com/Nexoniarz/Finch/releases) (Linux x64 and ARM64:
unpack, keep `runtime/` and `std/` next to `finch`). **Programs for Windows, ARM64 and macOS** are built from Linux with
`--target windows|arm64|macos` (see the [guide](docs/en/for-technicians.md#3-the-finch-command)); the compiler itself does not run on
macOS or Windows yet (it needs a seed made with that system's headers: see [bootstrap/README.md](bootstrap/README.md)).
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:
@ -106,7 +101,9 @@ Pick the guide that fits you:
| 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 |
| Modules | `import shapes` → `shapes.area(b)` |
| Enums, match | `enum Color { Red, Green }`, `match c { Red -> ... else -> ... }` (also as a value) |
| Generics | `fn max[T](T a, T b) -> T`, `struct Stack[T]`, `Stack[int]()` |
| Modules | `import shapes` → `shapes.area(b)`; the standard library: `json`, `path`, `time`, `proc` |
| C interop | `import "stdio.h"`, `link "glfw"`, `link "mine.c"`, `addr(fn)` for callbacks |
| I/O | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Tools | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` for gdb/lldb, `-O0` |
@ -128,7 +125,7 @@ Pick the guide that fits you:
| [`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 |
| [`boot/`](boot/) | the Finch compiler, written in Finch |
| [`src/`](src/) | the compiler, written in Finch (the seed to build it is in [`bootstrap/`](bootstrap/)) |
## Roadmap
@ -148,18 +145,20 @@ Pick the guide that fits you:
- [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`)
- [ ] Generics, `match`, enums
- [ ] A standard library in Finch (JSON, paths, time, processes)
- [x] Generics (`fn max[T]`, `struct Stack[T]`), `match`, enums
- [x] A standard library in Finch: `json`, `path`, `time`, `proc`
## Layout
```
src/ compiler: lexer, parser, AST, codegen, C import + ABI, language server, driver (C++17, ~7k lines)
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
runtime/ the small C runtime linked into every program
boot/ the Finch compiler written in Finch (~3k lines)
bootstrap/ build.sh and the seeds: the compiler's own LLVM IR, one per system
examples/ example programs
editors/ VS Code extension, Kate highlighting and LSP setup
tests/ run/ (golden output), fail/ (expected errors), run.sh, boot.sh, cross.sh, lsp_test.py
tests/ run/ new/ (golden output), fail/ new/fail/ (expected errors), run.sh, cross.sh, shared.sh, lsp_test.py
docs/ en/ and pl/ guides for three audiences
```

View file

@ -48,37 +48,33 @@ fn main() {
```
- **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.
- **Napisany w sobie.** Kompilator to kod w Finchu i nic więcej: `src/*.fch`, ok. 8,6 tys. linii. Buduje się go z ziarna
(własny kod LLVM IR, `bootstrap/`), potem z samego siebie, a oba wyniki są identyczne. Pierwsza wersja, w C++,
jest w gałęzi `before-bootstrap`.
## Szybki start
Gotowe wersje są w [wydaniach](https://github.com/Nexoniarz/Finch/releases):
| System | Plik | Potrzebuje też |
|---|---|---|
| Windows x64 | `finch-windows-x64.zip` | LLVM 21, Visual Studio Build Tools (C++); [kroki](docs/pl/dla-technikow.md#windows) |
| macOS (Apple Silicon) | `finch-macos-arm64.tar.gz` | `brew install llvm@21`; [kroki](docs/pl/dla-technikow.md#macos) |
| Linux x64 / ARM64 | `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` | biblioteki LLVM 21, kompilator C; [kroki](docs/pl/dla-technikow.md#linux) |
Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo każdy
edytor obsługujący LSP (`finch lsp`).
**Samodzielne budowanie** (Linux albo macOS, LLVM 21):
**Budowanie** (Linux x86-64 lub ARM64; potrzebny `clang` i libclang, LLVM 21):
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # albo zainstaluj samodzielnie LLVM 21 + libclang + cmake + ninja
cmake -S . -B build -G Ninja
ninja -C build
nix-shell # albo zainstaluj samodzielnie clang + libclang (LLVM 21) + pkg-config
bootstrap/build.sh # kompilator to build/finch: z ziarna, potem z samego siebie
./build/finch run examples/hello.fch
./build/finch build examples/tour.fch -o tour && ./tour
tests/run.sh # 74 passed, 0 failed
tests/boot.sh # kompilator napisany w Finchu buduje sam siebie
tests/run.sh # każdy program, każda odmowa, -g, biblioteka współdzielona, serwer języka
```
Gotowe kompilatory są w [wydaniach](https://github.com/Nexoniarz/Finch/releases) (Linux x64 i ARM64: rozpakuj, zostaw
`runtime/` i `std/` obok `finch`). **Programy dla Windowsa, ARM64 i macOS** buduje się z Linuksa przez
`--target windows|arm64|macos`; sam kompilator nie działa jeszcze na macOS ani Windowsie (potrzebuje ziarna
zrobionego z nagłówkami tego systemu: patrz [bootstrap/README.md](bootstrap/README.md)).
Edytory: **Kate** (`editors/kate/install.sh`), **VS Code** (plik `.vsix` `finch-lang` z wydania) albo dowolny
edytor z obsługą LSP (`finch lsp`).
## Dokumentacja
Wybierz przewodnik dla siebie:
@ -105,7 +101,9 @@ Wybierz przewodnik dla siebie:
| 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)` |
| Enumy, match | `enum Kolor { Czerwony, Zielony }`, `match k { Czerwony -> ... else -> ... }` (także jako wartość) |
| Generyki | `fn max[T](T a, T b) -> T`, `struct Stos[T]`, `Stos[int]()` |
| Moduły | `import ksztalty` → `ksztalty.pole(p)`; biblioteka standardowa: `json`, `path`, `time`, `proc` |
| Współpraca z C | `import "stdio.h"`, `link "glfw"`, `link "moje.c"`, `addr(fn)` dla callbacków |
| Wejście/wyjście | `print(...)`, `input("? ")`, `read_file`, `write_file`, `shell` |
| Narzędzia | `finch run/build/ir/lsp`, `--target windows/arm64/macos`, `-g` dla gdb/lldb, `-O0` |
@ -127,7 +125,7 @@ Wybierz przewodnik dla siebie:
| [`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 |
| [`src/`](src/) | kompilator napisany w Finchu (ziarno do jego zbudowania jest w [`bootstrap/`](bootstrap/)) |
## Plan rozwoju
@ -147,18 +145,20 @@ Wybierz przewodnik dla siebie:
- [x] Serwer języka (`finch lsp`) i rozszerzenie VS Code
- [x] macOS i ARM64 (Linux i Apple Silicon) z ich konwencjami wywołań C; obsługa Kate
- [x] Metody w strukturach, mapy, błędy jako wartości (`!`, `or`, `try`)
- [ ] Typy generyczne, `match`, enumy
- [ ] Biblioteka standardowa w Finchu (JSON, ścieżki, czas, procesy)
- [x] Typy generyczne (`fn max[T]`, `struct Stos[T]`), `match`, enumy
- [x] Biblioteka standardowa w Finchu: `json`, `path`, `time`, `proc`
## Struktura
```
src/ kompilator: lekser, parser, AST, codegen, import C + ABI, serwer języka, sterownik (C++17, ok. 7 tys. linii)
src/ kompilator, w Finchu: lekser, parser, AST, generowanie kodu (do LLVM IR jako tekst), import C (libclang)
i konwencje wywołań C, serwer języka, sterownik
std/ biblioteka standardowa, w Finchu: json, path, time, proc
runtime/ mały runtime w C dołączany do każdego programu
boot/ kompilator Fincha napisany w Finchu (ok. 3 tys. linii)
bootstrap/ build.sh i ziarna: własny LLVM IR kompilatora, po jednym na system
examples/ przykładowe programy
editors/ rozszerzenie VS Code, podświetlanie i konfiguracja LSP dla Kate
tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh, boot.sh, cross.sh, lsp_test.py
tests/ run/ new/ (wzorcowe wyjście), fail/ new/fail/ (oczekiwane błędy), run.sh, cross.sh, shared.sh, lsp_test.py
docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
```

View file

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

45
bootstrap/README.md Normal file
View file

@ -0,0 +1,45 @@
# 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 | Checked |
|---|---|---|
| `linux-x86_64.ll` | Linux x86-64 | built and tested here and in CI |
| `linux-aarch64.ll` | Linux ARM64 | made with the ARM64 cross compiler; CI builds and tests it on an ARM64 runner |
| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | built and run under Wine: it compiles itself, and every test program runs |
| `macos-arm64.ll`, `macos-x86_64.ll` | macOS | made on Linux with the macOS 11.3 SDK; checked as IR only (nobody has run them yet) |
`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; the CI job sets
`FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE`. If the macOS build fails, that job is where it shows first.

57
bootstrap/build.sh Executable file
View file

@ -0,0 +1,57 @@
#!/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=
[ -z "${FINCH_LLVM_INCLUDE:-}" ] || INC="-I $FINCH_LLVM_INCLUDE" # where clang-c/Index.h is, if clang does not find it itself
export FINCH_RT=$PWD/runtime/finch_rt.c
# 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

14
bootstrap/install.sh Executable file
View file

@ -0,0 +1,14 @@
#!/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 Normal file

File diff suppressed because one or more lines are too long

117512
bootstrap/linux-x86_64.ll Normal file

File diff suppressed because one or more lines are too long

117504
bootstrap/macos-arm64.ll Normal file

File diff suppressed because one or more lines are too long

117509
bootstrap/macos-x86_64.ll Normal file

File diff suppressed because one or more lines are too long

6
bootstrap/pack.sh Executable file
View file

@ -0,0 +1,6 @@
#!/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

8
bootstrap/winbuild.sh Executable file
View file

@ -0,0 +1,8 @@
#!/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 Normal file

File diff suppressed because one or more lines are too long

9
bootstrap/winseed.sh Executable file
View file

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

View file

@ -12,6 +12,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README
|---|---|
| `finch run file.fch [args…]` | compile and run (args go to the program) |
| `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) |
| `finch build file.fch --shared [-I dir]` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) |
| `finch ir file.fch` | print the LLVM IR |
| `finch lsp` | the language server (editors use it) |
| `finch version` | version |
@ -370,7 +371,35 @@ Any LSP editor: `finch lsp`.
---
## 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()` |
| `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 defer or try`
`fn return if else while for in break continue true false null import link struct enum match defer or try`
(and `self` inside methods)

View file

@ -63,8 +63,7 @@ these lines one by one, pressing **Enter** after each:
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell
cmake -S . -B build -G Ninja
ninja -C build
bootstrap/build.sh
```
If the last line finishes without the word `error`, Finch is ready.
@ -77,7 +76,7 @@ To check it works, type:
./build/finch version
```
You should see something like `finch 2.4.0`.
You should see something like `Finch 2.4.0`.
---

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,10 @@ compiler works inside. For that, see the [guide for engineers](for-engineers.md)
20. [Debugging](#20-debugging)
21. [Troubleshooting](#21-troubleshooting)
22. [Project layout and tests](#22-project-layout-and-tests)
23. [Current limits](#23-current-limits)
23. [Enums and match](#23-enums-and-match)
24. [Generics](#24-generics)
25. [The standard library](#25-the-standard-library)
26. [Current limits](#26-current-limits)
---
@ -60,78 +63,40 @@ messages that tell you how to fix the problem.
## 2. Installing
Finch runs on **Linux** (x86-64 and ARM64), **macOS** (Apple Silicon and Intel) and **Windows** (x86-64),
and is built with **LLVM 21**. Each [release](https://github.com/Nexoniarz/Finch/releases) has a ready
`finch` for Windows, Linux x86-64, Linux ARM64 and macOS ARM64.
The Finch compiler is written in Finch (see the [engineers' guide](for-engineers.md)). It runs on **Linux**
(x86-64 and ARM64) and builds programs for **Linux, Windows and macOS**. It needs **clang 21** and **libclang**
(LLVM 21) to read C headers and to compile its output.
### Windows
The compiler does not run on macOS or Windows yet; it needs a seed made with that system's headers
([bootstrap/README.md](../../bootstrap/README.md) says why). Until then, build your Windows and macOS programs from Linux
with `--target` (below), or use the older C++ compiler from the `before-bootstrap` branch.
1. Download **`finch-windows-x64.zip`** from the [releases](https://github.com/Nexoniarz/Finch/releases)
and unpack it, for example to `C:\finch`. It contains `finch.exe` and `libclang.dll`.
2. Install **LLVM 21** (`LLVM-21.x.x-win64.exe` from the
[LLVM releases](https://github.com/llvm/llvm-project/releases)) and tick *Add LLVM to the system PATH*.
Finch uses its `clang` to link programs.
3. Install **Visual Studio Build Tools 2022** with *Desktop development with C++*: it provides the Windows
libraries every program is linked with.
4. Add `C:\finch` to your PATH, open a new terminal, and run `finch version`.
### Option A: a ready build (Linux)
Programs are normal `.exe` files. To build from source on Windows instead, follow `.github/workflows/ci.yml`
(Visual Studio 2022 + the `clang+llvm-21.x-x86_64-pc-windows-msvc` package from LLVM's releases).
Download `finch-linux-x64.tar.gz` or `finch-linux-arm64.tar.gz` from the
[releases](https://github.com/Nexoniarz/Finch/releases) and unpack it. Keep `runtime/` and `std/` next to the `finch` file
(or install it, below). You also need `clang` 21 and its libclang library:
### macOS
| Distribution | Packages |
|---|---|
| Debian / Ubuntu | `clang-21 libclang-21-dev` from [apt.llvm.org](https://apt.llvm.org), `pkg-config` |
| Fedora | `clang clang-devel pkgconf` |
| Arch | `clang pkgconf` |
| Nix | `nix-shell` in the repository gives all of it |
```sh
brew install llvm@21 # Finch's LLVM; programs are linked with Apple's clang
```
Then either unpack **`finch-macos-arm64.tar.gz`** from the releases (it uses Homebrew's `llvm@21`),
or build it yourself (Xcode command line tools: `xcode-select --install`):
```sh
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
ninja -C build
./build/finch version
```
Apple frameworks link with `link "Cocoa.framework"` (`-framework Cocoa`), and their headers import as usual:
`import "OpenGL/gl.h"`.
### Linux
The ready `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` need LLVM 21's libraries
(Debian/Ubuntu: `libllvm21 libclang1-21` from [apt.llvm.org](https://apt.llvm.org)) and a C compiler (`cc`).
Building it yourself works the same on x86-64 and ARM64 (Raspberry Pi 4/5, Graviton, …).
Other LLVM versions will likely fail to compile, because LLVM's C++ API changes between releases.
### Option A: Nix (recommended, nothing to install by hand)
### Option B: build it yourself
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # downloads LLVM, libclang, clang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja
ninja -C build
./build/finch version # finch 2.4.0 (LLVM 21.x)
nix-shell # clang, libclang, pkg-config (or install them with your distribution)
bootstrap/build.sh # build/finch
./build/finch version
```
### Option B: your distribution's packages
You need: a C/C++ compiler, CMake ≥ 3.20, Ninja (or Make), LLVM 21 development files,
libclang 21 development files, and optionally `pkg-config`.
| Distribution | Packages (names can differ slightly by version) |
|---|---|
| Debian / Ubuntu | `build-essential cmake ninja-build pkg-config llvm-21-dev libclang-21-dev` |
| Fedora | `gcc-c++ cmake ninja-build pkgconf llvm-devel clang-devel` |
| Arch | `base-devel cmake ninja pkgconf llvm clang` |
These have not been tested; if CMake cannot find LLVM, point it there:
```sh
cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
ninja -C build
```
`bootstrap/build.sh` compiles the seed (`bootstrap/<system>.ll`, the compiler's own LLVM IR) into a first compiler, builds
`src/` with it, builds `src/` again with the result and checks the two are identical. If libclang isn't where the linker
looks, tell it: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`.
### Building for another system
@ -157,12 +122,13 @@ On Nix the compilers are `pkgsCross.mingwW64.buildPackages.gcc` and
### Putting `finch` on your PATH (optional)
```sh
sudo cp build/finch /usr/local/bin/
sudo bootstrap/install.sh # /usr/local/bin/finch, and /usr/local/share/finch/{runtime,std}
bootstrap/install.sh ~/.local # or into your own folder
finch version
```
At run time Finch needs a C compiler (`cc`). It is used to link programs, and once to build
Finch's small runtime library, which is kept in `~/.cache/finch`. Set `CC` to use a different one.
At run time Finch needs `clang` (it compiles each program's IR and links it). The small runtime library is built once and
kept in `~/.cache/finch`. `FINCH_CC` names a different compiler to link with; `FINCH_PATH` adds folders to look for modules in.
---
@ -935,37 +901,156 @@ Visual Studio debugger and WinDbg read.
```
Finch/
├── src/ the compiler (C++)
├── runtime/ finch_rt.c: the small runtime (text, arrays, input, files)
├── boot/ the Finch compiler written in Finch (see the engineers' guide)
├── src/ the compiler, in Finch (lexer, parser, gen: LLVM IR as text, cimport: libclang, lsp, main ...)
├── std/ the standard library, in Finch: json, path, time, proc
├── runtime/ finch_rt.c: the small runtime (text, arrays, maps, input, files)
├── bootstrap/ build.sh, install.sh and the seeds (the compiler's own IR, one per system)
├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm
├── tests/
│ ├── run/ programs + the exact output they must print (.out), input (.in)
│ ├── fail/ programs that must fail, with the expected error in line 1
│ ├── run/ new/ programs + the exact output they must print (.out), input (.in)
│ ├── fail/ new/fail/ programs that must fail, with the expected error in line 1
│ ├── run.sh the test runner (MEMCHECK=1 also checks memory with valgrind)
│ └── boot.sh builds the self-hosted compiler with itself and compares
│ ├── cross.sh every test built for Windows / ARM64 and run under Wine / QEMU
│ └── lsp_test.py the language server, talked to like an editor does
├── editors/ the VS Code extension (vscode/), Kate highlighting and LSP setup (kate/)
├── docs/ this documentation (en, pl), including the cheat sheet
├── shell.nix the Nix development environment
└── CMakeLists.txt
└── shell.nix the Nix development environment
```
```sh
tests/run.sh # → 74 passed, 0 failed
tests/run.sh # programs, refusals, -g builds, a shared library, the language server
MEMCHECK=1 tests/run.sh # the same under valgrind: no leaks, no bad memory access
tests/boot.sh # the self-hosting check (needs clang)
tests/cross.sh windows # every test built for Windows and run with Wine
tests/cross.sh arm64 # every test built for ARM64 Linux and run with QEMU
python3 tests/lsp_test.py # the language server
```
### Variants that hold values
A variant can carry values. Make one with `Shape.Circle(2.0)`, take them out with `match`, which names them:
```c
enum Shape {
Circle(float)
Rect(float, float)
Point
}
fn Shape.area() -> float {
return match self {
Circle(r) -> 3.14 * r * r
Rect(w, h) -> w * h
Point -> 0.0
}
}
```
`_` ignores a value. `match` must handle every variant (or end with `else`). Printing shows `Rect(2, 3.5)`. Such an enum is
copied and freed like a struct; it can't be compared with `==` (match on it). Variants of these enums can't have numbers.
---
## 23. Current limits
## 23. Enums and match
An **enum** is a type with a short list of named values.
```
enum Color { Red, Green, Blue }
enum Status { Ok = 200, NotFound = 404 } // give a variant a number if it needs one
c := Color.Green
print(c) // Green
print(int(Status.NotFound)) // 404
print(Color(2)) // Blue: a number turned into the enum (checked: Color(7) is an error)
```
Variants are written `Color.Red`. They can be compared with `==` and `!=` (not with `<`), used as map keys,
struct fields and array elements, and an enum can have methods like a struct: `fn Color.next() -> Color { ... }`.
**match** picks what to do from a value, instead of a ladder of `if`:
```
match c {
Red -> print("warm") // inside a match on an enum, the bare name is enough
Green, Blue -> print("cool") // several values for one arm
}
size := match n { // as an expression: it gives a value
1, 2, 3 -> "small"
4, 5 -> "medium"
else -> "large"
}
match word {
"quit" -> {
print("bye")
return
}
else -> print("what?")
}
```
`match` works on enums, whole numbers, chars, text and bools. The arms are tried in order. A match on an enum has to
handle **every variant** or end with `else` (the compiler says which are missing); any other match needs `else`.
In an expression, every arm gives the same type.
## 24. Generics
Write a function or a struct once for any type, with the type as a name in square brackets:
```
fn max[T](T a, T b) -> T {
if a > b {
return a
}
return b
}
struct Stack[T] {
[]T items
}
fn Stack[T].push(T x) {
self.items.push(x)
}
print(max(3, 9), max("apple", "pear")) // T is worked out from the arguments
print(max[float](1, 2.5)) // or written out, where it can't be worked out
s := Stack[int]() // a generic struct is always written with its type
s.push(7)
```
Several parameters work (`struct Pair[A, B]`), and so do nested types (`Stack[Pair[int, str]]`). Each use makes its
own copy of the code, so a generic is as fast as code written by hand. There are no constraints: if `T` can't do
what the function does (`a > b` on a struct), the error says so and tells which version it was building, like
`(while building bigger[Point])`.
## 25. The standard library
Modules that come with Finch, written in Finch. `import json` finds `json.fch` next to your file first, and the
library's second, so your own file of the same name wins.
| Module | What it does |
|---|---|
| `path` | `join`, `base`, `dir`, `ext`, `stem`, `parts`, `clean`, `is_abs`: paths as text, separated by `/` |
| `time` | `now()`, `millis()` (a clock for measuring), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` |
| `proc` | `run(cmd)` (the exit code), `output(cmd)` (what it printed; an error if it failed), `env(name, fallback)`, `pid()`, `cwd()` |
| `json` | `parse(text)`, `stringify(v)`, `pretty(v)`, and the value type `Json` with `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... |
```
import json
import path
doc := try json.parse(read_file("config.json"))
name := doc.get("name").str_or("unnamed") // a missing key is null, so chains never fail
doc.set("name", json.new_text(name + "!"))
write_file(path.join("out", "config.json"), json.pretty(doc))
```
## 26. Current limits
Not there yet:
- Generics (your own `List[T]`), interfaces, `match`, enums written in Finch.
- Interfaces.
- Unicode-aware text: `.len`, `s[i]` and `upper()` work on bytes / ASCII.
- Threads.
- Windows on ARM64, 32-bit systems, WebAssembly.

File diff suppressed because it is too large Load diff

View file

@ -63,8 +63,7 @@ i wpisz te linijki po kolei, naciskając **Enter** po każdej:
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell
cmake -S . -B build -G Ninja
ninja -C build
bootstrap/build.sh
```
Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.

View file

@ -33,7 +33,10 @@ kompilator działa w środku. Do tego służy [przewodnik dla inżynierów](dla-
20. [Debugowanie](#20-debugowanie)
21. [Rozwiązywanie problemów](#21-rozwiązywanie-problemów)
22. [Struktura projektu i testy](#22-struktura-projektu-i-testy)
23. [Obecne ograniczenia](#23-obecne-ograniczenia)
23. [Enumy i match](#23-enumy-i-match)
24. [Typy generyczne](#24-typy-generyczne)
25. [Biblioteka standardowa](#25-biblioteka-standardowa)
26. [Obecne ograniczenia](#26-obecne-ograniczenia)
---
@ -60,77 +63,40 @@ i komunikaty błędów, które mówią, jak problem naprawić.
## 2. Instalacja
Finch działa na **Linuksie** (x86-64 i ARM64), **macOS** (Apple Silicon i Intel) i **Windowsie** (x86-64),
i jest budowany z **LLVM 21**. Każde [wydanie](https://github.com/Nexoniarz/Finch/releases) ma gotowy
`finch` dla Windowsa, Linuksa x86-64, Linuksa ARM64 i macOS ARM64.
Kompilator Fincha jest napisany w Finchu (zobacz [przewodnik dla inżynierów](dla-inzynierow.md)). Działa na **Linuksie**
(x86-64 i ARM64) i buduje programy dla **Linuksa, Windowsa i macOS**. Potrzebuje **clanga 21** i **libclang** (LLVM 21),
żeby czytać nagłówki C i kompilować to, co wygeneruje.
### Windows
Kompilator nie działa jeszcze na macOS ani Windowsie: potrzebuje ziarna zrobionego z nagłówkami tego systemu
([bootstrap/README.md](../../bootstrap/README.md) tłumaczy dlaczego). Do tego czasu programy na Windows i macOS buduj z Linuksa
przez `--target` (niżej) albo użyj starszego kompilatora w C++ z gałęzi `before-bootstrap`.
1. Pobierz **`finch-windows-x64.zip`** z [wydań](https://github.com/Nexoniarz/Finch/releases) i rozpakuj,
np. do `C:\finch`. W środku są `finch.exe` i `libclang.dll`.
2. Zainstaluj **LLVM 21** (`LLVM-21.x.x-win64.exe` z [wydań LLVM](https://github.com/llvm/llvm-project/releases))
i zaznacz *Add LLVM to the system PATH*. Finch używa jego `clang` do linkowania programów.
3. Zainstaluj **Visual Studio Build Tools 2022** z pakietem *Desktop development with C++*: dostarcza biblioteki
Windowsa, z którymi linkowany jest każdy program.
4. Dodaj `C:\finch` do PATH, otwórz nowy terminal i wpisz `finch version`.
### Opcja A: gotowa wersja (Linux)
Programy to zwykłe pliki `.exe`. Żeby zbudować Fincha ze źródeł na Windowsie, idź za `.github/workflows/ci.yml`
(Visual Studio 2022 + pakiet `clang+llvm-21.x-x86_64-pc-windows-msvc` z wydań LLVM).
Pobierz `finch-linux-x64.tar.gz` albo `finch-linux-arm64.tar.gz` z [wydań](https://github.com/Nexoniarz/Finch/releases)
i rozpakuj. Zostaw `runtime/` i `std/` obok pliku `finch` (albo zainstaluj, niżej). Potrzebny jest też `clang` 21
z biblioteką libclang:
### macOS
| Dystrybucja | Pakiety |
|---|---|
| Debian / Ubuntu | `clang-21 libclang-21-dev` z [apt.llvm.org](https://apt.llvm.org), `pkg-config` |
| Fedora | `clang clang-devel pkgconf` |
| Arch | `clang pkgconf` |
| Nix | `nix-shell` w repozytorium daje wszystko |
```sh
brew install llvm@21 # LLVM dla Fincha; programy linkuje clang od Apple
```
Potem rozpakuj **`finch-macos-arm64.tar.gz`** z wydań (korzysta z `llvm@21` z Homebrew)
albo zbuduj go sam (narzędzia wiersza poleceń Xcode: `xcode-select --install`):
```sh
git clone https://github.com/Nexoniarz/Finch.git && cd Finch
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_DIR="$(brew --prefix llvm@21)/lib/cmake/llvm"
ninja -C build
./build/finch version
```
Frameworki Apple linkuje się przez `link "Cocoa.framework"` (`-framework Cocoa`), a ich nagłówki importuje
się normalnie: `import "OpenGL/gl.h"`.
### Linux
Gotowe `finch-linux-x64.tar.gz` / `finch-linux-arm64.tar.gz` potrzebują bibliotek LLVM 21
(Debian/Ubuntu: `libllvm21 libclang1-21` z [apt.llvm.org](https://apt.llvm.org)) i kompilatora C (`cc`).
Samodzielne budowanie wygląda tak samo na x86-64 i ARM64 (Raspberry Pi 4/5, Graviton, …).
Z innymi wersjami LLVM Finch prawdopodobnie się nie skompiluje, bo API C++ LLVM zmienia się między wydaniami.
### Opcja A: Nix (zalecana, nic nie instalujesz ręcznie)
### Opcja B: zbuduj sam
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # pobiera LLVM, libclang, clang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja
ninja -C build
./build/finch version # finch 2.4.0 (LLVM 21.x)
nix-shell # clang, libclang, pkg-config (albo zainstaluj z dystrybucji)
bootstrap/build.sh # build/finch
./build/finch version
```
### Opcja B: pakiety z twojej dystrybucji
Potrzebujesz: kompilatora C/C++, CMake ≥ 3.20, Ninja (albo Make), plików deweloperskich LLVM 21
i libclang 21, a opcjonalnie `pkg-config`.
| Dystrybucja | Pakiety (nazwy mogą się nieco różnić zależnie od wersji) |
|---|---|
| Debian / Ubuntu | `build-essential cmake ninja-build pkg-config llvm-21-dev libclang-21-dev` |
| Fedora | `gcc-c++ cmake ninja-build pkgconf llvm-devel clang-devel` |
| Arch | `base-devel cmake ninja pkgconf llvm clang` |
Ta opcja nie była testowana. Jeśli CMake nie znajduje LLVM, wskaż mu katalog:
```sh
cmake -S . -B build -G Ninja -DLLVM_DIR=/usr/lib/llvm-21/lib/cmake/llvm
ninja -C build
```
`bootstrap/build.sh` kompiluje ziarno (`bootstrap/<system>.ll`, własny LLVM IR kompilatora) do pierwszego kompilatora,
buduje nim `src/`, buduje `src/` jeszcze raz wynikiem i sprawdza, że oba są identyczne. Jeśli libclang nie leży tam, gdzie
szuka linker: `FINCH_LIBCLANG="-L/usr/lib/llvm-21/lib -lclang-21" bootstrap/build.sh`.
### Budowanie na inny system
@ -156,13 +122,14 @@ Na Niksie te kompilatory to `pkgsCross.mingwW64.buildPackages.gcc` i
### Dodanie `finch` do PATH (opcjonalnie)
```sh
sudo cp build/finch /usr/local/bin/
sudo bootstrap/install.sh # /usr/local/bin/finch oraz /usr/local/share/finch/{runtime,std}
bootstrap/install.sh ~/.local # albo do własnego katalogu
finch version
```
Podczas pracy Finch potrzebuje kompilatora C (`cc`). Używa go do linkowania programów i jednorazowo
do zbudowania swojej małej biblioteki uruchomieniowej, którą trzyma w `~/.cache/finch`.
Inny kompilator wskażesz zmienną `CC`.
Podczas działania Finch potrzebuje `clanga` (kompiluje IR każdego programu i go linkuje). Mała biblioteka uruchomieniowa
budowana jest raz i trzymana w `~/.cache/finch`. `FINCH_CC` wskazuje inny kompilator do linkowania; `FINCH_PATH` dodaje
katalogi, w których szukać modułów.
---
@ -938,37 +905,156 @@ CodeView, który czyta debugger Visual Studio i WinDbg.
```
Finch/
├── src/ kompilator (C++)
├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, wejście, pliki)
├── boot/ kompilator Fincha napisany w Finchu (zobacz przewodnik dla inżynierów)
├── src/ kompilator, w Finchu (lekser, parser, gen: LLVM IR jako tekst, cimport: libclang, lsp, main ...)
├── std/ biblioteka standardowa, w Finchu: json, path, time, proc
├── runtime/ finch_rt.c: mała biblioteka uruchomieniowa (teksty, tablice, mapy, wejście, pliki)
├── bootstrap/ build.sh, install.sh i ziarna (własny IR kompilatora, po jednym na system)
├── examples/ hello, tour, types, structs, todo, guess, c_import, window, opengl, vulkan, raylib, llvm
├── tests/
│ ├── run/ programy + dokładny wynik (.out) i wejście (.in)
│ ├── fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce
│ ├── run/ new/ programy + dokładny wynik (.out) i wejście (.in)
│ ├── fail/ new/fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce
│ ├── run.sh uruchamia testy (MEMCHECK=1 sprawdza też pamięć valgrindem)
│ └── boot.sh buduje kompilator samohostujący nim samym i porównuje
│ ├── cross.sh każdy test zbudowany na Windows / ARM64 i uruchomiony w Wine / QEMU
│ └── lsp_test.py serwer języka, obsługiwany jak przez edytor
├── editors/ rozszerzenie VS Code (vscode/), podświetlanie i konfiguracja LSP dla Kate (kate/)
├── docs/ ta dokumentacja (en, pl), łącznie ze ściągą
├── shell.nix środowisko deweloperskie Nix
└── CMakeLists.txt
└── shell.nix środowisko deweloperskie Nix
```
```sh
tests/run.sh # → 74 passed, 0 failed
tests/run.sh # programy, odmowy, buildy z -g, biblioteka współdzielona, serwer języka
MEMCHECK=1 tests/run.sh # to samo pod valgrindem: bez wycieków i złych dostępów do pamięci
tests/boot.sh # sprawdzenie samohostowania (wymaga clanga)
tests/cross.sh windows # każdy test zbudowany na Windowsa i uruchomiony w Wine
tests/cross.sh arm64 # każdy test zbudowany na Linuksa ARM64 i uruchomiony w QEMU
python3 tests/lsp_test.py # serwer języka
```
### Warianty z wartościami
Wariant może nieść wartości. Tworzysz go przez `Shape.Circle(2.0)`, a wyciągasz `match`-em, który je nazywa:
```c
enum Shape {
Circle(float)
Rect(float, float)
Point
}
fn Shape.area() -> float {
return match self {
Circle(r) -> 3.14 * r * r
Rect(w, h) -> w * h
Point -> 0.0
}
}
```
`_` pomija wartość. `match` musi obsłużyć każdy wariant (albo kończyć się `else`). Wypisanie pokazuje `Rect(2, 3.5)`. Taki enum
jest kopiowany i zwalniany jak struktura; nie da się go porównać przez `==` (użyj `match`). Warianty takich enumów nie mają numerów.
---
## 23. Obecne ograniczenia
## 23. Enumy i match
**Enum** to typ z krótką listą nazwanych wartości.
```
enum Kolor { Czerwony, Zielony, Niebieski }
enum Status { Ok = 200, Brak = 404 } // wariantowi można nadać numer
k := Kolor.Zielony
print(k) // Zielony
print(int(Status.Brak)) // 404
print(Kolor(2)) // Niebieski: liczba zamieniona na enum (sprawdzana: Kolor(7) to błąd)
```
Warianty pisze się `Kolor.Czerwony`. Można je porównywać przez `==` i `!=` (nie przez `<`), używać jako klucze map,
pola struktur i elementy tablic, a enum może mieć metody jak struktura: `fn Kolor.nastepny() -> Kolor { ... }`.
**match** wybiera, co zrobić, na podstawie wartości — zamiast drabinki `if`:
```
match k {
Czerwony -> print("ciepły") // w matchu na enumie wystarczy sama nazwa
Zielony, Niebieski -> print("zimny") // kilka wartości w jednej gałęzi
}
rozmiar := match n { // jako wyrażenie: daje wartość
1, 2, 3 -> "mały"
4, 5 -> "średni"
else -> "duży"
}
match slowo {
"koniec" -> {
print("pa")
return
}
else -> print("co?")
}
```
`match` działa na enumach, liczbach całkowitych, znakach, tekście i bool. Gałęzie sprawdzane są po kolei. Match na
enumie musi obsłużyć **każdy wariant** albo kończyć się `else` (kompilator wskaże brakujące); każdy inny match
potrzebuje `else`. W wyrażeniu wszystkie gałęzie dają ten sam typ.
## 24. Typy generyczne
Napisz funkcję lub strukturę raz dla dowolnego typu, z typem jako nazwą w nawiasach kwadratowych:
```
fn max[T](T a, T b) -> T {
if a > b {
return a
}
return b
}
struct Stos[T] {
[]T elementy
}
fn Stos[T].dodaj(T x) {
self.elementy.push(x)
}
print(max(3, 9), max("jabłko", "gruszka")) // T wynika z argumentów
print(max[float](1, 2.5)) // albo zapisane wprost, gdzie nie da się go ustalić
s := Stos[int]() // generyczną strukturę zawsze pisze się z typem
s.dodaj(7)
```
Parametrów może być kilka (`struct Para[A, B]`), typy można zagnieżdżać (`Stos[Para[int, str]]`). Każde użycie tworzy
własną kopię kodu, więc generyk jest tak szybki jak kod napisany ręcznie. Nie ma ograniczeń: jeśli `T` nie potrafi
tego, co robi funkcja (`a > b` na strukturze), błąd mówi o tym i podaje, którą wersję budowano, np.
`(while building wiekszy[Punkt])`.
## 25. Biblioteka standardowa
Moduły dołączone do Fincha, napisane w Finchu. `import json` szuka najpierw `json.fch` obok Twojego pliku, potem w
bibliotece — więc Twój własny plik o tej nazwie ma pierwszeństwo.
| Moduł | Co robi |
|---|---|
| `path` | `join`, `base`, `dir`, `ext`, `stem`, `parts`, `clean`, `is_abs`: ścieżki jako tekst, rozdzielone `/` |
| `time` | `now()`, `millis()` (zegar do mierzenia), `sleep(ms)`, `date(t)`, `local(t)`, `format(t, "%Y-%m-%d")`, `days_in_month` |
| `proc` | `run(cmd)` (kod wyjścia), `output(cmd)` (co wypisało; błąd, jeśli się nie udało), `env(nazwa, domyślna)`, `pid()`, `cwd()` |
| `json` | `parse(tekst)`, `stringify(v)`, `pretty(v)` oraz typ wartości `Json` z `get`, `at`, `set`, `push`, `has`, `str_or`, `int_or` ... |
```
import json
import path
dok := try json.parse(read_file("config.json"))
nazwa := dok.get("name").str_or("bez nazwy") // brakujący klucz to null, więc łańcuch nigdy się nie psuje
dok.set("name", json.new_text(nazwa + "!"))
write_file(path.join("out", "config.json"), json.pretty(dok))
```
## 26. Obecne ograniczenia
Jeszcze nie ma:
- Typów generycznych (własnego `Lista[T]`), interfejsów, `match`, enumów pisanych w Finchu.
- Interfejsów.
- Tekstu świadomego Unicode: `.len`, `s[i]` i `upper()` działają na bajtach / ASCII.
- Wątków.
- Windowsa na ARM64, systemów 32-bitowych, WebAssembly.

View file

@ -370,6 +370,34 @@ Każdy edytor z LSP: `finch lsp`.
---
## Enumy i `match`
```
enum Kolor { Czerwony, Zielony } enum Status { Ok = 200, Brak = 404 }
k := Kolor.Zielony int(k) Kolor(1) k == Kolor.Czerwony // tylko == i !=; Kolor(7) to błąd
fn Kolor.nastepny() -> Kolor { ... } // metody działają
match k { Czerwony -> ... Zielony -> { ... } else -> ... } // same nazwy wariantów; każdy wariant albo else
x := match n { 1, 2 -> "mało" else -> "dużo" } // wyrażenie: gałęzie tego samego typu
```
## Typy generyczne
```
fn max[T](T a, T b) -> T { ... } max(3, 9) max[float](1, 2.5) // wynika z argumentów albo zapisane
struct Stos[T] { []T elementy } fn Stos[T].dodaj(T x) { ... } // typ zawsze podaje się przy tworzeniu:
s := Stos[int]() Stos[int] t Para[str, int]("wiek", 31) // nigdy samo Stos()
```
## Biblioteka standardowa (`import nazwa`; własny plik o tej 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, fmt)` `days_in_month(r, m)` |
| `proc` | `run(cmd)` `output(cmd)` (błąd z jego wyjściem) `env(nazwa, domyślna)` `pid()` `cwd()` |
| `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
| Słowo | Po polsku |
@ -383,6 +411,8 @@ Każdy edytor z LSP: `finch lsp`.
| `true` / `false` / `null` | prawda / fałsz / nic |
| `import` / `link` | zaimportuj / dolinkuj |
| `struct` | struktura |
| `enum` | wyliczenie |
| `match` | dopasuj |
| `defer` | odłóż (na koniec bloku) |
| `or` | albo (co zrobić przy porażce) |
| `try` | spróbuj (porażkę przekaż dalej) |

View file

@ -11,6 +11,8 @@
<list name="keywords">
<item>fn</item>
<item>struct</item>
<item>enum</item>
<item>export</item>
</list>
<list name="control">
<item>if</item>
@ -24,6 +26,7 @@
<item>defer</item>
<item>or</item>
<item>try</item>
<item>match</item>
</list>
<list name="imports">
<item>import</item>

View file

@ -156,5 +156,43 @@
"}"
],
"description": "Every key and value of a map (or index and element of an array)"
},
"enum": {
"prefix": "enum",
"body": [
"enum ${1:Name} {",
"\t${2:First},",
"\t${3:Second}",
"}"
],
"description": "An enum: a type with a few named values"
},
"match": {
"prefix": "match",
"body": [
"match ${1:value} {",
"\t${2:pattern} -> $0",
"\telse -> ",
"}"
],
"description": "match: pick what to do from a value"
},
"generic function": {
"prefix": "fng",
"body": [
"fn ${1:name}[${2:T}](${3:T x}) -> ${2:T} {",
"\t$0",
"}"
],
"description": "A function that works for any type"
},
"generic struct": {
"prefix": "structg",
"body": [
"struct ${1:Name}[${2:T}] {",
"\t$0",
"}"
],
"description": "A struct that holds any type"
}
}

View file

@ -74,7 +74,7 @@
]
},
"struct": {
"match": "\\b(struct)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(struct|enum)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": {
"name": "storage.type.struct.finch"
@ -117,7 +117,7 @@
},
{
"name": "keyword.control.finch",
"match": "\\b(if|else|while|for|in|break|continue|return|defer|or|try)\\b"
"match": "\\b(if|else|while|for|in|break|continue|return|defer|or|try|match)\\b"
},
{
"name": "storage.type.function.finch",
@ -125,7 +125,11 @@
},
{
"name": "storage.type.struct.finch",
"match": "\\bstruct\\b"
"match": "\\b(struct|enum)\\b"
},
{
"name": "storage.modifier.finch",
"match": "\\bexport\\b"
},
{
"name": "constant.language.finch",

View file

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

View file

@ -1 +0,0 @@
Buy milk

View file

@ -37,12 +37,26 @@ static char empty[1] = "";
// ---------- runtime errors ----------
__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {
// 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; }
static __attribute__((noreturn)) void fatal(const char *msg) {
if (fatal_hook) fatal_hook(msg);
fflush(stdout);
fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg);
fprintf(stderr, "%s\n", msg);
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);
}
__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {
char msg[128];
snprintf(msg, sizeof msg, "index %lld is out of range (the length is %lld)", (long long)i, (long long)len);
@ -52,8 +66,7 @@ __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t
static void *must(void *p) {
if (!p) {
fflush(stdout);
fprintf(stderr, "runtime error: out of memory\n");
exit(1);
fatal("runtime error: out of memory");
}
return p;
}

View file

@ -4,11 +4,9 @@ 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

View file

@ -1,367 +0,0 @@
// The C calling convention for structs passed and returned by value, the way clang does it:
// System V x86-64 (Linux, macOS): small structs split into eightbytes, INTEGER or SSE registers
// Microsoft x64 (Windows): 1/2/4/8 bytes in one register, everything else by address
// AAPCS64 (ARM64 Linux, Apple): float groups in vector registers, else up to 16 bytes in x registers
#include "codegen_impl.h"
#include "target.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 (g_target.triple.isAArch64()) {
// A homogeneous float aggregate (1-4 floats or 1-4 doubles) travels in vector registers.
std::vector<Leaf> ls;
leaves(*this, t, 0, ls);
bool hfa = !ls.empty() && ls.size() <= 4;
for (const Leaf &l : ls) hfa &= l.isFloat && l.isDouble == ls[0].isDouble;
if (hfa) {
llvm::Type *ft = ls[0].isDouble ? (llvm::Type *)b.getDoubleTy() : b.getFloatTy();
a.kind = AbiArg::Expand;
a.parts.push_back(ArrayType::get(ft, ls.size()));
a.coerced = StructType::get(ctx, std::vector<llvm::Type *>(ls.size(), ft));
a.stackAlign8 = !g_target.triple.isOSDarwin();
return a;
}
if (size > 16) {
a.kind = AbiArg::Indirect; // a copy, passed by address; returned through sret
return a;
}
// up to 16 bytes in general registers: i64 or [2 x i64]; returned as an exact-size integer
uint64_t align = dl->getABITypeAlign(ty(t)).value();
a.kind = AbiArg::Expand;
if (align >= 16) {
a.parts.push_back(b.getInt128Ty());
a.coerced = b.getInt128Ty();
} else if (size <= 8) {
a.parts.push_back(b.getInt64Ty());
a.coerced = b.getIntNTy(size * 8);
} else {
a.parts.push_back(ArrayType::get(b.getInt64Ty(), 2));
a.coerced = a.parts[0];
}
return a;
}
if (g_target.windows) {
// Microsoft x64: 1, 2, 4 or 8 bytes travel as one integer (even if they hold floats);
// anything else is copied by the caller and passed by address
if (size == 1 || size == 2 || size == 4 || size == 8) {
a.kind = AbiArg::Expand;
a.parts.push_back(b.getIntNTy(size * 8));
a.coerced = a.parts[0];
} else {
a.kind = AbiArg::Indirect;
}
return a;
}
if (size > 16 || size == 0) {
a.kind = AbiArg::Memory;
return a;
}
std::vector<Leaf> ls;
leaves(*this, t, 0, ls);
unsigned n = (size + 7) / 8;
Class cls[2] = {None, None};
bool hasDouble[2] = {false, false};
for (const Leaf &l : ls) {
if (l.offset % std::min<uint64_t>(l.size, 8) != 0) { // misaligned field (packed struct): memory
a.kind = AbiArg::Memory;
return a;
}
unsigned k = l.offset / 8;
cls[k] = (cls[k] == Integer || !l.isFloat) ? Integer : Sse;
hasDouble[k] |= l.isDouble;
}
a.kind = AbiArg::Expand;
for (unsigned k = 0; k < n; k++) {
uint64_t bytes = std::min<uint64_t>(8, size - 8 * k);
if (cls[k] == Sse) {
if (hasDouble[k]) a.parts.push_back(b.getDoubleTy());
else if (bytes > 4) a.parts.push_back(FixedVectorType::get(b.getFloatTy(), 2));
else a.parts.push_back(b.getFloatTy());
} else {
a.parts.push_back(b.getIntNTy(bytes * 8));
}
}
a.coerced = a.parts.size() == 1 ? a.parts[0] : StructType::get(ctx, a.parts);
return a;
}
namespace {
struct Plan {
AbiArg ret;
std::vector<AbiArg> params;
llvm::FunctionType *fty = nullptr;
};
} // namespace
// Decide how every parameter travels, counting the 6 integer and 8 SSE argument registers.
static Plan makePlan(Codegen &cg, const FType &ret, const std::vector<FType> &params, bool variadic,
std::vector<llvm::Type *> *extraTypes = nullptr) {
Plan pl;
int intRegs = 6, sseRegs = 8;
std::vector<llvm::Type *> types;
llvm::Type *retTy = cg.ty(ret);
if (ret.kind == FType::Str) retTy = cg.b.getPtrTy();
pl.ret = cg.classify(ret);
if (pl.ret.kind == AbiArg::Indirect) pl.ret.kind = AbiArg::Memory; // returned through a hidden pointer
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)++;
// only System V counts registers here; elsewhere the backend places each part
if (g_target.windows || g_target.triple.isAArch64() || (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 || a.kind == AbiArg::Indirect) 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;
}
// Is C's plain `char` signed? Yes on x86 and Apple; no on ARM64 Linux.
static bool charIsSigned() {
return !g_target.triple.isAArch64() || g_target.triple.isOSDarwin() || g_target.triple.isOSWindows();
}
static Attribute::AttrKind extAttr(const FType &t) {
if (t.bits() >= 32 || !(t.isInt() || t.kind == FType::Bool || t.kind == FType::Char)) return Attribute::None;
if (t.kind == FType::Char) return charIsSigned() ? Attribute::SExt : Attribute::ZExt;
return t.isSigned() ? Attribute::SExt : Attribute::ZExt;
}
// Attributes that tell LLVM how C passes each piece: sret, byval, signext/zeroext.
static AttributeList attrsFor(Codegen &cg, const Plan &pl, const FType &ret, const std::vector<FType> &params) {
LLVMContext &ctx = cg.ctx;
AttributeList al;
unsigned idx = 0;
if (pl.ret.kind == AbiArg::Memory) {
al = al.addParamAttribute(ctx, idx, Attribute::getWithStructRetType(ctx, cg.ty(ret)));
idx++;
} else if (Attribute::AttrKind k = extAttr(ret); k != Attribute::None) {
al = al.addRetAttribute(ctx, k);
}
for (size_t i = 0; i < params.size(); i++) {
const AbiArg &a = pl.params[i];
if (a.kind == AbiArg::Memory) {
uint64_t align = std::max<uint64_t>(8, params[i].info->isC ? params[i].info->cstruct->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::Indirect) {
idx++;
} else if (a.kind == AbiArg::Expand) {
if (a.stackAlign8) al = al.addParamAttribute(ctx, idx, Attribute::getWithStackAlignment(ctx, Align(8)));
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 && charIsSigned()));
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 || a.kind == AbiArg::Indirect) {
Value *s = tmpOf(ty(ps[i])); // a copy the callee may change
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 || a.kind == AbiArg::Indirect) {
args.push_back(b.CreateLoad(ty(fn.params[i]), th->getArg(ai++)));
} else if (a.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, fn.params[i], a);
for (size_t k = 0; k < a.parts.size(); k++)
b.CreateStore(th->getArg(ai++), b.CreateConstInBoundsGEP1_64(b.getInt8Ty(), s, 8 * k));
args.push_back(b.CreateLoad(ty(fn.params[i]), s));
} else {
args.push_back(th->getArg(ai++));
}
}
Value *r = b.CreateCall(fn.llvm, args);
if (fn.ret.kind == FType::Void) {
b.CreateRetVoid();
} else if (pl.ret.kind == AbiArg::Memory) {
b.CreateStore(r, sret);
b.CreateRetVoid();
} else if (pl.ret.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, fn.ret, pl.ret);
b.CreateStore(r, s);
b.CreateRet(b.CreateLoad(pl.ret.coerced, s));
} else {
b.CreateRet(r);
}
diFn = savedFn;
return th;
}
} // namespace finch

View file

@ -9,6 +9,7 @@ struct Type {
[]Type elem
str name
str module
int count // "fixed": [N]T, only inside C structs
}
fn simple(str kind) -> Type {
@ -39,10 +40,13 @@ fn same(Type a, Type b) -> bool {
if a.kind != b.kind {
return false
}
if a.kind == "struct" || a.kind == "named" {
if a.kind == "struct" || a.kind == "named" || a.kind == "enum" {
return a.name == b.name && a.module == b.module
}
if a.kind == "array" || a.kind == "ptr" || a.kind == "map" {
if a.kind == "fixed" && a.count != b.count {
return false
}
if a.kind == "array" || a.kind == "ptr" || a.kind == "map" || a.kind == "fixed" {
if a.elem.len != b.elem.len {
return false
}
@ -60,6 +64,9 @@ 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 == "map" {
return "map[" + show(t.elem[0]) + "]" + show(t.elem[1])
}
@ -69,7 +76,7 @@ fn show(Type t) -> str {
}
return "ptr[" + show(t.elem[0]) + "]"
}
if t.kind == "struct" || t.kind == "named" {
if t.kind == "struct" || t.kind == "named" || t.kind == "enum" {
if t.module.len > 0 {
return t.module + "." + t.name
}
@ -84,26 +91,34 @@ fn show(Type t) -> str {
// 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
// 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)
[]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
[]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
}

356
src/ast.h
View file

@ -1,356 +0,0 @@
#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
Map, // map[K]V: a hash map that keeps insertion order, owns its keys and values
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; Map: the value
std::shared_ptr<Type> key; // Map
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;
}
static Type mapOf(const Type &k, const Type &v) {
Type m(Map);
m.key = std::make_shared<Type>(k);
m.elem = std::make_shared<Type>(v);
return m;
}
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 Map: return *key == *o.key && *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 Map: return "map[" + key->show() + "]" + 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,
MapLit, Try, OrElse };
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) {}
};
struct MapLitExpr : Expr { // ["a": 1, "b": 2], or [:] for an empty map
std::vector<ExprPtr> keys, values;
explicit MapLitExpr(Pos p) : Expr(ExprKind::MapLit, p) {}
};
// obj.name(args): a.push(x), s.find("x"), p.move(1, 2), 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)) {}
};
struct BlockStmt;
// try f(x): if f fails, the current function fails with the same error
struct TryExpr : Expr {
ExprPtr call;
TryExpr(Pos p, ExprPtr c) : Expr(ExprKind::Try, p), call(std::move(c)) {}
};
// f(x) or fallback, f(x) or { ...err... }
struct OrElseExpr : Expr {
ExprPtr call;
ExprPtr fallback; // or a value
std::unique_ptr<BlockStmt> block; // or a block that sees `err`
OrElseExpr(Pos p, ExprPtr c) : Expr(ExprKind::OrElse, p), call(std::move(c)) {}
~OrElseExpr() override;
};
// ---------- 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;
Pos end; // the closing '}'
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
};
using BlockPtr = std::unique_ptr<BlockStmt>;
inline OrElseExpr::~OrElseExpr() = default;
struct VarDeclStmt : Stmt {
bool hasType; // `int x = 5` vs `x := 5`
Type type;
std::string name;
Pos namePos;
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;
Pos varPos;
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;
Pos varPos;
std::string var2; // for i, x in list / for k, v in map ("" = one variable)
Pos var2Pos;
ExprPtr list; // for var in list (an array, a str or a map)
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; // the type
Pos namePos; // the name
};
struct FnDecl {
Pos pos;
Pos namePos;
std::string name;
std::string recv; // fn Point.move(...): "Point" (a method; `self` is the Point it is called on)
Pos recvPos;
Type ret; // Void when there is no `-> type`
bool fallible = false; // -> int! / -> ! : can fail with an error
std::vector<Param> params;
BlockPtr body;
};
struct Field {
Type type;
std::string name;
ExprPtr init; // default value, may be null
Pos pos;
Pos namePos;
};
struct StructDecl {
Pos pos;
Pos namePos;
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;
};

View file

@ -1,80 +0,0 @@
#pragma once
// One description of every built-in function and method, shared by the language server
// (hover, completion, signature help).
struct BuiltinDoc {
const char *name;
const char *signature;
const char *doc;
};
inline const BuiltinDoc kBuiltins[] = {
{"print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."},
{"input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."},
{"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)."},
{"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."},
{"file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"},
{"delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."},
{"error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."},
{"shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."},
{"exit", "exit(int code)", "Ends the program right away with this exit code."},
{"addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."},
{"new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."},
{"free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."},
{"str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."},
{"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)."},
{"float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."},
{"char", "char(int n) -> char", "The character with this code."},
{"bool", "bool(int n) -> bool", "true for any number but 0."},
{"ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."},
};
inline const BuiltinDoc kArrayMethods[] = {
{"len", ".len -> int", "The number of elements."},
{"ptr", ".ptr -> ptr[T]", "The address of the first element, for C."},
{"push", "push(T x)", "Adds x at the end."},
{"pop", "pop() -> T", "Removes the last element and gives it back."},
{"insert", "insert(int i, T x)", "Puts x at position i, moving the rest."},
{"remove", "remove(int i) -> T", "Removes position i and gives it back."},
{"clear", "clear()", "Removes everything."},
{"resize", "resize(int n)", "Makes the array n long; new elements are zero."},
{"find", "find(T x) -> int", "The index of x, or -1."},
{"contains", "contains(T x) -> bool", "Is x in the array?"},
{"sort", "sort()", "Sorts in place (numbers, chars, str)."},
{"reverse", "reverse()", "Reverses in place."},
{"slice", "slice(int start, int end) -> []T", "A new array with the elements start to end-1."},
{"join", "join(str sep) -> str", "For []str: one text with sep between the parts."},
};
inline const BuiltinDoc kStrMethods[] = {
{"len", ".len -> int", "The number of bytes."},
{"ptr", ".ptr -> ptr[char]", "The address of the text, for C (valid while the str lives)."},
{"sub", "sub(int start, int end) -> str", "The part from start to end-1."},
{"find", "find(str t) -> int", "Where t starts, or -1."},
{"contains", "contains(str t) -> bool", "Does the text contain t?"},
{"starts_with", "starts_with(str t) -> bool", "Does the text start with t?"},
{"ends_with", "ends_with(str t) -> bool", "Does the text end with t?"},
{"split", "split(str sep) -> []str", "The parts between each sep; split(\"\") gives single characters."},
{"trim", "trim() -> str", "Without spaces and newlines at both ends."},
{"upper", "upper() -> str", "With a-z changed to A-Z."},
{"lower", "lower() -> str", "With A-Z changed to a-z."},
{"replace", "replace(str from, str to) -> str", "Every from replaced by to."},
{"repeat", "repeat(int n) -> str", "The text n times."},
{"bytes", "bytes() -> []u8", "The bytes of the text."},
};
inline const BuiltinDoc kMapMethods[] = {
{"len", ".len -> int", "The number of keys."},
{"has", "has(K key) -> bool", "Is the key in the map?"},
{"get", "get(K key, V default) -> V", "The value for key, or default if the key isn't there."},
{"remove", "remove(K key) -> bool", "Removes the key and its value; true if it was there."},
{"clear", "clear()", "Removes everything."},
{"keys", "keys() -> []K", "The keys, in the order they were added."},
{"values", "values() -> []V", "The values, in the order their keys were added."},
};
inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true",
"false", "null", "import", "link", "struct", "defer", "or", "try", "self"};
inline const char *kTypeNames[] = {"int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64",
"u8", "u16", "u32", "u64", "f32", "f64"};

View file

@ -1,373 +0,0 @@
#include "cimport.h"
#include "error.h"
#include "target.h"
#include <clang-c/Index.h>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
namespace {
// Ask the system C compiler where it looks for headers (and, on macOS, frameworks), so we find
// the same stdio.h it would. Returns ready -isystem / -iframework arguments.
std::vector<std::string> systemIncludeArgs() {
std::string output;
capture(g_target.cc + " -E -v -x c " + nullDevice(), output);
std::vector<std::string> dirs;
std::stringstream lines(output);
bool inList = false;
for (std::string l; std::getline(lines, l);) {
if (!l.empty() && l.back() == '\r') l.pop_back();
l += "\n";
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);
// macOS: "/…/System/Library/Frameworks (framework directory)" holds <OpenGL/gl.h> and friends
if (d.size() > 22 && d.compare(d.size() - 22, 22, " (framework directory)") == 0)
dirs.push_back("-iframework" + d.substr(0, d.size() - 22));
else
dirs.push_back("-isystem" + d);
}
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;
// parse for the target system: sizes (long is 32 bits on Windows) and headers differ
std::vector<std::string> args = {"-x", "c", "-std=gnu11", "--target=" + g_target.triple.str()};
if (g_target.windows) args.push_back("-D_USE_MATH_DEFINES"); // M_PI and friends, as on Linux
for (const std::string &d : dirs) args.push_back("-I" + d);
for (const std::string &d : systemIncludeArgs()) args.push_back(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;
}

569
src/cimport.fch Normal file
View file

@ -0,0 +1,569 @@
// 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. The same job as
// src/cimport.cpp, done by calling libclang's C API through Finch's own C import -- which is
// what makes this the part that closes the bootstrap: to compile this file the self-hosted
// compiler has to read 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
str unsupported // non-empty: why Finch can't call it (yet)
}
struct CConst { // enum values and simple #define numbers
bool isFloat
int i
float f
str text // a float as written, so no digits are lost: 3.14159265358979323846
}
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
}
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
}
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"
}
// 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 {
f.hidden = true // unions are kept as opaque bytes
} else {
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 {
if clang_getCursorKind(clang_getTypeDeclaration(t)) == CXCursor_UnionDecl {
return no("uses a C union by value")
}
name := defineRecord(imp, t)
if name.len == 0 {
return no("uses an incomplete C struct by value")
}
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)
}
// Simple #defines: a single number, maybe negative, maybe in parentheses.
fn macroValue(ptr[Importer] imp, CXCursor c) -> Number {
ptr[CXToken] tokens = null
u32 n = 0
clang_tokenize(imp.tu, clang_getCursorExtent(c), addr(tokens), addr(n))
[]str parts
for k in 1..int(n) { // token 0 is the macro's name
if clang_getTokenKind(tokens[k]) == CXToken_Comment {
continue
}
parts.push(text(clang_getTokenSpelling(imp.tu, tokens[k])))
}
clang_disposeTokens(imp.tu, tokens, n)
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
}
}
} 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.unsupported = "is a static inline C function (it has no code to link against)"
imp.out.fns[name] = f
return
}
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 {
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
}
v := macroValue(imp, c)
if v.ok && !imp.out.consts.has(name) {
imp.out.consts[name] = v.c
}
}
}
// 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)
}
clang_disposeIndex(index)
return imp.out
}

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,905 +0,0 @@
// Code generation, part 3: calls, built-in functions, conversions and methods.
#include "codegen_impl.h"
#include "builtins_doc.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", "delete_file", "exit", "shell", "error"};
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}}},
{"finch_delete_file", {I32, {P}}},
{"finch_std_stream", {P, {I32}}},
{"finch_str_try_int", {I32, {P, P, P}}},
{"finch_str_try_float", {I32, {P, P, P}}},
{"finch_try_read_file", {I32, {P, P, P}}},
{"finch_try_write_file", {I32, {P, P, P}}},
{"finch_try_delete_file", {I32, {P, P}}},
{"finch_report_error", {V, {P}}},
{"finch_map_find", {I, {P, P, I, I, I32}}},
{"finch_map_slot", {I, {P, P, I, I, I32, P}}},
{"finch_map_remove_at", {V, {P, I, I}}},
{"finch_map_clear", {V, {P}}},
{"finch_map_free", {V, {P}}},
{"finch_map_clone_raw", {V, {P, P, I}}},
{"finch_map_missing", {V, {P, I, P}}},
{"finch_map_changed", {V, {P, I}}},
{"finch_buf_add", {V, {P, P, I}}},
{"finch_buf_int", {V, {P, I}}},
{"finch_buf_uint", {V, {P, I}}},
{"finch_buf_float", {V, {P, D}}},
{"finch_buf_bool", {V, {P, I32}}},
{"finch_buf_ptr", {V, {P, P}}},
{"finch_buf_char", {V, {P, b.getInt8Ty(), I32}}},
{"finch_buf_str", {V, {P, P, I32}}},
{"finch_buf_print", {V, {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" || name == "finch_map_missing" || name == "finch_map_changed") {
f->addFnAttr(Attribute::NoReturn);
f->addFnAttr(Attribute::Cold);
}
if (name == "finch_str_from_char" || name == "finch_buf_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 ----------
static std::string docHover(const BuiltinDoc &d) {
return "```finch\n" + std::string(d.signature) + "\n```\n" + d.doc;
}
static std::string cSignature(const CFunc &f) {
std::string s = f.ret.show() + " " + f.name + "(";
for (size_t i = 0; i < f.params.size(); i++) s += (i ? ", " : "") + f.params[i].show();
if (f.variadic) s += f.params.empty() ? "..." : ", ...";
return s + ")";
}
Value_ Codegen::call(const CallExpr &c) {
const std::string &n = c.callee;
Pos p = c.pos;
if (g_index)
for (const BuiltinDoc &d : kBuiltins)
if (n == d.name) note(p, n.size(), docHover(d));
auto one = [&](const char *what) -> const Expr & {
checkArgs(c.args, c.argNames, 1, p, n);
(void)what;
return *c.args[0];
};
bool canFail = &c == fallibleTarget; // inside `... or ...` / `try ...`: report failures instead of stopping
// a failure: set the flag and message that `or` / `try` look at
auto failedIf = [&](Value *ok, Value *errSlot) {
fallibleFailed = b.CreateICmpEQ(ok, b.getInt32(0));
fallibleMsg = b.CreateLoad(ty(FType::Str), errSlot);
};
if (n == "error")
failAt(p.file, p.line, p.col, "error(...) makes an error to return from a function that can fail: return error(\"...\")");
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 == "delete_file") {
Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name");
Value *ss = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_copy"), {ss, tmp(path.v)}); // NUL-terminated copy
if (canFail) {
Value *err = tmp(zero(FType::Str));
failedIf(b.CreateCall(rt("finch_try_delete_file"), {ss, err}), err);
dropAt(ss, FType::Str);
release(path);
return {nullptr, FType::Void};
}
Value *r = b.CreateCall(rt("finch_delete_file"), {ss});
dropAt(ss, FType::Str);
release(path);
return {b.CreateICmpNE(r, b.getInt32(0)), FType::Bool};
}
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 if (canFail) {
Value *out = tmp(zero(FType::Str)), *err = tmp(zero(FType::Str));
failedIf(b.CreateCall(rt("finch_try_read_file"), {out, pp, err}), err);
res = {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
} 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");
Held held(*this);
hold(path);
Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text");
if (canFail) {
Value *err = tmp(zero(FType::Str));
failedIf(b.CreateCall(rt("finch_try_write_file"), {tmp(path.v), tmp(text.v), err}), err);
release(path);
release(text);
return {nullptr, FType::Void};
}
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, canFail);
}
ModuleScope &m = modules[curModule];
if (auto it = m.fns.find(n); it != m.fns.end()) {
note(p, n.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos);
return callFinch(it->second, c.args, c.argNames, p, n, &c);
}
if (StructInfo *s = findStruct("", n, false)) {
note(p, n.size(), structHover(s), s->decl ? s->decl->namePos : Pos{});
return construct(s, c.args, c.argNames, p);
}
if (auto cf = cimports.fns.find(n); cf != cimports.fns.end()) {
note(p, n.size(), "C function from `" + cf->second.header + "`\n```c\n" + cSignature(cf->second) + "\n```");
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 Expr *site, Value *self, const std::string &selfRoot) {
const FnDecl &d = *fn.decl;
if (fn.module.empty() && d.name == "main" && !fn.recv) failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts");
if (fn.fallible && site != fallibleTarget)
failAt(p.file, p.line, p.col, "'" + shown + "' can fail, so say what happens then: " + shown + "(...) or <value>, " + shown +
"(...) or { ... }, or try " + shown + "(...)");
checkArgs(args, names, fn.params.size(), p, shown);
std::vector<Value *> vals;
std::vector<Value_> fresh;
Held held(*this);
if (self) vals.push_back(self);
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 + "'");
// a method that changes self gets its own copy of anything that lives inside self (p.add(p.items))
if (fn.mutatesSelf && !selfRoot.empty() && owning(v.type) && !v.fresh && rootVar(*args[i]) == selfRoot)
v = {copyValue(v.v, v.type), v.type, false, true};
vals.push_back(v.v); // owning values are lent: the function never drops them
if (v.fresh) fresh.push_back(v);
hold(v);
}
setLoc(p);
Value *r = b.CreateCall(fn.llvm, vals);
for (auto &v : fresh) release(v);
if (fn.fallible) {
fallibleFailed = b.CreateNot(b.CreateExtractValue(r, 0));
fallibleMsg = b.CreateExtractValue(r, 2);
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
return {b.CreateExtractValue(r, 1), fn.ret, false, owning(fn.ret)};
}
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);
Held held(*this);
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
}
Value *fo = own(fv);
hold({fo, f.type, false, true});
v = b.CreateInsertValue(v, fo, 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;
Held held(*this);
for (size_t i = 0; i < args.size(); i++) {
if (!vals.empty()) hold(vals.back());
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, bool canFail) {
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 if (from.kind == FType::Array || from.kind == FType::Fixed || from.kind == FType::Map || from.kind == FType::Struct) {
b.CreateStore(zero(FType::Str), out); // the same text print(...) shows
emitFormat(out, v.v, from, false);
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()) && canFail) { // int(text) or 0
Value *sp = tmp(v.v), *err = tmp(zero(FType::Str));
Value *out = tmp(to.isInt() ? (Value *)b.getInt64(0) : ConstantFP::get(b.getDoubleTy(), 0));
Value *ok = b.CreateCall(rt(to.isInt() ? "finch_str_try_int" : "finch_str_try_float"), {sp, out, err});
fallibleFailed = b.CreateICmpEQ(ok, b.getInt32(0));
fallibleMsg = b.CreateLoad(ty(FType::Str), err);
Value_ r;
if (to.isInt()) r = {intCast(b.CreateLoad(b.getInt64Ty(), out), FType::I64, to), to};
else r = {b.CreateFPCast(b.CreateLoad(b.getDoubleTy(), out), ty(to)), to};
release(v);
return r;
}
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");
note(m.obj->pos, mn.size(), "module `" + mn + "`");
Pos namePos{p.line, p.col + 1, p.file};
if (auto it = mi->second.fns.find(m.name); it != mi->second.fns.end()) {
note(namePos, m.name.size(), "```finch\n" + signature(it->second) + "\n```", it->second.decl->namePos);
return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name, &m);
}
if (auto it = mi->second.structs.find(m.name); it != mi->second.structs.end()) {
note(namePos, m.name.size(), structHover(it->second), it->second->decl->namePos);
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"};
// writing through a map's m[key] adds the key if it's missing: groups[k].push(x)
LRef r = ref(*m.obj, changing.count(m.name) > 0 || mutatingMethods.count(m.name) > 0);
FType t = r.isPlace ? r.pl.type : r.val.type;
if (t.kind == FType::Struct || (t.isPtr() && t.elem && t.elem->kind == FType::Struct)) return structMethod(m, r, t);
if (changing.count(m.name) && (t.kind == FType::Array || t.kind == FType::Map)) {
std::string root = rootVar(*m.obj);
if (Var *v = root.empty() ? nullptr : lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy);
}
Held held(*this);
if (t.kind == FType::Map) {
Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : Value_{tmp(r.val.v), t, false, r.val.fresh};
if (!r.isPlace && r.val.fresh) holdAddr(recv.v, t);
Value_ out = mapMethod(m, &recv);
if (!r.isPlace && r.val.fresh) dropAt(recv.v, t);
return out;
}
if (g_index && (t.kind == FType::Array || t.kind == FType::Str)) {
auto look = [&](const BuiltinDoc *first, const BuiltinDoc *last) {
for (; first != last; ++first)
if (m.name == first->name) note(Pos{p.line, p.col + 1, p.file}, m.name.size(), docHover(*first));
};
if (t.kind == FType::Str) look(std::begin(kStrMethods), std::end(kStrMethods));
else look(std::begin(kArrayMethods), std::end(kArrayMethods));
}
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};
if (r.val.fresh) holdAddr(recv.v, t);
}
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
if (!r.isPlace) hold(sv);
auto argStr = [&](size_t i, const char *what) {
Value_ a = coerce(expr(*m.args[i]), FType::Str, m.args[i]->pos, what);
hold(a);
return a;
};
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;
}
failAt(p.file, p.line, p.col, t.show() + " has no methods");
}
// p.move(1, 2): a method of a struct (also through a ptr[Struct]); self is passed by address
Value_ Codegen::structMethod(const MethodExpr &m, LRef &r, const FType &t) {
Pos p = m.pos;
FType st = t.isPtr() ? *t.elem : t;
auto ms = methods.find(st.info);
const Fn *fn = nullptr;
if (ms != methods.end())
if (auto it = ms->second.find(m.name); it != ms->second.end()) fn = &it->second;
if (!fn) {
std::string names;
if (ms != methods.end())
for (auto &[n, f] : ms->second) names += (names.empty() ? "" : ", ") + n;
if (names.empty())
failAt(p.file, p.line, p.col, "the struct " + st.show() + " has no methods; add one with fn " + st.name + "." + m.name + "(...) { ... }");
failAt(p.file, p.line, p.col, "the struct " + st.show() + " has no method '" + m.name + "' (it has: " + names + ")");
}
note(Pos{p.line, p.col + 1, p.file}, m.name.size(), "```finch\n" + signature(*fn) + "\n```", fn->decl->namePos);
std::string root = rootVar(*m.obj);
if (fn->mutatesSelf && !root.empty() && !t.isPtr())
if (Var *v = lookup(root); v && v->readonly) failAt(p.file, p.line, p.col, v->readonlyWhy);
Value *self;
bool temp = false;
if (t.isPtr()) {
self = r.isPlace ? b.CreateLoad(b.getPtrTy(), r.pl.addr) : r.val.v;
panicIf(b.CreateIsNull(self), "called ." + m.name + "() on a null pointer", p);
root.clear(); // self lives somewhere else
} else if (r.isPlace) {
self = r.pl.addr;
} else {
self = tmp(r.val.v); // a temporary struct: Point(1, 2).length()
temp = true;
}
Held held(*this);
if (temp && r.val.fresh) holdAddr(self, st);
Value_ out = callFinch(*fn, m.args, m.argNames, p, st.name + "." + m.name, &m, self, root);
if (temp && r.val.fresh && owning(st)) dropAt(self, st);
return out;
}
// 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

View file

@ -1,327 +0,0 @@
#pragma once
// Internal to the code generator: the Codegen class, shared by codegen*.cpp and abi.cpp.
#include "codegen.h"
#include "error.h"
#include "index.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 *cstruct = 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
Pos pos; // Finch structs: where the field is declared
};
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
Pos pos; // where it was declared
};
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;
StructInfo *recv = nullptr; // a method of this struct: the first LLVM parameter is `self`'s address
bool mutatesSelf = false; // the method changes `self`
bool fallible = false; // -> T!: returns { i1 ok, T value, str error }
};
struct Loop {
llvm::BasicBlock *continueTo, *breakTo;
size_t scopeDepth; // scopes opened inside the loop are cleaned up by break/continue
size_t pendingDepth; // and so are temporaries held by expressions around it (see Codegen::pending)
};
// A fresh value an expression is holding while it computes another part (f(a + b, try g())): if that
// part leaves early (try, or a return/break/continue in an `or { }` block), it must be dropped.
struct Pending {
llvm::Value *v;
FType type;
bool isAddr; // v is the value's address
};
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: System V x86-64 (Linux) or Microsoft x64 (Windows).
struct AbiArg {
// Direct: as is. Expand: split into register-sized parts. Memory: on the stack (byval), or
// returned through a hidden pointer (sret). Indirect (Windows): a pointer to a copy the caller made.
enum Kind { Direct, Expand, Memory, Indirect } kind = Direct;
std::vector<llvm::Type *> parts; // Expand: one value per eightbyte
llvm::Type *coerced = nullptr; // Expand: { parts... } for returns
bool stackAlign8 = false; // AArch64 Linux: HFA arguments that end up on the stack
};
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;
std::vector<Pending> pending;
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
std::map<StructInfo *, std::map<std::string, Fn>> methods;
std::set<std::string> mutatingMethods; // names of methods that change self (for the mutation analysis)
// errors as values: the call that `or` / `try` is handling reports here whether it failed
const Expr *fallibleTarget = nullptr;
llvm::Value *fallibleFailed = nullptr; // i1
llvm::Value *fallibleMsg = nullptr; // str
// 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);
llvm::Type *retType(const Fn &f);
void retValue(llvm::Value *v); // return from the current function (wrapped as success if it can fail)
void retError(llvm::Value *msg); // fail the current function with the str `msg`
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 hold(const Value_ &v); // see Pending
void holdAddr(llvm::Value *addr, const FType &t);
void dropPending(size_t downTo); // emit drops for pending[downTo..]
struct Held { // forgets what was held when the expression is done
Codegen &cg;
size_t mark;
explicit Held(Codegen &c) : cg(c), mark(c.pending.size()) {}
~Held() { cg.pending.erase(cg.pending.begin() + mark, cg.pending.end()); }
};
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);
Value_ fallible(const Expr &call, llvm::Value *&failed, llvm::Value *&msg);
Value_ tryExpr(const TryExpr &t);
Value_ orElse(const OrElseExpr &e, bool discard = false); // discard: a statement, the value isn't used
FType structType(StructInfo *s);
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);
void emitFormat(llvm::Value *buf, llvm::Value *v, const FType &t, bool quoted); // append v as text to the str at buf
void bufText(llvm::Value *buf, const std::string &text);
llvm::Function *formatFn(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, const Expr *site, llvm::Value *self = nullptr,
const std::string &selfRoot = "");
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, bool canFail = false);
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);
Value_ structMethod(const MethodExpr &m, LRef &r, const FType &t);
// ---------- codegen_map.cpp: maps ----------
llvm::StructType *mapEntryTy(const FType &t);
Value_ mapLit(const MapLitExpr &e, const FType *want);
LRef mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite);
Value_ mapMethod(const MethodExpr &m, Value_ *recv);
std::vector<llvm::Value *> mapArgs(llvm::Value *map, llvm::Value *keyAddr, const FType &t);
llvm::Value *mapSlot(llvm::Value *map, const FType &t, const Value_ &key, Pos p, bool fill);
llvm::Value *mapEntry(llvm::Value *map, const FType &t, llvm::Value *i);
void forEachEntry(llvm::Value *map, const FType &t, const std::function<void(llvm::Value *i, llvm::Value *entry)> &body);
llvm::Value *keyText(const Value_ &key);
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);
// ---------- language server index ----------
void note(Pos at, size_t len, const std::string &hover, Pos def = Pos{});
std::string signature(const Fn &fn);
std::string structHover(StructInfo *s);
// ---------- debug info ----------
void setLoc(Pos p);
llvm::DIType *diType(const FType &t);
};
} // namespace finch

View file

@ -1,246 +0,0 @@
// Code generation, part 4: maps.
//
// map[K]V is { entries, len, used, cap, index, icap } (see runtime/finch_rt.c). The runtime finds and
// places entries; the compiler stores, copies and drops the keys and values in them, like it does
// for array elements. An entry is { i64 hash, K key, V value }; hash 0 marks a removed entry.
#include "codegen_impl.h"
#include "builtins_doc.h"
using namespace llvm;
namespace finch {
StructType *Codegen::mapEntryTy(const FType &t) {
return StructType::get(ctx, {b.getInt64Ty(), ty(*t.key), ty(*t.elem)});
}
// The runtime's (map, key, entry size, key size, key is str) arguments.
std::vector<Value *> Codegen::mapArgs(Value *map, Value *keyAddr, const FType &t) {
return {map, keyAddr, b.getInt64(dl->getTypeAllocSize(mapEntryTy(t))), elemSize(*t.key),
b.getInt32(t.key->kind == FType::Str ? 1 : 0)};
}
Value *Codegen::mapEntry(Value *map, const FType &t, Value *i) {
Value *entries = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), map, 0));
return b.CreateInBoundsGEP(mapEntryTy(t), entries, i);
}
// The entry for `key`, added if it's missing: the key is copied in, and the value is its type's
// default when `fill` (m[k] += 1 starts from 0).
Value *Codegen::mapSlot(Value *map, const FType &t, const Value_ &key, Pos p, bool fill) {
StructType *ET = mapEntryTy(t);
Value *keyAddr = tmp(key.v);
Value *added = tmpOf(b.getInt32Ty());
std::vector<Value *> args = mapArgs(map, keyAddr, t);
args.push_back(added);
Value *entry = mapEntry(map, t, b.CreateCall(rt("finch_map_slot"), args));
BasicBlock *fresh = newBlock("map.new"), *have = newBlock("map.have");
b.CreateCondBr(b.CreateICmpNE(b.CreateLoad(b.getInt32Ty(), added), b.getInt32(0)), fresh, have);
b.SetInsertPoint(fresh);
copyAt(b.CreateStructGEP(ET, entry, 1), keyAddr, *t.key);
if (fill) b.CreateStore(own(defaultValue(*t.elem, p)), b.CreateStructGEP(ET, entry, 2));
b.CreateBr(have);
b.SetInsertPoint(have);
return entry;
}
// Every live entry, in insertion order: body(entry number, entry address).
void Codegen::forEachEntry(Value *map, const FType &t, const std::function<void(Value *, Value *)> &body) {
Value *used = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 2));
forN(used, [&](Value *i) {
Value *entry = mapEntry(map, t, i);
Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(mapEntryTy(t), entry, 0));
BasicBlock *live = newBlock("entry.live"), *next = newBlock("entry.next");
b.CreateCondBr(b.CreateICmpNE(hash, b.getInt64(0)), live, next);
b.SetInsertPoint(live);
body(i, entry);
b.CreateBr(next);
b.SetInsertPoint(next);
});
}
// A key as text for error messages: "bob" or 42
Value *Codegen::keyText(const Value_ &key) {
Value *out = tmpOf(ty(FType::Str));
const FType &t = key.type;
if (t.kind == FType::Str) {
Value *q = tmp(strConst("\"")), *half = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_concat"), {half, q, tmp(key.v)});
b.CreateCall(rt("finch_str_concat"), {out, half, q});
} else if (t.isSigned()) {
b.CreateCall(rt("finch_str_from_int"), {out, b.CreateSExt(key.v, b.getInt64Ty())});
} else if (t.isUnsigned()) {
b.CreateCall(rt("finch_str_from_uint"), {out, b.CreateZExt(key.v, b.getInt64Ty())});
} else if (t.kind == FType::Char) {
b.CreateCall(rt("finch_str_from_char"), {out, key.v});
} else {
b.CreateCall(rt("finch_str_from_bool"), {out, b.CreateZExt(key.v, b.getInt32Ty())});
}
return out;
}
// m[key]: reading a missing key stops the program; writing (m[k] = v, m[k] += 1, m[k].push(x)) adds it
LRef Codegen::mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite) {
Pos p = e.pos;
FType kt = *t.key, vt = *t.elem;
Value_ key = coerce(exprWant(*e.index, kt), kt, e.index->pos, "the key");
if (forWrite) {
if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary map, it can't be changed");
Value *entry = mapSlot(obj.pl.addr, t, key, p, true);
release(key);
return {true, {b.CreateStructGEP(mapEntryTy(t), entry, 2), vt}, {}};
}
Value *map = obj.isPlace ? obj.pl.addr : tmp(obj.val.v);
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(key.v), t));
BasicBlock *missing = newBlock("map.missing"), *found = newBlock("map.found");
b.CreateCondBr(b.CreateICmpSLT(i, b.getInt64(0)), missing, found);
b.SetInsertPoint(missing);
b.CreateCall(rt("finch_map_missing"), {fileName(p), b.getInt64(p.line), keyText(key)});
b.CreateUnreachable();
b.SetInsertPoint(found);
release(key);
Value *valueAddr = b.CreateStructGEP(mapEntryTy(t), mapEntry(map, t, i), 2);
if (obj.isPlace) return {true, {valueAddr, vt}, {}};
Value *v = b.CreateLoad(ty(vt), valueAddr); // from a temporary map: take the value, drop the rest
Value_ out{v, vt};
if (owning(vt) && obj.val.fresh) out = {copyValue(v, vt), vt, false, true};
release(obj.val);
return {false, {}, out};
}
// ["a": 1, "b": 2], or [:] where the type is known
Value_ Codegen::mapLit(const MapLitExpr &e, const FType *want) {
Pos p = e.pos;
FType t;
Value_ k0, v0; // the first entry, when it decides the types
bool first = false;
if (want && want->kind == FType::Map) {
t = *want;
} else if (e.keys.empty()) {
failAt(p.file, p.line, p.col, "an empty [:] needs a type, like: map[str]int m = [:]");
} else {
// number values written in the code are all floats if any of them has a dot
bool allNum = true, anyFloat = false;
for (auto &x : e.values) {
const Expr *y = x.get();
if (y->kind == ExprKind::Unary && static_cast<const UnaryExpr *>(y)->op == '-') y = static_cast<const UnaryExpr *>(y)->operand.get();
if (y->kind == ExprKind::Float) anyFloat = true;
else if (y->kind != ExprKind::Int) allNum = false;
}
k0 = expr(*e.keys[0]);
v0 = expr(*e.values[0]);
Pos kp = e.keys[0]->pos, vp = e.values[0]->pos;
if (k0.type.kind == FType::Void || k0.type.kind == FType::Null) failAt(kp.file, kp.line, kp.col, "a map key needs a value with a type");
if (v0.type.kind == FType::Void || v0.type.kind == FType::Null) failAt(vp.file, vp.line, vp.col, "a map value needs a value with a type");
t = resolve(FType::mapOf(k0.type, allNum && anyFloat ? FType(FType::F64) : v0.type), kp);
first = true;
}
Value *map = tmp(zero(t));
StructType *ET = mapEntryTy(t);
Held held(*this);
holdAddr(map, t); // the entries so far, if a later one leaves early
for (size_t i = 0; i < e.keys.size(); i++) {
Held entry(*this);
Value_ k = i == 0 && first ? k0 : exprWant(*e.keys[i], *t.key);
k = coerce(k, *t.key, e.keys[i]->pos, "every key of this map");
hold(k);
Value_ v = i == 0 && first ? v0 : exprWant(*e.values[i], *t.elem);
Value *vv = own(coerce(v, *t.elem, e.values[i]->pos, "every value of this map"));
Value *valueAddr = b.CreateStructGEP(ET, mapSlot(map, t, k, p, false), 2);
dropAt(valueAddr, *t.elem); // a key given twice: the last value wins
b.CreateStore(vv, valueAddr);
release(k);
}
return {b.CreateLoad(ty(t), map), t, false, true};
}
Value_ Codegen::mapMethod(const MethodExpr &m, Value_ *recv) {
Pos p = m.pos;
const std::string &n = m.name;
FType t = recv->type, kt = *t.key, vt = *t.elem;
StructType *ET = mapEntryTy(t);
Value *map = recv->v;
auto need = [&](size_t k) { checkArgs(m.args, m.argNames, k, p, n); };
auto keyArg = [&]() { return coerce(exprWant(*m.args[0], kt), kt, m.args[0]->pos, "the key"); };
if (g_index)
for (const BuiltinDoc &d : kMapMethods)
if (n == d.name) note(Pos{p.line, p.col + 1, p.file}, n.size(), "```finch\n" + std::string(d.signature) + "\n```\n" + d.doc);
if (n == "has") {
need(1);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
return {b.CreateICmpSGE(i, b.getInt64(0)), FType::Bool};
}
if (n == "get") { // m.get(key, default): the default is only computed when the key is missing
need(2);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
BasicBlock *found = newBlock("get.found"), *missing = newBlock("get.missing"), *end = newBlock("get.end");
b.CreateCondBr(b.CreateICmpSGE(i, b.getInt64(0)), found, missing);
b.SetInsertPoint(found);
Value *v = b.CreateLoad(ty(vt), b.CreateStructGEP(ET, mapEntry(map, t, i), 2));
v = copyValue(v, vt);
BasicBlock *foundEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(missing);
Value *d = own(coerce(exprWant(*m.args[1], vt), vt, m.args[1]->pos, "the default"));
BasicBlock *missingEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(end);
PHINode *phi = b.CreatePHI(ty(vt), 2);
phi->addIncoming(v, foundEnd);
phi->addIncoming(d, missingEnd);
return {phi, vt, false, owning(vt)};
}
if (n == "remove") { // true if the key was there
need(1);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
BasicBlock *hit = newBlock("remove.hit"), *end = newBlock("remove.end");
Value *was = b.CreateICmpSGE(i, b.getInt64(0));
b.CreateCondBr(was, hit, end);
b.SetInsertPoint(hit);
Value *entry = mapEntry(map, t, i);
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
b.CreateCall(rt("finch_map_remove_at"), {map, i, b.getInt64(dl->getTypeAllocSize(ET))});
b.CreateBr(end);
b.SetInsertPoint(end);
return {was, FType::Bool};
}
if (n == "clear") {
need(0);
if (owning(kt) || owning(vt))
forEachEntry(map, t, [&](Value *, Value *entry) {
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
});
b.CreateCall(rt("finch_map_clear"), {map});
return {nullptr, FType::Void};
}
if (n == "keys" || n == "values") { // a new array, in insertion order
need(0);
FType et = n == "keys" ? kt : vt;
unsigned field = n == "keys" ? 1 : 2;
FType at = FType::arrayOf(et);
Value *out = tmpOf(ty(at));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 1));
b.CreateCall(rt("finch_arr_make"), {out, len, elemSize(et)});
Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(at), out, 0));
Value *j = tmp(b.getInt64(0));
forEachEntry(map, t, [&](Value *, Value *entry) {
Value *jv = b.CreateLoad(b.getInt64Ty(), j);
copyAt(b.CreateInBoundsGEP(ty(et), data, jv), b.CreateStructGEP(ET, entry, field), et);
b.CreateStore(b.CreateAdd(jv, b.getInt64(1)), j);
});
return {b.CreateLoad(ty(at), out), at, false, true};
}
failAt(p.file, p.line, p.col, "a map has no method '" + n + "' (it has: has, get, remove, clear, keys, values; and m[key], .len)");
}
} // namespace finch

View file

@ -1,580 +0,0 @@
// 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::Map: {
llvm::Type *P = b.getPtrTy(), *I = b.getInt64Ty();
return StructType::get(ctx, {P, I, I, I, P, I, I}); // entries, len, used, cap, index, icap, adds
}
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::Map: {
FType k = resolveT(*t.key, p, true);
if (!k.isInt() && k.kind != FType::Char && k.kind != FType::Bool && k.kind != FType::Str)
failAt(p.file, p.line, p.col, "a map key must be a whole number, a char, a bool or a str, not " + k.show());
FType v = resolveT(*t.elem, p, false);
if (v.kind == FType::Void) failAt(p.file, p.line, p.col, "a map value can't have the type 'nothing'");
return FType::mapOf(k, v);
}
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.pos = f.namePos;
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->cstruct->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->cstruct->size > off) elems.push_back(ArrayType::get(b.getInt8Ty(), s->cstruct->size - off));
s->llvm->setBody(elems, true);
if (s->cstruct->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:
case FType::Map: 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::Map: return "map_" + typeKey(*t.key) + "_" + 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::Map) {
FType kt = *t.key, vt = *t.elem;
if (owning(kt) || owning(vt))
forEachEntry(a, t, [&](Value *, Value *entry) {
dropAt(b.CreateStructGEP(mapEntryTy(t), entry, 1), kt);
dropAt(b.CreateStructGEP(mapEntryTy(t), entry, 2), vt);
});
b.CreateCall(rt("finch_map_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::Map) {
StructType *ET = mapEntryTy(t);
FType kt = *t.key, vt = *t.elem;
b.CreateCall(rt("finch_map_clone_raw"), {dst, src, b.getInt64(dl->getTypeAllocSize(ET))});
if (owning(kt) || owning(vt)) // the slots were copied byte for byte: now give the copy its own keys and values
forEachEntry(src, t, [&](Value *i, Value *from) {
Value *to = mapEntry(dst, t, i);
copyAt(b.CreateStructGEP(ET, to, 1), b.CreateStructGEP(ET, from, 1), kt);
copyAt(b.CreateStructGEP(ET, to, 2), b.CreateStructGEP(ET, from, 2), vt);
});
} 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 { // pointers, arrays, maps, structs: built as text first (the same text str(x) gives)
Value *buf = tmp(zero(FType::Str));
emitFormat(buf, v, t, quoted);
b.CreateCall(rt("finch_buf_print"), {buf});
}
}
void Codegen::bufText(Value *buf, const std::string &text) {
b.CreateCall(rt("finch_buf_add"), {buf, b.CreateGlobalString(text, "txt"), b.getInt64(text.size())});
}
void Codegen::emitFormat(Value *buf, Value *v, const FType &t, bool quoted) {
if (t.isSigned()) b.CreateCall(rt("finch_buf_int"), {buf, b.CreateSExt(v, b.getInt64Ty())});
else if (t.isUnsigned()) b.CreateCall(rt("finch_buf_uint"), {buf, b.CreateZExt(v, b.getInt64Ty())});
else if (t.isFloat()) b.CreateCall(rt("finch_buf_float"), {buf, b.CreateFPExt(v, b.getDoubleTy())});
else if (t.kind == FType::Char) b.CreateCall(rt("finch_buf_char"), {buf, v, b.getInt32(quoted)});
else if (t.kind == FType::Bool) b.CreateCall(rt("finch_buf_bool"), {buf, b.CreateZExt(v, b.getInt32Ty())});
else if (t.kind == FType::Str) b.CreateCall(rt("finch_buf_str"), {buf, tmp(v), b.getInt32(quoted)});
else if (t.isPtr() || t.kind == FType::Null) b.CreateCall(rt("finch_buf_ptr"), {buf, v}); // the same everywhere: null or 0x...
else b.CreateCall(formatFn(t), {buf, tmp(v)});
}
// One function per array / map / struct type: (str *buf, T *value)
Function *Codegen::formatFn(const FType &t) {
std::string key = "format." + 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 *buf = f->getArg(0), *a = f->getArg(1);
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));
}
bufText(buf, "[");
forN(len, [&](Value *i) {
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item);
b.SetInsertPoint(sep);
bufText(buf, ", ");
b.CreateBr(item);
b.SetInsertPoint(item);
emitFormat(buf, b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true);
});
bufText(buf, "]");
} else if (t.kind == FType::Map) { // {"a": 1, "b": 2}
StructType *ET = mapEntryTy(t);
Value *firstItem = tmp(b.getInt1(true));
bufText(buf, "{");
forEachEntry(a, t, [&](Value *, Value *entry) {
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
b.CreateCondBr(b.CreateLoad(b.getInt1Ty(), firstItem), item, sep);
b.SetInsertPoint(sep);
bufText(buf, ", ");
b.CreateBr(item);
b.SetInsertPoint(item);
b.CreateStore(b.getInt1(false), firstItem);
emitFormat(buf, b.CreateLoad(ty(*t.key), b.CreateStructGEP(ET, entry, 1)), *t.key, true);
bufText(buf, ": ");
emitFormat(buf, b.CreateLoad(ty(*t.elem), b.CreateStructGEP(ET, entry, 2)), *t.elem, true);
});
bufText(buf, "}");
} else if (t.kind == FType::Struct) {
bufText(buf, t.info->name + "(");
bool first = true;
for (auto &fl : t.info->fields) {
if (fl.hidden) continue;
bufText(buf, (first ? "" : ", ") + fl.name + ": ");
first = false;
emitFormat(buf, b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true);
}
bufText(buf, ")");
}
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::Map: { // shown by its parts; the entries are behind `entries`
DIType *i64 = diType(FType::I64), *pt = di->createPointerType(nullptr, 64);
DIFile *f = diFiles[0];
const char *names[] = {"entries", "len", "used", "cap", "index", "icap", "adds"};
std::vector<Metadata *> els;
for (unsigned k = 0; k < 7; k++)
els.push_back(di->createMemberType(diUnit, names[k], f, 0, 64, 64, 64 * k, DINode::FlagZero, k == 0 || k == 4 ? pt : i64));
d = di->createStructType(diUnit, t.show(), f, 0, 448, 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

109
src/docs.fch Normal file
View file

@ -0,0 +1,109 @@
// 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."),
doc("addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."),
doc("new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."),
doc("free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."),
doc("str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."),
doc("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).")
]
}
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 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"]
}
fn typeNames() -> []str {
return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"]
}
// 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
}

28
src/embedded.fch Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,48 +0,0 @@
#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
// The language server needs errors as values: it shows them in the editor and keeps running.
struct FinchError {
int file, line, col;
std::string msg;
};
inline bool g_throwErrors = false;
// 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) {
if (g_throwErrors) throw FinchError{file, line, col, 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); }

File diff suppressed because it is too large Load diff

83
src/host.fch Normal file
View file

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

104
src/host_windows.fch Normal file
View file

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

View file

@ -1,39 +0,0 @@
#pragma once
// What the compiler learns about a program while checking it, for the language server:
// what each name is, its type, and where it was defined.
#include <string>
#include <vector>
struct SymRef { // a name in the source
int file, line, col, len;
std::string hover; // markdown shown on mouse-over
int defFile = -1, defLine = 0, defCol = 0;
};
struct VarInfo { // a variable or parameter, for completion
std::string name, type;
int file, line, col;
int fnFile, fnLine, fnEnd; // the function it lives in
};
struct FnInfo {
std::string module, name, signature, doc;
std::vector<std::string> params;
int file, line, col, endLine;
};
struct StructIndex {
std::string module, name;
std::vector<std::pair<std::string, std::string>> fields; // name, type
int file, line, col;
};
struct SemIndex {
std::vector<SymRef> refs;
std::vector<VarInfo> vars;
std::vector<FnInfo> fns;
std::vector<StructIndex> structs;
};
inline SemIndex *g_index = nullptr; // set by the language server; null when compiling

View file

@ -1,214 +0,0 @@
#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}, {"or", Tok::OrElse}, {"try", Tok::Try},
};
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::OrElse: return "'or'";
case Tok::Try: return "'try'";
case Tok::Colon: return "':'";
case Tok::LParen: return "'('";
case Tok::RParen: return "')'";
case Tok::LBrace: return "'{'";
case Tok::RBrace: return "'}'";
case Tok::LBracket: return "'['";
case Tok::RBracket: return "']'";
case Tok::Comma: return "','";
case Tok::Dot: return "'.'";
case Tok::Arrow: return "'->'";
case Tok::DotDot: return "'..'";
case Tok::Plus: return "'+'";
case Tok::Minus: return "'-'";
case Tok::Star: return "'*'";
case Tok::Slash: return "'/'";
case Tok::Percent: return "'%'";
case Tok::Amp: return "'&'";
case Tok::Pipe: return "'|'";
case Tok::Caret: return "'^'";
case Tok::Tilde: return "'~'";
case Tok::Shl: return "'<<'";
case Tok::Shr: return "'>>'";
case Tok::PercentEq: return "'%='";
case Tok::Assign: return "'='";
case Tok::Declare: return "':='";
case Tok::PlusEq: return "'+='";
case Tok::MinusEq: return "'-='";
case Tok::StarEq: return "'*='";
case Tok::SlashEq: return "'/='";
case Tok::Eq: return "'=='";
case Tok::NotEq: return "'!='";
case Tok::Less: return "'<'";
case Tok::LessEq: return "'<='";
case Tok::Greater: return "'>'";
case Tok::GreaterEq: return "'>='";
case Tok::And: return "'&&'";
case Tok::Or: return "'||'";
case Tok::Not: return "'!'";
}
return "?";
}

View file

@ -24,7 +24,7 @@ fn isHex(char c) -> bool {
fn isKeyword(str w) -> bool {
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false",
"null", "import", "link", "struct", "defer", "or", "try"]
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export"]
return keywords.contains(w)
}
@ -195,7 +195,20 @@ fn lex(str src, str path) -> []Token {
} else {
op := operator(lx)
if op.len == 0 {
fail(lx, t.line, t.col, "unexpected character '" + str(c) + "'")
// 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)

View file

@ -1,36 +0,0 @@
#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,
OrElse, Try, // or, try
// 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);

299
src/linker.fch Normal file
View file

@ -0,0 +1,299 @@
// 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("")
}
fn quote(str a) -> str {
return host.quote(a)
}
// "-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("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
}
// IR -> program. 0 if it worked.
fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
clang := getenv("FINCH_CLANG")
if clang.len == 0 {
clang = "clang"
}
// 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
}
ll := b.out + ".ll"
obj := b.out + ".o"
write_file(ll, b.ir)
target := ""
if tg.cross || tg.windows {
target = " --target=" + tg.triple
}
log := b.out + ".log"
code := host.run(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1")
host.remove(ll)
if code != 0 {
print(read_file(log) or "")
print("error: clang couldn't compile the program's IR")
host.remove(obj)
host.remove(log)
return 1
}
cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
if !tg.msvc {
cmd = cmd + " -lm"
}
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"
} else {
cmd = cmd + " -shared"
}
}
[]str temps
for l in b.links {
if l.ends_with(".c") {
// the program's own C file, built along with it
o := b.out + "." + str(temps.len) + ".o"
pic := " -fPIC"
if tg.windows {
pic = ""
}
c := host.run(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1")
if c != 0 {
print((read_file(log) or "") + "error: couldn't compile " + l)
host.remove(obj)
host.remove(log)
host.remove(o)
return 1
}
temps.push(o)
cmd = cmd + " " + quote(o)
} else if l.ends_with(".o") || l.ends_with(".a") {
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(obj)
host.remove(log)
for t in temps {
host.remove(t)
}
if status == 127 {
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")
return 1
}
if status != 0 {
return linkFailed(output, ci, tg)
}
if output.len > 0 {
print(output) // warnings, if any
}
return 0
}

View file

@ -1,74 +0,0 @@
#include "loader.h"
#include "error.h"
#include "lexer.h"
#include "parser.h"
#include "target.h"
#include <llvm/ADT/SmallString.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
using namespace llvm;
static bool readFile(const std::string &path, std::string &out) {
std::ifstream in(path, std::ios::binary);
if (!in) return false;
std::stringstream ss;
ss << in.rdbuf();
out = ss.str();
return true;
}
std::string normalizePath(const std::string &path) {
SmallString<256> p(path);
sys::fs::make_absolute(p);
sys::path::remove_dots(p, true);
sys::path::native(p);
return std::string(p.str());
}
void Loader::load(const std::string &path, const std::string &module, Pos from) {
std::string text;
bool found = false;
if (overrides) {
auto it = overrides->find(normalizePath(path));
if (it != overrides->end()) text = it->second, found = true;
}
if (!found && !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
std::string Loader::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, pathListSeparator());)
if (!d.empty() && sys::fs::exists(d + "/" + name + ".fch")) return d + "/" + name + ".fch";
}
return here;
}

View file

@ -1,20 +0,0 @@
#pragma once
// Reads the main file and, recursively, every Finch module it imports.
#include "ast.h"
#include <map>
#include <string>
#include <vector>
struct Loader {
std::vector<Program> progs;
std::map<std::string, bool> loaded; // module name -> seen
const std::map<std::string, std::string> *overrides = nullptr; // path -> text (files open in an editor)
void load(const std::string &path, const std::string &module, Pos from);
static std::string findModule(const std::string &dir, const std::string &name);
};
// A path made absolute and tidy, so two spellings of one file compare equal.
std::string normalizePath(const std::string &path);

View file

@ -1,580 +0,0 @@
// The Finch language server: `finch lsp` speaks the Language Server Protocol on stdin/stdout.
//
// Every time a document changes it is checked by the real compiler (lexer, parser, type
// checker and code generator, without optimizing or writing anything), with errors thrown
// instead of ending the process and a semantic index (index.h) collecting what each name is.
// Hover, go-to-definition, completion, signature help and document symbols read that index.
#include "builtins_doc.h"
#include "cimport.h"
#include "codegen.h"
#include "error.h"
#include "index.h"
#include "loader.h"
#include "target.h"
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/JSON.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Target/TargetMachine.h>
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#endif
using namespace llvm;
namespace {
// ---------- URIs ----------
std::string uriToPath(const std::string &uri) {
std::string s = uri.rfind("file://", 0) == 0 ? uri.substr(7) : uri;
std::string out;
for (size_t i = 0; i < s.size(); i++) {
if (s[i] == '%' && i + 2 < s.size()) {
out += (char)std::stoi(s.substr(i + 1, 2), nullptr, 16);
i += 2;
} else {
out += s[i];
}
}
#ifdef _WIN32
if (out.size() > 2 && out[0] == '/' && out[2] == ':') out = out.substr(1); // /c:/x -> c:/x
#endif
return normalizePath(out);
}
std::string pathToUri(const std::string &path) {
std::string p = normalizePath(path), out = "file://";
#ifdef _WIN32
for (char &c : p)
if (c == '\\') c = '/';
out += "/";
#endif
for (unsigned char c : p) {
if (isalnum(c) || c == '/' || c == '-' || c == '_' || c == '.' || c == '~' || c == ':') out += (char)c;
else {
char buf[4];
std::snprintf(buf, sizeof buf, "%%%02X", c);
out += buf;
}
}
return out;
}
// ---------- one check of a document ----------
struct Diagnostic {
std::string path;
int line, col;
std::string msg;
};
struct Analysis {
std::vector<SourceFile> files;
SemIndex index;
std::vector<Diagnostic> diags;
std::set<std::string> modules; // modules the document imports
const CImports *c = nullptr;
};
class Server {
public:
int run();
private:
std::map<std::string, std::string> docs; // normalized path -> text
std::map<std::string, std::shared_ptr<Analysis>> results; // last check of each open document
std::map<std::string, std::shared_ptr<Analysis>> lastGood; // last check without errors
std::map<std::string, std::unique_ptr<CImports>> headerCache;
std::set<std::string> diagnosed; // paths we published errors for
TargetMachine *tm = nullptr;
bool shutdown = false;
// protocol
bool readMessage(json::Value &out);
void send(const json::Value &v);
void reply(const json::Value &id, json::Value result);
void notify(const std::string &method, json::Value params);
void handle(const json::Object &msg);
// features
void check(const std::string &path);
json::Value hover(const std::string &path, int line, int col);
json::Value definition(const std::string &path, int line, int col);
json::Value completion(const std::string &path, int line, int col);
json::Value symbols(const std::string &path);
json::Value signatureHelp(const std::string &path, int line, int col);
const SymRef *refAt(const Analysis &a, const std::string &path, int line, int col);
std::string lineText(const std::string &path, int line);
};
bool Server::readMessage(json::Value &out) {
size_t length = 0;
std::string header;
while (std::getline(std::cin, header)) {
if (!header.empty() && header.back() == '\r') header.pop_back();
if (header.empty()) break;
if (header.rfind("Content-Length:", 0) == 0) length = std::stoul(header.substr(15));
}
if (!std::cin || length == 0) return false;
std::string body(length, '\0');
std::cin.read(&body[0], length);
Expected<json::Value> v = json::parse(body);
if (!v) {
consumeError(v.takeError());
return true; // skip a broken message
}
out = std::move(*v);
return true;
}
void Server::send(const json::Value &v) {
std::string body;
raw_string_ostream os(body);
os << v;
os.flush();
std::cout << "Content-Length: " << body.size() << "\r\n\r\n" << body;
std::cout.flush();
}
void Server::reply(const json::Value &id, json::Value result) {
send(json::Object{{"jsonrpc", "2.0"}, {"id", id}, {"result", std::move(result)}});
}
void Server::notify(const std::string &method, json::Value params) {
send(json::Object{{"jsonrpc", "2.0"}, {"method", method}, {"params", std::move(params)}});
}
int Server::run() {
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
#endif
initTargets();
setTarget("");
std::string err;
const Target *target = TargetRegistry::lookupTarget(g_target.triple, err);
if (!target) return 1;
tm = target->createTargetMachine(g_target.triple, "generic", "", TargetOptions(), Reloc::PIC_);
json::Value msg(nullptr);
while (readMessage(msg)) {
if (const json::Object *o = msg.getAsObject()) handle(*o);
if (shutdown && !std::cin) break;
}
return 0;
}
static int intAt(const json::Object *o, const char *key) {
if (!o) return 0;
if (auto v = o->getInteger(key)) return (int)*v;
return 0;
}
void Server::handle(const json::Object &msg) {
std::string method = msg.getString("method").value_or("").str();
const json::Value *id = msg.get("id");
const json::Object *params = msg.getObject("params");
auto docPath = [&]() -> std::string {
if (!params) return "";
const json::Object *td = params->getObject("textDocument");
return td ? uriToPath(td->getString("uri").value_or("").str()) : "";
};
// positions: LSP is 0-based (line, UTF-16 column); Finch is 1-based (line, character)
auto position = [&](int &line, int &col) {
const json::Object *pos = params ? params->getObject("position") : nullptr;
line = intAt(pos, "line") + 1;
col = intAt(pos, "character") + 1;
};
if (method == "initialize") {
json::Object caps{
{"textDocumentSync", json::Object{{"openClose", true}, {"change", 1}, {"save", true}}},
{"hoverProvider", true},
{"definitionProvider", true},
{"documentSymbolProvider", true},
{"completionProvider", json::Object{{"triggerCharacters", json::Array{"."}}}},
{"signatureHelpProvider", json::Object{{"triggerCharacters", json::Array{"(", ","}}}},
};
reply(*id, json::Object{{"capabilities", std::move(caps)},
{"serverInfo", json::Object{{"name", "finch"}, {"version", FINCH_VERSION}}}});
} else if (method == "shutdown") {
shutdown = true;
reply(*id, nullptr);
} else if (method == "exit") {
std::exit(shutdown ? 0 : 1);
} else if (method == "textDocument/didOpen" || method == "textDocument/didChange") {
std::string path = docPath();
if (method == "textDocument/didOpen") {
docs[path] = params->getObject("textDocument")->getString("text").value_or("").str();
} else if (const json::Array *changes = params->getArray("contentChanges")) {
if (!changes->empty())
if (const json::Object *ch = changes->back().getAsObject()) docs[path] = ch->getString("text").value_or("").str();
}
check(path);
} else if (method == "textDocument/didSave") {
check(docPath());
} else if (method == "textDocument/didClose") {
std::string path = docPath();
docs.erase(path);
results.erase(path);
lastGood.erase(path);
notify("textDocument/publishDiagnostics", json::Object{{"uri", pathToUri(path)}, {"diagnostics", json::Array{}}});
} else if (id) {
int line = 0, col = 0;
position(line, col);
std::string path = docPath();
if (method == "textDocument/hover") reply(*id, hover(path, line, col));
else if (method == "textDocument/definition") reply(*id, definition(path, line, col));
else if (method == "textDocument/completion") reply(*id, completion(path, line, col));
else if (method == "textDocument/documentSymbol") reply(*id, symbols(path));
else if (method == "textDocument/signatureHelp") reply(*id, signatureHelp(path, line, col));
else send(json::Object{{"jsonrpc", "2.0"}, {"id", *id},
{"error", json::Object{{"code", -32601}, {"message", "not supported: " + method}}}});
}
}
// Check `path` with the real compiler, keep the index, publish errors.
void Server::check(const std::string &path) {
auto a = std::make_shared<Analysis>();
g_files.clear();
g_throwErrors = true;
g_index = &a->index;
try {
Loader loader;
loader.overrides = &docs;
loader.load(path, "", Pos{});
std::vector<Import> imports;
std::vector<std::string> dirs;
std::string key;
for (const Program &p : loader.progs) {
for (const Import &im : p.imports) {
imports.push_back(im);
if (im.isC) key += im.path + "\n";
else if (p.module.empty()) a->modules.insert(im.path);
}
std::string d = dirOf(p.path);
if (std::find(dirs.begin(), dirs.end(), d) == dirs.end()) dirs.push_back(d), key += "@" + d + "\n";
}
auto &cached = headerCache[key]; // headers are slow to read: once per set of imports
if (!cached) cached = std::make_unique<CImports>(importHeaders(imports, dirs));
a->c = cached.get();
LLVMContext ctx;
generate(loader.progs, *cached, ctx, *tm, false);
} catch (const FinchError &e) {
std::string file = e.file >= 0 && e.file < (int)g_files.size() ? normalizePath(g_files[e.file].path) : path;
a->diags.push_back({file, e.line, e.col, e.msg});
}
g_index = nullptr;
g_throwErrors = false;
a->files = g_files;
// While the code is half-typed (`p.`, `add(1, `) it doesn't compile; completion and signature
// help keep using what the last good version knew. Hover and definition use the newest refs.
if (a->diags.empty()) {
lastGood[path] = a;
} else if (auto good = lastGood.find(path); good != lastGood.end()) {
const Analysis &g = *good->second;
for (const FnInfo &f : g.index.fns) a->index.fns.push_back(f);
for (const StructIndex &st : g.index.structs) a->index.structs.push_back(st);
for (const VarInfo &v : g.index.vars) a->index.vars.push_back(v);
if (a->modules.empty()) a->modules = g.modules;
if (!a->c) a->c = g.c;
}
results[path] = a;
std::map<std::string, json::Array> byFile;
byFile[path];
for (const std::string &p : diagnosed) byFile[p]; // clear errors that went away
diagnosed.clear();
for (const Diagnostic &d : a->diags) {
int line = std::max(0, d.line - 1), col = std::max(0, d.col - 1);
byFile[d.path].push_back(json::Object{
{"range", json::Object{{"start", json::Object{{"line", line}, {"character", col}}},
{"end", json::Object{{"line", line}, {"character", col + 1}}}}},
{"severity", 1},
{"source", "finch"},
{"message", d.msg}});
diagnosed.insert(d.path);
}
for (auto &[p, list] : byFile)
notify("textDocument/publishDiagnostics", json::Object{{"uri", pathToUri(p)}, {"diagnostics", std::move(list)}});
}
const SymRef *Server::refAt(const Analysis &a, const std::string &path, int line, int col) {
for (const SymRef &r : a.index.refs) {
if (r.line != line || col < r.col || col > r.col + r.len) continue;
if (r.file < 0 || r.file >= (int)a.files.size() || normalizePath(a.files[r.file].path) != path) continue;
return &r;
}
return nullptr;
}
json::Value Server::hover(const std::string &path, int line, int col) {
auto it = results.find(path);
if (it == results.end()) return nullptr;
const SymRef *r = refAt(*it->second, path, line, col);
if (!r) return nullptr;
return json::Object{{"contents", json::Object{{"kind", "markdown"}, {"value", r->hover}}}};
}
json::Value Server::definition(const std::string &path, int line, int col) {
auto it = results.find(path);
if (it == results.end()) return nullptr;
const Analysis &a = *it->second;
const SymRef *r = refAt(a, path, line, col);
if (!r || r->defFile < 0 || r->defFile >= (int)a.files.size()) return nullptr;
int l = r->defLine - 1, c = r->defCol - 1;
return json::Object{{"uri", pathToUri(a.files[r->defFile].path)},
{"range", json::Object{{"start", json::Object{{"line", l}, {"character", c}}},
{"end", json::Object{{"line", l}, {"character", c}}}}}};
}
std::string Server::lineText(const std::string &path, int line) {
auto it = docs.find(path);
if (it == docs.end()) return "";
size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) {
start = it->second.find('\n', start);
if (start != std::string::npos) start++;
}
if (start == std::string::npos) return "";
return it->second.substr(start, it->second.find('\n', start) - start);
}
// LSP CompletionItemKind
enum { KMethod = 2, KFunction = 3, KField = 5, KVariable = 6, KModule = 9, KKeyword = 14, KSnippet = 15,
KConstant = 21, KStruct = 22, KTypeParam = 25 };
static json::Value item(const std::string &label, int kind, const std::string &detail = "", const std::string &doc = "") {
json::Object o{{"label", label}, {"kind", kind}};
if (!detail.empty()) o["detail"] = detail;
if (!doc.empty()) o["documentation"] = json::Object{{"kind", "markdown"}, {"value", doc}};
return o;
}
json::Value Server::completion(const std::string &path, int line, int col) {
json::Array items;
auto it = results.find(path);
std::shared_ptr<Analysis> a = it == results.end() ? nullptr : it->second;
// what's before the cursor: "name." or "name.partial"?
std::string text = lineText(path, line);
// columns count characters, but we only need the ASCII part right before the cursor
std::string before;
{
int c = 1;
size_t k = 0;
for (; k < text.size() && c < col; k++)
if (((unsigned char)text[k] & 0xC0) != 0x80) c++;
before = text.substr(0, k);
}
size_t end = before.size();
while (end > 0 && (isalnum((unsigned char)before[end - 1]) || before[end - 1] == '_')) end--;
bool afterDot = end > 0 && before[end - 1] == '.';
if (afterDot) {
size_t s = end - 1;
while (s > 0 && (isalnum((unsigned char)before[s - 1]) || before[s - 1] == '_' || before[s - 1] == ']' || before[s - 1] == '[')) s--;
std::string owner = before.substr(s, end - 1 - s);
if (size_t br = owner.find('['); br != std::string::npos) owner = owner.substr(0, br) + "[]";
if (!a) return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
// a module?
if (a->modules.count(owner)) {
std::set<std::string> seen;
for (const FnInfo &f : a->index.fns)
if (f.module == owner && seen.insert(f.name).second) items.push_back(item(f.name, KFunction, f.signature));
for (const StructIndex &s2 : a->index.structs)
if (s2.module == owner && seen.insert(s2.name).second) items.push_back(item(s2.name, KStruct, "struct " + s2.name));
return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
}
// a variable: find its type (the innermost one declared before the cursor)
bool element = owner.size() > 2 && owner.compare(owner.size() - 2, 2, "[]") == 0;
std::string name = element ? owner.substr(0, owner.size() - 2) : owner;
std::string type;
for (const VarInfo &v : a->index.vars)
if (v.name == name && v.line <= line && line <= v.fnEnd + 1) type = v.type;
if (element && type.rfind("[]", 0) == 0) type = type.substr(2);
if (element && type == "str") type = "char";
if (element && type.rfind("map[", 0) == 0) { // m[key]. -> the value type
int depth = 0;
for (size_t k = 3; k < type.size(); k++) {
if (type[k] == '[') depth++;
else if (type[k] == ']' && --depth == 0) {
type = type.substr(k + 1);
break;
}
}
}
if (type.rfind("ptr[", 0) == 0) type = type.substr(4, type.size() - 5); // fields through a pointer
if (type == "str") {
for (const BuiltinDoc &d : kStrMethods) items.push_back(item(d.name, d.name[0] == 'l' || d.name[0] == 'p' ? KField : KMethod, d.signature, d.doc));
} else if (type.rfind("[]", 0) == 0) {
for (const BuiltinDoc &d : kArrayMethods) items.push_back(item(d.name, std::string(d.name) == "len" || std::string(d.name) == "ptr" ? KField : KMethod, d.signature, d.doc));
} else if (type.rfind("map[", 0) == 0) {
for (const BuiltinDoc &d : kMapMethods) items.push_back(item(d.name, std::string(d.name) == "len" ? KField : KMethod, d.signature, d.doc));
} else if (!type.empty()) {
std::string mod, sname = type;
if (size_t dot = type.find('.'); dot != std::string::npos) mod = type.substr(0, dot), sname = type.substr(dot + 1);
for (const StructIndex &s2 : a->index.structs)
if (s2.name == sname && (mod.empty() || s2.module == mod))
for (auto &[fname, ftype] : s2.fields) items.push_back(item(fname, KField, ftype + " " + fname));
std::set<std::string> seen;
for (const FnInfo &f : a->index.fns) // methods: indexed as Struct.name
if (f.name.size() > sname.size() + 1 && f.name.compare(0, sname.size() + 1, sname + ".") == 0 &&
seen.insert(f.name).second)
items.push_back(item(f.name.substr(sname.size() + 1), KMethod, f.signature));
if (type.rfind("ptr", 0) == 0 || items.empty()) items.push_back(item("value", KField, "the value the pointer points to"));
}
return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
}
std::string prefix = before.substr(end);
for (const char *k : kKeywords) items.push_back(item(k, KKeyword));
for (const char *t : kTypeNames) items.push_back(item(t, KTypeParam, "type"));
for (const BuiltinDoc &d : kBuiltins) items.push_back(item(d.name, KFunction, d.signature, d.doc));
if (a) {
std::set<std::string> seen;
for (const VarInfo &v : a->index.vars) // variables of the function around the cursor
if (v.fnLine <= line && line <= v.fnEnd && v.line <= line && seen.insert(v.name).second)
items.push_back(item(v.name, KVariable, v.type + " " + v.name));
for (const FnInfo &f : a->index.fns)
if (f.module.empty() && seen.insert(f.name).second) items.push_back(item(f.name, KFunction, f.signature));
for (const StructIndex &s : a->index.structs)
if (s.module.empty() && seen.insert(s.name).second) items.push_back(item(s.name, KStruct, "struct " + s.name));
for (const std::string &m : a->modules) items.push_back(item(m, KModule, "module " + m));
// C names only once something is typed: headers declare thousands
if (a->c && !prefix.empty()) {
for (auto &[name, f] : a->c->fns)
if (name.rfind(prefix, 0) == 0 && f.unsupported.empty()) items.push_back(item(name, KFunction, "C: " + f.header));
for (auto &[name, k] : a->c->consts)
if (name.rfind(prefix, 0) == 0) items.push_back(item(name, KConstant, "C constant"));
for (auto &[name, s] : a->c->structs)
if (name.rfind(prefix, 0) == 0) items.push_back(item(name, KStruct, "C struct"));
}
}
return json::Object{{"isIncomplete", !prefix.empty()}, {"items", std::move(items)}};
}
json::Value Server::symbols(const std::string &path) {
json::Array out;
auto it = results.find(path);
if (it == results.end()) return out;
const Analysis &a = *it->second;
auto inDoc = [&](int file) { return file >= 0 && file < (int)a.files.size() && normalizePath(a.files[file].path) == path; };
auto range = [](int l1, int c1, int l2) {
return json::Object{{"start", json::Object{{"line", l1 - 1}, {"character", c1 - 1}}},
{"end", json::Object{{"line", std::max(l1, l2) - 1}, {"character", 0}}}};
};
for (const FnInfo &f : a.index.fns)
if (inDoc(f.file))
out.push_back(json::Object{{"name", f.name}, {"detail", f.signature}, {"kind", 12},
{"range", range(f.line, f.col, f.endLine + 1)}, {"selectionRange", range(f.line, f.col, f.line)}});
for (const StructIndex &s : a.index.structs)
if (inDoc(s.file)) {
json::Array kids;
for (auto &[n, t] : s.fields) kids.push_back(json::Object{{"name", n}, {"detail", t}, {"kind", 8},
{"range", range(s.line, s.col, s.line)}, {"selectionRange", range(s.line, s.col, s.line)}});
out.push_back(json::Object{{"name", s.name}, {"kind", 23}, {"range", range(s.line, s.col, s.line)},
{"selectionRange", range(s.line, s.col, s.line)}, {"children", std::move(kids)}});
}
return out;
}
// Inside `name(a, b|`: which function, and which argument.
json::Value Server::signatureHelp(const std::string &path, int line, int col) {
auto it = results.find(path);
std::shared_ptr<Analysis> a = it == results.end() ? nullptr : it->second;
std::string text;
for (int l = std::max(1, line - 5); l < line; l++) text += lineText(path, l) + "\n";
std::string cur = lineText(path, line);
text += cur.substr(0, std::min<size_t>(cur.size(), col - 1));
int depth = 0, commas = 0;
size_t k = text.size();
while (k > 0) {
char ch = text[--k];
if (ch == ')' || ch == ']') depth++;
else if (ch == '[') depth--;
else if (ch == '(') {
if (depth == 0) break;
depth--;
} else if (ch == ',' && depth == 0) commas++;
if (k == 0) return nullptr;
}
size_t e = k;
while (k > 0 && (isalnum((unsigned char)text[k - 1]) || text[k - 1] == '_' || text[k - 1] == '.')) k--;
std::string callee = text.substr(k, e - k);
if (callee.empty()) return nullptr;
std::string name = callee.substr(callee.rfind('.') == std::string::npos ? 0 : callee.rfind('.') + 1);
std::string mod = callee.find('.') == std::string::npos ? "" : callee.substr(0, callee.find('.'));
std::string label, doc;
std::vector<std::string> params;
if (a) {
for (const FnInfo &f : a->index.fns) {
bool method = f.name.size() > name.size() && f.name.compare(f.name.size() - name.size() - 1, name.size() + 1, "." + name) == 0;
if ((f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod))) ||
(method && !mod.empty() && !a->modules.count(mod))) // p.move( -> Point.move
label = f.signature, params = f.params;
}
if (label.empty())
for (const StructIndex &s : a->index.structs)
if (s.name == name) {
label = name + "(";
for (size_t i = 0; i < s.fields.size(); i++) {
params.push_back(s.fields[i].second + " " + s.fields[i].first);
label += (i ? ", " : "") + params.back();
}
label += ")";
}
if (label.empty() && a->c)
if (auto f = a->c->fns.find(name); f != a->c->fns.end()) {
label = name + "(";
for (size_t i = 0; i < f->second.params.size(); i++) {
params.push_back(f->second.params[i].show());
label += (i ? ", " : "") + params.back();
}
label += f->second.variadic ? ", ...)" : ")";
doc = "C function from " + f->second.header;
}
}
if (label.empty()) {
auto look = [&](const BuiltinDoc *f, const BuiltinDoc *l) {
for (; f != l && label.empty(); ++f)
if (name == f->name) label = f->signature, doc = f->doc;
};
look(std::begin(kBuiltins), std::end(kBuiltins));
if (!mod.empty()) {
look(std::begin(kArrayMethods), std::end(kArrayMethods));
look(std::begin(kStrMethods), std::end(kStrMethods));
look(std::begin(kMapMethods), std::end(kMapMethods));
}
}
if (label.empty()) return nullptr;
json::Array ps;
for (const std::string &p : params) ps.push_back(json::Object{{"label", p}});
json::Object sig{{"label", label}, {"parameters", std::move(ps)}};
if (!doc.empty()) sig["documentation"] = doc;
return json::Object{{"signatures", json::Array{std::move(sig)}}, {"activeSignature", 0}, {"activeParameter", commas}};
}
} // namespace
int runLanguageServer() { return Server().run(); }

876
src/lsp.fch Normal file
View file

@ -0,0 +1,876 @@
// 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 "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
Diag diag
}
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
}
}
return str(out)
}
fn pathToUri(str path) -> str {
digits := "0123456789ABCDEF"
[]str parts = ["file://"]
for c in path {
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.split("\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 == "@I" && f.len >= 2 {
a.imports.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
}
}
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)
}
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) {
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 {
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))
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/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
}

View file

@ -1,350 +0,0 @@
#include "cimport.h"
#include "codegen.h"
#include "error.h"
#include "lexer.h"
#include "loader.h"
#include "parser.h"
#include "target.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 <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"
" finch lsp the language server, for editors (VS Code, Kate, ...)\n"
"\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n"
" --target <name> build for another system: windows, linux, arm64 (Linux), macos,\n"
" or an LLVM triple\n"
" -g add debug info (gdb / lldb / Visual Studio)\n"
" -O0 skip optimizations (to read the raw IR)\n");
std::exit(1);
}
// ---------- 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 *targetMachine() {
initTargets();
const Triple &triple = g_target.triple;
if (!triple.isX86() && !triple.isAArch64()) {
std::fprintf(stderr, "error: Finch generates code for x86-64 and ARM64, not for %s\n", triple.getArchName().str().c_str());
std::exit(1);
}
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 ----------
// The runtime is compiled once per version and kept in ~/.cache/finch.
static std::string runtimeObject() {
std::string dir = cacheDir();
size_t hash = std::hash<std::string>()(std::string(kRuntimeSource) + FINCH_VERSION + g_target.cc + g_target.triple.str());
std::string obj = dir + (g_target.windows && !g_target.cross ? "\\" : "/") + "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;
std::string pic = g_target.windows ? "" : " -fPIC";
int status = capture(g_target.cc + " -O2" + pic + " -c " + shellQuote(src) + " -o " + shellQuote(part), output);
std::remove(src.c_str());
if (status != 0) {
std::fprintf(stderr, "%serror: couldn't build the Finch runtime with '%s' (is a C compiler installed?)\n", output.c_str(),
g_target.cc.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;
// macOS: link "Cocoa.framework" -> -framework Cocoa
if (lib.size() > 10 && lib.compare(lib.size() - 10, 10, ".framework") == 0)
return " -framework " + shellQuote(lib.substr(0, lib.size() - 10));
if (!g_target.cross && capture("pkg-config --libs " + shellQuote(lib), out) == 0) {
while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back();
return " " + out;
}
return " " + shellQuote("-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("unresolved external symbol ")) != std::string::npos) { // MSVC link.exe
a += 27;
addOnce(missing, l.substr(a, l.find_first_of(" \n\r", a) - a));
} else if ((a = l.find("cannot open file '")) != std::string::npos || // MSVC: LNK1104
(a = l.find("cannot open input file '")) != std::string::npos || // MSVC: LNK1181
(a = l.find("could not open '")) != std::string::npos) { // lld-link
a = l.find('\'', a) + 1;
std::string lib = l.substr(a, l.find('\'', a) - a);
if (lib.size() > 4 && lib.compare(lib.size() - 4, 4, ".lib") == 0) lib = lib.substr(0, lib.size() - 4);
addOnce(missingLibs, lib);
} else if ((a = l.find("cannot find -l")) != std::string::npos ||
(a = l.find("library not found for -l")) != std::string::npos) { // older Apple ld
a = l.find("-l", a) + 2;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n\r", a) - a));
} else if ((a = l.find("ld: library '")) != std::string::npos) { // Apple ld: library 'x' not found
a += 13;
addOnce(missingLibs, l.substr(a, l.find('\'', a) - a));
} else if ((a = l.find("framework '")) != std::string::npos && l.find("not found") != std::string::npos) {
a += 11;
addOnce(missingLibs, l.substr(a, l.find('\'', a) - a) + ".framework");
} else if (l.rfind(" \"", 0) == 0 && (a = l.find("\", referenced from:")) != std::string::npos) {
// Apple ld: Undefined symbols for architecture arm64:\n "_name", referenced from:
std::string name = l.substr(3, a - 3);
if (g_target.darwin && !name.empty() && name[0] == '_') name.erase(0, 1);
addOnce(missing, name);
}
}
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 = g_target.cc + " " + shellQuote(obj) + " " + shellQuote(runtimeObject()) + " -o " + shellQuote(exe);
if (!g_target.msvc) cmd += " -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;
std::string pic = g_target.windows ? "" : " -fPIC";
if (capture(g_target.cc + " -O2" + pic + " -c " + shellQuote(l) + " -o " + shellQuote(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 += " " + shellQuote(o);
} else if (endsWith(l, ".o") || endsWith(l, ".a")) {
cmd += " " + shellQuote(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 ('%s'; install one, or set FINCH_CC)\n", g_target.cc.c_str());
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);
}
int runLanguageServer(); // lsp.cpp
int main(int argc, char **argv) {
if (argc == 2 && std::string(argv[1]) == "lsp") return runLanguageServer();
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;
std::string targetName;
bool optimizeCode = true, debug = false, outGiven = 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], outGiven = true;
else if (a == "--target" && i + 1 < argc) targetName = 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();
}
setTarget(targetName);
if (!outGiven) out += g_target.exe;
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 && !sys::path::is_absolute(l.lib) ? 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 = targetMachine();
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", g_target.windows ? "exe" : "", 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);
line = runPrefix() + line; // wine / qemu for programs built for another system
#ifdef _WIN32
line = "\"" + line + "\""; // cmd /c strips one pair of outer quotes
#endif
std::string crash;
int status = runCommand(line, &crash);
std::remove(out.c_str());
if (!crash.empty()) std::fprintf(stderr, "\nthe program crashed: %s\n", crash.c_str());
return status;
}
return 0;
}

330
src/main.fch Normal file
View file

@ -0,0 +1,330 @@
// 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
import cimport
import platform
import linker
import lsp
import host
import embedded
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)
}
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 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
}
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
imports := prog.imports
l.progs.push(prog)
for name in imports {
if !l.loaded.contains(name) {
l.loaded.push(name)
load(l, findModule(l, path, name), name)
}
}
}
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 ir <file.fch> print the LLVM IR")
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(" --target <name> build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple")
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
}
fn main([]str args) -> int {
if args.len == 2 && (args[1] == "version" || args[1] == "--version") {
print("Finch 2.4.0 (the compiler is written in Finch)")
return 0
}
if args.len == 2 && args[1] == "lsp" {
return lsp.run(args[0]) // the language server, for editors
}
if args.len < 3 {
usage()
}
cmd := args[1]
file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" {
usage()
}
out := stem(file)
outGiven := false
shared := false
debug := false
targetName := ""
opt := "-O2"
[]str extraLibs
[]str extraDirs
[]str overlayPaths
[]str overlayFiles
[]str progArgs
ai := 3
while ai < args.len {
a := args[ai]
if cmd == "run" && progArgs.len > 0 { // after the first argument of the program, all of them are its
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 == "--target" && ai + 1 < args.len {
targetName = args[ai + 1]
ai += 1
} else if cmd == "run" {
progArgs.push(a)
} else {
usage()
}
ai += 1
}
tg := platform.select(targetName)
if cmd == "run" {
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
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 l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") {
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
}
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg))
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
}
if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links), tg, ci) != 0 {
return 1
}
if cmd == "run" {
[]str quoted
for a in progArgs {
quoted.push(linker.quote(a))
}
status := host.run(tg.runner + linker.quote(out) + " " + quoted.join(" "))
host.remove(out)
return status
}
return 0
}

View file

@ -1,602 +0,0 @@
#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, map[str]int, 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)");
if (cur().text == "map" && peekTok().kind == Tok::LBracket) {
next();
next();
Type k = type();
expect(Tok::RBracket, "']' (a map type looks like map[str]int)");
return Type::mapOf(k, type());
}
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" || cur().text == "map") && 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.namePos = pos();
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.namePos = pos();
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.namePos = pos();
fn.name = expect(Tok::Ident, "a function name").text;
if (accept(Tok::Dot)) { // fn Point.move(...): a method
fn.recv = fn.name;
fn.recvPos = fn.namePos;
fn.namePos = pos();
fn.name = expect(Tok::Ident, "a method name (fn Point.name(...))").text;
}
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
Param p;
p.pos = pos();
p.type = type();
p.namePos = pos();
p.name = expect(Tok::Ident, "a parameter name").text;
fn.params.push_back(p);
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
fn.ret = Type(Type::Void);
if (accept(Tok::Arrow)) {
if (!at(Tok::Not)) fn.ret = type();
fn.fallible = accept(Tok::Not); // -> int! or -> !
if (!fn.fallible && fn.ret.kind == Type::Void) unexpected("(after '->' comes a type, or ! for a function that can fail)");
}
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;
b->end = pos();
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();
Pos varPos = pos();
std::string var = expect(Tok::Ident, "a loop variable name").text;
std::string var2;
Pos var2Pos;
if (accept(Tok::Comma)) { // for i, x in list / for key, value in map
var2Pos = pos();
var2 = expect(Tok::Ident, "a second loop variable name").text;
}
expect(Tok::In);
ExprPtr first = expr();
if (!var2.empty() && at(Tok::DotDot)) fail(var2Pos.line, var2Pos.col, "a range (for i in 0..n) has one loop variable");
if (accept(Tok::DotDot)) { // for i in 0..10
auto s = std::make_unique<ForStmt>(p);
s->var = var;
s->varPos = varPos;
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->varPos = varPos;
s->var2 = var2;
s->var2Pos = var2Pos;
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->namePos = pos();
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->namePos = pos();
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 && e->kind != ExprKind::Try && e->kind != ExprKind::OrElse)
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) ----------
// f(x) or fallback / f(x) or { ... }: the lowest precedence, right to left
ExprPtr expr() {
ExprPtr l = orExpr();
if (!sameLine() || !at(Tok::OrElse)) return l;
Pos p = pos();
next();
auto e = std::make_unique<OrElseExpr>(p, std::move(l));
if (at(Tok::LBrace)) e->block = block();
else e->fallback = expr();
return e;
}
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());
if (accept(Tok::Try)) return std::make_unique<TryExpr>(p, postfix());
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] or a map: ["a": 1, "b": 2], [:]
next();
parenDepth++;
if (accept(Tok::Colon)) {
parenDepth--;
expect(Tok::RBracket, "']' (an empty map is [:])");
return std::make_unique<MapLitExpr>(p);
}
if (!at(Tok::RBracket)) {
ExprPtr first = expr();
if (accept(Tok::Colon)) {
auto m = std::make_unique<MapLitExpr>(p);
m->keys.push_back(std::move(first));
m->values.push_back(expr());
while (accept(Tok::Comma) && !at(Tok::RBracket)) { // a trailing comma is fine
m->keys.push_back(expr());
expect(Tok::Colon, "':' (every map entry is key: value)");
m->values.push_back(expr());
}
parenDepth--;
expect(Tok::RBracket);
return m;
}
auto a = std::make_unique<ArrayLitExpr>(p);
a->elems.push_back(std::move(first));
while (accept(Tok::Comma) && !at(Tok::RBracket)) a->elems.push_back(expr());
parenDepth--;
expect(Tok::RBracket);
return a;
}
auto a = std::make_unique<ArrayLitExpr>(p);
parenDepth--;
expect(Tok::RBracket);
return a;
}
case Tok::Ident: {
next();
if (at(Tok::LParen) && !cur().newlineBefore) {
auto call = std::make_unique<CallExpr>(p, t.text);
args(call->args, call->argNames);
return call;
}
if (isTypeName(t.text))
fail(p.line, p.col, "a type name alone is not a value (did you mean " + t.text + "(...)?)");
return std::make_unique<VarExpr>(p, t.text);
}
default: unexpected("(expected a value)");
}
}
};
} // namespace
Program parse(const std::vector<Token> &tokens, int file) {
return Parser(tokens, file).program();
}

View file

@ -125,10 +125,80 @@ fn parseType(ptr[Parser] p) -> ast.Type {
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) == "]"
}
@ -159,12 +229,31 @@ fn parseProgram(ptr[Parser] p) -> 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)")
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'")
}
prog.imports.push(next(p).text)
}
prog.imports.push(next(p).text)
} else if at(p, "link") {
lt := next(p)
lib := expect(p, "string", "a library name in quotes, like link \"glfw\"").text
ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") // your own C code, next to the .fch file
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, "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 {
prog.fns.push(parseFn(p))
}
@ -172,10 +261,151 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
return prog
}
// 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") {
@ -199,8 +429,15 @@ fn parseStruct(ptr[Parser] p) -> ast.Node {
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' 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
@ -231,6 +468,7 @@ fn parseFn(ptr[Parser] p) -> ast.Node {
}
}
f.kids.push(parseBlock(p))
f.endLine = f.kids[f.kids.len - 1].endLine
return f
}
@ -249,6 +487,7 @@ fn parseBlock(ptr[Parser] p) -> ast.Node {
endOfStatement(p)
}
p.depth = saved
blk.endLine = cur(p).line
next(p)
return blk
}
@ -285,6 +524,9 @@ fn parseStatement(ptr[Parser] p) -> ast.Node {
s.kids.push(parseBlock(p))
return s
}
if k == "match" {
return parseMatch(p, false)
}
if k == "return" {
s := node(p, "return")
next(p)
@ -524,6 +766,9 @@ fn parsePrimary(ptr[Parser] p) -> ast.Node {
expect(p, ")", "')'")
return inner
}
if k == "match" {
return parseMatch(p, true)
}
if k == "[" {
next(p)
e.kind = "array"
@ -552,6 +797,21 @@ fn parsePrimary(ptr[Parser] p) -> ast.Node {
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))

View file

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

97
src/platform.fch Normal file
View file

@ -0,0 +1,97 @@
// 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.
// The same as src/target.cpp.
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
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 == "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")
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.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.runner = "qemu-aarch64 "
}
return tg
}
// C's plain `char`: signed on x86 and Apple, not on ARM64 Linux
fn charIsSigned(Target t) -> bool {
return !t.arm64 || t.darwin || t.windows
}

View file

@ -1,158 +0,0 @@
#include "target.h"
#include <llvm/Support/TargetSelect.h>
#include <llvm/TargetParser/Host.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#else
#include <sys/wait.h>
#endif
static bool hostIsWindows() {
#ifdef _WIN32
return true;
#else
return false;
#endif
}
// The Finch-generated code needs these LLVM backends (the same C functions LLVM's own tools call).
extern "C" {
#ifdef FINCH_TARGET_X86
void LLVMInitializeX86TargetInfo();
void LLVMInitializeX86Target();
void LLVMInitializeX86TargetMC();
void LLVMInitializeX86AsmPrinter();
#endif
#ifdef FINCH_TARGET_AARCH64
void LLVMInitializeAArch64TargetInfo();
void LLVMInitializeAArch64Target();
void LLVMInitializeAArch64TargetMC();
void LLVMInitializeAArch64AsmPrinter();
#endif
}
void initTargets() {
#ifdef FINCH_TARGET_X86
LLVMInitializeX86TargetInfo();
LLVMInitializeX86Target();
LLVMInitializeX86TargetMC();
LLVMInitializeX86AsmPrinter();
#endif
#ifdef FINCH_TARGET_AARCH64
LLVMInitializeAArch64TargetInfo();
LLVMInitializeAArch64Target();
LLVMInitializeAArch64TargetMC();
LLVMInitializeAArch64AsmPrinter();
#endif
llvm::InitializeNativeTarget();
llvm::InitializeNativeTargetAsmPrinter();
}
void setTarget(const std::string &name) {
std::string host = llvm::sys::getDefaultTargetTriple();
llvm::Triple hostTriple(llvm::Triple::normalize(host));
std::string t = name;
if (name.empty()) t = host;
else if (name == "windows") t = hostIsWindows() ? host : "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 == "macos") t = hostTriple.isAArch64() ? "arm64-apple-macosx14.0.0" : "x86_64-apple-macosx14.0.0";
g_target.triple = llvm::Triple(llvm::Triple::normalize(t));
g_target.windows = g_target.triple.isOSWindows();
g_target.msvc = g_target.triple.isWindowsMSVCEnvironment();
g_target.darwin = g_target.triple.isOSDarwin();
g_target.cross = hostTriple.getOS() != g_target.triple.getOS() || hostTriple.getArch() != g_target.triple.getArch();
g_target.exe = g_target.windows ? ".exe" : "";
// FINCH_CC chooses the C compiler for any target; CC only when not cross-compiling.
if (const char *fc = std::getenv("FINCH_CC")) g_target.cc = fc;
else if (!g_target.cross && std::getenv("CC")) g_target.cc = std::getenv("CC");
else if (g_target.cross && g_target.windows) g_target.cc = "x86_64-w64-mingw32-gcc";
else if (g_target.cross && g_target.triple.isAArch64() && g_target.triple.isOSLinux()) g_target.cc = "aarch64-linux-gnu-gcc";
else if (g_target.cross) g_target.cc = "clang --target=" + g_target.triple.str();
else if (hostIsWindows()) g_target.cc = "clang";
else g_target.cc = "cc";
}
std::string runPrefix() {
if (!g_target.cross) return "";
if (g_target.windows && !hostIsWindows()) return "wine ";
if (g_target.triple.isAArch64() && g_target.triple.isOSLinux()) return "qemu-aarch64 ";
return "";
}
std::string shellQuote(const std::string &s) {
#ifdef _WIN32
// cmd.exe: double quotes; a " inside a file name is impossible on Windows
return "\"" + s + "\"";
#else
std::string out = "'";
for (char ch : s) out += ch == '\'' ? std::string("'\\''") : std::string(1, ch);
return out + "'";
#endif
}
static int exitStatus(int status) {
#ifdef _WIN32
return status;
#else
if (status == -1) return -1;
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
return -1;
#endif
}
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);
return exitStatus(pclose(p));
}
int runCommand(const std::string &cmd, std::string *crash) {
std::fflush(stdout);
int status = std::system(cmd.c_str());
#ifndef _WIN32
if (crash && status != -1 && WIFSIGNALED(status)) *crash = strsignal(WTERMSIG(status));
#else
// a crashed Windows program exits with an NTSTATUS code like 0xC0000005
if (crash && (unsigned)status >= 0xC0000000u) {
char buf[64];
std::snprintf(buf, sizeof buf, "exception 0x%08X%s", (unsigned)status,
(unsigned)status == 0xC0000005u ? " (access violation)" : (unsigned)status == 0xC00000FDu ? " (stack overflow)" : "");
*crash = buf;
}
#endif
return exitStatus(status);
}
const char *nullDevice() { return hostIsWindows() ? "NUL" : "/dev/null"; }
std::string cacheDir() {
#ifdef _WIN32
if (const char *l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\finch";
if (const char *t = std::getenv("TEMP")) return std::string(t) + "\\finch";
return "finch-cache";
#else
if (const char *x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/finch";
if (const char *h = std::getenv("HOME")) return std::string(h) + "/.cache/finch";
return "/tmp/finch-cache";
#endif
}
char pathListSeparator() { return hostIsWindows() ? ';' : ':'; }
std::string dirOf(const std::string &path) {
size_t slash = hostIsWindows() ? path.find_last_of("/\\") : path.find_last_of('/');
return slash == std::string::npos ? "." : path.substr(0, slash);
}

View file

@ -1,49 +0,0 @@
#pragma once
// The system Finch compiles for, and everything that depends on it:
// the C compiler used to link, how the shell quotes, file names, the C ABI.
#include <llvm/TargetParser/Triple.h>
#include <string>
struct TargetInfo {
llvm::Triple triple;
bool darwin = false; // macOS
bool windows = false; // Windows (MSVC or MinGW): Microsoft x64 calling convention, .exe
bool msvc = false; // the MSVC environment (link with clang + the Windows SDK)
bool cross = false; // compiling for another system than the one we run on
std::string cc; // the C compiler that links programs and builds the runtime
std::string exe; // ".exe" or ""
};
inline TargetInfo g_target;
// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = the system finch runs on
void setTarget(const std::string &name);
// Register the LLVM backends Finch can generate code for (x86-64, ARM64).
void initTargets();
// How to run a program built for g_target on this machine ("" = directly, "wine", "qemu-aarch64").
std::string runPrefix();
// Quote one argument for the shell that runs our commands (sh on Unix, cmd.exe on Windows).
std::string shellQuote(const std::string &s);
// Run a command; its exit status, and everything it printed in `output`.
int capture(const std::string &cmd, std::string &output);
// Run a command with its output going to the terminal; its exit status (128+n for a signal).
int runCommand(const std::string &cmd, std::string *crash = nullptr);
// The null device: /dev/null or NUL.
const char *nullDevice();
// The folder for cached files (~/.cache/finch, %LOCALAPPDATA%\finch).
std::string cacheDir();
// Separator of FINCH_PATH entries (':' or ';').
char pathListSeparator();
// The folder part of a path (both / and \ on Windows).
std::string dirOf(const std::string &path);

6
std/finch_win.h Normal file
View file

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

518
std/json.fch Normal file
View file

@ -0,0 +1,518 @@
// 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 Normal file
View file

@ -0,0 +1,128 @@
// 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 Normal file
View file

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

45
std/proc_windows.fch Normal file
View file

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

88
std/time.fch Normal file
View file

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

75
std/time_windows.fch Normal file
View file

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

View file

@ -1,50 +0,0 @@
#!/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
# (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 ' $files; then
skipped=$((skipped + 1))
continue
fi
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
if ! "$tmp/boot2" build "$f" -o "$tmp/prog" > "$tmp/err" 2>&1; then
failed=$((failed + 1))
echo "FAIL (compile) $f"
head -3 "$tmp/err"
continue
fi
got=$("$tmp/prog" < "$input" 2>&1)
if [ "$got" == "$(cat "${f%.fch}.out")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL (output) $f"
diff <(echo "$got") "${f%.fch}.out" | head -10
fi
done
echo "self-hosted compiler: $pass passed, $failed failed, $skipped skipped (features it doesn't have)"
[ $failed -eq 0 ]

View file

@ -4,7 +4,7 @@
# 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 tests/cross.sh arm64'
# --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")"
@ -24,7 +24,7 @@ run_exe() { # program, stdin -> output without Windows' \r
$runner "$1" < "$2" 2>&1 | tr -d '\r'
}
for f in run/*.fch; do
for f in run/*.fch new/*.fch; do
grep -q "^fn main" "$f" || continue
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
@ -40,7 +40,7 @@ for f in run/*.fch; do
fi
done
for f in fail/*.fch; do
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

75
tests/new/enums.fch Normal file
View file

@ -0,0 +1,75 @@
enum Color { Red, Green, Blue }
enum Status { Ok = 200, NotFound = 404, Teapot = 418 }
struct Pixel {
Color color
int x
}
fn Color.warm() -> bool {
return self == Color.Red
}
fn Color.next() -> Color {
if self == Color.Red {
return Color.Green
}
if self == Color.Green {
return Color.Blue
}
return Color.Red
}
fn describe(Color c) -> str {
return match c {
Red -> "red"
Green -> "green"
Blue -> "blue"
}
}
fn code(Status s) -> str {
match s {
Status.Ok -> return "fine"
Status.NotFound, Status.Teapot -> return "odd"
}
}
fn main() {
c := Color.Green
print(c, Color.Blue, int(Color.Blue), int(Status.Teapot))
print(c == Color.Green, c != Color.Green)
print(describe(Color.Red), describe(c.next()), c.warm(), Color.Red.warm())
print(code(Status.Ok), code(Status.NotFound))
p := Pixel(color: Color.Blue, x: 3)
print(p)
[]Color all = [Color.Red, Color.Blue]
print(all, all.len)
for col in all {
match col {
Color.Red -> print("a red one")
else -> print("something else")
}
}
s := Status(404)
print(s, Status(200))
n := 5
kind := match n {
1, 2, 3 -> "small"
4, 5 -> "medium"
else -> "large"
}
print(kind)
word := "two"
match word {
"one" -> print(1)
"two" -> {
print(2)
print("two!")
}
else -> print("many")
}
counts := [Color.Red: 2, Color.Blue: 5]
print(counts[Color.Blue], counts)
print(str(Color.Green) + "!")
}

14
tests/new/enums.out Normal file
View file

@ -0,0 +1,14 @@
Green Blue 2 418
true false
red blue false true
fine odd
Pixel(color: Blue, x: 3)
[Red, Blue] 2
a red one
something else
NotFound Ok
medium
2
two!
5 {Red: 2, Blue: 5}
Green!

View file

@ -0,0 +1,6 @@
// expect: 7 is not the number of any variant of Color
enum Color { Red, Green }
fn main() {
print(Color(7))
}

View file

@ -0,0 +1,6 @@
// expect: enums can only be compared with == and !=
enum Color { Red, Green }
fn main() {
print(Color.Red < Color.Green)
}

View file

@ -0,0 +1,6 @@
// expect: the enum Color has no variant 'Blue'
enum Color { Red, Green }
fn main() {
print(Color.Blue)
}

View file

@ -0,0 +1,15 @@
// expect: (while building bigger[Point])
struct Point {
int x
}
fn bigger[T](T a, T b) -> T {
if a > b {
return a
}
return b
}
fn main() {
print(bigger(Point(1), Point(2)))
}

View file

@ -0,0 +1,10 @@
// expect: can't tell what T is here
fn make[T]() -> []T {
[]T out
return out
}
fn main() {
x := make()
print(x)
}

View file

@ -0,0 +1,9 @@
// expect: 'Stack' is generic, so say which type it holds
struct Stack[T] {
[]T items
}
fn main() {
s := Stack()
print(s)
}

View file

@ -0,0 +1,8 @@
// expect: 'add' is not generic, so it takes no type arguments
fn add(int a, int b) -> int {
return a + b
}
fn main() {
print(add[int](1, 2))
}

View file

@ -0,0 +1,9 @@
// expect: Stack takes 1 type argument(s)
struct Stack[T] {
[]T items
}
fn main() {
Stack[int, str] s
print(s)
}

View file

@ -0,0 +1,8 @@
// expect: this arm can never be used
fn main() {
n := 3
match n {
else -> print("any")
1 -> print("one")
}
}

View file

@ -0,0 +1,9 @@
// expect: the arms of a match have to give the same type
fn main() {
n := 3
x := match n {
1 -> "one"
else -> 2
}
print(x)
}

View file

@ -0,0 +1,10 @@
// expect: doesn't handle Blue
enum Color { Red, Green, Blue }
fn main() {
c := Color.Red
match c {
Red -> print(1)
Green -> print(2)
}
}

View file

@ -0,0 +1,8 @@
// expect: needs an 'else' arm
fn main() {
n := 3
match n {
1 -> print("one")
2 -> print("two")
}
}

View file

@ -0,0 +1,11 @@
// expect: 'Purple' is not a variant of Color
enum Color { Red, Green }
fn main() {
c := Color.Red
match c {
Red -> print(1)
Purple -> print(2)
else -> print(3)
}
}

View file

@ -0,0 +1,12 @@
// expect: this match doesn't handle Point
enum Shape {
Circle(float)
Point
}
fn main() {
s := Shape.Circle(1.0)
match s {
Circle(r) -> print(r)
}
}

95
tests/new/generics.fch Normal file
View file

@ -0,0 +1,95 @@
struct Stack[T] {
[]T items
}
fn Stack[T].push(T x) {
self.items.push(x)
}
fn Stack[T].pop() -> T {
return self.items.pop()
}
fn Stack[T].size() -> int {
return self.items.len
}
struct Pair[A, B] {
A first
B second
}
fn Pair[A, B].swapped() -> Pair[B, A] {
return Pair[B, A](self.second, self.first)
}
struct Point {
int x
int y
}
fn max[T](T a, T b) -> T {
if a > b {
return a
}
return b
}
fn first[T]([]T items) -> T {
return items[0]
}
fn count[T]([]T items, T wanted) -> int {
n := 0
for x in items {
if x == wanted {
n += 1
}
}
return n
}
fn show[T](T x) -> str {
return "<" + str(x) + ">"
}
fn convert[T](str s) -> T {
return T(s)
}
fn fill[T](int n, T value) -> []T {
[]T out
for i in 0..n {
out.push(value)
}
return out
}
fn main() {
print(max(3, 9), max(2.5, 1.5), max("apple", "pear"), max('a', 'b'))
print(max[float](1, 2.5))
print(first(["x", "y"]), first([7, 8]), count([1, 2, 1, 3, 1], 1), count(["a", "b"], "b"))
print(show(5), show("hi"), show(Point(1, 2)), show([1, 2]))
print(fill(3, "ab"), fill(2, 1.5), fill[int](2, 7))
Stack[int] s
s.push(10)
s.push(20)
print(s.pop(), s.size(), s)
names := Stack[str]()
names.push("ann")
names.push("bob")
print(names, names.size())
pts := Stack[Point]()
pts.push(Point(1, 2))
print(pts.pop().x)
p := Pair[str, int]("age", 31)
q := p.swapped()
print(p, q, q.first + 1)
nested := Stack[Pair[int, str]]()
nested.push(Pair[int, str](1, "one"))
print(nested)
}

10
tests/new/generics.out Normal file
View file

@ -0,0 +1,10 @@
9 2.5 pear b
2.5
x 7 3 1
<5> <hi> <Point(x: 1, y: 2)> <[1, 2]>
["ab", "ab", "ab"] [1.5, 1.5] [7, 7]
20 1 Stack[int](items: [10])
Stack[str](items: ["ann", "bob"]) 2
1
Pair[str, int](first: "age", second: 31) Pair[int, str](first: 31, second: "age") 32
Stack[Pair[int, str]](items: [Pair[int, str](first: 1, second: "one")])

View file

@ -0,0 +1,38 @@
enum Shape {
Circle(float)
Rect(float, float)
Named(str, []int)
Point
}
fn Shape.area() -> float {
return match self {
Circle(r) -> 3.0 * r * r
Rect(w, h) -> w * h
Named(_, sizes) -> float(sizes.len)
Point -> 0.0
}
}
fn describe(Shape s) -> str {
match s {
Shape.Circle(r) -> return "circle " + str(r)
Shape.Named(name, _) -> return "named " + name
else -> return "other"
}
}
fn main() {
shapes := [Shape.Circle(2.0), Shape.Rect(2.0, 3.5), Shape.Named("tri", [1, 2, 3]), Shape.Point]
for s in shapes {
print(s, s.area(), describe(s))
}
copy := shapes[2]
shapes.clear()
print(copy)
p := Shape.Point
match p {
Point -> print("a point")
else -> print("no")
}
}

View file

@ -0,0 +1,6 @@
Circle(2) 12 circle 2
Rect(2, 3.5) 7 other
Named("tri", [1, 2, 3]) 3 named tri
Point 0 other
Named("tri", [1, 2, 3])
a point

35
tests/new/std_json.fch Normal file
View file

@ -0,0 +1,35 @@
import json
fn main() -> ! {
doc := try json.parse("{\"name\": \"Finch\", \"tags\": [\"fast\", \"small\"], \"stars\": 5, \"pi\": 3.25, \"ok\": true, \"none\": null, \"nested\": {\"deep\": [1, [2, 3]]}}")
print(doc.get("name").str_or(""), doc.get("tags").at(1).str_or(""), doc.get("stars").int_or(0), doc.get("pi").float_or(0.0))
print(doc.get("ok").bool_or(false), doc.get("none").is_null(), doc.get("missing").is_null(), doc.has("name"), doc.has("nope"))
print(doc.get("nested").get("deep").at(1).at(0).int_or(-1), doc.get("tags").len(), doc.names())
print(json.stringify(doc))
out := json.new_object()
out.set("greeting", json.new_text("hi \"there\"\n"))
list := json.new_list()
list.push(json.new_number(1))
list.push(json.new_number(2.5))
list.push(json.new_bool(false))
list.push(json.new_null())
out.set("values", list)
out.set("greeting", json.new_text("replaced"))
out.set("empty", json.new_object())
print(json.stringify(out))
print(json.pretty(out))
again := try json.parse(json.pretty(out))
print(json.stringify(again) == json.stringify(out))
print((try json.parse("\"caf\\u00e9 \\ud83d\\ude00 \\/\"")).str_or(""))
print((try json.parse(" [ ] ")).len(), (try json.parse("-12.5e1")).float_or(0.0))
for bad in ["{\"a\": }", "[1, 2", "{\"a\" 1}", "tru", "[1] x", "\"open", "", "[1,\n oops]"] {
json.parse(bad) or {
print(err)
continue
}
print("parsed?!", bad)
}
}

26
tests/new/std_json.out Normal file
View file

@ -0,0 +1,26 @@
Finch small 5 3.25
true true true true false
2 2 ["name", "tags", "stars", "pi", "ok", "none", "nested"]
{"name":"Finch","tags":["fast","small"],"stars":5,"pi":3.25,"ok":true,"none":null,"nested":{"deep":[1,[2,3]]}}
{"greeting":"replaced","values":[1,2.5,false,null],"empty":{}}
{
"greeting": "replaced",
"values": [
1,
2.5,
false,
null
],
"empty": {}
}
true
café 😀 /
0 -125
json: found '}' where a value should be (line 1, column 7)
json: the text ends where ',' or ']' should be (line 1, column 6)
json: found '1' where ':' should be (line 1, column 6)
json: found 't' where 'true' should be (line 1, column 1)
json: found 'x' where the end of the text should be (line 1, column 5)
json: a text is never closed (line 1, column 6)
json: the text ends where a value should be (line 1, column 1)
json: found 'o' where a value should be (line 2, column 3)

10
tests/new/std_path.fch Normal file
View file

@ -0,0 +1,10 @@
import path
fn main() {
print(path.join("a", "b.txt"), path.join("a/", "b"), path.join("", "x"), path.join("a", "/abs"))
print(path.base("/tmp/x/a.tar.gz"), path.base("a/b/"), path.base(""), path.base("/"))
print(path.dir("/tmp/x/a.txt"), path.dir("b.txt"), path.dir("/b"), path.dir("a/b/"))
print(path.ext("a.tar.gz"), path.ext("noext"), path.ext(".bashrc"), path.stem("/x/a.tar.gz"))
print(path.clean("a//b/../c/./d"), path.clean("/../a"), path.clean("../../a"), path.clean(""), path.clean("a/.."))
print(path.parts("/a//b/c/"), path.is_abs("/a"), path.is_abs("a"))
}

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