Finch 1.0.0

A small, fast programming language on LLVM with C-like syntax.

- Lexer, recursive-descent parser, single-pass type checker + LLVM IR codegen, O2 pipeline
- Types: int/float/bool/char/str, sized i8..u64/f32/f64, pointers ptr[T] with addr()/.value/null
- Functions (fn ... -> T), if/else, while, for..in, break/continue, Go-style operator precedence
- Implicit conversions only when lossless; literals adapt to their target type
- Runtime checks for division by zero, MIN/-1 and null .value
- C interop: import "header.h" via libclang, link "lib" with pkg-config, link-error advice
- Human-readable errors with source excerpts
- Test suite (34 tests), examples, docs for beginners/technicians/engineers in English and Polish

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-08 17:10:45 +02:00
commit 0591cd20f3
73 changed files with 8066 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
build/
*.o

37
CMakeLists.txt Normal file
View file

@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.20)
project(finch VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Using LLVM ${LLVM_PACKAGE_VERSION}")
add_executable(finch
src/main.cpp
src/lexer.cpp
src/parser.cpp
src/codegen.cpp
src/cimport.cpp
)
# libclang reads C headers for `import "stdio.h"`
find_path(LIBCLANG_INCLUDE clang-c/Index.h HINTS ${LLVM_INCLUDE_DIRS})
find_library(LIBCLANG NAMES clang libclang HINTS ${LLVM_LIBRARY_DIRS})
if(NOT LIBCLANG_INCLUDE OR NOT LIBCLANG)
message(FATAL_ERROR "libclang not found (on Nix: run inside nix-shell)")
endif()
target_include_directories(finch SYSTEM PRIVATE ${LLVM_INCLUDE_DIRS} ${LIBCLANG_INCLUDE})
target_compile_definitions(finch PRIVATE ${LLVM_DEFINITIONS} FINCH_VERSION="${PROJECT_VERSION}")
target_compile_options(finch PRIVATE -Wall -Wextra)
# Link the one shared libLLVM when available: libclang uses it too, and two copies
# of LLVM in one process crash on exit.
if(TARGET LLVM)
set(LLVM_LIBS LLVM)
else()
llvm_map_components_to_libnames(LLVM_LIBS core support passes native)
endif()
target_link_libraries(finch PRIVATE ${LLVM_LIBS} ${LIBCLANG})

202
LICENSE Normal file
View file

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

119
README.md Normal file
View file

@ -0,0 +1,119 @@
# Finch 🐦
**Small, quick, sharp.** A programming language on LLVM with C-like syntax, simple enough
to learn in an hour and as fast as C.
> 🇵🇱 [Polska wersja](README.pl.md)
```c
import "math.h"
fn hypot2(float a, float b) -> float {
return sqrt(a * a + b * b)
}
fn main() {
name := "Finch"
print("Hello from", name, "!")
for i in 1..4 {
print(i, "→", hypot2(float(i), 1.0))
}
}
```
## Why Finch
- **Simple.** No semicolons, no headers, no preprocessor. Each thing has one way to do it.
- **Fast.** It compiles through LLVM's `-O2` pipeline to native code, with no runtime or garbage collector.
Recursive `fib(40)` runs in about 0.19 s (`clang -O2` on the same C: about 0.23 s).
- **Safer than C, where it is cheap.** No silent narrowing and no signed/unsigned mixing. Division by zero
and null `.value` stop with a clear runtime error instead of undefined behavior. Operator precedence
that does not bite (`x & 1 == 0` means what it says).
- **Errors that talk like a person:**
```
game.fn:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
3 | i32 y = x
| ^
```
- **Any C library, directly.** `import "GLFW/glfw3.h"` + `link "glfw"`, and you can call it.
Headers are read by libclang, so there is nothing to declare by hand.
## Quick start
Linux x86-64, 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
./build/finch run examples/hello.fn
./build/finch build examples/tour.fn -o tour && ./tour
tests/run.sh # 34 passed, 0 failed
```
## Documentation
Pick the guide that fits you:
| Guide | For | English | Polski |
|---|---|---|---|
| **Beginners** | Never programmed: students, seniors, anyone curious | [for-beginners.md](docs/en/for-beginners.md) | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) |
| **Technicians** | Know computers, new to programming: install, whole language, C libraries, troubleshooting | [for-technicians.md](docs/en/for-technicians.md) | [dla-technikow.md](docs/pl/dla-technikow.md) |
| **Engineers** | Everything: lexer, grammar, AST, type rules, IR lowering, libclang import, ABI, internals | [for-engineers.md](docs/en/for-engineers.md) | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) |
## The language at a glance
| | |
|---|---|
| Variables | `x := 5` (inferred) or `int x = 5` |
| Types | `int` `float` `bool` `char` `str`, sized `i8`…`i64` `u8`…`u64` `f32` `f64`, pointers `ptr[T]` / `ptr` |
| Functions | `fn add(int a, int b) -> int { return a + b }` |
| Control | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue` |
| Pointers | `p := addr(x)`, `p.value = 5`, `null` |
| Conversions | `int(3.9)`, `u8(x)`, `float(n)` (implicit only when nothing is lost) |
| Operators | `+ - * / %`, `& \| ^ << >> ~`, `== != < <= > >=`, `&& \|\| !` |
| C interop | `import "stdio.h"`, `link "glfw"` |
| Output | `print(a, b, c)` prints anything, separated by spaces |
## Examples
| File | Shows |
|---|---|
| [`examples/hello.fn`](examples/hello.fn) | the smallest program |
| [`examples/tour.fn`](examples/tour.fn) | variables, functions, conditions, loops |
| [`examples/types.fn`](examples/types.fn) | sized numbers and pointers |
| [`examples/c_import.fn`](examples/c_import.fn) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
| [`examples/window.fn`](examples/window.fn) | a GLFW + OpenGL window |
| [`examples/llvm.fn`](examples/llvm.fn) | Finch building LLVM IR through the LLVM-C API |
## Roadmap
- [x] Lexer, parser, LLVM codegen, O2 optimization
- [x] Types incl. sized integers, pointers, conversions; functions; `if`/`while`/`for`
- [x] Readable compile errors; runtime checks (division, null)
- [x] Bitwise operators with sane precedence
- [x] C headers through libclang; `link` with pkg-config; link-error advice
- [ ] `struct` (and C structs by value)
- [ ] Arrays/slices with `.len` and bounds checks
- [ ] Memory: automatic free at the end of the owning block, `defer`, manual `free` when wanted
- [ ] Strings: `+`, `len`, `str(x)`; `input()`
- [ ] Finch modules (`import math`)
- [ ] Debug info (DWARF)
- [ ] Bootstrap: the Finch compiler written in Finch
## Layout
```
src/ compiler: lexer, parser, AST, codegen, C import, driver (C++17, ~2.4k lines)
examples/ example programs
tests/ run/ (golden output), fail/ (expected errors), run.sh
docs/ en/ and pl/ guides for three audiences
```
## License
[Apache License 2.0](LICENSE). Copyright 2026 Nexoniarz.

119
README.pl.md Normal file
View file

@ -0,0 +1,119 @@
# Finch 🐦
**Mały, zwinny, skuteczny.** Język programowania na LLVM ze składnią podobną do C,
na tyle prosty, że nauczysz się go w godzinę, i tak szybki jak C.
> 🇬🇧 [English version](README.md)
```c
import "math.h"
fn przeciwprostokatna(float a, float b) -> float {
return sqrt(a * a + b * b)
}
fn main() {
imie := "Finch"
print("Cześć od", imie, "!")
for i in 1..4 {
print(i, "→", przeciwprostokatna(float(i), 1.0))
}
}
```
## Dlaczego Finch
- **Prosty.** Bez średników, bez plików nagłówkowych, bez preprocesora. Każdą rzecz robi się na jeden sposób.
- **Szybki.** Kompiluje się przez potok `-O2` z LLVM do kodu natywnego, bez środowiska uruchomieniowego i garbage collectora.
Rekurencyjne `fib(40)` trwa ok. 0,19 s (`clang -O2` na tym samym kodzie C: ok. 0,23 s).
- **Bezpieczniejszy niż C tam, gdzie to tanie.** Nie ma cichego zwężania typów ani mieszania liczb ze znakiem
i bez znaku. Dzielenie przez zero i `.value` na null zatrzymują program czytelnym błędem, zamiast
powodować niezdefiniowane zachowanie. Priorytety operatorów nie gryzą (`x & 1 == 0` znaczy to, co widać).
- **Błędy mówią po ludzku:**
```
gra.fn:3:13: error: 'y' must be i32, but this is int (use i32(...) to convert)
3 | i32 y = x
| ^
```
- **Każda biblioteka C, bezpośrednio.** Piszesz `import "GLFW/glfw3.h"` i `link "glfw"`, i możesz z niej korzystać.
Nagłówki czyta libclang, więc nie trzeba niczego deklarować ręcznie.
## Szybki start
Linux x86-64, 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
./build/finch run examples/hello.fn
./build/finch build examples/tour.fn -o tour && ./tour
tests/run.sh # 34 passed, 0 failed
```
## Dokumentacja
Wybierz przewodnik dla siebie:
| Przewodnik | Dla kogo | Polski | English |
|---|---|---|---|
| **Początkujący** | Nigdy nie programowałeś: uczniowie, seniorzy, ciekawscy | [dla-poczatkujacych.md](docs/pl/dla-poczatkujacych.md) | [for-beginners.md](docs/en/for-beginners.md) |
| **Technicy** | Znasz komputery, programowanie jest nowe: instalacja, cały język, biblioteki C, problemy | [dla-technikow.md](docs/pl/dla-technikow.md) | [for-technicians.md](docs/en/for-technicians.md) |
| **Inżynierowie** | Wszystko: lekser, gramatyka, AST, reguły typów, tłumaczenie na IR, import przez libclang, ABI, wnętrze | [dla-inzynierow.md](docs/pl/dla-inzynierow.md) | [for-engineers.md](docs/en/for-engineers.md) |
## Język w pigułce
| | |
|---|---|
| Zmienne | `x := 5` (typ zgadnięty) albo `int x = 5` |
| Typy | `int` `float` `bool` `char` `str`, z rozmiarem `i8`…`i64` `u8`…`u64` `f32` `f64`, wskaźniki `ptr[T]` / `ptr` |
| Funkcje | `fn add(int a, int b) -> int { return a + b }` |
| Sterowanie | `if` / `else if` / `else`, `while`, `for i in 0..10`, `break`, `continue` |
| Wskaźniki | `p := addr(x)`, `p.value = 5`, `null` |
| Konwersje | `int(3.9)`, `u8(x)`, `float(n)` (niejawne tylko wtedy, gdy nic nie ginie) |
| Operatory | `+ - * / %`, `& \| ^ << >> ~`, `== != < <= > >=`, `&& \|\| !` |
| Współpraca z C | `import "stdio.h"`, `link "glfw"` |
| Wypisywanie | `print(a, b, c)` wypisuje wszystko, oddzielając spacjami |
## Przykłady
| Plik | Pokazuje |
|---|---|
| [`examples/hello.fn`](examples/hello.fn) | najmniejszy program |
| [`examples/tour.fn`](examples/tour.fn) | zmienne, funkcje, warunki, pętle |
| [`examples/types.fn`](examples/types.fn) | liczby z rozmiarem i wskaźniki |
| [`examples/c_import.fn`](examples/c_import.fn) | `printf`, `math.h`, `malloc`/`free`, `stderr` |
| [`examples/window.fn`](examples/window.fn) | okno GLFW + OpenGL |
| [`examples/llvm.fn`](examples/llvm.fn) | Finch budujący LLVM IR przez API LLVM-C |
## Plan rozwoju
- [x] Lekser, parser, generowanie kodu LLVM, optymalizacja O2
- [x] Typy, w tym liczby z rozmiarem, wskaźniki, konwersje; funkcje; `if`/`while`/`for`
- [x] Czytelne błędy kompilacji; kontrole w czasie działania (dzielenie, null)
- [x] Operatory bitowe z rozsądnymi priorytetami
- [x] Nagłówki C przez libclang; `link` z pkg-config; porady przy błędach linkowania
- [ ] `struct` (i struktury C przez wartość)
- [ ] Tablice/wycinki z `.len` i kontrolą zakresu
- [ ] Pamięć: automatyczne zwalnianie na końcu bloku, `defer`, ręczne `free` dla chętnych
- [ ] Teksty: `+`, `len`, `str(x)`; `input()`
- [ ] Moduły Fincha (`import math`)
- [ ] Informacje dla debuggera (DWARF)
- [ ] Bootstrap: kompilator Fincha napisany w Finchu
## Struktura
```
src/ kompilator: lekser, parser, AST, codegen, import C, sterownik (C++17, ok. 2,4 tys. linii)
examples/ przykładowe programy
tests/ run/ (wzorcowe wyjście), fail/ (oczekiwane błędy), run.sh
docs/ przewodniki en/ i pl/ dla trzech grup odbiorców
```
## Licencja
[Apache License 2.0](LICENSE). Copyright 2026 Nexoniarz.

650
docs/en/for-beginners.md Normal file
View file

@ -0,0 +1,650 @@
# Finch for Beginners
**Never written a program before? This guide is for you.**
No experience needed, and no math beyond what you learned in primary school.
Take it slowly, one section at a time, and type every example yourself: that is how it sticks.
> 🇵🇱 Polska wersja: [dla-poczatkujacych.md](../pl/dla-poczatkujacych.md)
---
## Contents
1. [What is a program?](#1-what-is-a-program)
2. [Getting Finch ready](#2-getting-finch-ready)
3. [Your first program](#3-your-first-program)
4. [Showing things on the screen](#4-showing-things-on-the-screen)
5. [Boxes with names: variables](#5-boxes-with-names-variables)
6. [Kinds of values](#6-kinds-of-values)
7. [Doing math](#7-doing-math)
8. [Making decisions: if](#8-making-decisions-if)
9. [Repeating things: loops](#9-repeating-things-loops)
10. [Your own commands: functions](#10-your-own-commands-functions)
11. [When something goes wrong](#11-when-something-goes-wrong)
12. [Small projects to try](#12-small-projects-to-try)
13. [Cheat sheet](#13-cheat-sheet)
14. [Words you will hear](#14-words-you-will-hear)
---
## 1. What is a program?
A computer is very fast, but it does not think. It only does exactly what it is told,
one step at a time.
A **program** is a list of those steps, written down. Like a recipe:
> 1. Take two eggs.
> 2. Break them into a bowl.
> 3. Stir.
A **programming language** is the way we write that recipe so the computer understands it.
**Finch** is one of those languages. It was made to be small and simple, so you
can learn it quickly.
The computer reads your program from top to bottom, line by line.
---
## 2. Getting Finch ready
Finch runs on **Linux** computers. If a teacher, parent or friend has already set it up
for you, skip to the next section.
Otherwise, open the **Terminal** program (a window where you type commands) and type
these lines one by one, pressing **Enter** after each:
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell
cmake -S . -B build -G Ninja
ninja -C build
```
If the last line finishes without the word `error`, Finch is ready.
(If your computer does not have `nix-shell`, ask someone to follow the
[guide for technicians](for-technicians.md#2-installing). It explains the other way.)
To check it works, type:
```sh
./build/finch version
```
You should see something like `finch 1.0.0`.
---
## 3. Your first program
1. Open any text editor (for example *Text Editor*, *Kate*, *gedit* or *VS Code*).
2. Type exactly this:
```c
fn main() {
print("Hello!")
}
```
3. Save it in the `Finch` folder as **`hello.fn`**. The `.fn` at the end tells everyone it is a Finch program.
4. In the Terminal, type:
```sh
./build/finch run hello.fn
```
You will see:
```
Hello!
```
🎉 **You have just written and run a program.**
### What did we write?
```text
fn main() { ← "here starts the main part of my program"
print("Hello!") ← "show the text Hello! on the screen"
} ← "the main part ends here"
```
- `fn main()` is where every Finch program **starts**. Each program has exactly one `main`.
- The curly brackets `{` and `}` hold everything that belongs to `main`, like the two covers of a book.
- `print(...)` shows something on the screen.
- Text always goes inside double quotes: `"Hello!"`.
The spaces at the start of `print` are not required, but they make it easy to see
what is inside the brackets. Always add them. It is a good habit.
---
## 4. Showing things on the screen
`print` can show text and numbers. Put a comma between things, and Finch adds a space for you:
```c
fn main() {
print("I am", 10, "years old")
print("2 + 2 =", 2 + 2)
print()
print("The line above is empty")
}
```
Shows:
```
I am 10 years old
2 + 2 = 4
The line above is empty
```
Every `print` starts a new line. `print()` with nothing inside makes an empty line.
### Notes for yourself: comments
Anything after `//` is ignored by the computer. Use it to leave notes:
```c
// This program says hi
fn main() {
print("Hi") // this shows Hi
}
```
---
## 5. Boxes with names: variables
Imagine a box with a label on it. You can put something in the box and later look
at what is inside by reading the label. In programming that box is called a **variable**.
```c
fn main() {
age := 10
print(age)
}
```
`age := 10` means: *"make a new box, label it `age`, and put 10 in it."*
You can change what is in the box later, using just `=`:
```c
fn main() {
score := 0
print("Score:", score)
score = 5
print("Score:", score)
score = score + 1
print("Score:", score)
}
```
```
Score: 0
Score: 5
Score: 6
```
`score = score + 1` reads as: *"take what is in `score`, add 1, and put the result back into `score`."*
There is a shorter way to write the same thing: `score += 1`.
**Remember:**
- `:=` makes a **new** box. You use it only once per name.
- `=` puts a new value into a box **that already exists**.
### Naming your boxes
Names can use the English letters `a`–`z` and `A`–`Z`, digits and `_`, but cannot start with a digit:
`age`, `total_points`, `player2` are fine. `2player` is not.
Letters like `é`, `ü` or `ż` cannot be used in names, only inside text in quotes: `print("Zürich")` is fine.
Pick names that say what is inside: `price` is better than `p`.
---
## 6. Kinds of values
Finch keeps track of what *kind* of thing is in each box. The kind is called a **type**.
These are the ones you will use most:
| Type | What it holds | Examples |
|---------|------------------------------|--------------------------|
| `int` | whole numbers | `0`, `42`, `-7` |
| `float` | numbers with a decimal point | `3.14`, `0.5`, `-2.0` |
| `str` | text | `"Hello"`, `"Ola"` |
| `bool` | yes or no | `true`, `false` |
| `char` | a single letter or sign | `'A'`, `'z'`, `'?'` |
Finch figures out the type by itself when you use `:=`:
```c
name := "Ola" // str
age := 10 // int
height := 1.42 // float
likes_cats := true // bool
```
You can also write the type yourself, in front of the name:
```c
int age = 10
str name = "Ola"
```
Both ways do the same thing. Use the one you like.
**A box keeps its type.** If `age` holds whole numbers, you cannot put text in it later.
Finch will stop and tell you, which helps you catch mistakes early.
---
## 7. Doing math
| Sign | Meaning | Example | Result |
|------|----------------------------|----------|--------|
| `+` | add | `7 + 2` | `9` |
| `-` | subtract | `7 - 2` | `5` |
| `*` | multiply | `7 * 2` | `14` |
| `/` | divide | `7 / 2` | `3` |
| `%` | what is left after dividing | `7 % 2` | `1` |
**Careful with dividing whole numbers:** `7 / 2` gives `3`, not `3.5`, because both are
whole numbers, so the answer is a whole number too (the part after the dot is dropped).
If you want `3.5`, use a decimal number: `7.0 / 2`.
Multiplication and division happen before adding and subtracting, just like at school.
Use brackets to change the order:
```c
print(2 + 3 * 4) // 14
print((2 + 3) * 4) // 20
```
`%` is handy to check if a number is even: an even number divided by 2 leaves nothing,
so `n % 2` is `0`.
You **cannot divide by zero**. If your program tries, it stops and tells you which line did it.
---
## 8. Making decisions: if
Programs can choose what to do. `if` means *"only do this when something is true"*.
```c
fn main() {
temperature := 25
if temperature > 20 {
print("It's warm, wear a t-shirt")
}
}
```
Add `else` for *"otherwise, do this"*:
```c
if temperature > 20 {
print("It's warm")
} else {
print("Take a jacket")
}
```
And `else if` for more choices. Finch checks them from top to bottom and does only
the **first** one that is true:
```c
if temperature > 25 {
print("Hot!")
} else if temperature > 15 {
print("Nice")
} else {
print("Cold")
}
```
### Comparing things
| Sign | Means |
|------|-----------------------------|
| `==` | is equal to |
| `!=` | is not equal to |
| `<` | is smaller than |
| `>` | is bigger than |
| `<=` | is smaller than or equal to |
| `>=` | is bigger than or equal to |
⚠️ **One `=` puts a value in a box. Two `==` ask "are these equal?"**
You can compare text too: `if name == "Ola" { ... }`.
### Joining questions
- `&&` means **and**: both must be true. `if age > 6 && age < 12 { ... }`
- `||` means **or**: at least one must be true. `if day == "Sat" || day == "Sun" { ... }`
- `!` means **not**: it flips true and false. `if !raining { ... }`
---
## 9. Repeating things: loops
Computers never get bored. A **loop** repeats the same steps many times.
### Counting with `for`
```c
fn main() {
for i in 1..6 {
print("Number", i)
}
}
```
```
Number 1
Number 2
Number 3
Number 4
Number 5
```
`for i in 1..6` means: *"count with `i` from 1, up to but **not including** 6"*.
So `0..10` gives you ten numbers: 0, 1, 2 … 9.
### Repeating while something is true: `while`
```c
fn main() {
count := 3
while count > 0 {
print(count)
count -= 1
}
print("Go!")
}
```
```
3
2
1
Go!
```
⚠️ Make sure the loop can end. If `count` never went down, the loop would run forever.
If that happens, press **Ctrl + C** in the Terminal to stop it.
### Stopping early and skipping
- `break` leaves the loop right away.
- `continue` skips the rest of this round and goes to the next one.
```c
for i in 1..100 {
if i == 5 {
break // stop when we reach 5
}
print(i) // prints 1 2 3 4
}
```
---
## 10. Your own commands: functions
When you do the same thing in many places, give it a name. That named block is a **function**.
You already know one: `main`.
```c
fn say_hello(str name) {
print("Hello,", name)
}
fn main() {
say_hello("Ola")
say_hello("Tom")
}
```
```
Hello, Ola
Hello, Tom
```
`str name` in brackets means: *"this function needs one piece of text, and inside it
we call that text `name`."* The thing you put in brackets when using it is passed in.
### Functions that give back an answer
Write `->` and the type of the answer, and use `return` to give it back:
```c
fn add(int a, int b) -> int {
return a + b
}
fn main() {
result := add(2, 3)
print(result) // 5
}
```
It does not matter where in the file you put your functions, above or below `main`.
---
## 11. When something goes wrong
Everyone makes mistakes when programming, even experts, every day.
Finch tries hard to explain what went wrong in plain words. Example:
```c
fn main() {
age := 10
print(agee)
}
```
```
hello.fn:3:11: error: there is no variable named 'agee'
3 | print(agee)
| ^
```
How to read it:
- `hello.fn` is the file,
- `3` is the **line** number,
- `11` is how far into the line (the **column**),
- then the explanation, and the line itself with a `^` pointing at the problem.
Here we simply misspelled `age`.
### Common mistakes
| Finch says | What it means | Fix |
|---|---|---|
| `there is no variable named 'x'` | The name is misspelled, or you never made that box | Check spelling. Create it with `x := ...` |
| `a variable named 'x' already exists here` | You used `:=` twice for the same name | Use `=` the second time |
| `'x' must be int, but this is str` | Wrong kind of value for that box | Put the right kind of value in |
| `put each statement on its own line` | Two commands on one line | Press Enter between them |
| `this '{' is never closed` | A `}` is missing | Add the `}` |
| `a condition must be bool` | `if` needs a yes/no question | Compare something: `if x > 0` |
| `runtime error: division by zero` | You divided by 0 while the program ran | Check the number before dividing |
---
## 12. Small projects to try
Try writing each one yourself first. Look at the solution only if you get stuck.
### Project 1: Times table
Show the 7 times table, from 7 × 1 to 7 × 10.
<details><summary>Solution</summary>
```c
fn main() {
for i in 1..11 {
print("7 x", i, "=", 7 * i)
}
}
```
</details>
### Project 2: Even or odd
For every number from 1 to 10, show whether it is even or odd.
<details><summary>Solution</summary>
```c
fn main() {
for n in 1..11 {
if n % 2 == 0 {
print(n, "is even")
} else {
print(n, "is odd")
}
}
}
```
</details>
### Project 3: Celsius to Fahrenheit
Show a table from 0 °C to 100 °C, every 10 degrees. Formula: `F = C * 9 / 5 + 32`.
<details><summary>Solution</summary>
```c
fn to_fahrenheit(float c) -> float {
return c * 9 / 5 + 32
}
fn main() {
for c in 0..11 {
celsius := float(c * 10)
print(celsius, "C =", to_fahrenheit(celsius), "F")
}
}
```
`float(...)` turns a whole number into a decimal number.
</details>
### Project 4: Fizz Buzz
Count from 1 to 20. For numbers divisible by 3 say `Fizz`, by 5 say `Buzz`,
by both say `FizzBuzz`, otherwise say the number.
<details><summary>Solution</summary>
```c
fn main() {
for n in 1..21 {
if n % 15 == 0 {
print("FizzBuzz")
} else if n % 3 == 0 {
print("Fizz")
} else if n % 5 == 0 {
print("Buzz")
} else {
print(n)
}
}
}
```
</details>
### Project 5: Biggest of three
Write a function `biggest(int a, int b, int c) -> int` that gives back the largest number.
<details><summary>Solution</summary>
```c
fn biggest(int a, int b, int c) -> int {
if a >= b && a >= c {
return a
}
if b >= c {
return b
}
return c
}
fn main() {
print(biggest(4, 9, 2)) // 9
}
```
</details>
---
## 13. Cheat sheet
```c
// a comment
fn main() { // the program starts here
print("text", 42) // show things
x := 5 // new box (type guessed)
int y = 10 // new box (type written)
x = 7 // change a box
x += 1 // add 1 to x
if x > 5 { // decide
print("big")
} else if x > 0 {
print("small")
} else {
print("zero or less")
}
for i in 0..10 { // count 0 to 9
print(i)
}
while x > 0 { // repeat while true
x -= 1
}
}
fn add(int a, int b) -> int { // your own command
return a + b
}
```
---
## 14. Words you will hear
| Word | Meaning |
|---|---|
| **program** | A list of steps for the computer |
| **code** | The text of a program |
| **run** | Make the computer do the steps |
| **compile** | Translate your code into something the computer can run. `finch run` does this for you |
| **variable** | A named box holding a value |
| **type** | The kind of value: number, text, yes/no… |
| **function** | A named group of steps you can use again |
| **loop** | Steps that repeat |
| **condition** | A yes/no question, like `x > 5` |
| **error** | A message saying something is wrong, and where |
| **bug** | A mistake in a program |
---
**What next?** When this all feels easy, move on to the
[guide for technicians](for-technicians.md). It covers every part of Finch.

1131
docs/en/for-engineers.md Normal file

File diff suppressed because it is too large Load diff

541
docs/en/for-technicians.md Normal file
View file

@ -0,0 +1,541 @@
# Finch for Technicians
**For people who know their way around a computer, but little or no programming.**
This guide covers installing Finch, using the `finch` tool, the whole language, using
C libraries, and fixing common problems. It does not cover how the compiler works inside.
For that, see the [guide for engineers](for-engineers.md).
> 🇵🇱 Polska wersja: [dla-technikow.md](../pl/dla-technikow.md)
---
## Contents
1. [What Finch is](#1-what-finch-is)
2. [Installing](#2-installing)
3. [The finch command](#3-the-finch-command)
4. [How a Finch file looks](#4-how-a-finch-file-looks)
5. [Variables](#5-variables)
6. [Types](#6-types)
7. [Operators](#7-operators)
8. [Control flow](#8-control-flow)
9. [Functions](#9-functions)
10. [Pointers](#10-pointers)
11. [Using C libraries](#11-using-c-libraries)
12. [Errors](#12-errors)
13. [Troubleshooting](#13-troubleshooting)
14. [Project layout and tests](#14-project-layout-and-tests)
15. [Current limits](#15-current-limits)
---
## 1. What Finch is
Finch is a **compiled** programming language. The `finch` tool translates your `.fn`
file into a real program (a native executable, like the ones made from C), using the
**LLVM** compiler toolkit, the same backend that powers clang, Rust and Swift.
What that gives you:
- **Speed close to C.** There is no interpreter, virtual machine or garbage collector.
- **Small, standalone programs** that you can copy and run.
- **Direct access to C libraries.** `import "stdio.h"` and call anything in it.
The design rules: one way to do each thing, C-like syntax without C's traps, and error
messages that tell you how to fix the problem.
---
## 2. Installing
Finch currently runs on **Linux, x86-64**. It has been tested with **LLVM 21**.
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)
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # downloads LLVM, libclang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja
ninja -C build
./build/finch version # finch 1.0.0 (LLVM 21.x)
```
### 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` |
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
```
### Putting `finch` on your PATH (optional)
```sh
sudo cp build/finch /usr/local/bin/
finch version
```
Finch needs a working C compiler (`cc`) at run time. It uses it as the **linker** that
turns compiled code into a program. Set the `CC` environment variable to use a different one.
---
## 3. The finch command
```
finch run <file.fn> compile and run right away
finch build <file.fn> [-o name] compile into a program (default name: the file's name)
finch ir <file.fn> print the LLVM IR (for the curious)
finch version show the version
options:
-l <lib> link a C library, same as link "lib" in the file
-O0 skip optimizations
```
Examples:
```sh
finch run hello.fn # prints the output
finch build game.fn -o mygame # creates ./mygame
./mygame
finch run window.fn -l glfw # with a C library
```
`finch run` passes on your program's exit code. If the program crashes, it tells you
how (for example `the program crashed: Segmentation fault`).
---
## 4. How a Finch file looks
```c
// comments start with two slashes
/* or span
several lines */
import "math.h" // C headers (optional), always at the top
link "m" // C libraries to link (optional)
fn main() { // the program starts here
print("Hello")
}
```
Rules worth knowing:
- **One statement per line.** There are no semicolons. To split a long expression,
break the line **after** an operator (`a +⏎ b`) or anywhere inside brackets `( )`.
- **Curly braces are always required** after `if`, `else`, `for`, `while`, and the opening
`{` goes on the same line.
- **No brackets around conditions:** `if x > 5 {`, not `if (x > 5) {`.
- Functions can be in any order in the file.
- `main` is either `fn main()` or `fn main() -> int` (returns the exit code).
---
## 5. Variables
```c
x := 5 // new variable, type inferred (int)
int y = 10 // new variable, type written explicitly
int z // no value: starts at zero ("" for str, null for pointers)
x = 7 // assign
x += 1 // also -= *= /= %=
```
- A name can be declared **once** in a function. Finch does not allow reusing a name in an
inner block (no "shadowing"), because that is a common source of confusion.
- Variables exist from their declaration to the end of the `{ }` block they are in.
- Names: ASCII letters, digits and `_`, not starting with a digit. Non-ASCII letters are allowed only inside strings. Type names (`int`, `u8`, …) and
keywords are reserved.
**Keywords:** `fn return if else while for in break continue true false null import link`
---
## 6. Types
### Everyday types
| Type | Holds | Example |
|---------|-------|---------|
| `int` | whole number, 64-bit (same as `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
| `float` | decimal number, 64-bit (same as `f64`) | `3.14` |
| `bool` | `true` or `false` | |
| `char` | one byte-sized character | `'A'`, `'\n'` |
| `str` | text (a C string) | `"hello\tworld"` |
String and char escapes: `\n` new line, `\t` tab, `\r`, `\0`, `\\`, `\"`, `\'`.
### Sized types (for files, hardware, C libraries)
| Signed | Range | Unsigned | Range |
|---|---|---|---|
| `i8` | −128 … 127 | `u8` | 0 … 255 |
| `i16` | −32 768 … 32 767 | `u16` | 0 … 65 535 |
| `i32` | about ±2.1 billion | `u32` | 0 … about 4.29 billion |
| `i64` | about ±9.2 × 10¹⁸ | `u64` | 0 … about 1.8 × 10¹⁹ |
`f32` is a 32-bit decimal number (about 7 significant digits), `f64` a 64-bit one (about 15–16).
### Pointers
`ptr[T]` points to a value of type `T`. Plain `ptr` points to "something" with unknown
type, like `void*` in C. See [Pointers](#10-pointers).
### Conversions
Finch converts automatically **only when nothing can be lost**:
| From | To | Automatic? |
|---|---|---|
| a smaller whole number | a bigger one of the same signedness (`i8`→`i32`, `u8`→`u64`) | ✅ |
| unsigned | a strictly bigger signed (`u8`→`i16`, `u32`→`i64`) | ✅ |
| any whole number | `float` / `f32` | ✅ |
| `f32` | `f64` | ✅ |
| `null` | any pointer | ✅ |
| any pointer, `str` | `ptr` | ✅ |
| `ptr` | any `ptr[T]` | ✅ |
| anything else | | ❌ write it: `type(value)` |
**Number literals adapt.** A number written in the code fits into any type that can hold it:
`u8 b = 200` works, `u8 b = 300` is an error (`the number 300 doesn't fit in u8`).
The same applies to constants imported from C headers.
**Explicit conversions** use the type name like a function:
```c
int(3.99) // 3 (cuts off the fraction)
int(-3.99) // -3
float(7) / 2 // 3.5
u8(300) // 44 (keeps the lowest 8 bits)
i8(200) // -56
int('A') // 65
char(66) // 'B'
bool(0) // false (any non-zero number is true)
ptr(text) // a str as an untyped pointer
str(p) // a pointer as a str
```
**Fixed-size numbers wrap around**, like real hardware: a `u8` holding 255 plus 1 becomes 0.
Calculations happen in the type of the values involved, so `u8(250) + 10` is `4`, not `260`.
Convert first if you need more room: `int(x) + 10`.
---
## 7. Operators
### By priority (higher binds tighter)
| Priority | Operators | Meaning |
|---|---|---|
| 5 | `*` `/` `%` `<<` `>>` `&` | multiply, divide, remainder, shift left/right, bitwise AND |
| 4 | `+` `-` `\|` `^` | add, subtract, bitwise OR, bitwise XOR |
| 3 | `==` `!=` `<` `<=` `>` `>=` | comparisons |
| 2 | `&&` | logical AND |
| 1 | `\|\|` | logical OR |
Unary (in front of a value): `-x` negate, `!x` logical NOT, `~x` flip all bits.
> **Different from C, on purpose:** bit operators bind tighter than comparisons, so
> `flags & 4 != 0` means `(flags & 4) != 0`. In C it silently means something else.
Notes:
- `/` on whole numbers rounds toward zero: `7 / 2` is `3`, `-7 / 2` is `-3`. `%` takes the sign of the left side.
- Dividing by zero **stops the program** with `runtime error: division by zero` and the line number.
If the zero is written in the code, it is a compile error instead.
- `&&` and `||` only evaluate the right side if needed.
- Text (`str`) can be compared with `==` and `!=` (it compares the contents). `<` and `>` do not work on text.
- Pointers can be compared with `==` and `!=`, including against `null`.
- Bit operators need whole numbers. For `bool` use `&&` and `||`.
- `>>` on signed numbers keeps the sign (`-16 >> 2` is `-4`). On unsigned numbers it fills with zeros.
---
## 8. Control flow
```c
if x > 10 {
...
} else if x > 0 {
...
} else {
...
}
while count > 0 {
count -= 1
}
for i in 0..10 { // i = 0, 1, …, 9 (the end is not included)
if i == 3 { continue }
if i == 8 { break }
}
```
- A condition must be a `bool`. `if x {` with a number is an error; write `if x != 0 {`.
- In `for i in a..b`, both ends are whole numbers evaluated **once** before the loop starts.
`i` is an `int` and cannot be changed inside the loop.
- `while true { ... }` loops until a `break` or `return`.
- Code after `return`, `break` or `continue` in the same block is an error, because it can never run.
---
## 9. Functions
```c
fn name(type1 param1, type2 param2) -> return_type {
...
return value
}
fn no_result(str message) { // no "->": returns nothing
print(message)
}
```
- Parameters are copies. Changing a parameter does not change the caller's variable.
Use a pointer for that (see below).
- A function with `-> type` must return a value on every path. Finch checks this.
- A function cannot be defined inside another function.
- Built-in names you cannot reuse: `print`, `addr`, and all type names.
### Built-in functions
| Function | What it does |
|---|---|
| `print(a, b, ...)` | Prints all values separated by spaces, then a new line. Works with every type. |
| `addr(x)` | Gives a pointer to variable `x`. |
| `int(x)`, `u8(x)`, `float(x)`, … | Converts types (see [Conversions](#conversions)). |
---
## 10. Pointers
A pointer holds the **address** of a value: where it lives in memory. Finch uses words
instead of C's `*` and `&`:
| C | Finch | Meaning |
|---|---|---|
| `int *p` | `ptr[int] p` | p points to an int |
| `&x` | `addr(x)` | the address of x |
| `*p` | `p.value` | the value p points to |
| `NULL` | `null` | points nowhere |
| `void *` | `ptr` | points to something of unknown type |
```c
fn double(ptr[int] p) {
p.value = p.value * 2
}
fn main() {
x := 21
double(addr(x))
print(x) // 42
ptr[int] nothing = null
if nothing == null {
print("empty")
}
}
```
Pointers can point to pointers: `ptr[ptr[int]]`, then `pp.value.value`.
Using `.value` on a `null` pointer **stops the program** with
`runtime error: used .value on a null pointer` instead of crashing randomly.
⚠️ A pointer to a variable must not be used after the function that owns the variable
has returned. The variable no longer exists then. Finch does not check this yet.
---
## 11. Using C libraries
Almost every library on Linux has a C interface: graphics, sound, networking, databases.
Finch can use them directly.
### Step 1: import the header
```c
import "stdio.h"
import "math.h"
import "GLFW/glfw3.h" // a path inside the system include folders
import "mylib.h" // a header next to your .fn file
```
Finch reads the header with **libclang** (clang's C parser) and makes these usable:
- **functions**, including ones with a variable number of arguments, like `printf`,
- **`enum` values**,
- **simple `#define` constants** that are a single number, like `M_PI`, `EOF`, `GL_COLOR_BUFFER_BIT`,
- **global variables**, like `stdout` and `stderr`.
### Step 2: link the library
A header only *describes* functions. Their actual code is in a library file (`libNAME.so`).
Tell Finch which library to use, at the top of the file:
```c
link "glfw" // uses libglfw.so
link "GL" // uses libGL.so
```
Write just the name: for `libglfw.so` that is `glfw`. If `pkg-config` knows the name, Finch uses
its settings (paths and extra libraries). Otherwise it passes `-lNAME` to the linker.
The C standard library and the math library (`libm`) are always linked.
On the command line, `-l NAME` does the same as `link "NAME"`.
If you forget the `link` line, Finch tells you exactly what is missing:
```
error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked
the header only says the functions exist; their code lives in a library.
add this at the top of your file (with the library's real name):
link "glfw"
```
### How C types appear in Finch
| C | Finch |
|---|---|
| `char`, `signed char`, `unsigned char` | `char`, `i8`, `u8` |
| `short`, `int`, `long`, `long long` | `i16`, `i32`, `i64`, `i64` (by actual size) |
| `unsigned …` | `u16`, `u32`, `u64` |
| `float`, `double` | `f32`, `f64` |
| `_Bool` / `bool` | `bool` |
| `enum` | the matching signed integer |
| `char *`, `const char *` | `str` |
| `int *`, `double *`, … | `ptr[i32]`, `ptr[f64]`, … |
| `void *`, `struct X *`, `FILE *`, function pointers | `ptr` |
So C's `int` is Finch's `i32`. Literals adapt automatically (`abs(-5)` works),
but a Finch `int` variable must be converted: `abs(i32(x))`.
### Example
```c
import "stdio.h"
import "math.h"
import "stdlib.h"
fn main() {
printf("%d + %d = %d\n", 2, 3, 5)
print(sqrt(2), M_PI, RAND_MAX)
ptr[i32] n = malloc(4) // ask C for 4 bytes of memory
n.value = 7
print(n.value)
free(n) // and give them back
fprintf(stderr, "this goes to the error output\n")
}
```
A complete graphics example is in `examples/window.fn`: a GLFW + OpenGL window that changes color.
`examples/llvm.fn` shows that Finch can even drive LLVM itself through its C API.
### What does not work yet
- C functions that take or return a **`struct` by value**, such as `div()`. Finch says so
clearly if you try. Functions taking a *pointer* to a struct are fine.
- **Function-like macros** (`#define MAX(a,b) ...`) and macros that are not a single number.
- `long double`, 128-bit integers.
- Reading the **fields** of a C struct.
---
## 12. Errors
### Compile errors
Finch stops at the first problem and shows where it is:
```
game.fn:12:9: error: 'score' must be int, but this is str
12 | score = "high"
| ^
```
Format: `file:line:column: error: explanation`, then the line with a `^` under the spot.
### Runtime errors
Some problems can only be seen while the program runs. Instead of undefined behavior
(as in C), Finch stops with a clear message on the error output and exit code 1:
| Message | Cause |
|---|---|
| `runtime error: division by zero` | `/` or `%` by a variable that was 0 |
| `runtime error: division overflows …` | the smallest signed number divided by −1 |
| `runtime error: used .value on a null pointer` | reading or writing `p.value` while `p` is `null` |
---
## 13. Troubleshooting
| Problem | Solution |
|---|---|
| `can't find the C header 'x.h'` | The library's development files are missing. Install them (on Debian: `libx-dev`; on Nix: add to `shell.nix`). For your own header, put it next to the `.fn` file. |
| `… come from "x.h", but its library isn't linked` | Add `link "name"` at the top of the file. |
| `the library 'x' wasn't found` | The library is not installed, or the name is wrong. On Nix, add it to `shell.nix` and run inside `nix-shell`. |
| `the C function 'f' can't be used from Finch yet` | It uses a struct by value, or another unsupported type. Look for a variant that takes pointers. |
| `no C compiler found to link with` | Install gcc or clang, or set `CC`. |
| The program runs, but a C library is not found at start | It was linked from a path the system does not search. Run it inside the same `nix-shell`, or install the library system-wide. |
| CMake: `libclang not found` | Install the libclang dev package, or run `cmake` inside `nix-shell`. |
| Build fails with LLVM API errors | You have a different LLVM version than 21. |
| A program never ends | A loop condition never becomes false. Press Ctrl+C. |
---
## 14. Project layout and tests
```
Finch/
├── src/ the compiler (C++)
├── examples/ example programs (hello, tour, types, c_import, window, llvm)
├── tests/
│ ├── run/ programs + the exact output they must print (.out)
│ ├── fail/ programs that must fail, with the expected error in line 1
│ └── run.sh the test runner
├── docs/ this documentation (en, pl)
├── shell.nix the Nix development environment
└── CMakeLists.txt
```
Run the tests after building:
```sh
tests/run.sh # → 34 passed, 0 failed
```
---
## 15. Current limits
Finch 1.0 is a solid core, not a finished language. Not there yet:
- `struct`, arrays, reading keyboard input (`input()`), string building (`"a" + "b"`).
For now, use C functions for these (`scanf`, `snprintf`, `malloc`…).
- Automatic memory management (planned: memory freed at the end of the block, plus `defer`).
- Finch modules (one program = one `.fn` file for now).
- Platforms other than Linux x86-64.
See the [roadmap in the README](../../README.md#roadmap).

1136
docs/pl/dla-inzynierow.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,670 @@
# Finch dla początkujących
**Ten przewodnik jest dla ciebie, jeśli jeszcze nigdy nie pisałeś programu.**
Nie potrzebujesz żadnego doświadczenia ani matematyki ponad to, czego uczy się w podstawówce.
Czytaj powoli, po jednym rozdziale, i przepisuj każdy przykład samodzielnie. Tak zostaje w głowie najlepiej.
> 🇬🇧 English version: [for-beginners.md](../en/for-beginners.md)
---
## Spis treści
1. [Co to jest program?](#1-co-to-jest-program)
2. [Przygotowanie Fincha](#2-przygotowanie-fincha)
3. [Twój pierwszy program](#3-twój-pierwszy-program)
4. [Wypisywanie na ekran](#4-wypisywanie-na-ekran)
5. [Pudełka z nazwami: zmienne](#5-pudełka-z-nazwami-zmienne)
6. [Rodzaje wartości](#6-rodzaje-wartości)
7. [Liczenie](#7-liczenie)
8. [Podejmowanie decyzji: if](#8-podejmowanie-decyzji-if)
9. [Powtarzanie: pętle](#9-powtarzanie-pętle)
10. [Własne polecenia: funkcje](#10-własne-polecenia-funkcje)
11. [Kiedy coś pójdzie nie tak](#11-kiedy-coś-pójdzie-nie-tak)
12. [Małe projekty do zrobienia](#12-małe-projekty-do-zrobienia)
13. [Ściąga](#13-ściąga)
14. [Słowniczek](#14-słowniczek)
---
## 1. Co to jest program?
Komputer jest bardzo szybki, ale nie myśli. Robi dokładnie to, co mu się każe,
krok po kroku.
**Program** to spisana lista takich kroków. Trochę jak przepis kuchenny:
> 1. Weź dwa jajka.
> 2. Wbij je do miski.
> 3. Wymieszaj.
**Język programowania** to sposób zapisania takiego przepisu, który komputer rozumie.
**Finch** (po polsku: zięba) jest jednym z takich języków. Zrobiono go tak, żeby był
mały i prosty, więc nauczysz się go szybko.
Komputer czyta program od góry do dołu, linijka po linijce.
---
## 2. Przygotowanie Fincha
Finch działa na komputerach z **Linuksem**. Jeśli nauczyciel, rodzic albo znajomy już go
dla ciebie zainstalował, przejdź od razu do następnego rozdziału.
W przeciwnym razie otwórz program **Terminal** (okienko, w którym wpisuje się polecenia)
i wpisz te linijki po kolei, naciskając **Enter** po każdej:
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell
cmake -S . -B build -G Ninja
ninja -C build
```
Jeśli ostatnia linijka skończy się bez słowa `error`, Finch jest gotowy.
(Jeśli komputer nie zna polecenia `nix-shell`, poproś kogoś o skorzystanie z
[przewodnika dla techników](dla-technikow.md#2-instalacja), gdzie opisano inny sposób.)
Żeby sprawdzić, czy działa, wpisz:
```sh
./build/finch version
```
Powinieneś zobaczyć coś w stylu `finch 1.0.0`.
---
## 3. Twój pierwszy program
1. Otwórz dowolny edytor tekstu (na przykład *Edytor tekstu*, *Kate*, *gedit* albo *VS Code*).
2. Wpisz dokładnie to:
```c
fn main() {
print("Cześć!")
}
```
3. Zapisz plik w folderze `Finch` pod nazwą **`czesc.fn`**. Końcówka `.fn` mówi, że to program w Finchu.
4. W Terminalu wpisz:
```sh
./build/finch run czesc.fn
```
Zobaczysz:
```
Cześć!
```
🎉 **Właśnie napisałeś i uruchomiłeś program.**
### Co właściwie napisaliśmy?
```text
fn main() { ← „tu zaczyna się główna część mojego programu”
print("Cześć!") ← „pokaż na ekranie tekst Cześć!”
} ← „tu główna część się kończy”
```
- `fn main()` to miejsce, od którego **zaczyna się** każdy program w Finchu. Każdy program ma dokładnie jedno `main`.
- Nawiasy klamrowe `{` i `}` obejmują wszystko, co należy do `main`, jak dwie okładki książki.
- `print(...)` pokazuje coś na ekranie.
- Tekst zawsze piszemy w cudzysłowie: `"Cześć!"`.
Spacje przed `print` nie są obowiązkowe, ale dzięki nim od razu widać, co jest w środku
klamer. Zawsze je dodawaj, to dobry nawyk.
---
## 4. Wypisywanie na ekran
`print` umie pokazać tekst i liczby. Między kolejne rzeczy wstaw przecinek, a Finch sam doda spację:
```c
fn main() {
print("Mam", 10, "lat")
print("2 + 2 =", 2 + 2)
print()
print("Linijka wyżej jest pusta")
}
```
Pokaże:
```
Mam 10 lat
2 + 2 = 4
Linijka wyżej jest pusta
```
Każdy `print` zaczyna nową linijkę. `print()` bez niczego w środku robi pustą linijkę.
### Notatki dla siebie: komentarze
Wszystko po `//` komputer pomija. Możesz tam zostawiać sobie notatki:
```c
// Ten program się wita
fn main() {
print("Cześć") // to pokazuje Cześć
}
```
---
## 5. Pudełka z nazwami: zmienne
Wyobraź sobie pudełko z naklejoną etykietą. Możesz coś do niego włożyć, a potem sprawdzić,
co jest w środku, czytając etykietę. W programowaniu takie pudełko nazywa się **zmienną**.
```c
fn main() {
wiek := 10
print(wiek)
}
```
`wiek := 10` znaczy: *„zrób nowe pudełko, nazwij je `wiek` i włóż do niego 10”*.
Później możesz zmienić zawartość pudełka, używając samego `=`:
```c
fn main() {
punkty := 0
print("Punkty:", punkty)
punkty = 5
print("Punkty:", punkty)
punkty = punkty + 1
print("Punkty:", punkty)
}
```
```
Punkty: 0
Punkty: 5
Punkty: 6
```
`punkty = punkty + 1` czytamy tak: *„weź to, co jest w `punkty`, dodaj 1 i włóż wynik z powrotem do `punkty`”*.
To samo można zapisać krócej: `punkty += 1`.
**Zapamiętaj:**
- `:=` robi **nowe** pudełko. Używasz tego tylko raz dla danej nazwy.
- `=` wkłada nową wartość do pudełka, **które już istnieje**.
### Nazywanie pudełek
Nazwy mogą zawierać angielskie litery `a`–`z` i `A`–`Z`, cyfry i znak `_`, ale nie mogą zaczynać się od cyfry:
`wiek`, `suma_punktow`, `gracz2` są w porządku. `2gracz` już nie.
⚠️ **Bez polskich liter w nazwach!** `ą`, `ę`, `ł`, `ż` i inne mogą być tylko w tekście w cudzysłowie.
Pisz `zolw`, a nie `żółw`. Natomiast `print("Żółw")` działa bez problemu.
Wybieraj nazwy, które mówią, co jest w środku: `cena` jest lepsza niż `c`.
---
## 6. Rodzaje wartości
Finch pilnuje, *jakiego rodzaju* rzecz jest w każdym pudełku. Ten rodzaj nazywa się **typem**.
Najczęściej będziesz używać tych:
| Typ | Co przechowuje | Przykłady |
|---------|---------------------------|--------------------------|
| `int` | liczby całkowite | `0`, `42`, `-7` |
| `float` | liczby z przecinkiem | `3.14`, `0.5`, `-2.0` |
| `str` | tekst | `"Cześć"`, `"Ola"` |
| `bool` | tak albo nie | `true` (prawda), `false` (fałsz) |
| `char` | jeden znak | `'A'`, `'z'`, `'?'` |
⚠️ W liczbach zamiast przecinka piszemy **kropkę**: `3.14`, a nie `3,14`. Tak jest w prawie wszystkich językach programowania.
Kiedy używasz `:=`, Finch sam rozpoznaje typ:
```c
imie := "Ola" // str
wiek := 10 // int
wzrost := 1.42 // float
lubi_koty := true // bool
```
Typ możesz też napisać sam, przed nazwą:
```c
int wiek = 10
str imie = "Ola"
```
Oba sposoby robią to samo. Wybierz ten, który ci bardziej pasuje.
**Pudełko nie zmienia typu.** Jeśli w `wiek` są liczby całkowite, nie włożysz tam potem tekstu.
Finch zatrzyma się i powie o tym, co pomaga wcześnie wyłapywać pomyłki.
---
## 7. Liczenie
| Znak | Co robi | Przykład | Wynik |
|------|-----------------------------|----------|-------|
| `+` | dodawanie | `7 + 2` | `9` |
| `-` | odejmowanie | `7 - 2` | `5` |
| `*` | mnożenie | `7 * 2` | `14` |
| `/` | dzielenie | `7 / 2` | `3` |
| `%` | reszta z dzielenia | `7 % 2` | `1` |
**Uwaga na dzielenie liczb całkowitych:** `7 / 2` daje `3`, a nie `3.5`. Obie liczby są
całkowite, więc wynik też jest całkowity (część po kropce jest odrzucana).
Jeśli chcesz dostać `3.5`, użyj liczby z kropką: `7.0 / 2`.
Mnożenie i dzielenie wykonują się przed dodawaniem i odejmowaniem, tak jak w szkole.
Nawiasy zmieniają kolejność:
```c
print(2 + 3 * 4) // 14
print((2 + 3) * 4) // 20
```
`%` przydaje się do sprawdzania, czy liczba jest parzysta. Liczba parzysta dzielona przez 2
nie daje reszty, więc `n % 2` wynosi `0`.
**Nie wolno dzielić przez zero.** Jeśli program spróbuje, zatrzyma się i powie, w której linijce to się stało.
---
## 8. Podejmowanie decyzji: if
Program może wybierać, co zrobić. `if` (czyt. „if”, po polsku „jeżeli”) znaczy *„zrób to tylko wtedy, gdy coś jest prawdą”*.
```c
fn main() {
temperatura := 25
if temperatura > 20 {
print("Jest ciepło, załóż koszulkę")
}
}
```
Dodaj `else` („w przeciwnym razie”):
```c
if temperatura > 20 {
print("Jest ciepło")
} else {
print("Weź kurtkę")
}
```
A `else if` dodaje kolejne możliwości. Finch sprawdza je od góry i wykonuje tylko
**pierwszą** prawdziwą:
```c
if temperatura > 25 {
print("Gorąco!")
} else if temperatura > 15 {
print("Przyjemnie")
} else {
print("Zimno")
}
```
### Porównywanie
| Znak | Znaczy |
|------|------------------------------|
| `==` | jest równe |
| `!=` | nie jest równe |
| `<` | jest mniejsze niż |
| `>` | jest większe niż |
| `<=` | jest mniejsze lub równe |
| `>=` | jest większe lub równe |
⚠️ **Jedno `=` wkłada wartość do pudełka. Dwa `==` pytają „czy to jest równe?”.**
Tekst też można porównywać: `if imie == "Ola" { ... }`.
### Łączenie pytań
- `&&` znaczy **i**: oba warunki muszą być prawdziwe. `if wiek > 6 && wiek < 12 { ... }`
- `||` znaczy **lub**: wystarczy, że jeden jest prawdziwy. `if dzien == "sob" || dzien == "nd" { ... }`
- `!` znaczy **nie**: odwraca prawdę i fałsz. `if !pada { ... }`
---
## 9. Powtarzanie: pętle
Komputer nigdy się nie nudzi. **Pętla** powtarza te same kroki wiele razy.
### Liczenie z `for`
```c
fn main() {
for i in 1..6 {
print("Liczba", i)
}
}
```
```
Liczba 1
Liczba 2
Liczba 3
Liczba 4
Liczba 5
```
`for i in 1..6` znaczy: *„licz za pomocą `i` od 1 do 6, ale **bez** 6”*.
Czyli `0..10` daje dziesięć liczb: 0, 1, 2 … 9.
### Powtarzanie, dopóki coś jest prawdą: `while`
```c
fn main() {
licznik := 3
while licznik > 0 {
print(licznik)
licznik -= 1
}
print("Start!")
}
```
```
3
2
1
Start!
```
⚠️ Upewnij się, że pętla może się skończyć. Gdyby `licznik` nigdy nie malał, pętla kręciłaby się
w nieskończoność. Jeśli tak się stanie, naciśnij w Terminalu **Ctrl + C**, żeby ją zatrzymać.
### Wcześniejsze wyjście i pomijanie
- `break` od razu wychodzi z pętli.
- `continue` pomija resztę bieżącego okrążenia i przechodzi do następnego.
```c
for i in 1..100 {
if i == 5 {
break // stop, gdy dojdziemy do 5
}
print(i) // pokaże 1 2 3 4
}
```
---
## 10. Własne polecenia: funkcje
Kiedy robisz to samo w wielu miejscach, nadaj temu nazwę. Taki nazwany kawałek kodu to **funkcja**.
Jedną już znasz: `main`.
```c
fn przywitaj(str imie) {
print("Cześć,", imie)
}
fn main() {
przywitaj("Ola")
przywitaj("Tomek")
}
```
```
Cześć, Ola
Cześć, Tomek
```
`str imie` w nawiasie znaczy: *„ta funkcja potrzebuje jednego tekstu, a w środku nazywamy go `imie`”*.
To, co wpiszesz w nawias przy wywołaniu, zostanie przekazane do środka.
### Funkcje, które oddają odpowiedź
Napisz `->` i typ odpowiedzi, a odpowiedź oddaj słowem `return`:
```c
fn dodaj(int a, int b) -> int {
return a + b
}
fn main() {
wynik := dodaj(2, 3)
print(wynik) // 5
}
```
Nie ma znaczenia, gdzie w pliku umieścisz swoje funkcje, nad czy pod `main`.
---
## 11. Kiedy coś pójdzie nie tak
Każdy się myli przy programowaniu, nawet eksperci, codziennie.
Finch stara się tłumaczyć, co poszło nie tak, prostymi słowami (na razie po angielsku). Przykład:
```c
fn main() {
wiek := 10
print(wiekk)
}
```
```
czesc.fn:3:11: error: there is no variable named 'wiekk'
3 | print(wiekk)
| ^
```
Jak to czytać:
- `czesc.fn` to plik,
- `3` to numer **linijki**,
- `11` to który znak w linijce (**kolumna**),
- potem wyjaśnienie, a pod nim ta linijka ze strzałką `^` wskazującą problem.
Tutaj po prostu źle napisaliśmy `wiek`. Komunikat znaczy: „nie ma zmiennej o nazwie 'wiekk'”.
### Częste błędy
| Finch mówi | Co to znaczy | Jak naprawić |
|---|---|---|
| `there is no variable named 'x'` | Nie ma zmiennej `x`: literówka albo zapomniałeś ją zrobić | Sprawdź pisownię. Utwórz ją przez `x := ...` |
| `a variable named 'x' already exists here` | Użyłeś `:=` dwa razy dla tej samej nazwy | Za drugim razem użyj `=` |
| `'x' must be int, but this is str` | Zły rodzaj wartości dla tego pudełka | Włóż wartość właściwego rodzaju |
| `put each statement on its own line` | Dwa polecenia w jednej linijce | Wciśnij Enter między nimi |
| `this '{' is never closed` | Brakuje `}` | Dopisz `}` |
| `a condition must be bool` | `if` potrzebuje pytania tak/nie | Porównaj coś: `if x > 0` |
| `unexpected character 'ż'` | Polska litera w nazwie | Zmień na zwykłą literę: `zolw` |
| `runtime error: division by zero` | Program podzielił przez 0 w trakcie działania | Sprawdź liczbę przed dzieleniem |
---
## 12. Małe projekty do zrobienia
Spróbuj najpierw napisać każdy sam. Do rozwiązania zajrzyj dopiero, gdy utkniesz.
### Projekt 1: Tabliczka mnożenia
Pokaż tabliczkę mnożenia przez 7, od 7 × 1 do 7 × 10.
<details><summary>Rozwiązanie</summary>
```c
fn main() {
for i in 1..11 {
print("7 x", i, "=", 7 * i)
}
}
```
</details>
### Projekt 2: Parzysta czy nieparzysta
Dla każdej liczby od 1 do 10 napisz, czy jest parzysta, czy nieparzysta.
<details><summary>Rozwiązanie</summary>
```c
fn main() {
for n in 1..11 {
if n % 2 == 0 {
print(n, "jest parzysta")
} else {
print(n, "jest nieparzysta")
}
}
}
```
</details>
### Projekt 3: Stopnie Celsjusza na Fahrenheita
Pokaż tabelkę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`.
<details><summary>Rozwiązanie</summary>
```c
fn na_fahrenheita(float c) -> float {
return c * 9 / 5 + 32
}
fn main() {
for c in 0..11 {
celsjusz := float(c * 10)
print(celsjusz, "C =", na_fahrenheita(celsjusz), "F")
}
}
```
`float(...)` zamienia liczbę całkowitą na liczbę z przecinkiem.
</details>
### Projekt 4: Fizz Buzz
Policz od 1 do 20. Dla liczb podzielnych przez 3 napisz `Fizz`, przez 5 napisz `Buzz`,
przez obie naraz napisz `FizzBuzz`, a w pozostałych przypadkach samą liczbę.
<details><summary>Rozwiązanie</summary>
```c
fn main() {
for n in 1..21 {
if n % 15 == 0 {
print("FizzBuzz")
} else if n % 3 == 0 {
print("Fizz")
} else if n % 5 == 0 {
print("Buzz")
} else {
print(n)
}
}
}
```
</details>
### Projekt 5: Największa z trzech
Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która oddaje największą z trzech liczb.
<details><summary>Rozwiązanie</summary>
```c
fn najwieksza(int a, int b, int c) -> int {
if a >= b && a >= c {
return a
}
if b >= c {
return b
}
return c
}
fn main() {
print(najwieksza(4, 9, 2)) // 9
}
```
</details>
---
## 13. Ściąga
```c
// komentarz
fn main() { // tu zaczyna się program
print("tekst", 42) // pokaż coś
x := 5 // nowe pudełko (typ zgadnięty)
int y = 10 // nowe pudełko (typ podany)
x = 7 // zmień zawartość
x += 1 // dodaj 1 do x
if x > 5 { // decyzja
print("duże")
} else if x > 0 {
print("małe")
} else {
print("zero albo mniej")
}
for i in 0..10 { // licz od 0 do 9
print(i)
}
while x > 0 { // powtarzaj, dopóki prawda
x -= 1
}
}
fn dodaj(int a, int b) -> int { // własne polecenie
return a + b
}
```
---
## 14. Słowniczek
| Słowo | Znaczenie |
|---|---|
| **program** | Lista kroków dla komputera |
| **kod** | Tekst programu |
| **uruchomić** | Kazać komputerowi wykonać kroki |
| **kompilować** | Przetłumaczyć kod na coś, co komputer umie uruchomić. `finch run` robi to za ciebie |
| **zmienna** | Pudełko z nazwą, w którym jest wartość |
| **typ** | Rodzaj wartości: liczba, tekst, tak/nie… |
| **funkcja** | Nazwana grupa kroków, której można używać wiele razy |
| **pętla** | Kroki, które się powtarzają |
| **warunek** | Pytanie tak/nie, np. `x > 5` |
| **błąd** (error) | Komunikat, że coś jest nie tak, i gdzie |
| **bug** | Pomyłka w programie |
### Słówka po angielsku, które spotkasz w Finchu
| Słowo | Po polsku |
|---|---|
| `print` | wypisz |
| `if` / `else` | jeżeli / w przeciwnym razie |
| `for` … `in` | dla … w |
| `while` | dopóki |
| `break` / `continue` | przerwij / kontynuuj |
| `return` | zwróć (oddaj) |
| `true` / `false` | prawda / fałsz |
| `fn` (od *function*) | funkcja |
| `error` | błąd |
---
**Co dalej?** Kiedy to wszystko wyda ci się łatwe, przejdź do
[przewodnika dla techników](dla-technikow.md). Opisuje każdą część Fincha.

542
docs/pl/dla-technikow.md Normal file
View file

@ -0,0 +1,542 @@
# Finch dla techników
**Dla osób, które dobrze znają komputery, ale programować nie umieją albo umieją niewiele.**
Ten przewodnik opisuje instalację Fincha, obsługę polecenia `finch`, cały język,
korzystanie z bibliotek C i rozwiązywanie typowych problemów. Nie opisuje, jak kompilator
działa w środku. Do tego służy [przewodnik dla inżynierów](dla-inzynierow.md).
> 🇬🇧 English version: [for-technicians.md](../en/for-technicians.md)
---
## Spis treści
1. [Czym jest Finch](#1-czym-jest-finch)
2. [Instalacja](#2-instalacja)
3. [Polecenie finch](#3-polecenie-finch)
4. [Jak wygląda plik Fincha](#4-jak-wygląda-plik-fincha)
5. [Zmienne](#5-zmienne)
6. [Typy](#6-typy)
7. [Operatory](#7-operatory)
8. [Sterowanie przebiegiem](#8-sterowanie-przebiegiem)
9. [Funkcje](#9-funkcje)
10. [Wskaźniki](#10-wskaźniki)
11. [Biblioteki z C](#11-biblioteki-z-c)
12. [Błędy](#12-błędy)
13. [Rozwiązywanie problemów](#13-rozwiązywanie-problemów)
14. [Struktura projektu i testy](#14-struktura-projektu-i-testy)
15. [Obecne ograniczenia](#15-obecne-ograniczenia)
---
## 1. Czym jest Finch
Finch to język **kompilowany**. Narzędzie `finch` tłumaczy plik `.fn` na prawdziwy program
(natywny plik wykonywalny, taki sam jak te z C). Korzysta przy tym z **LLVM**, tego samego
zaplecza kompilatorów, na którym działają clang, Rust i Swift.
Co to daje:
- **Szybkość zbliżoną do C.** Nie ma interpretera, maszyny wirtualnej ani garbage collectora.
- **Małe, samodzielne programy**, które można skopiować i uruchomić.
- **Bezpośredni dostęp do bibliotek C.** Piszesz `import "stdio.h"` i wołasz, co chcesz.
Zasady projektu: jeden sposób na każdą rzecz, składnia podobna do C bez jej pułapek
i komunikaty błędów, które mówią, jak problem naprawić.
---
## 2. Instalacja
Finch działa obecnie na **Linuksie x86-64**. Testowany był z **LLVM 21**.
Z innymi wersjami LLVM prawdopodobnie się nie skompiluje, bo API C++ LLVM zmienia się między wydaniami.
### Opcja A: Nix (zalecana, nic nie instalujesz ręcznie)
```sh
git clone https://github.com/Nexoniarz/Finch.git
cd Finch
nix-shell # pobiera LLVM, libclang, cmake, ninja, pkg-config
cmake -S . -B build -G Ninja
ninja -C build
./build/finch version # finch 1.0.0 (LLVM 21.x)
```
### 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` |
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
```
### Dodanie `finch` do PATH (opcjonalnie)
```sh
sudo cp build/finch /usr/local/bin/
finch version
```
Podczas pracy Finch potrzebuje działającego kompilatora C (`cc`). Używa go jako **linkera**,
który skleja skompilowany kod w gotowy program. Inny kompilator wskażesz zmienną środowiskową `CC`.
---
## 3. Polecenie finch
```
finch run <plik.fn> skompiluj i od razu uruchom
finch build <plik.fn> [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku)
finch ir <plik.fn> pokaż LLVM IR (dla ciekawskich)
finch version pokaż wersję
opcje:
-l <lib> dolinkuj bibliotekę C, to samo co link "lib" w pliku
-O0 bez optymalizacji
```
Przykłady:
```sh
finch run hello.fn # wypisuje wynik
finch build gra.fn -o mojagra # tworzy ./mojagra
./mojagra
finch run okno.fn -l glfw # z biblioteką C
```
`finch run` przekazuje dalej kod wyjścia twojego programu. Jeśli program się wysypie,
powie jak (np. `the program crashed: Segmentation fault`).
---
## 4. Jak wygląda plik Fincha
```c
// komentarz zaczyna się od dwóch ukośników
/* albo zajmuje
kilka linijek */
import "math.h" // nagłówki C (opcjonalne), zawsze na górze
link "m" // biblioteki C do dolinkowania (opcjonalne)
fn main() { // tu zaczyna się program
print("Cześć")
}
```
Zasady, które warto znać:
- **Jedna instrukcja na linijkę.** Średników nie ma. Długie wyrażenie możesz złamać
**po** operatorze (`a +⏎ b`) albo w dowolnym miejscu wewnątrz nawiasów `( )`.
- **Klamry są zawsze obowiązkowe** po `if`, `else`, `for`, `while`, a otwierająca `{` stoi w tej samej linijce.
- **Bez nawiasów wokół warunków:** `if x > 5 {`, a nie `if (x > 5) {`.
- Funkcje mogą być w pliku w dowolnej kolejności.
- `main` to `fn main()` albo `fn main() -> int` (zwraca kod wyjścia).
---
## 5. Zmienne
```c
x := 5 // nowa zmienna, typ zgadnięty (int)
int y = 10 // nowa zmienna, typ podany jawnie
int z // bez wartości: startuje od zera ("" dla str, null dla wskaźników)
x = 7 // przypisanie
x += 1 // także -= *= /= %=
```
- Nazwę można zadeklarować **raz** w funkcji. Finch nie pozwala użyć tej samej nazwy ponownie
w bloku wewnętrznym (nie ma „przesłaniania”), bo to częste źródło pomyłek.
- Zmienna istnieje od deklaracji do końca bloku `{ }`, w którym powstała.
- Nazwy: litery ASCII, cyfry i `_`, bez cyfry na początku. Polskie litery tylko w tekstach w cudzysłowie.
Nazwy typów (`int`, `u8`, …) i słowa kluczowe są zarezerwowane.
**Słowa kluczowe:** `fn return if else while for in break continue true false null import link`
---
## 6. Typy
### Typy na co dzień
| Typ | Przechowuje | Przykład |
|---------|-------------|----------|
| `int` | liczba całkowita, 64-bitowa (to samo co `i64`) | `42`, `-7`, `0xFF`, `1_000_000` |
| `float` | liczba z przecinkiem, 64-bitowa (to samo co `f64`) | `3.14` |
| `bool` | `true` albo `false` | |
| `char` | jeden znak wielkości bajtu | `'A'`, `'\n'` |
| `str` | tekst (napis w stylu C) | `"cześć\tświecie"` |
Znaki specjalne w tekstach: `\n` nowa linia, `\t` tabulator, `\r`, `\0`, `\\`, `\"`, `\'`.
### Typy z rozmiarem (do plików, sprzętu, bibliotek C)
| Ze znakiem | Zakres | Bez znaku | Zakres |
|---|---|---|---|
| `i8` | −128 … 127 | `u8` | 0 … 255 |
| `i16` | −32 768 … 32 767 | `u16` | 0 … 65 535 |
| `i32` | ok. ±2,1 mld | `u32` | 0 … ok. 4,29 mld |
| `i64` | ok. ±9,2 × 10¹⁸ | `u64` | 0 … ok. 1,8 × 10¹⁹ |
`f32` to 32-bitowa liczba z przecinkiem (ok. 7 cyfr znaczących), `f64` to 64-bitowa (ok. 15–16).
### Wskaźniki
`ptr[T]` wskazuje na wartość typu `T`. Samo `ptr` wskazuje na „coś” nieznanego typu,
jak `void*` w C. Szczegóły w rozdziale [Wskaźniki](#10-wskaźniki).
### Zamiana typów
Finch zamienia typy sam **tylko wtedy, gdy nic nie może zginąć**:
| Z | Na | Automatycznie? |
|---|---|---|
| mniejsza liczba całkowita | większa o tej samej „znakowości” (`i8`→`i32`, `u8`→`u64`) | ✅ |
| bez znaku | ściśle większa ze znakiem (`u8`→`i16`, `u32`→`i64`) | ✅ |
| dowolna liczba całkowita | `float` / `f32` | ✅ |
| `f32` | `f64` | ✅ |
| `null` | dowolny wskaźnik | ✅ |
| dowolny wskaźnik, `str` | `ptr` | ✅ |
| `ptr` | dowolny `ptr[T]` | ✅ |
| wszystko inne | | ❌ napisz to jawnie: `typ(wartość)` |
**Liczby wpisane w kod się dopasowują.** Liczba napisana wprost w kodzie pasuje do każdego typu,
który ją pomieści: `u8 b = 200` działa, a `u8 b = 300` to błąd (`the number 300 doesn't fit in u8`).
To samo dotyczy stałych z nagłówków C.
**Jawna zamiana** wygląda jak wywołanie funkcji o nazwie typu:
```c
int(3.99) // 3 (obcina część ułamkową)
int(-3.99) // -3
float(7) / 2 // 3.5
u8(300) // 44 (zostawia najniższe 8 bitów)
i8(200) // -56
int('A') // 65
char(66) // 'B'
bool(0) // false (każda liczba różna od zera to true)
ptr(tekst) // str jako wskaźnik bez typu
str(p) // wskaźnik jako str
```
**Liczby o stałym rozmiarze się „zawijają”**, jak w prawdziwym sprzęcie: `u8` równe 255 plus 1 daje 0.
Działania liczą się w typie użytych wartości, więc `u8(250) + 10` daje `4`, a nie `260`.
Jeśli potrzebujesz więcej miejsca, najpierw zamień typ: `int(x) + 10`.
---
## 7. Operatory
### Według priorytetu (wyższy wiąże mocniej)
| Priorytet | Operatory | Znaczenie |
|---|---|---|
| 5 | `*` `/` `%` `<<` `>>` `&` | mnożenie, dzielenie, reszta, przesunięcie bitów w lewo/prawo, bitowe AND |
| 4 | `+` `-` `\|` `^` | dodawanie, odejmowanie, bitowe OR, bitowe XOR |
| 3 | `==` `!=` `<` `<=` `>` `>=` | porównania |
| 2 | `&&` | logiczne I |
| 1 | `\|\|` | logiczne LUB |
Jednoargumentowe (przed wartością): `-x` zmiana znaku, `!x` logiczne NIE, `~x` odwrócenie wszystkich bitów.
> **Celowo inaczej niż w C:** operatory bitowe wiążą mocniej niż porównania, więc
> `flagi & 4 != 0` znaczy `(flagi & 4) != 0`. W C to samo po cichu znaczy co innego.
Uwagi:
- `/` na liczbach całkowitych zaokrągla w stronę zera: `7 / 2` to `3`, `-7 / 2` to `-3`. `%` ma znak lewej strony.
- Dzielenie przez zero **zatrzymuje program** komunikatem `runtime error: division by zero` z numerem linijki.
Jeśli zero jest wpisane wprost w kod, jest to błąd już przy kompilacji.
- `&&` i `||` liczą prawą stronę tylko wtedy, gdy to potrzebne.
- Tekst (`str`) można porównywać przez `==` i `!=` (porównuje zawartość). `<` i `>` na tekstach nie działają.
- Wskaźniki porównuje się przez `==` i `!=`, także z `null`.
- Operatory bitowe wymagają liczb całkowitych. Dla `bool` używaj `&&` i `||`.
- `>>` na liczbach ze znakiem zachowuje znak (`-16 >> 2` to `-4`). Na liczbach bez znaku wypełnia zerami.
---
## 8. Sterowanie przebiegiem
```c
if x > 10 {
...
} else if x > 0 {
...
} else {
...
}
while licznik > 0 {
licznik -= 1
}
for i in 0..10 { // i = 0, 1, …, 9 (koniec nie jest wliczany)
if i == 3 { continue }
if i == 8 { break }
}
```
- Warunek musi być typu `bool`. `if x {` z liczbą to błąd; napisz `if x != 0 {`.
- W `for i in a..b` oba końce są liczbami całkowitymi obliczanymi **raz**, przed startem pętli.
`i` jest typu `int` i nie można go zmieniać wewnątrz pętli.
- `while true { ... }` kręci się do `break` albo `return`.
- Kod po `return`, `break` lub `continue` w tym samym bloku to błąd, bo nigdy się nie wykona.
---
## 9. Funkcje
```c
fn nazwa(typ1 param1, typ2 param2) -> typ_wyniku {
...
return wartosc
}
fn bez_wyniku(str wiadomosc) { // bez "->": nic nie zwraca
print(wiadomosc)
}
```
- Parametry są kopiami. Zmiana parametru nie zmienia zmiennej u wywołującego.
Do tego służą wskaźniki (niżej).
- Funkcja z `-> typ` musi zwrócić wartość na każdej ścieżce. Finch to sprawdza.
- Funkcji nie można definiować wewnątrz innej funkcji.
- Wbudowane nazwy, których nie możesz użyć: `print`, `addr` i wszystkie nazwy typów.
### Funkcje wbudowane
| Funkcja | Co robi |
|---|---|
| `print(a, b, ...)` | Wypisuje wszystkie wartości oddzielone spacjami i przechodzi do nowej linii. Działa z każdym typem. |
| `addr(x)` | Daje wskaźnik na zmienną `x`. |
| `int(x)`, `u8(x)`, `float(x)`, … | Zamienia typy (zobacz [Zamiana typów](#zamiana-typów)). |
---
## 10. Wskaźniki
Wskaźnik przechowuje **adres** wartości, czyli miejsce w pamięci, gdzie ona leży. Finch zamiast
`*` i `&` z C używa słów:
| C | Finch | Znaczenie |
|---|---|---|
| `int *p` | `ptr[int] p` | p wskazuje na int |
| `&x` | `addr(x)` | adres x |
| `*p` | `p.value` | wartość, na którą wskazuje p |
| `NULL` | `null` | wskazuje donikąd |
| `void *` | `ptr` | wskazuje na coś nieznanego typu |
```c
fn podwoj(ptr[int] p) {
p.value = p.value * 2
}
fn main() {
x := 21
podwoj(addr(x))
print(x) // 42
ptr[int] nic = null
if nic == null {
print("pusty")
}
}
```
Wskaźnik może wskazywać na wskaźnik: `ptr[ptr[int]]`, a potem `pp.value.value`.
Użycie `.value` na wskaźniku `null` **zatrzymuje program** komunikatem
`runtime error: used .value on a null pointer`, zamiast losowo go wysypać.
⚠️ Wskaźnika na zmienną nie wolno używać po zakończeniu funkcji, do której ta zmienna należała,
bo zmienna wtedy już nie istnieje. Finch jeszcze tego nie sprawdza.
---
## 11. Biblioteki z C
Prawie każda biblioteka na Linuksie ma interfejs w C: grafika, dźwięk, sieć, bazy danych.
Finch może z nich korzystać bezpośrednio.
### Krok 1: zaimportuj nagłówek
```c
import "stdio.h"
import "math.h"
import "GLFW/glfw3.h" // ścieżka w systemowych katalogach nagłówków
import "mojalib.h" // nagłówek leżący obok twojego pliku .fn
```
Finch czyta nagłówek przez **libclang** (parser C z clanga) i udostępnia:
- **funkcje**, także te ze zmienną liczbą argumentów, jak `printf`,
- **wartości `enum`**,
- **proste stałe `#define`**, które są pojedynczą liczbą, np. `M_PI`, `EOF`, `GL_COLOR_BUFFER_BIT`,
- **zmienne globalne**, np. `stdout` i `stderr`.
### Krok 2: dolinkuj bibliotekę
Nagłówek tylko *opisuje* funkcje. Ich właściwy kod jest w pliku biblioteki (`libNAZWA.so`).
Powiedz Finchowi, której biblioteki użyć, na górze pliku:
```c
link "glfw" // używa libglfw.so
link "GL" // używa libGL.so
```
Wpisz samą nazwę: dla `libglfw.so` to `glfw`. Jeśli `pkg-config` zna tę nazwę, Finch użyje jego
ustawień (ścieżek i dodatkowych bibliotek). W przeciwnym razie przekaże linkerowi `-lNAZWA`.
Biblioteka standardowa C i matematyczna (`libm`) są dolinkowane zawsze.
Na linii poleceń `-l NAZWA` robi to samo co `link "NAZWA"`.
Jeśli zapomnisz linijki `link`, Finch powie dokładnie, czego brakuje:
```
error: glfwInit, glfwCreateWindow, glfwMakeContextCurrent and 5 more come from "GLFW/glfw3.h", but its library isn't linked
the header only says the functions exist; their code lives in a library.
add this at the top of your file (with the library's real name):
link "glfw"
```
### Jak typy C wyglądają w Finchu
| C | Finch |
|---|---|
| `char`, `signed char`, `unsigned char` | `char`, `i8`, `u8` |
| `short`, `int`, `long`, `long long` | `i16`, `i32`, `i64`, `i64` (według faktycznego rozmiaru) |
| `unsigned …` | `u16`, `u32`, `u64` |
| `float`, `double` | `f32`, `f64` |
| `_Bool` / `bool` | `bool` |
| `enum` | odpowiadająca liczba całkowita ze znakiem |
| `char *`, `const char *` | `str` |
| `int *`, `double *`, … | `ptr[i32]`, `ptr[f64]`, … |
| `void *`, `struct X *`, `FILE *`, wskaźniki na funkcje | `ptr` |
Czyli `int` z C to `i32` w Finchu. Liczby wpisane w kod dopasowują się same (`abs(-5)` działa),
ale zmienną typu `int` z Fincha trzeba zamienić: `abs(i32(x))`.
### Przykład
```c
import "stdio.h"
import "math.h"
import "stdlib.h"
fn main() {
printf("%d + %d = %d\n", 2, 3, 5)
print(sqrt(2), M_PI, RAND_MAX)
ptr[i32] n = malloc(4) // poproś C o 4 bajty pamięci
n.value = 7
print(n.value)
free(n) // i oddaj je
fprintf(stderr, "to idzie na wyjście błędów\n")
}
```
Pełny przykład z grafiką jest w `examples/window.fn`: okno GLFW + OpenGL, które zmienia kolor.
`examples/llvm.fn` pokazuje, że Finch potrafi nawet sterować samym LLVM przez jego API w C.
### Co jeszcze nie działa
- Funkcje C, które przyjmują lub zwracają **`struct` przez wartość**, np. `div()`. Finch mówi o tym
wprost, jeśli spróbujesz. Funkcje przyjmujące *wskaźnik* na strukturę działają.
- **Makra-funkcje** (`#define MAX(a,b) ...`) i makra, które nie są pojedynczą liczbą.
- `long double`, liczby 128-bitowe.
- Odczytywanie **pól** struktury C.
---
## 12. Błędy
### Błędy kompilacji
Finch zatrzymuje się na pierwszym problemie i pokazuje, gdzie jest:
```
gra.fn:12:9: error: 'wynik' must be int, but this is str
12 | wynik = "wysoki"
| ^
```
Format: `plik:linijka:kolumna: error: wyjaśnienie`, a pod spodem linijka ze znakiem `^` pod miejscem błędu.
Komunikaty są po angielsku.
### Błędy w trakcie działania
Niektóre problemy widać dopiero, gdy program działa. Zamiast niezdefiniowanego zachowania
(jak w C) Finch zatrzymuje się z czytelnym komunikatem na wyjściu błędów i kodem wyjścia 1:
| Komunikat | Przyczyna |
|---|---|
| `runtime error: division by zero` | `/` albo `%` przez zmienną równą 0 |
| `runtime error: division overflows …` | najmniejsza liczba ze znakiem podzielona przez −1 |
| `runtime error: used .value on a null pointer` | odczyt lub zapis `p.value`, gdy `p` jest `null` |
---
## 13. Rozwiązywanie problemów
| Problem | Rozwiązanie |
|---|---|
| `can't find the C header 'x.h'` | Brakuje plików deweloperskich biblioteki. Zainstaluj je (Debian: `libx-dev`; Nix: dopisz do `shell.nix`). Własny nagłówek połóż obok pliku `.fn`. |
| `… come from "x.h", but its library isn't linked` | Dopisz `link "nazwa"` na górze pliku. |
| `the library 'x' wasn't found` | Biblioteka nie jest zainstalowana albo nazwa jest zła. Na Niksie dopisz ją do `shell.nix` i pracuj wewnątrz `nix-shell`. |
| `the C function 'f' can't be used from Finch yet` | Używa struktury przez wartość albo innego nieobsługiwanego typu. Poszukaj wariantu, który przyjmuje wskaźniki. |
| `no C compiler found to link with` | Zainstaluj gcc lub clang albo ustaw `CC`. |
| Program się kompiluje, ale przy starcie nie znajduje biblioteki C | Został zlinkowany ze ścieżki, której system nie przeszukuje. Uruchamiaj go w tym samym `nix-shell` albo zainstaluj bibliotekę systemowo. |
| `unexpected character 'ż'` | Polska litera w nazwie. W nazwach używaj tylko a–z. |
| CMake: `libclang not found` | Zainstaluj pakiet deweloperski libclang albo uruchom `cmake` w `nix-shell`. |
| Budowanie kończy się błędami API LLVM | Masz inną wersję LLVM niż 21. |
| Program nigdy się nie kończy | Warunek pętli nigdy nie staje się fałszywy. Naciśnij Ctrl+C. |
---
## 14. Struktura projektu i testy
```
Finch/
├── src/ kompilator (C++)
├── examples/ przykładowe programy (hello, tour, types, c_import, window, llvm)
├── tests/
│ ├── run/ programy + dokładny wynik, który muszą wypisać (.out)
│ ├── fail/ programy, które muszą się nie udać, z oczekiwanym błędem w 1. linijce
│ └── run.sh skrypt uruchamiający testy
├── docs/ ta dokumentacja (en, pl)
├── shell.nix środowisko deweloperskie Nix
└── CMakeLists.txt
```
Testy uruchomisz po zbudowaniu:
```sh
tests/run.sh # → 34 passed, 0 failed
```
---
## 15. Obecne ograniczenia
Finch 1.0 to solidny rdzeń, a nie skończony język. Jeszcze nie ma:
- `struct`, tablic, czytania z klawiatury (`input()`), sklejania tekstów (`"a" + "b"`).
Na razie użyj do tego funkcji z C (`scanf`, `snprintf`, `malloc`…).
- Automatycznego zarządzania pamięcią (w planach: zwalnianie na końcu bloku i `defer`).
- Modułów Fincha (na razie jeden program = jeden plik `.fn`).
- Platform innych niż Linux x86-64.
Plan rozwoju jest w [README](../../README.pl.md#plan-rozwoju).

23
examples/c_import.fn Normal file
View file

@ -0,0 +1,23 @@
// Use any C library: import its header and call it.
import "stdio.h"
import "math.h"
import "stdlib.h"
import "string.h"
fn main() {
printf("printf works: %d + %d = %d\n", 2, 3, 2 + 3)
puts("so does puts")
print("sqrt(2) =", sqrt(2))
print("pi from math.h:", M_PI)
print("strlen(\"finch\") =", strlen("finch"))
print("RAND_MAX =", RAND_MAX, "EOF =", EOF)
// memory from C: malloc gives a ptr, free gives it back
ptr[i32] n = malloc(4)
n.value = 7
print("from the heap:", n.value)
free(n)
fprintf(stderr, "this goes to stderr\n")
}

3
examples/hello.fn Normal file
View file

@ -0,0 +1,3 @@
fn main() {
print("Hello, Finch!")
}

18
examples/llvm.fn Normal file
View file

@ -0,0 +1,18 @@
// Finch driving LLVM itself through the LLVM-C API: the first step toward
// a compiler written in Finch. Builds a module with `answer() -> 42` and prints its IR.
import "llvm-c/Core.h"
link "LLVM"
fn main() {
mod := LLVMModuleCreateWithName("from_finch")
i64t := LLVMInt64Type()
fnType := LLVMFunctionType(i64t, null, 0, 0)
f := LLVMAddFunction(mod, "answer", fnType)
entry := LLVMAppendBasicBlock(f, "entry")
b := LLVMCreateBuilder()
LLVMPositionBuilderAtEnd(b, entry)
LLVMBuildRet(b, LLVMConstInt(i64t, 42, 0))
LLVMDumpModule(mod)
LLVMDisposeBuilder(b)
LLVMDisposeModule(mod)
}

62
examples/tour.fn Normal file
View file

@ -0,0 +1,62 @@
// A quick tour of Finch.
fn add(int a, int b) -> int {
return a + b
}
fn fib(int n) -> int {
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
fn isEven(int n) -> bool {
return n % 2 == 0
}
fn greet(str name) {
if name == "Finch" {
print("Hi, it's me!")
} else {
print("Hi,", name)
}
}
fn main() {
// two ways to make a variable
int age = 10
name := "Ola" // type is guessed: str
pi := 3.14 // float
letter := 'F' // char
print(name, "is", age, "years old")
print("2 + 3 =", add(2, 3))
print("pi * 2 =", pi * 2)
print("first letter:", letter)
print("10 is even?", isEven(10))
greet("Finch")
greet(name)
// loops
for i in 0..10 {
if i == 3 {
continue
}
if i == 7 {
break
}
print("i =", i)
}
count := 3
while count > 0 {
print("countdown", count)
count -= 1
}
print("fib(30) =", fib(30))
print("7 / 2 =", 7 / 2, "and as float:", float(7) / 2)
print("int(9.99) =", int(9.99))
}

31
examples/types.fn Normal file
View file

@ -0,0 +1,31 @@
// Sized numbers and pointers.
fn double(ptr[int] p) {
p.value = p.value * 2
}
fn main() {
// everyday: int and float. For bytes and hardware: exact sizes.
u8 small = 255
i32 points = -1000
big := u64(0xFFFF_FFFF)
f32 half = 0.5
small += 1 // u8 wraps around, like real hardware
print("u8 255 + 1 =", small)
print("points:", points, "big:", big, "half:", half)
i64 wide = points // a smaller type fits into a bigger one by itself
print("wide:", wide)
// pointers: addr(x) gives you where x lives, p.value is what is there
int x = 21
p := addr(x)
double(p)
print("x is now", x)
ptr[int] nothing = null
if nothing == null {
print("nothing points nowhere")
}
}

34
examples/window.fn Normal file
View file

@ -0,0 +1,34 @@
// A window that slowly changes color. Needs GLFW and OpenGL
// (on Nix they are in shell.nix: run this inside nix-shell).
import "GLFW/glfw3.h"
import "GL/gl.h"
import "math.h"
link "glfw"
link "GL"
fn main() {
if glfwInit() == 0 {
print("could not start GLFW")
return
}
window := glfwCreateWindow(800, 600, "Finch", null, null)
if window == null {
print("could not open a window")
glfwTerminate()
return
}
glfwMakeContextCurrent(window)
while glfwWindowShouldClose(window) == 0 {
t := glfwGetTime()
glClearColor(f32(sin(t) * 0.5 + 0.5), 0.3, 0.6, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glfwSwapBuffers(window)
glfwPollEvents()
}
glfwDestroyWindow(window)
glfwTerminate()
}

17
shell.nix Normal file
View file

@ -0,0 +1,17 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
packages = with pkgs; [
# building Finch
llvmPackages.llvm
llvmPackages.libclang # reads C headers for `import "stdio.h"`
llvmPackages.clang
cmake
ninja
pkg-config # finds the right flags for `link "..."`
# for examples/window.fn
glfw
libGL
];
}

235
src/ast.h Normal file
View file

@ -0,0 +1,235 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
struct Type {
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null };
Kind kind = Void;
std::shared_ptr<Type> elem; // what a Ptr points to; null for the untyped `ptr`
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;
}
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;
if (kind != Ptr) return true;
if (!elem || !o.elem) return !elem && !o.elem;
return *elem == *o.elem;
}
bool operator!=(const Type &o) const { return !(*this == o); }
std::string name() 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->name() + "]" : "ptr";
case Null: return "null";
}
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;
};
// ---------- expressions ----------
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member };
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)) {}
};
struct CallExpr : Expr {
std::string callee;
std::vector<ExprPtr> args;
CallExpr(Pos p, std::string c) : Expr(ExprKind::Call, p), callee(std::move(c)) {}
};
struct MemberExpr : Expr { // obj.field (for now only p.value)
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)) {}
};
// ---------- statements ----------
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, Return, Break, Continue, Block };
struct Stmt {
StmtKind kind;
Pos pos;
Stmt(StmtKind k, Pos p) : kind(k), pos(p) {}
virtual ~Stmt() = default;
};
using StmtPtr = std::unique_ptr<Stmt>;
struct BlockStmt : Stmt {
std::vector<StmtPtr> body;
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
};
using BlockPtr = std::unique_ptr<BlockStmt>;
struct VarDeclStmt : Stmt {
bool hasType; // `int x = 5` vs `x := 5`
Type type;
std::string name;
ExprPtr init; // may be null only when hasType
VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {}
};
struct AssignStmt : Stmt {
ExprPtr target; // a variable or p.value
char op; // '=', '+', '-', '*', '/', '%'
ExprPtr value;
AssignStmt(Pos p) : Stmt(StmtKind::Assign, p) {}
};
struct ExprStmt : Stmt {
ExprPtr expr;
ExprStmt(Pos p, ExprPtr e) : Stmt(StmtKind::Expr, p), expr(std::move(e)) {}
};
struct IfStmt : Stmt {
ExprPtr cond;
BlockPtr then;
StmtPtr otherwise; // null, BlockStmt, or IfStmt (else if)
IfStmt(Pos p) : Stmt(StmtKind::If, p) {}
};
struct WhileStmt : Stmt {
ExprPtr cond;
BlockPtr body;
WhileStmt(Pos p) : Stmt(StmtKind::While, p) {}
};
struct ForStmt : Stmt {
std::string var;
ExprPtr from, to; // for var in from..to (to is exclusive)
BlockPtr body;
ForStmt(Pos p) : Stmt(StmtKind::For, p) {}
};
struct 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) {}
};
// ---------- top level ----------
struct Param {
Type type;
std::string name;
Pos pos;
};
struct FnDecl {
Pos pos;
std::string name;
Type ret; // Void when there is no `-> type`
std::vector<Param> params;
BlockPtr body;
};
struct Import {
Pos pos;
std::string path; // "stdio.h" for C headers
};
struct Link {
Pos pos;
std::string lib; // "glfw" -> libglfw.so
};
struct Program {
std::vector<Import> imports;
std::vector<Link> links;
std::vector<FnDecl> fns;
};

246
src/cimport.cpp Normal file
View file

@ -0,0 +1,246 @@
#include "cimport.h"
#include "error.h"
#include <clang-c/Index.h>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace {
// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would.
std::vector<std::string> systemIncludeDirs() {
const char *cc = std::getenv("CC");
std::string cmd = std::string(cc ? cc : "cc") + " -E -v -x c /dev/null 2>&1";
std::vector<std::string> dirs;
FILE *p = popen(cmd.c_str(), "r");
if (!p) return dirs;
char line[4096];
bool inList = false;
while (std::fgets(line, sizeof line, p)) {
std::string l = line;
if (l.rfind("#include <...> search starts here:", 0) == 0) { inList = true; continue; }
if (l.rfind("End of search list.", 0) == 0) break;
if (!inList) continue;
size_t a = l.find_first_not_of(' ');
size_t z = l.find_last_not_of(" \n");
if (a == std::string::npos) continue;
std::string d = l.substr(a, z - a + 1);
if (d.size() > 20 && d.compare(d.size() - 20, 20, " (framework directory)") == 0) continue;
dirs.push_back(d);
}
pclose(p);
return dirs;
}
std::string str(CXString s) {
std::string out = clang_getCString(s) ? clang_getCString(s) : "";
clang_disposeString(s);
return out;
}
// C type -> Finch type. Returns false (with a reason) if Finch can't express it yet.
bool mapType(CXType t, Type &out, std::string &why) {
t = clang_getCanonicalType(t);
long long size = clang_Type_getSizeOf(t);
auto sized = [&](bool isSigned) {
switch (size) {
case 1: out = isSigned ? Type::I8 : Type::U8; return true;
case 2: out = isSigned ? Type::I16 : Type::U16; return true;
case 4: out = isSigned ? Type::I32 : Type::U32; return true;
case 8: out = 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: out = Type::Void; return true;
case CXType_Bool: out = Type::Bool; return true;
case CXType_Char_S:
case CXType_Char_U: out = 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: out = Type::F32; return true;
case CXType_Double: out = 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) {
out = Type::Str; // char* is a C string
return true;
}
Type inner;
std::string ignored;
// pointers to structs, functions, void...: an opaque `ptr` you can pass around
if (pointee.kind == CXType_Void || !mapType(pointee, inner, ignored) || inner.kind == Type::Void)
out = Type::Ptr;
else
out = Type::ptrTo(inner);
return true;
}
case CXType_Record: why = "passes a C struct by value (structs are coming)"; return false;
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(CXTranslationUnit tu, CXCursor c, CConst &out) {
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) {
out.type = Type::F64;
out.f = std::strtod(lit.c_str(), &end);
if (neg) out.f = -out.f;
} else {
out.type = Type::I64;
out.i = (long long)std::strtoull(lit.c_str(), &end, 0); // C rules: 0x.., 0755
if (neg) out.i = -out.i;
}
for (; *end; end++) // allow suffixes: 10UL, 1.0f
if (!std::strchr("uUlLfF", *end)) return false;
return true;
}
struct Visit {
CImports *out;
CXTranslationUnit tu;
std::string header;
};
CXChildVisitResult visit(CXCursor c, CXCursor, CXClientData data) {
Visit &v = *static_cast<Visit *>(data);
CImports &out = *v.out;
std::string name = str(clang_getCursorSpelling(c));
switch (clang_getCursorKind(c)) {
case CXCursor_FunctionDecl: {
if (out.fns.count(name)) break;
CFunc f;
f.name = name;
f.header = v.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)) 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)) 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)) out.globals[name] = g;
break;
}
case CXCursor_EnumDecl:
clang_visitChildren(c, visit, data);
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(v.tu, c, k)) out.consts.emplace(name, k);
break;
}
default: break;
}
return CXChildVisit_Continue;
}
} // namespace
CImports importHeaders(const std::vector<Import> &imports, const std::string &dir) {
CImports out;
if (imports.empty()) return out;
std::vector<std::string> args = {"-x", "c", "-std=gnu11", "-I" + dir};
for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d);
std::vector<const char *> argv;
for (const std::string &a : args) argv.push_back(a.c_str());
CXIndex index = clang_createIndex(0, 0);
for (const Import &im : imports) {
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) fail(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)
fail(im.pos.line, im.pos.col, "can't find the C header '" + im.path + "'");
fail(im.pos.line, im.pos.col, "C header '" + im.path + "' has an error: " + msg);
}
clang_disposeDiagnostic(d);
}
Visit v{&out, tu, im.path};
clang_visitChildren(clang_getTranslationUnitCursor(tu), visit, &v);
clang_disposeTranslationUnit(tu);
}
clang_disposeIndex(index);
return out;
}

37
src/cimport.h Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include "ast.h"
#include <string>
#include <unordered_map>
#include <vector>
// What Finch learned from `import "something.h"`.
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 CImports {
std::unordered_map<std::string, CFunc> fns;
std::unordered_map<std::string, CConst> consts;
std::unordered_map<std::string, CGlobal> globals;
};
// `dir` is the folder of the .fn file, searched first for "local.h" headers.
CImports importHeaders(const std::vector<Import> &imports, const std::string &dir);

887
src/codegen.cpp Normal file
View file

@ -0,0 +1,887 @@
#include "codegen.h"
#include "error.h"
#include <llvm/IR/Constants.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/Verifier.h>
#include <llvm/Support/raw_ostream.h>
#include <unordered_map>
using namespace llvm;
using FType = ::Type; // Finch types (llvm::Type is the LLVM one)
namespace {
struct Value_ {
Value *v;
FType type;
bool literal = false; // a number written in the code (or a C constant): adapts to the type around it
};
struct Var {
AllocaInst *slot;
FType type;
bool readonly; // loop variables
};
struct Fn {
Function *llvm;
const FnDecl *decl;
};
struct Loop {
BasicBlock *continueTo, *breakTo;
};
struct Place { // something that can be assigned to: a variable or p.value
Value *addr;
FType type;
};
class Codegen {
public:
Codegen(LLVMContext &c, const CImports &ci, TargetMachine &tm)
: ctx(c), b(c), mod(std::make_unique<Module>("finch", c)), cimports(ci) {
mod->setTargetTriple(tm.getTargetTriple());
mod->setDataLayout(tm.createDataLayout());
}
std::unique_ptr<Module> run(const Program &prog) {
// Declare every function first, so order in the file doesn't matter.
for (const FnDecl &fn : prog.fns) declare(fn);
if (!fns.count("main")) fail(1, 1, "there is no main function (add 'fn main() { ... }')");
for (const FnDecl &fn : prog.fns) define(fn);
std::string err;
raw_string_ostream os(err);
if (verifyModule(*mod, &os)) {
std::fprintf(stderr, "internal compiler error (please report):\n%s\n", err.c_str());
std::exit(2);
}
return std::move(mod);
}
private:
LLVMContext &ctx;
IRBuilder<> b;
std::unique_ptr<Module> mod;
const CImports &cimports;
std::unordered_map<std::string, Fn> fns;
std::vector<std::unordered_map<std::string, Var>> scopes;
std::vector<Loop> loops;
const FnDecl *curFn = nullptr;
// ---------- types ----------
llvm::Type *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::Ptr:
case FType::Null: return b.getPtrTy();
}
return nullptr;
}
Constant *zero(const FType &t) {
if (t.kind == FType::Str) return cast<Constant>(b.CreateGlobalString("", "empty"));
return Constant::getNullValue(ty(t));
}
// Can every value of `from` be stored in `to`? (i8 -> i32 yes, i32 -> u32 no)
static bool widens(const FType &from, const FType &to) {
if (!from.isInt() || !to.isInt()) return false;
if (from.isSigned() && to.isUnsigned()) return false;
if (from.isSigned() == to.isSigned()) return to.bits() > from.bits();
return to.bits() > from.bits(); // unsigned -> bigger signed
}
// Does the constant number `c` (of type `from`) fit into `to`?
static bool 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 *intCast(Value *v, const FType &from, const FType &to) {
return b.CreateIntCast(v, ty(to), from.isSigned());
}
Value *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 *coerce(Value_ v, const FType &want, Pos p, const std::string &what) {
const FType &from = v.type;
if (from == want) return v.v;
if (from.kind == FType::Null && (want.isPtr() || want.kind == FType::Str))
return ConstantPointerNull::get(b.getPtrTy());
// any pointer (or str) fits an untyped `ptr`; an untyped ptr fits any pointer
if (want.isPtr() && !want.elem && (from.isPtr() || from.kind == FType::Str)) return v.v;
if (from.isPtr() && !from.elem && want.isPtr()) return v.v;
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);
if (c) fail(p.line, p.col, "the number " + std::to_string(from.isSigned() ? c->getSExtValue() : (long long)c->getZExtValue()) +
" doesn't fit in " + want.name());
}
if (from.isInt() && want.isFloat()) return intToFloat(v.v, from, want);
if (from.kind == FType::F32 && want.kind == FType::F64) return b.CreateFPExt(v.v, ty(want));
if (from.kind == FType::F64 && want.kind == FType::F32 && v.literal)
return b.CreateFPTrunc(v.v, ty(want));
std::string msg = what + " must be " + want.name() + ", but this is " + from.name();
if (from.isNumber() && want.isNumber())
msg += " (use " + want.name() + "(...) to convert" + (from.isFloat() && want.isInt() ? ", it cuts off the fraction)" : ")");
if (from.kind == FType::Null) msg = what + " can't be null (only pointers can)";
fail(p.line, p.col, msg);
}
// ---------- helpers ----------
bool terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; }
BasicBlock *newBlock(const char *name) {
return BasicBlock::Create(ctx, name, b.GetInsertBlock()->getParent());
}
// Variables live in stack slots created at the top of the function; LLVM turns them into registers.
AllocaInst *slot(const FType &t, const std::string &name) {
Function *f = b.GetInsertBlock()->getParent();
IRBuilder<> entry(&f->getEntryBlock(), f->getEntryBlock().begin());
return entry.CreateAlloca(ty(t), nullptr, name);
}
Var *lookup(const std::string &name) {
for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) {
auto found = it->find(name);
if (found != it->end()) return &found->second;
}
return nullptr;
}
void addVar(Pos p, const std::string &name, const FType &t, Value *init, bool readonly = false) {
if (lookup(name)) fail(p.line, p.col, "a variable named '" + name + "' already exists here");
if (fns.count(name)) fail(p.line, p.col, "'" + name + "' is already the name of a function");
if (isBuiltin(name)) fail(p.line, p.col, "'" + name + "' is a built-in name, pick another one");
AllocaInst *s = slot(t, name);
b.CreateStore(init, s);
scopes.back()[name] = {s, t, readonly};
}
Function *libc(const char *name, llvm::Type *ret, std::vector<llvm::Type *> params, bool vararg = false) {
return cast<Function>(mod->getOrInsertFunction(name, FunctionType::get(ret, params, vararg)).getCallee());
}
// ---------- functions ----------
void declare(const FnDecl &fn) {
if (fns.count(fn.name)) fail(fn.pos.line, fn.pos.col, "function '" + fn.name + "' is defined twice");
if (isBuiltin(fn.name)) fail(fn.pos.line, fn.pos.col, "'" + fn.name + "' is a built-in function, pick another name");
bool isMain = fn.name == "main";
if (isMain && !fn.params.empty()) fail(fn.pos.line, fn.pos.col, "main takes no parameters");
if (isMain && fn.ret.kind != FType::Void && fn.ret.kind != FType::I64 && fn.ret.kind != FType::I32)
fail(fn.pos.line, fn.pos.col, "main returns nothing (fn main()) or an exit code (fn main() -> int)");
std::vector<llvm::Type *> params;
for (const Param &p : fn.params) params.push_back(ty(p.type));
llvm::Type *ret = isMain ? b.getInt32Ty() : ty(fn.ret);
// User functions get a prefix so they never clash with C library names.
Function *f = Function::Create(FunctionType::get(ret, params, false),
isMain ? Function::ExternalLinkage : Function::InternalLinkage,
isMain ? "main" : "finch." + fn.name, mod.get());
fns[fn.name] = {f, &fn};
}
void define(const FnDecl &fn) {
Function *f = fns[fn.name].llvm;
curFn = &fn;
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", f));
scopes.push_back({});
unsigned i = 0;
for (Argument &arg : f->args()) {
const Param &p = fn.params[i++];
arg.setName(p.name);
addVar(p.pos, p.name, p.type, &arg);
}
blockBody(*fn.body);
scopes.pop_back();
if (!terminated()) {
if (fn.name == "main") b.CreateRet(b.getInt32(0));
else if (fn.ret.kind == FType::Void) b.CreateRetVoid();
else fail(fn.pos.line, fn.pos.col, "function '" + fn.name + "' can reach its end without returning " + fn.ret.name());
}
}
// ---------- statements ----------
void block(const BlockStmt &blk) {
scopes.push_back({});
blockBody(blk);
scopes.pop_back();
}
void blockBody(const BlockStmt &blk) {
for (const StmtPtr &s : blk.body) {
if (terminated()) fail(s->pos.line, s->pos.col, "this code can never run (it comes after return, break or continue)");
stmt(*s);
}
}
void stmt(const Stmt &s) {
switch (s.kind) {
case StmtKind::Block: block(static_cast<const BlockStmt &>(s)); break;
case StmtKind::VarDecl: varDecl(static_cast<const VarDeclStmt &>(s)); break;
case StmtKind::Assign: assign(static_cast<const AssignStmt &>(s)); break;
case StmtKind::Expr: expr(*static_cast<const ExprStmt &>(s).expr); break;
case StmtKind::If: ifStmt(static_cast<const IfStmt &>(s)); break;
case StmtKind::While: whileStmt(static_cast<const WhileStmt &>(s)); break;
case StmtKind::For: forStmt(static_cast<const ForStmt &>(s)); break;
case StmtKind::Return: returnStmt(static_cast<const ReturnStmt &>(s)); break;
case StmtKind::Break:
case StmtKind::Continue: {
bool isBreak = s.kind == StmtKind::Break;
if (loops.empty()) fail(s.pos.line, s.pos.col, std::string(isBreak ? "break" : "continue") + " can only be used inside a loop");
b.CreateBr(isBreak ? loops.back().breakTo : loops.back().continueTo);
break;
}
}
}
void varDecl(const VarDeclStmt &s) {
if (!s.hasType) {
Value_ v = expr(*s.init);
if (v.type.kind == FType::Void) fail(s.init->pos.line, s.init->pos.col, "this gives back nothing, so it can't be stored");
if (v.type.kind == FType::Null) fail(s.init->pos.line, s.init->pos.col, "null needs a type, like: ptr[int] " + s.name + " = null");
addVar(s.pos, s.name, v.type, v.v);
return;
}
Value *init = s.init ? coerce(expr(*s.init), s.type, s.init->pos, "'" + s.name + "'") : zero(s.type);
addVar(s.pos, s.name, s.type, init);
}
// Where does `x` or `p.value` live in memory?
Place place(const Expr &e) {
if (e.kind == ExprKind::Var) {
auto &v = static_cast<const VarExpr &>(e);
Var *var = lookup(v.name);
if (!var) {
if (cimports.globals.count(v.name)) return {cGlobal(v.name), cimports.globals.at(v.name).type};
fail(e.pos.line, e.pos.col, "there is no variable named '" + v.name + "'");
}
return {var->slot, var->type};
}
if (e.kind == ExprKind::Member) {
auto &m = static_cast<const MemberExpr &>(e);
Value_ obj = expr(*m.obj);
if (!obj.type.isPtr())
fail(m.pos.line, m.pos.col, obj.type.name() + " has no '." + m.field + "'");
if (m.field != "value")
fail(m.pos.line, m.pos.col, "a pointer only has '.value' (the thing it points to)");
if (!obj.type.elem)
fail(m.pos.line, m.pos.col, "this ptr has no type, so Finch doesn't know what's inside (use ptr[int] etc.)");
// reading or writing through null is undefined in C; in Finch it stops the program
if (!isa<AllocaInst>(obj.v) && !isa<GlobalValue>(obj.v))
panicIf(b.CreateIsNull(obj.v), "used .value on a null pointer", m.pos);
return {obj.v, *obj.type.elem};
}
fail(e.pos.line, e.pos.col, "this can't be assigned to");
}
void assign(const AssignStmt &s) {
if (s.target->kind == ExprKind::Var) {
auto &name = static_cast<const VarExpr &>(*s.target).name;
Var *var = lookup(name);
if (!var && !cimports.globals.count(name))
fail(s.pos.line, s.pos.col, "there is no variable named '" + name + "' (create it with " + name + " := ...)");
if (var && var->readonly) fail(s.pos.line, s.pos.col, "the loop variable '" + name + "' can't be changed");
}
Place dst = place(*s.target);
std::string what = s.target->kind == ExprKind::Var ? "'" + static_cast<const VarExpr &>(*s.target).name + "'" : "the value";
Value *v;
if (s.op == '=') {
v = coerce(expr(*s.value), dst.type, s.value->pos, what);
} else {
BinOp op = s.op == '+' ? BinOp::Add : s.op == '-' ? BinOp::Sub : s.op == '*' ? BinOp::Mul
: s.op == '/' ? BinOp::Div : BinOp::Mod;
Value_ cur{b.CreateLoad(ty(dst.type), dst.addr), dst.type};
v = coerce(arith(op, cur, expr(*s.value), s.pos), dst.type, s.pos, what);
}
b.CreateStore(v, dst.addr);
}
Value *condition(const Expr &e) { return coerce(expr(e), FType::Bool, e.pos, "a condition"); }
void ifStmt(const IfStmt &s) {
Value *cond = condition(*s.cond);
BasicBlock *thenBB = newBlock("then");
BasicBlock *elseBB = s.otherwise ? newBlock("else") : nullptr;
BasicBlock *end = newBlock("endif");
b.CreateCondBr(cond, thenBB, elseBB ? elseBB : end);
b.SetInsertPoint(thenBB);
block(*s.then);
BasicBlock *last = b.GetInsertBlock();
if (!terminated()) b.CreateBr(end);
if (elseBB) {
b.SetInsertPoint(elseBB);
stmt(*s.otherwise);
last = b.GetInsertBlock();
if (!terminated()) b.CreateBr(end);
}
continueAt(end, last);
}
// Continue emitting into `bb`; if nothing jumps there, drop it and stay "after a return".
void continueAt(BasicBlock *bb, BasicBlock *deadEnd) {
if (pred_empty(bb)) {
bb->eraseFromParent();
b.SetInsertPoint(deadEnd);
} else {
b.SetInsertPoint(bb);
}
}
void whileStmt(const WhileStmt &s) {
BasicBlock *condBB = newBlock("while.cond");
BasicBlock *body = newBlock("while.body");
BasicBlock *end = newBlock("while.end");
b.CreateBr(condBB);
b.SetInsertPoint(condBB);
Value *cond = condition(*s.cond);
if (auto *c = dyn_cast<ConstantInt>(cond); c && c->isOne()) b.CreateBr(body); // while true
else b.CreateCondBr(cond, body, end);
b.SetInsertPoint(body);
loops.push_back({condBB, end});
block(*s.body);
loops.pop_back();
if (!terminated()) b.CreateBr(condBB);
continueAt(end, b.GetInsertBlock());
}
void forStmt(const ForStmt &s) {
Value *from = coerce(expr(*s.from), FType::I64, s.from->pos, "the start of a range");
Value *to = coerce(expr(*s.to), FType::I64, s.to->pos, "the end of a range");
scopes.push_back({});
addVar(s.pos, s.var, FType::I64, from, true);
AllocaInst *i = scopes.back()[s.var].slot;
BasicBlock *condBB = newBlock("for.cond");
BasicBlock *body = newBlock("for.body");
BasicBlock *step = newBlock("for.step");
BasicBlock *end = newBlock("for.end");
b.CreateBr(condBB);
b.SetInsertPoint(condBB);
b.CreateCondBr(b.CreateICmpSLT(b.CreateLoad(b.getInt64Ty(), i), to), body, end);
b.SetInsertPoint(body);
loops.push_back({step, end});
block(*s.body);
loops.pop_back();
if (!terminated()) b.CreateBr(step);
b.SetInsertPoint(step);
b.CreateStore(b.CreateAdd(b.CreateLoad(b.getInt64Ty(), i), b.getInt64(1)), i);
b.CreateBr(condBB);
scopes.pop_back();
b.SetInsertPoint(end);
}
void returnStmt(const ReturnStmt &s) {
bool isMain = curFn->name == "main";
if (curFn->ret.kind == FType::Void) {
if (s.value) fail(s.value->pos.line, s.value->pos.col, "'" + curFn->name + "' doesn't return a value (it has no '-> type')");
if (isMain) b.CreateRet(b.getInt32(0));
else b.CreateRetVoid();
return;
}
if (!s.value) fail(s.pos.line, s.pos.col, "'" + curFn->name + "' must return " + curFn->ret.name());
Value *v = coerce(expr(*s.value), curFn->ret, s.value->pos, "the returned value");
b.CreateRet(isMain ? b.CreateIntCast(v, b.getInt32Ty(), true) : v);
}
// ---------- expressions ----------
Value_ expr(const Expr &e) {
switch (e.kind) {
case ExprKind::Int: return {b.getInt64(static_cast<const IntExpr &>(e).value), FType::I64, true};
case ExprKind::Float: return {ConstantFP::get(b.getDoubleTy(), static_cast<const FloatExpr &>(e).value), FType::F64, true};
case ExprKind::Bool: return {b.getInt1(static_cast<const BoolExpr &>(e).value), FType::Bool};
case ExprKind::Char: return {b.getInt8(static_cast<const CharExpr &>(e).value), FType::Char};
case ExprKind::Str: return {b.CreateGlobalString(static_cast<const StrExpr &>(e).value, "str"), FType::Str};
case ExprKind::Null: return {ConstantPointerNull::get(b.getPtrTy()), FType::Null};
case ExprKind::Var: {
auto &v = static_cast<const VarExpr &>(e);
if (!lookup(v.name)) {
auto c = cimports.consts.find(v.name);
if (c != cimports.consts.end()) {
if (c->second.type.isFloat()) return {ConstantFP::get(b.getDoubleTy(), c->second.f), FType::F64, true};
return {b.getInt64(c->second.i), FType::I64, true};
}
}
Place pl = place(e);
return {b.CreateLoad(ty(pl.type), pl.addr, v.name), pl.type};
}
case ExprKind::Member: {
Place pl = place(e);
return {b.CreateLoad(ty(pl.type), pl.addr), pl.type};
}
case ExprKind::Unary: {
auto &u = static_cast<const UnaryExpr &>(e);
Value_ v = expr(*u.operand);
if (u.op == '!') return {b.CreateNot(coerce(v, FType::Bool, u.operand->pos, "the value after '!'")), FType::Bool};
if (u.op == '~') {
if (!v.type.isInt()) fail(e.pos.line, e.pos.col, "'~' flips the bits of a whole number, not of " + v.type.name());
return {b.CreateNot(v.v), v.type};
}
if (v.type.isSigned()) return {b.CreateNeg(v.v), v.type, v.literal};
if (v.type.isFloat()) return {b.CreateFNeg(v.v), v.type, v.literal};
if (v.type.isUnsigned()) fail(e.pos.line, e.pos.col, v.type.name() + " can't be negative (it's unsigned)");
fail(e.pos.line, e.pos.col, "can't put '-' in front of " + v.type.name());
}
case ExprKind::Binary: return binary(static_cast<const BinaryExpr &>(e));
case ExprKind::Call: return call(static_cast<const CallExpr &>(e));
}
return {nullptr, FType::Void};
}
static const char *opName(BinOp op) {
switch (op) {
case BinOp::Add: return "+";
case BinOp::Sub: return "-";
case BinOp::Mul: return "*";
case BinOp::Div: return "/";
case BinOp::Mod: return "%";
case BinOp::BitAnd: return "&";
case BinOp::BitOr: return "|";
case BinOp::BitXor: return "^";
case BinOp::Shl: return "<<";
case BinOp::Shr: return ">>";
case BinOp::Eq: return "==";
case BinOp::NotEq: return "!=";
case BinOp::Less: return "<";
case BinOp::LessEq: return "<=";
case BinOp::Greater: return ">";
case BinOp::GreaterEq: return ">=";
case BinOp::And: return "&&";
case BinOp::Or: return "||";
}
return "?";
}
[[noreturn]] void badOperands(BinOp op, const Value_ &l, const Value_ &r, Pos p) {
std::string msg = std::string("can't use '") + opName(op) + "' on " + l.type.name() +
(l.type == r.type ? "" : " and " + r.type.name());
if (l.type.isInt() && r.type.isInt() && l.type != r.type) msg += " (convert one side, like " + l.type.name() + "(...))";
if (l.type.kind == FType::Bool && (op == BinOp::BitAnd || op == BinOp::BitOr))
msg += std::string(" (for true/false use '") + (op == BinOp::BitAnd ? "&&" : "||") + "')";
else if ((l.type.isFloat() || r.type.isFloat()) && op >= BinOp::BitAnd && op <= BinOp::Shr)
msg += " (bit operators work on whole numbers only)";
fail(p.line, p.col, msg);
}
// Bring both sides to one type: a number literal adapts to the other side,
// otherwise the smaller type grows into the bigger one.
bool unify(Value_ &l, Value_ &r) {
if (l.type == r.type) return true;
if (l.type.isInt() && r.type.isInt()) {
auto *lc = l.literal ? dyn_cast<ConstantInt>(l.v) : nullptr;
auto *rc = r.literal ? dyn_cast<ConstantInt>(r.v) : nullptr;
if (lc && !rc && fits(lc, l.type, r.type)) { l = {intCast(l.v, l.type, r.type), r.type, true}; return true; }
if (rc && !lc && fits(rc, r.type, l.type)) { r = {intCast(r.v, r.type, l.type), l.type, true}; return true; }
if (widens(l.type, r.type)) { l = {intCast(l.v, l.type, r.type), r.type}; return true; }
if (widens(r.type, l.type)) { r = {intCast(r.v, r.type, l.type), l.type}; return true; }
return false;
}
if (l.type.isInt() && r.type.isFloat()) { l = {intToFloat(l.v, l.type, r.type), r.type, l.literal}; return true; }
if (l.type.isFloat() && r.type.isInt()) { r = {intToFloat(r.v, r.type, l.type), l.type, r.literal}; return true; }
if (l.type.isFloat() && r.type.isFloat()) {
if (l.type.kind == FType::F32) l = {b.CreateFPExt(l.v, b.getDoubleTy()), FType::F64};
if (r.type.kind == FType::F32) r = {b.CreateFPExt(r.v, b.getDoubleTy()), FType::F64};
return true;
}
return false;
}
Value_ arith(BinOp op, Value_ l, Value_ r, Pos p) {
Value_ v = arithOp(op, l, r, p);
v.literal = l.literal && r.literal && isa<Constant>(v.v);
return v;
}
Value_ arithOp(BinOp op, Value_ l, Value_ r, Pos p) {
if (!unify(l, r)) badOperands(op, l, r, p);
if (l.type.isInt()) {
bool s = l.type.isSigned();
switch (op) {
case BinOp::Add: return {b.CreateAdd(l.v, r.v), l.type};
case BinOp::Sub: return {b.CreateSub(l.v, r.v), l.type};
case BinOp::Mul: return {b.CreateMul(l.v, r.v), l.type};
case BinOp::Div:
checkDivisor(l, r, p);
return {s ? b.CreateSDiv(l.v, r.v) : b.CreateUDiv(l.v, r.v), l.type};
case BinOp::Mod:
checkDivisor(l, r, p);
return {s ? b.CreateSRem(l.v, r.v) : b.CreateURem(l.v, r.v), l.type};
case BinOp::BitAnd: return {b.CreateAnd(l.v, r.v), l.type};
case BinOp::BitOr: return {b.CreateOr(l.v, r.v), l.type};
case BinOp::BitXor: return {b.CreateXor(l.v, r.v), l.type};
case BinOp::Shl: return {b.CreateShl(l.v, r.v), l.type};
case BinOp::Shr: return {s ? b.CreateAShr(l.v, r.v) : b.CreateLShr(l.v, r.v), l.type};
default: break;
}
}
if (l.type.isFloat()) {
switch (op) {
case BinOp::Add: return {b.CreateFAdd(l.v, r.v), l.type};
case BinOp::Sub: return {b.CreateFSub(l.v, r.v), l.type};
case BinOp::Mul: return {b.CreateFMul(l.v, r.v), l.type};
case BinOp::Div: return {b.CreateFDiv(l.v, r.v), l.type};
case BinOp::Mod: return {b.CreateFRem(l.v, r.v), l.type};
default: break;
}
}
badOperands(op, l, r, p);
}
// ---------- runtime errors ----------
// finch.panic(msg, line): print "file:line: runtime error: msg" and stop the program.
Function *panicFn() {
if (Function *f = mod->getFunction("finch.panic")) return f;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getInt64Ty()}, false),
Function::InternalLinkage, "finch.panic", mod.get());
f->addFnAttr(Attribute::NoReturn);
f->addFnAttr(Attribute::Cold);
f->addFnAttr(Attribute::NoInline);
IRBuilder<> pb(BasicBlock::Create(ctx, "entry", f));
Function *dprintf = libc("dprintf", b.getInt32Ty(), {b.getInt32Ty(), b.getPtrTy()}, true);
pb.CreateCall(dprintf, {pb.getInt32(2), pb.CreateGlobalString("%s:%lld: runtime error: %s\n", "panic.fmt"),
pb.CreateGlobalString(g_file, "panic.file"), f->getArg(1), f->getArg(0)});
pb.CreateCall(libc("exit", b.getVoidTy(), {b.getInt32Ty()}), {pb.getInt32(1)});
pb.CreateUnreachable();
return f;
}
// if (cond) panic(msg)
void panicIf(Value *cond, const char *msg, Pos p) {
BasicBlock *bad = newBlock("panic");
BasicBlock *ok = newBlock("ok");
b.CreateCondBr(cond, bad, ok);
b.SetInsertPoint(bad);
b.CreateCall(panicFn(), {b.CreateGlobalString(msg, "panic.msg"), b.getInt64(p.line)});
b.CreateUnreachable();
b.SetInsertPoint(ok);
}
// Dividing by zero is undefined in C; in Finch it is a clear error.
void checkDivisor(const Value_ &l, const Value_ &r, Pos p) {
auto *c = dyn_cast<ConstantInt>(r.v);
if (c && c->isZero()) fail(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_ compare(BinOp op, Value_ l, Value_ r, Pos p) {
bool equality = op == BinOp::Eq || op == BinOp::NotEq;
auto pointerLike = [](const FType &t) { return t.isPtr() || t.kind == FType::Str || t.kind == FType::Null; };
// Pointers (and `s == null`) compare addresses.
if (pointerLike(l.type) && pointerLike(r.type) &&
(l.type.isPtr() || r.type.isPtr() || l.type.kind == FType::Null || r.type.kind == FType::Null)) {
if (!equality) badOperands(op, l, r, p);
bool bothPtr = (l.type.isPtr() || l.type.kind == FType::Null) && (r.type.isPtr() || r.type.kind == FType::Null);
bool strNull = (l.type.kind == FType::Str && r.type.kind == FType::Null) || (r.type.kind == FType::Str && l.type.kind == FType::Null);
if (!bothPtr && !strNull) badOperands(op, l, r, p);
return {op == BinOp::Eq ? b.CreateICmpEQ(l.v, r.v) : b.CreateICmpNE(l.v, r.v), FType::Bool};
}
if (!unify(l, r)) badOperands(op, l, r, p);
if (l.type.kind == FType::Str) {
if (!equality) badOperands(op, l, r, p);
Function *strcmp = libc("strcmp", b.getInt32Ty(), {b.getPtrTy(), b.getPtrTy()});
Value *c = b.CreateCall(strcmp, {l.v, r.v});
return {op == BinOp::Eq ? b.CreateICmpEQ(c, b.getInt32(0)) : b.CreateICmpNE(c, b.getInt32(0)), FType::Bool};
}
if (l.type.isFloat()) {
CmpInst::Predicate pr;
switch (op) {
case BinOp::Eq: pr = CmpInst::FCMP_OEQ; break;
case BinOp::NotEq: pr = CmpInst::FCMP_UNE; break;
case BinOp::Less: pr = CmpInst::FCMP_OLT; break;
case BinOp::LessEq: pr = CmpInst::FCMP_OLE; break;
case BinOp::Greater: pr = CmpInst::FCMP_OGT; break;
default: pr = CmpInst::FCMP_OGE; break;
}
return {b.CreateFCmp(pr, l.v, r.v), FType::Bool};
}
if (l.type.kind == FType::Bool && !equality) badOperands(op, l, r, p);
bool s = l.type.isSigned();
CmpInst::Predicate pr;
switch (op) {
case BinOp::Eq: pr = CmpInst::ICMP_EQ; break;
case BinOp::NotEq: pr = CmpInst::ICMP_NE; break;
case BinOp::Less: pr = s ? CmpInst::ICMP_SLT : CmpInst::ICMP_ULT; break;
case BinOp::LessEq: pr = s ? CmpInst::ICMP_SLE : CmpInst::ICMP_ULE; break;
case BinOp::Greater: pr = s ? CmpInst::ICMP_SGT : CmpInst::ICMP_UGT; break;
default: pr = s ? CmpInst::ICMP_SGE : CmpInst::ICMP_UGE; break;
}
return {b.CreateICmp(pr, l.v, r.v), FType::Bool};
}
// && and || only evaluate the right side when needed.
Value_ logic(const BinaryExpr &e) {
bool isAnd = e.op == BinOp::And;
Value *l = condition(*e.lhs);
BasicBlock *from = b.GetInsertBlock();
BasicBlock *rhsBB = newBlock(isAnd ? "and.rhs" : "or.rhs");
BasicBlock *end = newBlock(isAnd ? "and.end" : "or.end");
if (isAnd) b.CreateCondBr(l, rhsBB, end);
else b.CreateCondBr(l, end, rhsBB);
b.SetInsertPoint(rhsBB);
Value *r = condition(*e.rhs);
BasicBlock *rhsEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(end);
PHINode *phi = b.CreatePHI(b.getInt1Ty(), 2);
phi->addIncoming(b.getInt1(!isAnd), from);
phi->addIncoming(r, rhsEnd);
return {phi, FType::Bool};
}
Value_ binary(const BinaryExpr &e) {
switch (e.op) {
case BinOp::And:
case BinOp::Or: return logic(e);
case BinOp::Eq:
case BinOp::NotEq:
case BinOp::Less:
case BinOp::LessEq:
case BinOp::Greater:
case BinOp::GreaterEq: {
Value_ l = expr(*e.lhs);
return compare(e.op, l, expr(*e.rhs), e.pos);
}
default: {
Value_ l = expr(*e.lhs);
return arith(e.op, l, expr(*e.rhs), e.pos);
}
}
}
// ---------- calls & built-ins ----------
static bool isBuiltin(const std::string &n) {
FType t;
return n == "print" || n == "addr" || typeFromName(n, t);
}
Value_ call(const CallExpr &c) {
if (c.callee == "print") return print(c);
if (c.callee == "addr") return addrOf(c);
FType target;
if (typeFromName(c.callee, target)) return convert(c, target);
auto it = fns.find(c.callee);
if (it != fns.end()) return callFinch(c, it->second);
auto cf = cimports.fns.find(c.callee);
if (cf != cimports.fns.end()) return callC(c, cf->second);
if (lookup(c.callee)) fail(c.pos.line, c.pos.col, "'" + c.callee + "' is a variable, not a function");
fail(c.pos.line, c.pos.col, "there is no function named '" + c.callee + "'" + headerHint(c.callee));
}
// Help people who forgot an import.
static std::string 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"}, {"free", "stdlib.h"}, {"rand", "stdlib.h"}, {"exit", "stdlib.h"},
{"abs", "stdlib.h"}, {"atoi", "stdlib.h"}, {"strlen", "string.h"}, {"strcpy", "string.h"},
{"memcpy", "string.h"}, {"time", "time.h"}, {"sleep", "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 checkArgCount(const CallExpr &c, size_t want, bool variadic) {
if (c.args.size() == want || (variadic && c.args.size() > want)) return;
fail(c.pos.line, c.pos.col, "'" + c.callee + "' takes " + (variadic ? "at least " : "") + std::to_string(want) +
" argument(s), but got " + std::to_string(c.args.size()));
}
Value_ callFinch(const CallExpr &c, const Fn &fn) {
const FnDecl &d = *fn.decl;
if (d.name == "main") fail(c.pos.line, c.pos.col, "main can't be called, it is where the program starts");
checkArgCount(c, d.params.size(), false);
std::vector<Value *> args;
for (size_t i = 0; i < c.args.size(); i++)
args.push_back(coerce(expr(*c.args[i]), d.params[i].type, c.args[i]->pos, "argument '" + d.params[i].name + "'"));
return {b.CreateCall(fn.llvm, args), d.ret};
}
// C wants small integers widened by the caller: signed ones sign-extended, the rest zero-extended.
static Attribute::AttrKind extAttr(const FType &t) {
if (t.bits() >= 32 || !(t.isInt() || t.kind == FType::Bool || t.kind == FType::Char)) return Attribute::None;
return t.isSigned() || t.kind == FType::Char ? Attribute::SExt : Attribute::ZExt;
}
Value_ callC(const CallExpr &c, const CFunc &f) {
if (!f.unsupported.empty())
fail(c.pos.line, c.pos.col, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported);
checkArgCount(c, f.params.size(), f.variadic);
std::vector<llvm::Type *> ptypes;
for (const FType &t : f.params) ptypes.push_back(ty(t));
Function *fn = libc(f.name.c_str(), ty(f.ret), ptypes, f.variadic);
std::vector<Value *> args;
for (size_t i = 0; i < c.args.size(); i++) {
Value_ v = expr(*c.args[i]);
if (i < f.params.size()) {
args.push_back(coerce(v, f.params[i], c.args[i]->pos, "argument " + std::to_string(i + 1) + " of " + f.name));
continue;
}
// extra arguments of printf-like functions follow C's promotion rules
const FType &t = v.type;
if (t.kind == FType::Void) fail(c.args[i]->pos.line, c.args[i]->pos.col, "this gives back nothing");
if (t.kind == FType::F32) v.v = b.CreateFPExt(v.v, b.getDoubleTy());
else if (t.kind == FType::Bool || ((t.isInt() || t.kind == FType::Char) && t.bits() < 32))
v.v = b.CreateIntCast(v.v, b.getInt32Ty(), t.isSigned() || t.kind == FType::Char);
args.push_back(v.v);
}
CallInst *call = b.CreateCall(fn, args);
for (size_t i = 0; i < f.params.size(); i++) {
Attribute::AttrKind a = extAttr(f.params[i]);
if (a != Attribute::None) {
fn->addParamAttr(i, a);
call->addParamAttr(i, a);
}
}
if (Attribute::AttrKind a = extAttr(f.ret); a != Attribute::None) {
fn->addRetAttr(a);
call->addRetAttr(a);
}
return {call, f.ret};
}
Value *cGlobal(const std::string &name) {
const CGlobal &g = cimports.globals.at(name);
if (GlobalVariable *gv = mod->getNamedGlobal(name)) return gv;
return new GlobalVariable(*mod, ty(g.type), false, GlobalValue::ExternalLinkage, nullptr, name);
}
// addr(x) gives a pointer to x
Value_ addrOf(const CallExpr &c) {
if (c.args.size() != 1) fail(c.pos.line, c.pos.col, "addr(...) takes exactly one variable");
const Expr &e = *c.args[0];
if (e.kind != ExprKind::Var && e.kind != ExprKind::Member)
fail(e.pos.line, e.pos.col, "addr(...) needs a variable, like addr(x)");
if (e.kind == ExprKind::Var) {
Var *v = lookup(static_cast<const VarExpr &>(e).name);
if (v && v->readonly) fail(e.pos.line, e.pos.col, "can't take the address of a loop variable");
}
Place pl = place(e);
return {pl.addr, FType::ptrTo(pl.type)};
}
// print(a, b, c) prints the values separated by spaces, then a newline.
Value_ print(const CallExpr &c) {
std::string fmt;
std::vector<Value *> args;
for (size_t i = 0; i < c.args.size(); i++) {
if (i) fmt += ' ';
Value_ v = expr(*c.args[i]);
const FType &t = v.type;
if (t.isSigned()) { fmt += "%lld"; args.push_back(b.CreateSExt(v.v, b.getInt64Ty())); }
else if (t.isUnsigned()) { fmt += "%llu"; args.push_back(b.CreateZExt(v.v, b.getInt64Ty())); }
else if (t.isFloat()) { fmt += "%g"; args.push_back(b.CreateFPExt(v.v, b.getDoubleTy())); }
else if (t.kind == FType::Char) { fmt += "%c"; args.push_back(b.CreateZExt(v.v, b.getInt32Ty())); }
else if (t.kind == FType::Str) { fmt += "%s"; args.push_back(v.v); }
else if (t.isPtr()) { fmt += "%p"; args.push_back(v.v); }
else if (t.kind == FType::Null) fmt += "null";
else if (t.kind == FType::Bool) {
fmt += "%s";
args.push_back(b.CreateSelect(v.v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false")));
} else fail(c.args[i]->pos.line, c.args[i]->pos.col, "this gives back nothing, so there is nothing to print");
}
fmt += '\n';
args.insert(args.begin(), b.CreateGlobalString(fmt, "fmt"));
b.CreateCall(libc("printf", b.getInt32Ty(), {b.getPtrTy()}, true), args);
return {nullptr, FType::Void};
}
// Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), ptr(x) ...
Value_ convert(const CallExpr &c, const FType &to) {
if (c.args.size() != 1) fail(c.pos.line, c.pos.col, c.callee + "(...) takes exactly one value");
Value_ v = expr(*c.args[0]);
const FType &from = v.type;
Pos p = c.args[0]->pos;
if (from == to) return v;
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::Str || from.kind == FType::Null)) return {v.v, to};
if (to.kind == FType::Str && from.isPtr()) return {v.v, to};
fail(p.line, p.col, "can't turn " + from.name() + " into " + to.name());
}
};
} // namespace
std::unique_ptr<Module> generate(const Program &prog, const CImports &c, LLVMContext &ctx, TargetMachine &tm) {
return Codegen(ctx, c, tm).run(prog);
}

14
src/codegen.h Normal file
View file

@ -0,0 +1,14 @@
#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>
// `tm` describes the machine we compile for (sizes, alignment), which shapes the IR.
std::unique_ptr<llvm::Module> generate(const Program &prog, const CImports &c, llvm::LLVMContext &ctx,
llvm::TargetMachine &tm);

31
src/error.h Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <string>
// Finch stops at the first error and shows exactly where it is.
inline std::string g_file;
inline std::string g_source;
[[noreturn]] inline void fail(int line, int col, const std::string &msg) {
std::fprintf(stderr, "%s:%d:%d: error: %s\n", g_file.c_str(), line, col, msg.c_str());
size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) {
start = g_source.find('\n', start);
if (start != std::string::npos) start++;
}
if (start != std::string::npos && start < g_source.size()) {
std::string text = g_source.substr(start, g_source.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);
}

211
src/lexer.cpp Normal file
View file

@ -0,0 +1,211 @@
#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},
};
std::vector<Token> lex(const std::string &src, const std::string &file) {
g_file = file;
g_source = src;
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 ':':
if (n != '=') fail(line, col, "unexpected ':' (did you mean ':='?)");
two(Tok::Declare);
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::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 "?";
}

34
src/lexer.h Normal file
View file

@ -0,0 +1,34 @@
#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,
// punctuation
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, 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
};
std::vector<Token> lex(const std::string &src, const std::string &file);
const char *tokName(Tok t);

258
src/main.cpp Normal file
View file

@ -0,0 +1,258 @@
#include "cimport.h"
#include "codegen.h"
#include "lexer.h"
#include "parser.h"
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Passes/PassBuilder.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Config/llvm-config.h>
#include <llvm/Target/TargetMachine.h>
#include <llvm/TargetParser/Host.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <sys/wait.h>
#include <string>
#include <vector>
using namespace llvm;
static void usage() {
std::fprintf(stderr,
"Finch " FINCH_VERSION " - small, quick, sharp.\n\n"
"usage:\n"
" finch run <file.fn> compile and run\n"
" finch build <file.fn> [-o name] compile to a program\n"
"\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n"
" -O0 skip optimizations (to read the raw IR)\n"
" finch ir <file.fn> show the generated LLVM IR\n"
" finch version show the version\n");
std::exit(1);
}
static std::string readFile(const std::string &path) {
std::ifstream in(path);
if (!in) {
std::fprintf(stderr, "error: can't open '%s'\n", path.c_str());
std::exit(1);
}
std::stringstream ss;
ss << in.rdbuf();
return ss.str();
}
static void optimize(Module &mod, TargetMachine *tm) {
LoopAnalysisManager lam;
FunctionAnalysisManager fam;
CGSCCAnalysisManager cgam;
ModuleAnalysisManager mam;
PassBuilder pb(tm);
pb.registerModuleAnalyses(mam);
pb.registerCGSCCAnalyses(cgam);
pb.registerFunctionAnalyses(fam);
pb.registerLoopAnalyses(lam);
pb.crossRegisterProxies(lam, fam, cgam, mam);
pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam);
}
static TargetMachine *hostMachine() {
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
Triple triple(sys::getDefaultTargetTriple());
std::string err;
const Target *target = TargetRegistry::lookupTarget(triple, err);
if (!target) {
std::fprintf(stderr, "error: %s\n", err.c_str());
std::exit(1);
}
return target->createTargetMachine(triple, "generic", "", TargetOptions(), Reloc::PIC_);
}
static void emitObject(Module &mod, TargetMachine *tm, const std::string &path) {
std::error_code ec;
raw_fd_ostream out(path, ec, sys::fs::OF_None);
if (ec) {
std::fprintf(stderr, "error: can't write '%s': %s\n", path.c_str(), ec.message().c_str());
std::exit(1);
}
legacy::PassManager pm;
if (tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)) {
std::fprintf(stderr, "error: this machine can't produce object files\n");
std::exit(1);
}
pm.run(mod);
}
// Run a shell command; returns its exit status, everything it printed goes to `output`.
static int capture(const std::string &cmd, std::string &output) {
FILE *p = popen((cmd + " 2>&1").c_str(), "r");
if (!p) return -1;
char buf[4096];
size_t n;
while ((n = std::fread(buf, 1, sizeof buf, p)) > 0) output.append(buf, n);
int status = pclose(p);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
// pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>.
static std::string libFlags(const std::string &lib) {
std::string out;
if (capture("pkg-config --libs '" + lib + "'", out) == 0) {
while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back();
return " " + out;
}
return " '-l" + lib + "'";
}
// "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name.
static std::string guessLib(const std::string &header) {
std::string name = header.substr(header.find_last_of('/') + 1);
name = name.substr(0, name.rfind('.'));
while (!name.empty() && std::isdigit((unsigned char)name.back())) name.pop_back();
for (char &ch : name) ch = std::tolower((unsigned char)ch);
return name;
}
// The linker speaks in riddles; turn its complaints into something you can act on.
[[noreturn]] static void linkFailed(const std::string &output, const CImports &c) {
std::vector<std::string> missing, missingLibs;
auto addOnce = [](std::vector<std::string> &v, const std::string &x) {
if (std::find(v.begin(), v.end(), x) == v.end()) v.push_back(x);
};
std::istringstream lines(output);
for (std::string l; std::getline(lines, l);) {
size_t a;
if ((a = l.find("undefined reference to `")) != std::string::npos) {
a += 24;
addOnce(missing, l.substr(a, l.find('\'', a) - a));
} else if ((a = l.find("undefined symbol: ")) != std::string::npos) {
a += 18;
addOnce(missing, l.substr(a, l.find_first_of(" \n", a) - a));
} else if ((a = l.find("cannot find -l")) != std::string::npos) {
a += 14;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n", a) - a));
}
}
if (missing.empty() && missingLibs.empty()) {
std::fprintf(stderr, "%serror: linking failed\n", output.c_str());
std::exit(1);
}
for (const std::string &lib : missingLibs)
std::fprintf(stderr, "error: the library '%s' wasn't found. Is it installed? (on Nix: add it to shell.nix)\n", lib.c_str());
// group the missing functions by the header they came from
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 void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &libs,
const CImports &c) {
const char *cc = std::getenv("CC");
std::string cmd = std::string(cc ? cc : "cc") + " '" + obj + "' -o '" + exe + "' -lm";
for (const std::string &l : libs) cmd += libFlags(l);
std::string output;
int status = capture(cmd, output);
std::remove(obj.c_str());
if (status == 127) {
std::fprintf(stderr, "error: no C compiler found to link with (install gcc or clang, or set CC)\n");
std::exit(1);
}
if (status != 0) linkFailed(output, c);
std::fputs(output.c_str(), stderr); // warnings, if any
}
static std::string stem(const std::string &path) {
size_t slash = path.find_last_of('/');
std::string name = slash == std::string::npos ? path : path.substr(slash + 1);
size_t dot = name.rfind('.');
return dot == std::string::npos ? name : name.substr(0, dot);
}
int main(int argc, char **argv) {
if (argc == 2 && (std::string(argv[1]) == "version" || std::string(argv[1]) == "--version")) {
std::printf("finch %s (LLVM %s)\n", FINCH_VERSION, LLVM_VERSION_STRING);
return 0;
}
if (argc < 3) usage();
std::string cmd = argv[1], file = argv[2];
std::string out = stem(file);
std::vector<std::string> libs;
bool optimizeCode = true;
for (int i = 3; i < argc; i++) {
std::string a = argv[i];
if (a == "-o" && i + 1 < argc) out = argv[++i];
else if (a == "-O0") optimizeCode = false;
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 usage();
}
if (cmd != "run" && cmd != "build" && cmd != "ir") usage();
Program prog = parse(lex(readFile(file), file));
for (const Link &l : prog.links) libs.push_back(l.lib);
size_t slash = file.find_last_of('/');
CImports c = importHeaders(prog.imports, slash == std::string::npos ? "." : file.substr(0, slash));
LLVMContext ctx;
TargetMachine *tm = hostMachine();
std::unique_ptr<Module> mod = generate(prog, c, ctx, *tm);
if (optimizeCode) optimize(*mod, tm);
if (cmd == "ir") {
mod->print(outs(), nullptr);
return 0;
}
std::string obj = out + ".o";
if (cmd == "run") {
SmallString<128> tmp;
sys::fs::createTemporaryFile("finch", "", tmp);
out = tmp.str().str();
obj = out + ".o";
}
emitObject(*mod, tm, obj);
link(obj, out, libs, c);
if (cmd == "run") {
int status = std::system(("'" + out + "'").c_str());
std::remove(out.c_str());
if (WIFSIGNALED(status)) {
std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status)));
return 128 + WTERMSIG(status);
}
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
return 0;
}

425
src/parser.cpp Normal file
View file

@ -0,0 +1,425 @@
#include "parser.h"
#include "error.h"
#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdlib>
#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:
explicit Parser(const std::vector<Token> &t) : toks(t) {}
Program program() {
Program p;
while (!at(Tok::End)) {
if (at(Tok::Import)) p.imports.push_back(import());
else if (at(Tok::Link)) p.links.push_back(link());
else p.fns.push_back(function());
}
return p;
}
private:
const std::vector<Token> &toks;
size_t i = 0;
int parenDepth = 0; // inside ( ) 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}; }
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 parens).
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
// int, u8, ptr, ptr[int], ptr[ptr[u8]] ...
Type type() {
if (!at(Tok::Ident) || !isTypeName(cur().text))
unexpected("(expected a type like int, float, u8, str or ptr[int])");
Type t;
typeFromName(next().text, t);
if (t.kind == Type::Ptr && accept(Tok::LBracket)) {
t = Type::ptrTo(type());
expect(Tok::RBracket);
}
return t;
}
// Does a declaration like `int x` or `ptr[int] p` start here?
bool atDeclaration() const {
if (!at(Tok::Ident) || !isTypeName(cur().text)) return false;
Tok n = peekTok().kind;
return n == Tok::Ident || (cur().text == "ptr" && n == Tok::LBracket);
}
// ---------- top level ----------
Import import() {
Import im;
im.pos = pos();
expect(Tok::Import);
if (at(Tok::Ident))
fail(im.pos.line, im.pos.col, "Finch modules are not ready yet; for C headers write import \"stdio.h\"");
im.path = expect(Tok::String, "a header name in quotes, like \"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;
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;
}
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' (every function starts with fn)");
fn.name = expect(Tok::Ident, "a function name").text;
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
Param p;
p.pos = pos();
p.type = type();
p.name = expect(Tok::Ident, "a parameter name").text;
fn.params.push_back(p);
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
fn.ret = accept(Tok::Arrow) ? type() : Type(Type::Void);
fn.body = block();
return fn;
}
// ---------- statements ----------
BlockPtr block() {
auto b = std::make_unique<BlockStmt>(pos());
expect(Tok::LBrace);
int saved = parenDepth;
parenDepth = 0;
while (!at(Tok::RBrace)) {
if (at(Tok::End)) fail(b->pos.line, b->pos.col, "this '{' is never closed (missing '}')");
b->body.push_back(statement());
endOfStatement();
}
parenDepth = saved;
next();
return b;
}
void endOfStatement() {
if (at(Tok::RBrace) || at(Tok::End) || cur().newlineBefore) return;
unexpected("(put each statement on its own line)");
}
StmtPtr statement() {
Pos p = pos();
switch (cur().kind) {
case Tok::If: return ifStmt();
case Tok::While: {
next();
auto s = std::make_unique<WhileStmt>(p);
s->cond = expr();
s->body = block();
return s;
}
case Tok::For: {
next();
auto s = std::make_unique<ForStmt>(p);
s->var = expect(Tok::Ident, "a loop variable name").text;
expect(Tok::In);
s->from = expr();
expect(Tok::DotDot, "'..' (like 'for i in 0..10')");
s->to = expr();
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::LBrace: return block();
case Tok::Fn: fail(p.line, p.col, "functions can't be inside other functions (move it out)");
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
if (atDeclaration()) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = true;
s->type = type();
s->name = next().text;
if (accept(Tok::Assign)) s->init = expr();
return s;
}
// x := 5
if (at(Tok::Ident) && peekTok().kind == Tok::Declare) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = false;
s->name = next().text;
next();
s->init = expr();
return s;
}
ExprPtr e = expr();
// x = 5, p.value += 1, ...
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)
fail(p.line, p.col, "only a variable 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) fail(p.line, p.col, "this value is computed but never used");
return std::make_unique<ExprStmt>(p, std::move(e));
}
StmtPtr ifStmt() {
auto s = std::make_unique<IfStmt>(pos());
expect(Tok::If);
s->cond = expr();
s->then = block();
if (accept(Tok::Else)) {
if (at(Tok::If)) s->otherwise = ifStmt();
else s->otherwise = block();
}
return s;
}
// ---------- expressions (lowest to highest precedence) ----------
ExprPtr expr() { return orExpr(); }
ExprPtr orExpr() {
ExprPtr l = andExpr();
while (sameLine() && at(Tok::Or)) {
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, BinOp::Or, std::move(l), andExpr());
}
return l;
}
ExprPtr andExpr() {
ExprPtr l = compare();
while (sameLine() && at(Tok::And)) {
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, BinOp::And, std::move(l), compare());
}
return l;
}
ExprPtr compare() {
ExprPtr l = additive();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Eq: op = BinOp::Eq; break;
case Tok::NotEq: op = BinOp::NotEq; break;
case Tok::Less: op = BinOp::Less; break;
case Tok::LessEq: op = BinOp::LessEq; break;
case Tok::Greater: op = BinOp::Greater; break;
case Tok::GreaterEq: op = BinOp::GreaterEq; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), additive());
}
return l;
}
// Precedence is Go's, not C's: bit operators bind tighter than comparisons,
// so `x & 1 == 0` means `(x & 1) == 0`.
ExprPtr additive() { // + - | ^
ExprPtr l = multiplicative();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Plus: op = BinOp::Add; break;
case Tok::Minus: op = BinOp::Sub; break;
case Tok::Pipe: op = BinOp::BitOr; break;
case Tok::Caret: op = BinOp::BitXor; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), multiplicative());
}
return l;
}
ExprPtr multiplicative() { // * / % << >> &
ExprPtr l = unary();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Star: op = BinOp::Mul; break;
case Tok::Slash: op = BinOp::Div; break;
case Tok::Percent: op = BinOp::Mod; break;
case Tok::Shl: op = BinOp::Shl; break;
case Tok::Shr: op = BinOp::Shr; break;
case Tok::Amp: op = BinOp::BitAnd; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), unary());
}
return l;
}
ExprPtr unary() {
Pos p = pos();
if (accept(Tok::Minus)) return std::make_unique<UnaryExpr>(p, '-', unary());
if (accept(Tok::Not)) return std::make_unique<UnaryExpr>(p, '!', unary());
if (accept(Tok::Tilde)) return std::make_unique<UnaryExpr>(p, '~', unary());
return postfix();
}
// p.value
ExprPtr postfix() {
ExprPtr e = primary();
while (sameLine() && at(Tok::Dot)) {
Pos p = pos();
next();
e = std::make_unique<MemberExpr>(p, std::move(e), expect(Tok::Ident, "a name after '.'").text);
}
return e;
}
// name(arg, arg, ...) -- we are at the '('
ExprPtr callArgs(Pos p, const std::string &name) {
auto call = std::make_unique<CallExpr>(p, name);
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do call->args.push_back(expr());
while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
return call;
}
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::Ident: {
next();
if (at(Tok::LParen) && !cur().newlineBefore) return callArgs(p, t.text);
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) {
return Parser(tokens).program();
}

6
src/parser.h Normal file
View file

@ -0,0 +1,6 @@
#pragma once
#include "ast.h"
#include "lexer.h"
Program parse(const std::vector<Token> &tokens);

View file

@ -0,0 +1,3 @@
// expect: write just the library's name
link "libm.so"
fn main() {}

View file

@ -0,0 +1,4 @@
// expect: for true/false use '&&'
fn main() {
print(true & false)
}

View file

@ -0,0 +1,5 @@
// expect: a condition must be bool
fn main() {
if 1 {
}
}

View file

@ -0,0 +1,4 @@
// expect: division by zero
fn main() {
print(1 / 0)
}

View file

@ -0,0 +1,5 @@
// expect: 4: runtime error: division by zero
fn main() {
zero := 0
print(10 / zero)
}

View file

@ -0,0 +1,5 @@
// expect: cuts off the fraction
fn main() {
float f = 2.5
int n = f
}

View file

@ -0,0 +1,4 @@
// expect: import "math.h"
fn main() {
print(sqrt(2))
}

5
tests/fail/lib_hint.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: link "lib_hint"
import "lib_hint.h"
fn main() {
print(finch_test_missing(1))
}

1
tests/fail/lib_hint.h Normal file
View file

@ -0,0 +1 @@
int finch_test_missing(int x);

View file

@ -0,0 +1,4 @@
// expect: doesn't fit in u8
fn main() {
u8 b = 300
}

View file

@ -0,0 +1,6 @@
// expect: can't be changed
fn main() {
for i in 0..3 {
i = 5
}
}

View file

@ -0,0 +1,3 @@
// expect: can't find the C header
import "nope_not_here.h"
fn main() {}

View file

@ -0,0 +1,3 @@
// expect: the library 'definitely_not_a_lib' wasn't found
link "definitely_not_a_lib"
fn main() {}

View file

@ -0,0 +1,5 @@
// expect: can reach its end without returning int
fn f() -> int {
print(1)
}
fn main() {}

View file

@ -0,0 +1,6 @@
// expect: can't use '+' on i32 and u32
fn main() {
i32 a = 1
u32 b = 2
print(a + b)
}

2
tests/fail/no_main.fn Normal file
View file

@ -0,0 +1,2 @@
// expect: there is no main function
fn helper() {}

View file

@ -0,0 +1,4 @@
// expect: unexpected character 'ż' (names use only
fn main() {
żółw := 1
}

5
tests/fail/null_value.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: 4: runtime error: used .value on a null pointer
fn main() {
ptr[int] p = null
print(p.value)
}

View file

@ -0,0 +1,4 @@
// expect: functions start with 'fn'
int add(int a) {
return a
}

5
tests/fail/redeclare.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: already exists
fn main() {
x := 1
x := 2
}

View file

@ -0,0 +1,4 @@
// expect: put each statement on its own line
fn main() {
x := 1 print(x)
}

View file

@ -0,0 +1,4 @@
// expect: 'x' must be int, but this is str
fn main() {
int x = "hi"
}

View file

@ -0,0 +1,3 @@
// expect: never closed
fn main() {
print(1)

View file

@ -0,0 +1,4 @@
// expect: there is no variable named 'y'
fn main() {
print(y)
}

View file

@ -0,0 +1,5 @@
// expect: this code can never run
fn main() {
return
print(1)
}

View file

@ -0,0 +1,4 @@
// expect: null needs a type
fn main() {
p := null
}

View file

@ -0,0 +1,6 @@
// expect: this ptr has no type
import "stdlib.h"
fn main() {
p := malloc(8)
print(p.value)
}

36
tests/run.sh Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Finch test suite.
# tests/run/*.fn must compile and print exactly what is in the matching .out file
# tests/fail/*.fn must fail; the first line is "// expect: <text that must appear in the error>"
set -u
cd "$(dirname "$0")"
FINCH=${FINCH:-../build/finch}
pass=0; failed=0
for f in run/*.fn; do
want="${f%.fn}.out"
got=$("$FINCH" run "$f" 2>&1)
if [ "$got" == "$(cat "$want")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL $f"
diff <(echo "$got") "$want" | head -20
fi
done
for f in fail/*.fn; do
expect=$(head -1 "$f" | sed -n 's|^// expect: ||p')
got=$("$FINCH" run "$f" 2>&1)
status=$?
if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL $f (wanted: $expect)"
echo "$got" | head -5
fi
done
echo "$pass passed, $failed failed"
[ $failed -eq 0 ]

18
tests/run/basics.fn Normal file
View file

@ -0,0 +1,18 @@
fn main() {
int a = 7
b := 2
print(a + b, a - b, a * b, a / b, a % b)
print(-a / b, -a % b)
x := 1.5
print(x * 2, x + 1, 7 / 2.0)
print(true && false, true || false, !true)
print('A', "text", 1 < 2, 2 <= 2, 3 > 4, 4 >= 4, 1 == 1, 1 != 1)
a += 3
a -= 1
a *= 2
a /= 3
a %= 4
print(a)
print()
print("tab\there", "quote\"inside")
}

8
tests/run/basics.out Normal file
View file

@ -0,0 +1,8 @@
9 5 14 3 1
-3 -1
3 2.5 3.5
false true false
A text true true false true true false
2
tab here quote"inside

9
tests/run/bits.fn Normal file
View file

@ -0,0 +1,9 @@
fn main() {
print(6 & 3, 6 | 3, 6 ^ 3, 1 << 4, 256 >> 2, ~0)
print(5 & 1 == 1)
u8 flags = 0
flags = flags | 4
flags = flags | 1
print(flags, flags & 4 != 0, flags & 2 != 0)
print(-16 >> 2, u8(240) >> 4)
}

4
tests/run/bits.out Normal file
View file

@ -0,0 +1,4 @@
2 7 5 16 64 -1
true
5 true false
-4 15

16
tests/run/cimport.fn Normal file
View file

@ -0,0 +1,16 @@
import "stdio.h"
import "math.h"
import "stdlib.h"
import "string.h"
fn main() {
printf("%d-%s\n", 42, "printf")
print(sqrt(16), pow(2, 10), floor(2.7))
print(strlen("finch"), abs(-5), atoi("123"))
print(EOF, RAND_MAX > 0, M_PI > 3.14)
ptr[i32] n = malloc(4)
n.value = 7
print(n.value)
free(n)
fflush(stdout)
}

5
tests/run/cimport.out Normal file
View file

@ -0,0 +1,5 @@
42-printf
4 1024 2
5 5 123
-1 true true
7

42
tests/run/control.fn Normal file
View file

@ -0,0 +1,42 @@
fn classify(int n) -> str {
if n < 0 {
return "negative"
} else if n == 0 {
return "zero"
} else {
return "positive"
}
}
fn main() {
print(classify(-5), classify(0), classify(5))
total := 0
for i in 0..100 {
if i % 2 == 0 {
continue
}
if i > 10 {
break
}
total += i
}
print("odd sum below 10:", total)
n := 0
while true {
n += 1
if n == 5 {
break
}
}
print("n:", n)
for i in 3..3 {
print("never")
}
{
inner := "blocks have their own scope"
print(inner)
}
}

4
tests/run/control.out Normal file
View file

@ -0,0 +1,4 @@
negative zero positive
odd sum below 10: 25
n: 5
blocks have their own scope

4
tests/run/exitcode.fn Normal file
View file

@ -0,0 +1,4 @@
fn main() -> int {
print("exiting")
return 0
}

1
tests/run/exitcode.out Normal file
View file

@ -0,0 +1 @@
exiting

28
tests/run/functions.fn Normal file
View file

@ -0,0 +1,28 @@
fn fib(int n) -> int {
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
fn later() -> int {
return defined_below()
}
fn defined_below() -> int {
return 42
}
fn greet(str who) {
if who == "" {
return
}
print("hi", who)
}
fn main() {
print(fib(20))
print(later())
greet("")
greet("finch")
}

3
tests/run/functions.out Normal file
View file

@ -0,0 +1,3 @@
6765
42
hi finch

25
tests/run/pointers.fn Normal file
View file

@ -0,0 +1,25 @@
fn double(ptr[int] p) {
p.value = p.value * 2
}
fn swap(ptr[int] a, ptr[int] b) {
t := a.value
a.value = b.value
b.value = t
}
fn main() {
x := 21
double(addr(x))
print(x)
a := 1
b := 2
swap(addr(a), addr(b))
print(a, b)
p := addr(a)
pp := addr(p)
pp.value.value = 99
print(a)
ptr[int] none = null
print(none == null, p != null, none)
}

4
tests/run/pointers.out Normal file
View file

@ -0,0 +1,4 @@
42
2 1
99
true true (nil)

17
tests/run/types.fn Normal file
View file

@ -0,0 +1,17 @@
fn main() {
u8 small = 255
small += 1
print(small)
i8 tiny = -128
print(tiny, i8(200), u8(-1), u16(70000))
i32 p = 1000
i64 wide = p
print(wide, p + 1)
u32 big = 4000000000
print(big, big / 3)
f32 half = 0.5
print(half * 2, float(half))
print(int(3.99), int(-3.99), float(7) / 2, int('A'), char(66), bool(0), bool(5))
print(0xFF, 1_000_000)
print(u8(250) + 10)
}

8
tests/run/types.out Normal file
View file

@ -0,0 +1,8 @@
0
-128 -56 255 4464
1000 1001
4000000000 1333333333
1 0.5
3 -3 3.5 65 B false true
255 1000000
4