mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b24a7845ef | ||
|
|
b53e0910d8 | ||
|
|
3e45b62812 | ||
|
|
dc0334fa0e | ||
|
|
837ad45177 | ||
|
|
a0c54a1252 | ||
|
|
81df9f8c3e | ||
|
|
636eaefe59 | ||
|
|
b9378050ab | ||
|
|
750eca3e85 | ||
|
|
8c3b5b15b9 | ||
|
|
dd9cbe7611 | ||
|
|
94e41484dd | ||
|
|
ea6d2a57e5 | ||
|
|
04eb0cce55 | ||
|
|
609ceffe58 | ||
|
|
a5ba4bc8f0 | ||
|
|
04d5486477 | ||
|
|
13ef63a586 | ||
|
|
f71ae8b1ef | ||
|
|
e4ad3540f1 | ||
|
|
083d66c82b | ||
|
|
a5f417100d | ||
|
|
84693aa655 | ||
|
|
051af1a04e | ||
|
|
873b52565d | ||
|
|
393327b353 | ||
|
|
5f2ba99f2d | ||
|
|
68637a35a7 | ||
|
|
f9838dfe03 |
115 changed files with 599201 additions and 11368 deletions
164
.github/workflows/ci.yml
vendored
164
.github/workflows/ci.yml
vendored
|
|
@ -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
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
build/
|
||||
*.o
|
||||
core.*
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
52
README.md
52
README.md
|
|
@ -48,37 +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.
|
||||
- **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:
|
||||
|
|
@ -105,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` |
|
||||
|
|
@ -127,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
|
||||
|
||||
|
|
@ -147,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
|
||||
```
|
||||
|
||||
|
|
|
|||
52
README.pl.md
52
README.pl.md
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
2181
boot/gen.fch
2181
boot/gen.fch
File diff suppressed because it is too large
Load diff
116
boot/main.fch
116
boot/main.fch
|
|
@ -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
45
bootstrap/README.md
Normal 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
57
bootstrap/build.sh
Executable 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
14
bootstrap/install.sh
Executable 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
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
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
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
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
6
bootstrap/pack.sh
Executable 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
8
bootstrap/winbuild.sh
Executable 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
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
9
bootstrap/winseed.sh
Executable 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"
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Kupić mleko
|
||||
1
note.txt
1
note.txt
|
|
@ -1 +0,0 @@
|
|||
Buy milk
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -596,10 +609,11 @@ int finch_try_write_file(const FStr *path, const FStr *text, FStr *err) {
|
|||
}
|
||||
|
||||
// ---------- maps ----------
|
||||
// map[K]V = { entries; len; used; cap; index; icap }
|
||||
// map[K]V = { entries; len; used; cap; index; icap; adds }
|
||||
// entries: `used` slots of es bytes, each { uint64 hash; K key; V value }, in insertion order.
|
||||
// A removed entry keeps its slot (hash 0) until the next resize packs the slots.
|
||||
// index: icap slots (a power of two, at least 2 * cap): -1 empty, -2 removed, else an entry number.
|
||||
// adds: how many keys were ever added; a `for` over the map stops with an error if it changes.
|
||||
// The compiler stores, copies and drops keys and values; the runtime only finds and places entries.
|
||||
// Keys are compared byte for byte (numbers, chars, bools) or as text (kstr).
|
||||
|
||||
|
|
@ -608,6 +622,7 @@ typedef struct {
|
|||
int64_t len, used, cap;
|
||||
int64_t *index;
|
||||
int64_t icap;
|
||||
int64_t adds;
|
||||
} FMap;
|
||||
|
||||
static uint64_t map_hash(const void *key, int64_t ks, int kstr) {
|
||||
|
|
@ -699,6 +714,7 @@ int64_t finch_map_slot(FMap *m, const void *key, int64_t es, int64_t ks, int32_t
|
|||
while (m->index[s] >= 0) s = (s + 1) & (m->icap - 1);
|
||||
m->index[s] = i;
|
||||
m->len++;
|
||||
m->adds++;
|
||||
return i;
|
||||
}
|
||||
|
||||
|
|
@ -736,6 +752,10 @@ void finch_map_clone_raw(FMap *out, const FMap *m, int64_t es) {
|
|||
memcpy(out->index, m->index, (size_t)m->icap * sizeof(int64_t));
|
||||
}
|
||||
|
||||
__attribute__((noreturn, cold)) void finch_map_changed(const char *file, int64_t line) {
|
||||
finch_panic(file, line, "a key was added to the map while looping over it (collect the new keys, and add them after the loop)");
|
||||
}
|
||||
|
||||
__attribute__((noreturn, cold)) void finch_map_missing(const char *file, int64_t line, const FStr *key) {
|
||||
char msg[200];
|
||||
snprintf(msg, sizeof msg, "the key %.*s is not in the map (check with .has(key), or use .get(key, default))",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
367
src/abi.cpp
367
src/abi.cpp
|
|
@ -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> ¶ms, bool variadic,
|
||||
std::vector<llvm::Type *> *extraTypes = nullptr) {
|
||||
Plan pl;
|
||||
int intRegs = 6, sseRegs = 8;
|
||||
std::vector<llvm::Type *> types;
|
||||
llvm::Type *retTy = cg.ty(ret);
|
||||
if (ret.kind == FType::Str) retTy = cg.b.getPtrTy();
|
||||
pl.ret = cg.classify(ret);
|
||||
if (pl.ret.kind == AbiArg::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> ¶ms) {
|
||||
LLVMContext &ctx = cg.ctx;
|
||||
AttributeList al;
|
||||
unsigned idx = 0;
|
||||
if (pl.ret.kind == AbiArg::Memory) {
|
||||
al = al.addParamAttribute(ctx, idx, Attribute::getWithStructRetType(ctx, cg.ty(ret)));
|
||||
idx++;
|
||||
} else if (Attribute::AttrKind k = extAttr(ret); k != Attribute::None) {
|
||||
al = al.addRetAttribute(ctx, k);
|
||||
}
|
||||
for (size_t i = 0; i < params.size(); i++) {
|
||||
const AbiArg &a = pl.params[i];
|
||||
if (a.kind == AbiArg::Memory) {
|
||||
uint64_t align = std::max<uint64_t>(8, params[i].info->isC ? params[i].info->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
|
||||
|
|
@ -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 {
|
||||
|
|
@ -21,6 +22,14 @@ fn arrayOf(Type t) -> Type {
|
|||
return out
|
||||
}
|
||||
|
||||
// map[K]V: elem holds the key, then the value
|
||||
fn mapOf(Type k, Type v) -> Type {
|
||||
out := Type(kind: "map")
|
||||
out.elem.push(k)
|
||||
out.elem.push(v)
|
||||
return out
|
||||
}
|
||||
|
||||
fn ptrTo(Type t) -> Type {
|
||||
out := Type(kind: "ptr")
|
||||
out.elem.push(t)
|
||||
|
|
@ -31,17 +40,22 @@ 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" {
|
||||
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
|
||||
}
|
||||
if a.elem.len == 0 {
|
||||
return true
|
||||
for i in 0..a.elem.len {
|
||||
if !same(a.elem[i], b.elem[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return same(a.elem[0], b.elem[0])
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -50,13 +64,19 @@ 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])
|
||||
}
|
||||
if t.kind == "ptr" {
|
||||
if t.elem.len == 0 {
|
||||
return "ptr"
|
||||
}
|
||||
return "ptr[" + show(t.elem[0]) + "]"
|
||||
}
|
||||
if t.kind == "struct" || t.kind == "named" {
|
||||
if t.kind == "struct" || t.kind == "named" || t.kind == "enum" {
|
||||
if t.module.len > 0 {
|
||||
return t.module + "." + t.name
|
||||
}
|
||||
|
|
@ -71,24 +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; fn: has a body
|
||||
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
356
src/ast.h
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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"};
|
||||
373
src/cimport.cpp
373
src/cimport.cpp
|
|
@ -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
569
src/cimport.fch
Normal 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
|
||||
}
|
||||
|
|
@ -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);
|
||||
1457
src/codegen.cpp
1457
src/codegen.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
@ -1,887 +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_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") {
|
||||
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");
|
||||
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;
|
||||
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);
|
||||
}
|
||||
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);
|
||||
for (size_t k = 0; k < fields.size(); k++) {
|
||||
const StructInfo::F &f = *fields[k];
|
||||
Value_ fv;
|
||||
if (given[k]) {
|
||||
fv = coerce(exprWant(*given[k], f.type), f.type, given[k]->pos, "the field '" + f.name + "'");
|
||||
} else if (f.init) {
|
||||
// a default value is evaluated where the struct is defined, without the caller's variables
|
||||
std::vector<Scope> saved;
|
||||
std::swap(saved, scopes);
|
||||
scopes.reserve(256);
|
||||
pushScope();
|
||||
std::string savedModule = curModule;
|
||||
curModule = s->module;
|
||||
fv = coerce(exprWant(*f.init, f.type), f.type, f.init->pos, "the default of '" + f.name + "'");
|
||||
curModule = savedModule;
|
||||
scopes.pop_back();
|
||||
std::swap(saved, scopes);
|
||||
} else {
|
||||
continue; // zero
|
||||
}
|
||||
v = b.CreateInsertValue(v, own(fv), f.llvmIndex);
|
||||
}
|
||||
return {v, t, false, owning(t)};
|
||||
}
|
||||
|
||||
Value_ Codegen::callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p) {
|
||||
if (!f.unsupported.empty())
|
||||
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported);
|
||||
std::vector<Value_> vals;
|
||||
for (size_t i = 0; i < args.size(); i++) {
|
||||
if (i < f.params.size()) {
|
||||
FType pt = resolve(f.params[i], p);
|
||||
vals.push_back(coerce(exprWant(*args[i], pt), pt, args[i]->pos, "argument " + std::to_string(i + 1) + " of " + f.name));
|
||||
} else {
|
||||
Value_ v = expr(*args[i]);
|
||||
if (v.type.kind == FType::Void) failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "this gives back nothing");
|
||||
if (v.type.kind == FType::Struct || v.type.kind == FType::Array)
|
||||
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "structs and arrays can't be passed to the '...' part of a C function");
|
||||
vals.push_back(v);
|
||||
}
|
||||
}
|
||||
setLoc(p);
|
||||
Value_ r = emitCCall(f, declareC(f), vals, p);
|
||||
for (auto &v : vals) release(v);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------- built-ins ----------
|
||||
|
||||
// print(a, b, c) prints the values separated by spaces, then a newline.
|
||||
Value_ Codegen::print(const std::vector<ExprPtr> &args, Pos) {
|
||||
for (size_t i = 0; i < args.size(); i++) {
|
||||
Value_ v = expr(*args[i]);
|
||||
if (v.type.kind == FType::Void)
|
||||
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "this gives back nothing, so there is nothing to print");
|
||||
if (i) printf_(" ", {});
|
||||
emitPrint(v.v, v.type, false);
|
||||
release(v);
|
||||
}
|
||||
printf_("\n", {});
|
||||
return {nullptr, FType::Void};
|
||||
}
|
||||
|
||||
// addr(x) gives a pointer to x; addr(function) gives a C-callable function pointer.
|
||||
Value_ Codegen::addrOf(const std::vector<ExprPtr> &args, Pos p) {
|
||||
const Expr &e = *args[0];
|
||||
if (e.kind == ExprKind::Var) {
|
||||
const std::string &n = static_cast<const VarExpr &>(e).name;
|
||||
Var *v = lookup(n);
|
||||
if (!v) {
|
||||
auto &fns = modules[curModule].fns;
|
||||
if (auto it = fns.find(n); it != fns.end()) return {cThunk(it->second, p), FType::Ptr};
|
||||
}
|
||||
if (v && v->readonly) failAt(e.pos.file, e.pos.line, e.pos.col, "can't take the address of a loop variable");
|
||||
}
|
||||
if (e.kind != ExprKind::Var && e.kind != ExprKind::Member && e.kind != ExprKind::Index)
|
||||
failAt(e.pos.file, e.pos.line, e.pos.col, "addr(...) needs a variable, a field or an element, like addr(x)");
|
||||
Place pl = place(e, true);
|
||||
return {pl.addr, FType::ptrTo(pl.type)};
|
||||
}
|
||||
|
||||
// Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), str(x), ptr(x) ...
|
||||
Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name, 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);
|
||||
}
|
||||
if (t.kind == FType::Map) {
|
||||
Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : Value_{tmp(r.val.v), t, false, r.val.fresh};
|
||||
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};
|
||||
}
|
||||
Value_ out = arrayMethod(m, &recv);
|
||||
if (!r.isPlace && r.val.fresh) dropAt(recv.v, t);
|
||||
return out;
|
||||
}
|
||||
if (t.kind == FType::Str) {
|
||||
Value_ sv = r.isPlace ? Value_{b.CreateLoad(ty(t), r.pl.addr), t} : r.val;
|
||||
Value *addr = tmp(sv.v); // the runtime takes strings by address
|
||||
auto argStr = [&](size_t i, const char *what) {
|
||||
return coerce(expr(*m.args[i]), FType::Str, m.args[i]->pos, what);
|
||||
};
|
||||
auto argInt = [&](size_t i, const char *what) {
|
||||
return coerce(expr(*m.args[i]), FType::I64, m.args[i]->pos, what).v;
|
||||
};
|
||||
auto need = [&](size_t n) { checkArgs(m.args, m.argNames, n, p, m.name); };
|
||||
Value *out = tmpOf(ty(FType::Str));
|
||||
Value_ res;
|
||||
const std::string &n = m.name;
|
||||
auto strRes = [&]() { return Value_{b.CreateLoad(ty(FType::Str), out), FType::Str, false, true}; };
|
||||
if (n == "sub") {
|
||||
need(2);
|
||||
Value *a = argInt(0, "the start"), *e = argInt(1, "the end");
|
||||
b.CreateCall(rt("finch_str_sub"), {out, addr, a, e, fileName(p), b.getInt64(p.line)});
|
||||
res = strRes();
|
||||
} else if (n == "find" || n == "contains" || n == "starts_with" || n == "ends_with") {
|
||||
need(1);
|
||||
Value_ t2 = argStr(0, "the text to look for");
|
||||
Value *tp = tmp(t2.v);
|
||||
if (n == "find") res = {b.CreateCall(rt("finch_str_find"), {addr, tp}), FType::I64};
|
||||
else if (n == "contains") res = {b.CreateICmpSGE(b.CreateCall(rt("finch_str_find"), {addr, tp}), b.getInt64(0)), FType::Bool};
|
||||
else res = {b.CreateICmpNE(b.CreateCall(rt(n == "starts_with" ? "finch_str_starts" : "finch_str_ends"), {addr, tp}), b.getInt32(0)), FType::Bool};
|
||||
release(t2);
|
||||
} else if (n == "trim" || n == "upper" || n == "lower") {
|
||||
need(0);
|
||||
b.CreateCall(rt("finch_str_" + n), {out, addr});
|
||||
res = strRes();
|
||||
} else if (n == "replace") {
|
||||
need(2);
|
||||
Value_ a = argStr(0, "the text to replace"), c = argStr(1, "the new text");
|
||||
b.CreateCall(rt("finch_str_replace"), {out, addr, tmp(a.v), tmp(c.v)});
|
||||
release(a);
|
||||
release(c);
|
||||
res = strRes();
|
||||
} else if (n == "split") {
|
||||
need(1);
|
||||
Value_ sep = argStr(0, "the separator");
|
||||
FType at = FType::arrayOf(FType::Str);
|
||||
Value *ao = tmpOf(ty(at));
|
||||
b.CreateCall(rt("finch_str_split"), {ao, addr, tmp(sep.v)});
|
||||
release(sep);
|
||||
res = {b.CreateLoad(ty(at), ao), at, false, true};
|
||||
} else if (n == "repeat") {
|
||||
need(1);
|
||||
b.CreateCall(rt("finch_str_repeat"), {out, addr, argInt(0, "the count")});
|
||||
res = strRes();
|
||||
} else if (n == "bytes") {
|
||||
need(0);
|
||||
FType at = FType::arrayOf(FType::U8);
|
||||
Value *ao = tmpOf(ty(at));
|
||||
b.CreateCall(rt("finch_bytes_from_str"), {ao, addr});
|
||||
res = {b.CreateLoad(ty(at), ao), at, false, true};
|
||||
} else {
|
||||
failAt(p.file, p.line, p.col, "a str has no method '" + n + "' (it has: sub, find, contains, starts_with, ends_with, split, trim, upper, lower, replace, repeat, bytes)");
|
||||
}
|
||||
if (!r.isPlace) release(sv);
|
||||
return res;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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
|
||||
|
|
@ -1,308 +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
|
||||
};
|
||||
|
||||
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;
|
||||
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 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
|
||||
|
|
@ -1,242 +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);
|
||||
for (size_t i = 0; i < e.keys.size(); i++) {
|
||||
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");
|
||||
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
|
||||
|
|
@ -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}); // entries, len, used, cap, index, icap
|
||||
}
|
||||
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"};
|
||||
std::vector<Metadata *> els;
|
||||
for (unsigned k = 0; k < 6; 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, 384, 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
109
src/docs.fch
Normal 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
28
src/embedded.fch
Normal file
File diff suppressed because one or more lines are too long
48
src/error.h
48
src/error.h
|
|
@ -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); }
|
||||
5867
src/gen.fch
Normal file
5867
src/gen.fch
Normal file
File diff suppressed because it is too large
Load diff
83
src/host.fch
Normal file
83
src/host.fch
Normal 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
104
src/host_windows.fch
Normal 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)
|
||||
}
|
||||
39
src/index.h
39
src/index.h
|
|
@ -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
|
||||
214
src/lexer.cpp
214
src/lexer.cpp
|
|
@ -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 "?";
|
||||
}
|
||||
|
|
@ -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"]
|
||||
"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)
|
||||
36
src/lexer.h
36
src/lexer.h
|
|
@ -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
299
src/linker.fch
Normal 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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
20
src/loader.h
20
src/loader.h
|
|
@ -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);
|
||||
580
src/lsp.cpp
580
src/lsp.cpp
|
|
@ -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
876
src/lsp.fch
Normal 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
|
||||
}
|
||||
350
src/main.cpp
350
src/main.cpp
|
|
@ -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
330
src/main.fch
Normal 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
|
||||
}
|
||||
602
src/parser.cpp
602
src/parser.cpp
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -85,7 +85,8 @@ fn node(ptr[Parser] p, str kind) -> ast.Node {
|
|||
}
|
||||
|
||||
fn isTypeName(str n) -> bool {
|
||||
return n == "int" || n == "float" || n == "bool" || n == "char" || n == "str" || n == "ptr"
|
||||
names := ["int", "float", "bool", "char", "str", "ptr", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"]
|
||||
return names.contains(n)
|
||||
}
|
||||
|
||||
// int, str, ptr[T], []T, Name, module.Name
|
||||
|
|
@ -98,12 +99,24 @@ fn parseType(ptr[Parser] p) -> ast.Type {
|
|||
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (expected a type like int, str, []int or a struct name)")
|
||||
}
|
||||
name := next(p).text
|
||||
if name == "map" && at(p, "[") {
|
||||
next(p)
|
||||
k := parseType(p)
|
||||
expect(p, "]", "']' (a map type looks like map[str]int)")
|
||||
return ast.mapOf(k, parseType(p))
|
||||
}
|
||||
if isTypeName(name) {
|
||||
if name == "ptr" && accept(p, "[") {
|
||||
t := ast.ptrTo(parseType(p))
|
||||
expect(p, "]", "']'")
|
||||
return t
|
||||
}
|
||||
if name == "i64" {
|
||||
return ast.simple("int")
|
||||
}
|
||||
if name == "f64" {
|
||||
return ast.simple("float")
|
||||
}
|
||||
return ast.simple(name)
|
||||
}
|
||||
t := ast.Type(kind: "named", name: name)
|
||||
|
|
@ -112,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) == "]"
|
||||
}
|
||||
|
|
@ -126,7 +209,7 @@ fn atDeclaration(ptr[Parser] p) -> bool {
|
|||
if n == "ident" {
|
||||
return !p.toks[p.i + 1].nl
|
||||
}
|
||||
if cur(p).text == "ptr" && n == "[" {
|
||||
if (cur(p).text == "ptr" || cur(p).text == "map") && n == "[" {
|
||||
return true
|
||||
}
|
||||
return n == "." && peekKind(p, 2) == "ident" && peekKind(p, 3) == "ident" && !p.toks[p.i + 3].nl
|
||||
|
|
@ -146,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))
|
||||
}
|
||||
|
|
@ -159,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") {
|
||||
|
|
@ -186,8 +429,19 @@ 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
|
||||
}
|
||||
expect(p, "(", "'('")
|
||||
p.depth += 1
|
||||
if !at(p, ")") {
|
||||
|
|
@ -205,9 +459,16 @@ fn parseFn(ptr[Parser] p) -> ast.Node {
|
|||
expect(p, ")", "')'")
|
||||
f.ty = ast.simple("void")
|
||||
if accept(p, "->") {
|
||||
f.ty = parseType(p)
|
||||
if !at(p, "!") {
|
||||
f.ty = parseType(p)
|
||||
}
|
||||
f.fallible = accept(p, "!")
|
||||
if !f.fallible && f.ty.kind == "void" {
|
||||
fail(p, cur(p), "unexpected " + describe(cur(p)) + " (after '->' comes a type, or ! for a function that can fail)")
|
||||
}
|
||||
}
|
||||
f.kids.push(parseBlock(p))
|
||||
f.endLine = f.kids[f.kids.len - 1].endLine
|
||||
return f
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +487,7 @@ fn parseBlock(ptr[Parser] p) -> ast.Node {
|
|||
endOfStatement(p)
|
||||
}
|
||||
p.depth = saved
|
||||
blk.endLine = cur(p).line
|
||||
next(p)
|
||||
return blk
|
||||
}
|
||||
|
|
@ -246,9 +508,15 @@ fn parseStatement(ptr[Parser] p) -> ast.Node {
|
|||
s := node(p, "for")
|
||||
next(p)
|
||||
s.name = expect(p, "ident", "a loop variable name").text
|
||||
if accept(p, ",") {
|
||||
s.text = expect(p, "ident", "a second loop variable name").text
|
||||
}
|
||||
expect(p, "in", "'in'")
|
||||
s.kids.push(parseExpr(p))
|
||||
if accept(p, "..") {
|
||||
if s.text.len > 0 {
|
||||
fail(p, cur(p), "a range of numbers has one loop variable: for i in 0..n")
|
||||
}
|
||||
s.kids.push(parseExpr(p))
|
||||
} else {
|
||||
s.kind = "each"
|
||||
|
|
@ -256,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)
|
||||
|
|
@ -272,7 +543,16 @@ fn parseStatement(ptr[Parser] p) -> ast.Node {
|
|||
if k == "{" {
|
||||
return parseBlock(p)
|
||||
}
|
||||
if k == "fn" || k == "struct" || k == "import" || k == "defer" || k == "link" {
|
||||
if k == "defer" {
|
||||
s := node(p, "defer")
|
||||
next(p)
|
||||
if at(p, "defer") {
|
||||
fail(p, cur(p), "defer inside defer is not allowed")
|
||||
}
|
||||
s.kids.push(parseStatement(p))
|
||||
return s
|
||||
}
|
||||
if k == "fn" || k == "struct" || k == "import" || k == "link" {
|
||||
fail(p, cur(p), "'" + k + "' can't be used here in the self-hosted compiler")
|
||||
}
|
||||
|
||||
|
|
@ -307,7 +587,7 @@ fn parseStatement(ptr[Parser] p) -> ast.Node {
|
|||
s.kids.push(parseExpr(p))
|
||||
return s
|
||||
}
|
||||
if e.kind != "call" && e.kind != "method" {
|
||||
if e.kind != "call" && e.kind != "method" && e.kind != "try" && e.kind != "or" {
|
||||
fail(p, start, "this value is computed but never used")
|
||||
}
|
||||
s := ast.Node(kind: "expr", line: start.line, col: start.col, file: p.file)
|
||||
|
|
@ -351,8 +631,22 @@ fn binaryLevel(str op) -> int {
|
|||
return 0
|
||||
}
|
||||
|
||||
// f(x) or fallback / f(x) or { ... }: the lowest precedence, right to left
|
||||
fn parseExpr(ptr[Parser] p) -> ast.Node {
|
||||
return parseBinary(p, 1)
|
||||
l := parseBinary(p, 1)
|
||||
if !sameLine(p) || !at(p, "or") {
|
||||
return l
|
||||
}
|
||||
e := node(p, "or")
|
||||
next(p)
|
||||
e.kids.push(l)
|
||||
if at(p, "{") {
|
||||
e.flag = true
|
||||
e.kids.push(parseBlock(p))
|
||||
} else {
|
||||
e.kids.push(parseExpr(p))
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// precedence climbing; all binary operators are left-associative
|
||||
|
|
@ -372,6 +666,12 @@ fn parseBinary(ptr[Parser] p, int level) -> ast.Node {
|
|||
}
|
||||
|
||||
fn parseUnary(ptr[Parser] p) -> ast.Node {
|
||||
if at(p, "try") {
|
||||
e := node(p, "try")
|
||||
next(p)
|
||||
e.kids.push(parsePostfix(p))
|
||||
return e
|
||||
}
|
||||
if at(p, "-") || at(p, "!") || at(p, "~") {
|
||||
e := node(p, "unary")
|
||||
e.name = next(p).kind
|
||||
|
|
@ -466,12 +766,26 @@ 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"
|
||||
p.depth += 1
|
||||
if at(p, ":") && peekKind(p, 1) == "]" { // [:], an empty map
|
||||
next(p)
|
||||
e.kind = "map"
|
||||
}
|
||||
while !at(p, "]") {
|
||||
e.kids.push(parseExpr(p))
|
||||
if e.kids.len == 1 && at(p, ":") { // ["a": 1, ...]
|
||||
e.kind = "map"
|
||||
}
|
||||
if e.kind == "map" {
|
||||
expect(p, ":", "':' (a map entry looks like \"key\": value)")
|
||||
e.kids.push(parseExpr(p))
|
||||
}
|
||||
if !accept(p, ",") {
|
||||
break
|
||||
}
|
||||
|
|
@ -483,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))
|
||||
|
|
@ -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
97
src/platform.fch
Normal 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
|
||||
}
|
||||
158
src/target.cpp
158
src/target.cpp
|
|
@ -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);
|
||||
}
|
||||
49
src/target.h
49
src/target.h
|
|
@ -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
6
std/finch_win.h
Normal 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
518
std/json.fch
Normal 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
128
std/path.fch
Normal 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
48
std/proc.fch
Normal 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
45
std/proc_windows.fch
Normal 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
88
std/time.fch
Normal 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
75
std/time_windows.fch
Normal 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
|
||||
}
|
||||
|
|
@ -1,51 +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, sized numbers, defer, maps, methods or errors as values
|
||||
# (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 |defer |0x| or |try |map\[|!\s*\{|^fn [A-Z][A-Za-z0-9_]*\.|for [a-z_]+, ' $files ||
|
||||
grep -qwE 'u8|u16|u32|u64|i8|i16|i32|f32|f64' $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 ]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
7
tests/fail/map_add_while_looping.fch
Normal file
7
tests/fail/map_add_while_looping.fch
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// expect: a key was added to the map while looping over it
|
||||
fn main() {
|
||||
m := ["a": 1]
|
||||
for k in m {
|
||||
m[k + "!"] = 2
|
||||
}
|
||||
}
|
||||
75
tests/new/enums.fch
Normal file
75
tests/new/enums.fch
Normal 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
14
tests/new/enums.out
Normal 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!
|
||||
6
tests/new/fail/enum_bad_number.fch
Normal file
6
tests/new/fail/enum_bad_number.fch
Normal 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))
|
||||
}
|
||||
6
tests/new/fail/enum_compare_order.fch
Normal file
6
tests/new/fail/enum_compare_order.fch
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// expect: enums can only be compared with == and !=
|
||||
enum Color { Red, Green }
|
||||
|
||||
fn main() {
|
||||
print(Color.Red < Color.Green)
|
||||
}
|
||||
6
tests/new/fail/enum_no_variant.fch
Normal file
6
tests/new/fail/enum_no_variant.fch
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// expect: the enum Color has no variant 'Blue'
|
||||
enum Color { Red, Green }
|
||||
|
||||
fn main() {
|
||||
print(Color.Blue)
|
||||
}
|
||||
15
tests/new/fail/generic_body_error.fch
Normal file
15
tests/new/fail/generic_body_error.fch
Normal 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)))
|
||||
}
|
||||
10
tests/new/fail/generic_cant_infer.fch
Normal file
10
tests/new/fail/generic_cant_infer.fch
Normal 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)
|
||||
}
|
||||
9
tests/new/fail/generic_needs_type.fch
Normal file
9
tests/new/fail/generic_needs_type.fch
Normal 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)
|
||||
}
|
||||
8
tests/new/fail/generic_not_generic.fch
Normal file
8
tests/new/fail/generic_not_generic.fch
Normal 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))
|
||||
}
|
||||
9
tests/new/fail/generic_wrong_count.fch
Normal file
9
tests/new/fail/generic_wrong_count.fch
Normal 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)
|
||||
}
|
||||
8
tests/new/fail/match_arm_after_else.fch
Normal file
8
tests/new/fail/match_arm_after_else.fch
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// expect: this arm can never be used
|
||||
fn main() {
|
||||
n := 3
|
||||
match n {
|
||||
else -> print("any")
|
||||
1 -> print("one")
|
||||
}
|
||||
}
|
||||
9
tests/new/fail/match_arms_differ.fch
Normal file
9
tests/new/fail/match_arms_differ.fch
Normal 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)
|
||||
}
|
||||
10
tests/new/fail/match_missing_variant.fch
Normal file
10
tests/new/fail/match_missing_variant.fch
Normal 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)
|
||||
}
|
||||
}
|
||||
8
tests/new/fail/match_needs_else.fch
Normal file
8
tests/new/fail/match_needs_else.fch
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// expect: needs an 'else' arm
|
||||
fn main() {
|
||||
n := 3
|
||||
match n {
|
||||
1 -> print("one")
|
||||
2 -> print("two")
|
||||
}
|
||||
}
|
||||
11
tests/new/fail/match_not_a_variant.fch
Normal file
11
tests/new/fail/match_not_a_variant.fch
Normal 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)
|
||||
}
|
||||
}
|
||||
12
tests/new/fail/payload_missing.fch
Normal file
12
tests/new/fail/payload_missing.fch
Normal 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
95
tests/new/generics.fch
Normal 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
10
tests/new/generics.out
Normal 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")])
|
||||
38
tests/new/payload_enums.fch
Normal file
38
tests/new/payload_enums.fch
Normal 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")
|
||||
}
|
||||
}
|
||||
6
tests/new/payload_enums.out
Normal file
6
tests/new/payload_enums.out
Normal 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
35
tests/new/std_json.fch
Normal 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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue