commit 0591cd20f3b57cc586ce6e1f26bd49a824adac7c Author: Nexoniarz Date: Thu Oct 8 17:10:45 2026 +0200 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bde8c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.o diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..995b1dd --- /dev/null +++ b/CMakeLists.txt @@ -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}) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..57bc88a --- /dev/null +++ b/LICENSE @@ -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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffa7660 --- /dev/null +++ b/README.md @@ -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. diff --git a/README.pl.md b/README.pl.md new file mode 100644 index 0000000..d373542 --- /dev/null +++ b/README.pl.md @@ -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. diff --git a/docs/en/for-beginners.md b/docs/en/for-beginners.md new file mode 100644 index 0000000..aa17c75 --- /dev/null +++ b/docs/en/for-beginners.md @@ -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. + +
Solution + +```c +fn main() { + for i in 1..11 { + print("7 x", i, "=", 7 * i) + } +} +``` +
+ +### Project 2: Even or odd + +For every number from 1 to 10, show whether it is even or odd. + +
Solution + +```c +fn main() { + for n in 1..11 { + if n % 2 == 0 { + print(n, "is even") + } else { + print(n, "is odd") + } + } +} +``` +
+ +### Project 3: Celsius to Fahrenheit + +Show a table from 0 °C to 100 °C, every 10 degrees. Formula: `F = C * 9 / 5 + 32`. + +
Solution + +```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. +
+ +### 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. + +
Solution + +```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) + } + } +} +``` +
+ +### Project 5: Biggest of three + +Write a function `biggest(int a, int b, int c) -> int` that gives back the largest number. + +
Solution + +```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 +} +``` +
+ +--- + +## 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. diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md new file mode 100644 index 0000000..9a2a7cf --- /dev/null +++ b/docs/en/for-engineers.md @@ -0,0 +1,1131 @@ +# Finch for Engineers + +**The complete internals of the Finch 1.0 compiler.** It covers how source becomes tokens, +tokens become an AST, and the AST becomes LLVM IR, which is then optimized, emitted and +linked. It describes the exact type rules, how every construct lowers to IR (with real +compiler output), how C headers are imported through libclang, the ABI details, and what +each function in the compiler is for and how it differs from its neighbours. + +Assumed: you know C/C++ and have at least seen LLVM IR. +For the language from a user's point of view, see the [technicians' guide](for-technicians.md). + +> 🇵🇱 Polska wersja: [dla-inzynierow.md](../pl/dla-inzynierow.md) + +--- + +## Contents + +1. [Architecture](#1-architecture) +2. [Source tree](#2-source-tree) +3. [Lexer](#3-lexer) +4. [Grammar and parser](#4-grammar-and-parser) +5. [AST](#5-ast) +6. [Semantic model](#6-semantic-model) +7. [Type system](#7-type-system) +8. [Code generation](#8-code-generation) +9. [Runtime checks](#9-runtime-checks) +10. [C interop via libclang](#10-c-interop-via-libclang) +11. [Optimization, emission, linking](#11-optimization-emission-linking) +12. [Diagnostics](#12-diagnostics) +13. [Build system](#13-build-system) +14. [Testing](#14-testing) +15. [Semantics vs. C: defined and undefined behavior](#15-semantics-vs-c-defined-and-undefined-behavior) +16. [Function reference: who does what](#16-function-reference-who-does-what) +17. [Extending the compiler](#17-extending-the-compiler) +18. [Known limitations and roadmap](#18-known-limitations-and-roadmap) + +--- + +## 1. Architecture + +Finch is a classic single-pass front end in front of LLVM. Type checking and IR generation +are one walk over the AST. There is no separate semantic pass and no intermediate IR of its own. + +``` + file.fn + │ readFile() src/main.cpp + ▼ + lex() ──────────► std::vector src/lexer.cpp + ▼ + parse() ─────────► Program (AST) src/parser.cpp, src/ast.h + │ ├─ imports ──► importHeaders() ──► CImports src/cimport.cpp (libclang) + │ └─ links + ▼ + hostMachine() ───► llvm::TargetMachine (triple, data layout) + ▼ + generate() ──────► llvm::Module (type check + IRBuilder, then verifyModule) src/codegen.cpp + ▼ + optimize() ──────► PassBuilder default pipeline (skipped with -O0) + ▼ + emitObject() ────► file.o (legacy::PassManager + addPassesToEmitFile) + ▼ + link() ──────────► cc file.o -o file -lm [-l… | pkg-config --libs …] + ▼ + executable (finch run: executed from a temp file, then deleted) +``` + +Design decisions that shape everything else: + +- **Fail fast.** The first error prints a diagnostic and calls `exit(1)` (`fail()` in `src/error.h`). + There is no error recovery, so the AST and codegen never deal with half-valid input. +- **One walk.** `Codegen` carries the scope, loop and function state. Every expression returns + its IR value and its Finch type together (`Value_`). +- **Let LLVM do the work.** Locals are `alloca`s, and `mem2reg`/SROA promote them to SSA. + Finch never builds SSA or φ-nodes itself, except for `&&`/`||`. +- **The system C toolchain is the runtime.** `printf`, `strcmp`, `dprintf`, `exit` come from libc. + There is no Finch runtime library. + +--- + +## 2. Source tree + +| File | Lines | Responsibility | +|---|---|---| +| `src/error.h` | ~25 | `fail(line, col, msg)`: prints `file:line:col: error:` and a caret excerpt, then exits. Holds `g_file`, `g_source`. | +| `src/lexer.h/.cpp` | ~240 | `Tok` enum, `Token`, `lex()`, `tokName()` | +| `src/ast.h` | ~235 | `Type`, all `Expr`/`Stmt` nodes, `FnDecl`, `Import`, `Link`, `Program` | +| `src/parser.h/.cpp` | ~430 | Recursive-descent `Parser`, `typeFromName()` | +| `src/cimport.h/.cpp` | ~280 | libclang header import: `importHeaders()` → `CImports` (functions, constants, globals) | +| `src/codegen.h/.cpp` | ~900 | `Codegen`: type checking + LLVM IR generation, `generate()` | +| `src/main.cpp` | ~260 | Driver: CLI, target machine, optimization, object emission, linking, link-error analysis, `run` | +| `tests/run.sh` | | Golden-output and expected-failure tests | + +--- + +## 3. Lexer + +`lex(src, file)` makes one linear pass over the source and returns a `std::vector` +terminated by `Tok::End`. + +```cpp +struct Token { + Tok kind; + std::string text; // identifier / literal contents (escapes already decoded) + int line, col; // 1-based + bool newlineBefore; // a newline occurred between the previous token and this one +}; +``` + +### Newlines as statement terminators + +Finch has no semicolons. Instead of emitting `NEWLINE` tokens, as Python and Go do internally, +the lexer **marks** each token with `newlineBefore`. The parser decides what that means +(section 4). This keeps the grammar free of newline tokens, which would otherwise have to be +skipped everywhere: in argument lists, between `}` and `else`, and so on. + +### Tokens + +- **Positions:** `line` and `col` are 1-based, and `col` counts **characters**, not bytes: + UTF-8 continuation bytes do not advance it, so carets line up under text such as `"Błąd"`. + An unexpected non-ASCII character is reported whole (`'ż'`), with a hint that names are ASCII-only. +- **Identifiers / keywords:** `[A-Za-z_][A-Za-z0-9_]*`. Keywords: `fn return if else while for in break continue true false null import link`. + **Type names are not keywords.** `int`, `u8`, `ptr`… lex as `Ident`, and the parser recognises them through `typeFromName()`. + This keeps the lexer independent of the type list, and lets `u8(x)` parse as an ordinary call. +- **Integers:** decimal or `0x` hex, with `_` separators (removed from `text`). Leading zeros are decimal. + Finch has no octal, so `010` is 10. Value parsing happens in the parser (`strtoull`, base 10 or 16). +- **Floats:** `digits.digits`. A dot is only part of a number when **followed by a digit**, + so `0..10` lexes as `Int DotDot Int`, not `Float(0.) Dot …`. +- **Strings / chars:** `"…"` and `'…'`. Escapes `\n \t \r \0 \\ \" \'` are decoded into `text`. + An unknown escape, an unterminated literal or a newline inside a literal is an error. A char literal must decode to exactly one byte. +- **Operators:** maximal munch: `->` before `-`, `<<` and `<=` before `<`, `&&` before `&`, `..` before `.`, `:=` (a lone `:` is an error that suggests `:=`). +- **Comments:** `//` to end of line, `/* … */` (not nested; an unterminated one is an error). + +--- + +## 4. Grammar and parser + +`Parser` (`src/parser.cpp`) is a hand-written recursive-descent parser with one token of +lookahead (`cur()`), plus a second (`peekTok()`) in two places. + +### Grammar (EBNF) + +```ebnf +program = { import | link | function } ; +import = "import" STRING ; +link = "link" STRING ; +function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ; +param = type IDENT ; +type = TYPENAME | "ptr" "[" type "]" ; (* TYPENAME: int float bool char str ptr i8..u64 f32 f64 *) + +block = "{" { statement TERMINATOR } "}" ; +statement = if | while | for | return | "break" | "continue" | block + | type IDENT [ "=" expr ] (* declaration *) + | IDENT ":=" expr (* inferred declaration *) + | target assignop expr (* target: IDENT or postfix ending in .value *) + | call ; +if = "if" expr block [ "else" ( if | block ) ] ; +while = "while" expr block ; +for = "for" IDENT "in" expr ".." expr block ; +return = "return" [ expr ] ; (* expr only if on the same line *) +assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; + +expr = or ; +or = and { "||" and } ; +and = cmp { "&&" cmp } ; +cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ; +add = mul { ( "+" | "-" | "|" | "^" ) mul } ; +mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; +unary = ( "-" | "!" | "~" ) unary | postfix ; +postfix = primary { "." IDENT } ; +primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" + | IDENT [ "(" [ expr { "," expr } ] ")" ] + | "(" expr ")" ; +``` + +### The statement terminator rule + +`TERMINATOR` is not a token. After every statement, `endOfStatement()` requires that the next +token is `}`, end-of-file, or has `newlineBefore == true`. Otherwise the error is +`put each statement on its own line`. + +Inside expressions, `sameLine()` decides whether a binary operator, `.`, or a call's `(` +**continues** the current expression: + +```cpp +bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; } +``` + +So: + +```c +a := 1 + // '+' is on this line → the expression continues to the next line + 2 +b := (1 // inside parentheses (parenDepth > 0) newlines are ignored + + 2) +c := 1 +-5 // '-' starts a new line → the statement ended at '1'; "-5" is then an error +``` + +`parenDepth` is incremented around `( … )` groups, argument lists and parameter lists. It is +**saved and reset to 0** when entering a `{ }` block, so a block inside a parenthesised +context, which does not occur yet, would still use line rules. + +A call's `(` must be on the same line as the callee. Otherwise `f⏎(x)` would silently parse as a call. + +### Precedence: Go's, not C's + +| Level | Operators | +|---|---| +| 5 | `*` `/` `%` `<<` `>>` `&` | +| 4 | `+` `-` `\|` `^` | +| 3 | `==` `!=` `<` `<=` `>` `>=` | +| 2 | `&&` | +| 1 | `\|\|` | + +All binary operators are left-associative. Comparisons are also parsed left-associatively +(`a < b < c` parses), but the type checker then rejects comparing `bool < int`. +C puts `&`, `^`, `|` **below** `==`, which makes `x & MASK == 0` mean `x & (MASK == 0)`. +Finch adopts Go's table, which removes that whole class of bugs. + +### How statements are recognised + +`statement()` dispatches on the first token: + +1. Keywords (`if`, `while`, `for`, `return`, `break`, `continue`, `{`) dispatch directly. + `fn`, `import` and `link` inside a block produce targeted errors. +2. **Typed declaration:** `atDeclaration()` returns true when the current token is a type name + and the next is an identifier (`int x`), or when it is `ptr` followed by `[` (`ptr[int] p`). + This is the only place that needs two-token lookahead. +3. **Inferred declaration:** `IDENT` followed by `:=`. +4. Otherwise the parser **parses a full expression first**, then looks at the next token. If it is + an assignment operator on the same line, the expression becomes the assignment target. It must + be a `VarExpr` or `MemberExpr`, otherwise: `only a variable or p.value can be assigned to`. + This "expression, then decide" approach lets arbitrary l-value forms (`pp.value.value = 1`) + work without a separate l-value grammar. +5. A bare expression statement must be a `CallExpr`. Anything else is + `this value is computed but never used`. + +### Top level + +`program()` loops over `import`, `link` and `fn`. A top-level item that starts with a type name +gets the hint `functions start with 'fn', like: fn add(int a, int b) -> int { ... }`, which catches +C-style function definitions. `link` arguments are validated: `"libfoo.so"` or a path is +rejected with a suggestion for the bare name. + +--- + +## 5. AST + +All nodes live in `src/ast.h`. Expressions and statements are small class hierarchies with +an explicit `kind` tag. Consumers `switch` on `kind` and `static_cast`, which avoids RTTI and +`dynamic_cast` and keeps dispatch obvious. + +```cpp +struct Expr { ExprKind kind; Pos pos; virtual ~Expr(); }; +using ExprPtr = std::unique_ptr; +``` + +| ExprKind | Node | Fields | +|---|---|---| +| `Int` | `IntExpr` | `long long value` | +| `Float` | `FloatExpr` | `double value` | +| `Bool` / `Char` / `Str` | `BoolExpr` / `CharExpr` / `StrExpr` | `value` | +| `Null` | `NullExpr` | | +| `Var` | `VarExpr` | `name` | +| `Unary` | `UnaryExpr` | `char op` (`-` `!` `~`), `operand` | +| `Binary` | `BinaryExpr` | `BinOp op`, `lhs`, `rhs` | +| `Call` | `CallExpr` | `std::string callee`, `args` | +| `Member` | `MemberExpr` | `obj`, `field` (only `value` is meaningful in 1.0) | + +| StmtKind | Node | Fields | +|---|---|---| +| `Block` | `BlockStmt` | `std::vector body` | +| `VarDecl` | `VarDeclStmt` | `hasType`, `type`, `name`, `init` (nullable if `hasType`) | +| `Assign` | `AssignStmt` | `target` (Var/Member), `char op` (`= + - * / %`), `value` | +| `Expr` | `ExprStmt` | `expr` (always a call) | +| `If` | `IfStmt` | `cond`, `then`, `otherwise` (null, `BlockStmt`, or nested `IfStmt` for `else if`) | +| `While` | `WhileStmt` | `cond`, `body` | +| `For` | `ForStmt` | `var`, `from`, `to`, `body` | +| `Return` | `ReturnStmt` | `value` (nullable) | +| `Break` / `Continue` | | | + +Calls store the callee **by name**, not as an expression. Finch has no first-class functions, +and resolving names at codegen time lets one `CallExpr` mean a built-in (`print`, `addr`), +a conversion (`u8`), a Finch function or a C function (section 8.10). + +### Example + +```c +x += add(x, 2) +``` + +parses to: + +``` +AssignStmt (op '+') +├─ target: VarExpr "x" +└─ value: CallExpr "add" + ├─ VarExpr "x" + └─ IntExpr 2 +``` + +There is **no "load" node in the AST**. Whether a `VarExpr` means "the address of x" or "the +value in x" depends on where it appears. Codegen decides that (`place()` vs `expr()`, section 8.2). + +### `Type` + +```cpp +struct Type { + enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null }; + Kind kind; + std::shared_ptr elem; // Ptr only: pointee; null = untyped `ptr` +}; +``` + +- `int` and `i64` are **the same type** (`I64`), and `name()` prints it as `int`. Likewise `float` is `F64`. +- `Null` is the type of the `null` literal only. No variable can have it. +- `Void` is the "type" of calls that return nothing, and of functions without `->`. +- Equality is structural (`ptr[ptr[int]] == ptr[ptr[int]]`). +- Kinds are ordered so that `isInt()` is `I8..U64`, `isSigned()` is `I8..I64`, `isUnsigned()` is `U8..U64`. + +--- + +## 6. Semantic model + +All semantic checking happens inside `Codegen` while it emits IR. + +### Functions + +`Codegen::run()` runs two passes over `Program::fns`: + +1. `declare()` creates every `llvm::Function` (signature only), so **definition order does not matter** + and mutual recursion works. It also checks: duplicate names, built-in names, and `main`'s + signature (no parameters; returns nothing, `int` or `i32`). +2. `define()` generates each body. + +Without a `main`, compilation fails with `there is no main function`. + +### Scopes + +`scopes` is a `std::vector>`. A new map is pushed for +the function's parameters, for every `{ }` block, and for every `for` loop (which holds the +loop variable). + +```cpp +struct Var { AllocaInst *slot; FType type; bool readonly; }; +``` + +**No shadowing:** `addVar()` rejects a name that `lookup()` finds in *any* enclosing scope, +not just the current one. It also rejects names of functions and built-ins. This is a +deliberate simplicity rule: a name means one thing in a whole function body. + +**Read-only:** `for` variables are `readonly`. Assigning to them, or taking `addr()` of them, +is an error, so the loop counter cannot be changed behind the loop's back. + +### Name resolution for identifiers + +For a `VarExpr name` in value position, the order is: + +1. a local variable (`lookup`), +2. a C constant (`cimports.consts`: enum values, `#define` numbers), +3. a C global (`cimports.globals`: `stdout`, …), +4. error `there is no variable named 'name'`. + +For calls, the order is: `print` → `addr` → type name (conversion) → Finch function → C function → error. +Since Finch functions are checked before C functions, **a Finch function shadows a C function +of the same name**. The Finch one is emitted as `finch.`, so both symbols coexist at link time. + +--- + +## 7. Type system + +### LLVM representation + +| Finch | LLVM | Notes | +|---|---|---| +| `bool` | `i1` | | +| `char`, `i8`, `u8` | `i8` | signedness lives in the Finch type, not in LLVM | +| `i16`, `u16` | `i16` | | +| `i32`, `u32` | `i32` | | +| `int`/`i64`, `u64` | `i64` | | +| `f32` | `float` | | +| `float`/`f64` | `double` | | +| `str` | `ptr` | NUL-terminated C string | +| `ptr`, `ptr[T]` | `ptr` | LLVM 21 has opaque pointers; the pointee type exists only in the Finch type | +| `null` | `ptr` | `ConstantPointerNull` | +| nothing | `void` | | + +LLVM integers are signless. Every operation where signedness matters (`sdiv`/`udiv`, +`srem`/`urem`, `ashr`/`lshr`, `icmp slt`/`ult`, `sext`/`zext`, `sitofp`/`uitofp`, +`fptosi`/`fptoui`) chooses the instruction from the **Finch** type. + +### Values in codegen + +```cpp +struct Value_ { + Value *v; // the IR value + FType type; // its Finch type + bool literal; // written as a number in the source (or an imported C constant) +}; +``` + +### Literals ("untyped constants") + +`literal` is set on `IntExpr`, `FloatExpr` and imported C constants. It is preserved through +unary minus and through arithmetic **between two literals** that folds to a constant +(`arith()` sets `literal = l.literal && r.literal && isa(result)`). It is **cleared by +explicit conversions**: `u8(250)` is a typed `u8` constant, not a literal. + +A literal integer may become **any integer type it fits in** (`fits()` checks the range, +accounting for the source literal's signedness). This is what makes `u8 b = 200`, +`malloc(4 * 10)` (to `u64`) and `glClear(GL_COLOR_BUFFER_BIT)` (an `i64` macro to a `u32` parameter) +work without casts, while `u8 b = 300` is rejected with `the number 300 doesn't fit in u8`. +A literal `f64` may become `f32` (`f32 h = 0.5`). + +Why not simply check `isa`? `IRBuilder` constant-folds, so `u8(250)` is also a +`ConstantInt`. If "is a constant" were the rule, `u8(250) + 10` would quietly widen to +`int` and give 260, while the same code with a `u8` variable would wrap to 4. The explicit +flag makes the behavior depend on what you *wrote*, not on what the optimizer could see. + +### Implicit conversions: `coerce(value, want, pos, what)` + +Used wherever a value flows into a slot of known type: initializers, assignments, arguments, +return values, conditions (`want = bool`), range bounds (`want = int`). Allowed, in order: + +1. identical types, +2. `null` → any `ptr…` or `str`, +3. any `ptr…` or `str` → untyped `ptr`, and untyped `ptr` → any `ptr[T]` (the `void*` rules; this is how `ptr[i32] n = malloc(4)` works), +4. int → int when the value is a fitting literal, **or** `widens(from, to)`: + - same signedness, strictly more bits, **or** + - unsigned → signed with strictly more bits, + - (signed → unsigned never widens), +5. any int → any float (`sitofp`/`uitofp`), +6. `f32` → `f64` (`fpext`), +7. literal `f64` → `f32` (`fptrunc`, constant-folded). + +Anything else is an error. The message suggests the explicit conversion +(`use i32(...) to convert`) and warns when it would truncate (`it cuts off the fraction`). + +### Binary operands: `unify(l, r)` + +Before arithmetic or comparison, both sides are brought to one type: + +1. same type → done, +2. both ints: a literal side adapts to the other side if it fits; otherwise the side that + `widens()` into the other is extended; otherwise **error** (`i32` + `u32`: neither widens + into the other, and Finch refuses to guess), +3. int and float → the int converts to that float type, +4. `f32` and `f64` → both `f64`. + +The result type of arithmetic is the unified type. **There is no C-style integer promotion:** +`u8 + u8` is computed in `u8` and wraps. This is deliberate. It is what the sized type asks for, +and it matches Rust and Zig. + +### Explicit conversions: `convert(call, to)` + +`T(x)` is parsed as a call named after a type and lowered here: + +| From → To | Instruction | +|---|---| +| int/char → int/char | `CreateIntCast(signed = from.isSigned())` → `trunc` / `sext` / `zext` | +| bool → int/char | `zext` | +| float → int | `fptosi` / `fptoui` (by target signedness) | +| int → float | `sitofp` / `uitofp` (by source signedness) | +| float → float | `fpext` / `fptrunc` (`CreateFPCast`) | +| int → bool | `icmp ne x, 0` | +| ptr/str/null → ptr…, ptr → str | no instruction (all are `ptr`) | + +`fptosi` of an out-of-range value is poison in LLVM (UB in C). Finch 1.0 does not check this. See section 15. + +### Comparisons: `compare()` + +- **Pointers:** if either side is a `ptr…` or `null`, only `==`/`!=` are allowed and they compile to + `icmp eq/ne ptr`. A `str` compared with `null` also compares addresses. +- **`str == str`:** a call to `strcmp`, then `icmp eq/ne i32 %r, 0` (content equality). `<`/`>` on `str` are rejected. +- **Floats:** `oeq olt ole ogt oge` (ordered: false if NaN) and `une` for `!=` (unordered: `NaN != NaN` is true). This matches C. +- **Integers / char:** `eq ne`, and `slt sle sgt sge` or `ult ule ugt uge` by signedness. `char` compares unsigned. +- **bool:** only `==`/`!=`. + +--- + +## 8. Code generation + +`Codegen` owns an `llvm::IRBuilder<> b`, the `Module`, the function table `fns` +(`name → {Function*, const FnDecl*}`), `scopes`, the `loops` stack and `curFn`. + +### 8.1 Variables are stack slots + +Every variable, **including parameters**, gets an `alloca` created by `slot()`. +`slot()` always inserts at the **top of the entry block**, using a second `IRBuilder` positioned at +`entry.begin()`, no matter where the declaration appears. Allocas in the entry block are what +`mem2reg`/`SROA` can promote to SSA registers, so the O2 output contains no stack traffic for +scalars. Because each new slot goes to the very beginning, slots appear in **reverse declaration +order** in `-O0` IR (`%b2` before `%a1` below). + +Parameters are copied into their slots at function entry (`store %a, %a1`), which makes +parameters ordinary mutable locals. + +### 8.2 Loads and stores: `expr()` vs `place()` + +This is the heart of how memory access is generated: + +- `place(e)` returns a **`Place {Value *addr; FType type}`**, the *address* of an l-value. + - `VarExpr` → the variable's `alloca` (or the C global's `GlobalVariable`). + - `MemberExpr` `p.value` → `expr(p)` (which **loads the pointer** from p's slot), plus a null check + (section 9). The address is the loaded pointer itself, and the type is `*p.type.elem`. +- `expr(e)` returns a **`Value_`**, an r-value. For `Var` and `Member` it calls `place()` and then + emits **one `load`** of `ty(place.type)` from `place.addr`. + +So the `load` for an identifier is created in `Codegen::expr()`, case `ExprKind::Var`, as +`b.CreateLoad(ty(pl.type), pl.addr, v.name)`. Naming it after the variable is why `-O0` IR shows +`%x1`, `%x2` and so on (LLVM uniquifies repeated names). `p.value` costs **two** loads: one for the +pointer, one for the pointee. `pp.value.value` costs three. + +Stores are generated in exactly three places: `addVar()` (initialization), `assign()`, and the `for` step. + +### 8.3 Assignment and evaluation order + +```c +x += add(x, 2) +``` + +`assign()` computes `place(target)` **first**. For compound operators it then **loads the current +value**, then evaluates the right-hand side, then calls `arith()`, then `coerce()`s back to the +target type, then stores. Real `-O0` output: + +```llvm +define i32 @main() { +entry: + %x = alloca i64, align 8 + store i64 5, ptr %x, align 8 ; x := 5 + %0 = load i64, ptr %x, align 8 ; current x (for +=) + %x1 = load i64, ptr %x, align 8 ; argument x + %1 = call i64 @finch.add(i64 %x1, i64 2) + %2 = add i64 %0, %1 + store i64 %2, ptr %x, align 8 + %x2 = load i64, ptr %x, align 8 + %3 = call i32 (ptr, ...) @printf(ptr @fmt, i64 %x2) + ret i32 0 +} +``` + +Order of evaluation in Finch is **always left to right**: the target, then the operands of a +binary operator left to right, then arguments left to right. C leaves most of this unspecified. +Note that for `x += f()` the old value of `x` is read **before** `f()` runs. If `f` modified `x` +through a pointer, the modification would be overwritten. That is the same as `x = x + f()` +read left to right. + +After O2 the whole program above is constant-folded into `printf(@fmt, i64 12)`. + +### 8.4 Functions, linkage and `main` + +```llvm +define internal i64 @finch.add(i64 %a, i64 %b) { +entry: + %b2 = alloca i64, align 8 + %a1 = alloca i64, align 8 + store i64 %a, ptr %a1, align 8 + store i64 %b, ptr %b2, align 8 + %a3 = load i64, ptr %a1, align 8 + %b4 = load i64, ptr %b2, align 8 + %0 = add i64 %a3, %b4 + ret i64 %0 +} +``` + +- User functions are named **`finch.`** with **internal linkage**. The dot cannot appear in a C + identifier, so a Finch function can never collide with a libc or library symbol, and internal + linkage lets LLVM inline, specialise or delete them freely. In C, a non-`static` function is + external and must be kept as written. This likely contributes to recursive `fib(40)` running + slightly faster than the same C code at `clang -O2` (about 0.19 s vs 0.23 s on the author's machine). +- `main` is emitted as **`i32 @main()` with external linkage**. A `fn main()` returns `i32 0` on every + exit path, including an explicit `return`. A `fn main() -> int` truncates its `i64` result with + `CreateIntCast(…, i32, signed)`. +- Missing return: after the body, if the current block has no terminator, `void` functions get + `ret void` (or `ret i32 0` for `main`). Non-void functions are a compile error: + `function 'f' can reach its end without returning int`. This is checked on the CFG that was + actually built (section 8.5), so `if/else` that returns on both branches is accepted without any + extra analysis. +- Arithmetic is emitted **without `nsw`/`nuw` flags**, so integer overflow is defined two's-complement + wraparound (section 15). + +### 8.5 Control flow and the "terminated" state + +The builder's insert block is the single source of truth for reachability: + +```cpp +bool terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; } +``` + +After `return`, `break` or `continue`, the current block has a terminator. +`blockBody()` checks `terminated()` **before each statement**. If it is set, the statement is +unreachable and that is a compile error (`this code can never run`). No dead blocks are ever created +for code after a jump. + +`continueAt(bb, deadEnd)` is used after `if` and `while`. If the merge block `bb` has **no +predecessors** (all paths returned or broke), it is erased and the builder is left in `deadEnd`, +which is terminated. So the "terminated" state propagates outward through nested constructs, and +the missing-return check sees it. + +**`if / else if / else`:** blocks `then`, `else` (if present), and `endif`. `else if` is an +`IfStmt` in `otherwise`, generated recursively inside the `else` block. + +**`while`:** blocks `while.cond`, `while.body`, `while.end`. If the condition folds to the +constant `true`, an unconditional `br` is emitted, so `while.end` has predecessors only through +`break`. A `while true` without `break` therefore leaves the function "terminated", and a function +whose last statement is such a loop does not need a trailing `return`. + +**`for i in a..b`:** `a` and `b` are evaluated **once**, before the loop, and coerced to `int`. +Blocks: `for.cond` (`icmp slt i, end`), `for.body`, `for.step` (`i + 1`), `for.end`. `continue` +jumps to `for.step`, not `for.cond`, so the increment is never skipped. The loop variable lives in +its own scope around the loop. + +```llvm + store i64 0, ptr %i, align 8 + br label %for.cond +for.cond: + %3 = load i64, ptr %i, align 8 + %4 = icmp slt i64 %3, 2 + br i1 %4, label %for.body, label %for.end +for.body: + ... + br label %for.step +for.step: + %6 = load i64, ptr %i, align 8 + %7 = add i64 %6, 1 + store i64 %7, ptr %i, align 8 + br label %for.cond +for.end: +``` + +**`break` / `continue`:** `loops` is a stack of `{continueTo, breakTo}`. `while` pushes +`{while.cond, while.end}` and `for` pushes `{for.step, for.end}`. Outside a loop it is an error. + +### 8.6 Short-circuit `&&` / `||`: the only hand-built φ + +```c +fn check(int a, int b) -> bool { + return a > 0 && b > 0 +} +``` + +```llvm + %a3 = load i64, ptr %a1, align 8 + %0 = icmp sgt i64 %a3, 0 + br i1 %0, label %and.rhs, label %and.end +and.rhs: + %b4 = load i64, ptr %b2, align 8 + %1 = icmp sgt i64 %b4, 0 + br label %and.end +and.end: + %2 = phi i1 [ false, %entry ], [ %1, %and.rhs ] +``` + +`logic()` records the block where the left side **ended** (`from`) and the block where the right +side **ended** (`rhsEnd`). These are not necessarily the blocks where evaluation started, because a +nested `&&` creates its own blocks. The φ uses `!isAnd` as the short-circuit constant (`false` for +`&&`, `true` for `||`). + +### 8.7 `print` + +`print(a, b, …)` compiles to **one `printf` call**. The format string is built at compile time from +the argument types, joined with spaces and ending in `\n`: + +| Finch type | Format | Argument conversion | +|---|---|---| +| signed ints | `%lld` | `sext` → `i64` | +| unsigned ints | `%llu` | `zext` → `i64` | +| `f32`, `f64` | `%g` | `fpext` → `double` | +| `char` | `%c` | `zext` → `i32` | +| `str` | `%s` | as is | +| `bool` | `%s` | `select i1, "true", "false"` | +| `ptr…` | `%p` | as is (glibc prints `(nil)` for null) | +| `null` | literal text `null` | none | + +Since the format is generated, user strings are always passed as `%s` arguments, never as the +format, so a `%` in user text is safe. O2 often rewrites `printf("%s\n", s)` into `puts(s)`. + +### 8.8 Strings + +A string literal is a private `unnamed_addr` constant global (`CreateGlobalString`), and its value is +the `ptr` to it. Identical literals may be merged by the optimizer. A `str` variable declared +without an initializer points to a shared `""` constant. Strings are **immutable and static** in +1.0. There is no concatenation or allocation (section 18). + +### 8.9 `addr()` and pointers + +`addrOf()` requires a `Var` or `Member` argument and returns `place(arg).addr` with type +`ptr[T]`. No load happens: `addr(x)` *is* x's alloca. The `alloca` is therefore not promotable +by mem2reg, which is the expected cost of taking an address. `addr(p.value)` yields the pointer +already stored in `p`, after a null check. `addr` of a `for` variable is rejected (it is read-only). + +### 8.10 Calls: `callFinch()` vs `callC()` + +Both check the argument count and `coerce()` each argument to the parameter type. The differences: + +| | `callFinch()` | `callC()` | +|---|---|---| +| Callee | `finch.`, created in `declare()` | `getOrInsertFunction(name, …)`: declared lazily on first use, reused afterwards | +| Variadic | never | supported; extra args get C default promotions | +| Parameter attributes | none needed | `signext`/`zeroext` on params and return values narrower than 32 bits, on both the declaration and the call site | +| Unsupported signatures | impossible | rejected with the reason saved by cimport (`passes a C struct by value`, …) | +| Calling `main` | rejected | n/a | + +Default argument promotions for the variadic part: `f32` → `double`; `bool`, `char` and integers +narrower than 32 bits → `i32` (sign- or zero-extended). Pointers, `str` and 32/64-bit integers +pass unchanged. This mirrors C's rules, which is what `printf`'s `va_arg` expects. + +Why `signext`/`zeroext`: on x86-64 System V, clang passes `char`/`short`/`_Bool` already extended to +32 bits and marks them so. Some callees rely on it. Without the attribute LLVM would leave the upper +bits undefined, which would be an ABI mismatch with C code compiled by clang. + +--- + +## 9. Runtime checks + +Finch removes the two most common sources of C undefined behavior that can be checked cheaply. +All checks share one helper function per module: + +```llvm +; Function Attrs: cold noinline noreturn +define internal void @finch.panic(ptr %0, i64 %1) #0 { +entry: + %2 = call i32 (i32, ptr, ...) @dprintf(i32 2, ptr @panic.fmt, ptr @panic.file, i64 %1, ptr %0) + call void @exit(i32 1) + unreachable +} +``` + +`panicFn()` creates it on first use. `panicIf(cond, msg, pos)` emits +`br cond, %panic, %ok`, a call to `finch.panic(msg, line)` and `unreachable` in `%panic`, and +continues in `%ok`. It writes to fd 2 with `dprintf`, so it does not depend on libc's `stderr` +symbol. The function is `cold` and `noinline`, so branch layout favours the non-failing path and the +check costs one compare and a predicted branch. + +### Integer division and remainder: `checkDivisor()` + +```llvm +define internal i64 @finch.div(i64 %a, i64 %b) { + ... + %0 = icmp eq i64 %b4, 0 + br i1 %0, label %panic, label %ok +panic: + call void @finch.panic(ptr @panic.msg, i64 2) + unreachable +ok: + %1 = icmp eq i64 %b4, -1 + %2 = icmp eq i64 %a3, -9223372036854775808 + %3 = and i1 %2, %1 + br i1 %3, label %panic5, label %ok6 +panic5: + call void @finch.panic(ptr @panic.msg.1, i64 2) + unreachable +ok6: + %4 = sdiv i64 %a3, %b4 +``` + +- A divisor that is a **constant zero** is a **compile-time** error. +- A constant non-zero divisor (other than −1 for signed types) emits no check at all. +- For signed division, `MIN / -1` overflows (it traps with SIGFPE on x86). That is checked too. +- When everything is known, O2 removes the checks entirely: `div(10, 2)` above becomes `5` in `main`. + +### Null `.value`: in `place()` + +Every `p.value` whose pointer is not trivially non-null (not directly an `alloca` or a global) +gets `panicIf(icmp eq p, null, "used .value on a null pointer")`. Without this, LLVM treats a load +from null as UB and may **delete it**. Before this check existed, `print(p.value)` on a null `p` +printed a garbage value at O2 instead of crashing. + +--- + +## 10. C interop via libclang + +`importHeaders(imports, dir)` (`src/cimport.cpp`) runs **before** codegen and produces: + +```cpp +struct CImports { + std::unordered_map fns; // name, header, ret, params, variadic, unsupported reason + std::unordered_map consts; // enum values + numeric #defines + std::unordered_map globals; // extern variables +}; +``` + +### Parsing + +For **each** `import "h"`, a separate translation unit is parsed from an in-memory file +`finch_import.c` containing `#include "h"`. Separate units give precise error attribution: a missing +header points at its own `import` line. Flags: + +``` +-x c -std=gnu11 -I -isystem +CXTranslationUnit_DetailedPreprocessingRecord (to see macros) +CXTranslationUnit_SkipFunctionBodies (speed: inline bodies are irrelevant) +``` + +- **System include directories** come from running `$CC -E -v -x c /dev/null` and parsing the block + between `#include <...> search starts here:` and `End of search list.`. This is what makes + `stdio.h` resolve the same way it does for the system compiler. That matters on NixOS, where the + cc wrapper injects store paths that an unwrapped libclang knows nothing about. +- **`gnu11`** rather than `c11`: glibc hides `M_PI` and other non-ISO names under strict modes. +- **Diagnostics** of severity ≥ error abort the import. `file not found` becomes + `can't find the C header 'h'`. + +### The visitor + +`visit()` walks top-level cursors. The first declaration of a name wins. + +| Cursor | Action | +|---|---| +| `FunctionDecl` | Map the canonical **function type** (`clang_getArgType`, so array parameters are already decayed to pointers). `static` functions are recorded as unsupported (`static inline` has no symbol to link). `FunctionNoProto` (K&R `f()`) is unsupported. Variadic-ness comes from `clang_isFunctionTypeVariadic`. | +| `VarDecl` with `extern` storage | → `CGlobal` if the type maps | +| `EnumDecl` | recurse into children | +| `EnumConstantDecl` | → `CConst` (`i64`, literal) | +| `MacroDefinition` | skip built-ins and function-like macros; `macroValue()` tries to read a number | + +### Type mapping: `mapType()` + +Works on the **canonical** type, so typedefs (`size_t`, `uint32_t`, `GLuint`, `FILE`) are resolved. +Integer types map **by size** (`clang_Type_getSizeOf`), not by name, so `long` becomes `i64` on LP64. + +| C (canonical) | Finch | +|---|---| +| `void` | nothing | +| `_Bool` | `bool` | +| `char` (either signedness) | `char` | +| `signed char`, `short`, `int`, `long`, `long long`, `enum` | `i8`/`i16`/`i32`/`i64` by size | +| unsigned variants | `u8`…`u64` by size | +| `float` / `double` | `f32` / `f64` | +| `char *` (any qualifiers) | `str` | +| `T *` where T maps to a non-void type | `ptr[T]` | +| `void *`, pointer to struct/union/function/unmappable | `ptr` (opaque) | +| struct/union by value | **unsupported** ("passes a C struct by value") | +| `long double`, `__int128`, vectors, … | **unsupported** | + +Unsupported functions stay in the table, so calling one produces a precise error instead of +"no such function". + +### Macros: `macroValue()` + +It tokenizes the macro's extent, drops the name token and comments, strips **balanced outer +parentheses**, accepts one leading `-`, and then requires **exactly one** numeric literal. Integers +are parsed with `strtoull(…, 0)`, i.e. **C rules**: `0x` hex and leading-zero **octal**, unlike +Finch's own literals. Floats are anything containing `.`, `e` or `E` that is not hex. Trailing +`u U l L f F` suffixes are accepted. That covers `M_PI`, `EOF` (`(-1)`), `RAND_MAX`, `INT_MAX`, +`GL_*` and `GLFW_*` constants. Expressions (`(1 << 4)`), references to other macros, and strings +are skipped. + +### Calling + +Imported functions are declared in the module **only when called**, through +`getOrInsertFunction`. This reuses an existing declaration if `print` already created `printf`. +Since the signatures agree (`i32 (ptr, ...)`), there is no conflict. + +### Link errors + +When the link step fails, `linkFailed()` in `main.cpp` scans the linker's output for +`undefined reference to \`sym'` (GNU ld) and `undefined symbol: sym` (lld). It groups the symbols by +`CFunc::header` and prints which header they came from, with a guessed `link` line (`guessLib()`: +basename, minus extension and trailing digits, lowercase: `GLFW/glfw3.h` → `glfw`). +`cannot find -lfoo` becomes `the library 'foo' wasn't found`. + +--- + +## 11. Optimization, emission, linking + +### Target + +`hostMachine()` creates a `TargetMachine` for `sys::getDefaultTargetTriple()` with CPU +`"generic"`, no extra features, and `Reloc::PIC_`, which is required for the default PIE +executables on modern distributions. Its **data layout is installed on the module before any IR +is built**, in `Codegen`'s constructor. That matters: `IRBuilder` takes the alignment of loads and +stores from the data layout, and without it `i64` accesses would be emitted with `align 4`. + +`generic` means the binary runs on any x86-64. Adding `-march=native` behavior would be a CPU +string and feature change here. + +### Optimization + +`optimize()` builds the new pass manager pipeline: + +```cpp +PassBuilder pb(tm); +pb.register{Module,CGSCC,Function,Loop}Analyses(...); +pb.crossRegisterProxies(lam, fam, cgam, mam); +pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam); +``` + +That is clang's `-O2` middle end: SROA/mem2reg, instcombine, inlining, GVN, LICM, loop +unrolling, vectorization and so on. `-O0` skips this call, which is useful to read the IR that +`Codegen` produced (`finch ir file.fn -O0`). Back-end codegen still runs at the TargetMachine's +default level. + +### Emission + +`emitObject()` uses the **legacy** pass manager, because LLVM's machine-code pipeline is still only +reachable through it: `tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)`. + +### Linking + +``` +$CC file.o -o file -lm [pkg-config --libs NAME | -lNAME]... +``` + +The system C compiler driver (`cc` by default) is used as the linker. It knows the platform's crt +files, the dynamic loader path, libc and default library paths, which would be a lot to replicate. +`-lm` is always added. For each `link "NAME"` or `-l NAME`, `pkg-config --libs NAME` is tried +first, so `-L` paths and dependent libraries come along. On failure it falls back to `-lNAME`. +On Nix, the cc wrapper turns those `-L` paths into `RPATH` entries, so the result runs without +`LD_LIBRARY_PATH`. The object file is deleted afterwards whether linking succeeded or not. + +### `finch run` + +It compiles to a temporary file (`sys::fs::createTemporaryFile`), runs it with `system()`, deletes +it, and forwards the exit status. If the child died from a signal, it prints +`the program crashed: ` and returns `128 + signo`, like a shell. + +--- + +## 12. Diagnostics + +`fail(line, col, msg)` is `[[noreturn]]`: it prints + +``` +file:line:col: error: msg + line | source text + | ^ +``` + +and exits with status 1. The excerpt is taken from `g_source`, and tabs in the prefix are kept so +the caret lines up. Conventions used across the code base: + +- **Positions:** binary operators report the **operator's** position; calls report the callee's; + type errors in arguments report the **argument's** position. +- **Messages** say what is wrong in plain words, and **how to fix it** when there is an obvious fix + (`use int(...) to convert`, `did you mean ':='?`, `add import "math.h"`). +- `headerHint()` maps about two dozen common libc/libm names to their header, for calls to unknown functions. +- An internal inconsistency, if `verifyModule` ever fails, prints `internal compiler error (please report)` + with the verifier's output and exits with status 2. + +The compiler has no warnings. Everything that would be a warning is either fine or an error. + +--- + +## 13. Build system + +`CMakeLists.txt`: + +- `find_package(LLVM CONFIG)`. **If the `LLVM` target exists (a shared `libLLVM.so`), Finch links that + one library.** This is essential, not cosmetic: `libclang.so` itself links `libLLVM.so`. Linking + Finch against the static component libraries (`libLLVMCore.a`, …) puts a **second copy of LLVM** in + the process. Both copies register the same global objects, and the program crashed on exit in a + global destructor (`std::vector::~vector`, a double free). It also made the binary 82 MB + instead of 11 MB. +- libclang is found with `find_path(clang-c/Index.h)` and `find_library(clang)`, hinted by LLVM's dirs. +- `FINCH_VERSION` comes from `project(VERSION …)` and is printed by `finch version`, along with `LLVM_VERSION_STRING`. +- C++17, `-Wall -Wextra`, and the build is warning-free. + +--- + +## 14. Testing + +`tests/run.sh` (`FINCH=… ` overrides the compiler path): + +- `tests/run/*.fn`: run with `finch run`. Combined stdout+stderr must equal `*.out` exactly. +- `tests/fail/*.fn`: must exit non-zero, and the output must contain the text after `// expect: ` on line 1. + This covers compile errors, runtime panics (with line numbers), header and link failures. + `tests/fail/lib_hint.h` declares a function that exists in no library, to test the link-hint path. + +Current state: **34 passed, 0 failed.** Every new error message should get a `fail/` test, and every +feature a `run/` test. + +--- + +## 15. Semantics vs. C: defined and undefined behavior + +| Situation | C | Finch 1.0 | +|---|---|---| +| Signed integer overflow | UB | **Defined wraparound** (no `nsw` flags) | +| Unsigned overflow | wraps | wraps | +| Integer division by zero | UB | **Compile error** if constant, otherwise **runtime error** | +| `INT_MIN / -1` | UB | **Runtime error** | +| Load or store through null | UB | **Runtime error** for `.value` | +| `x & 1 == 0` | parses as `x & (1 == 0)` | parses as `(x & 1) == 0` | +| Implicit narrowing (`int` → `char`) | silent | **Compile error** | +| Signed/unsigned mixing | silent conversion | **Compile error** unless one side widens | +| Using a variable before it is set | UB | Impossible: every declaration initializes (zero by default) | +| Missing `return` in non-void function | UB if the value is used | **Compile error** | +| Shift by ≥ bit width | UB | **Still poison** (not checked in 1.0) | +| `float` → int out of range | UB | **Still poison** (not checked in 1.0) | +| Dangling pointer to a dead local | UB | **Still UB** (no lifetime checks yet) | +| Order of evaluation | mostly unspecified | Left to right | + +--- + +## 16. Function reference: who does what + +Groups of functions that sound alike, and how they differ. + +### Lexing and parsing + +| Function | Does | Differs from | +|---|---|---| +| `lex()` | text → tokens, sets `newlineBefore` | knows nothing about grammar or types | +| `tokName()` | token kind → human name for errors | | +| `Parser::cur()` / `peekTok()` / `next()` | current token / lookahead without consuming / consume | | +| `accept(k)` vs `expect(k)` | consume if it matches and return a bool / consume or **fail** with "expected …, found …" | | +| `unexpected(ctx)` | fail with "unexpected X (ctx)" at the current token | `expect` names the token it wanted; `unexpected` explains the context | +| `sameLine()` | may the current token continue the expression? | `endOfStatement()` is the statement-level counterpart (requires a newline or `}`) | +| `atDeclaration()` | `int x` / `ptr[...] p` lookahead | `type()` actually consumes a type | +| `typeFromName()` vs `isTypeName()` | name → `Type` / name → bool | | +| `statement()` vs `block()` | one statement / `{ statement* }` with terminator checks and `parenDepth` reset | | +| `callArgs()` | shared by calls and conversions: `( expr, … )` | | +| `primary()` / `postfix()` / `unary()` / `multiplicative()` / `additive()` / `compare()` / `andExpr()` / `orExpr()` | one function per precedence level | | + +### Codegen: types and values + +| Function | Does | Differs from | +|---|---|---| +| `ty(FType)` | Finch type → LLVM type | | +| `zero(FType)` | default value for an uninitialized declaration | | +| `widens(from, to)` | can every value of `from` be represented in `to`? (pure type rule) | `fits()` asks the same about one constant | +| `fits(c, from, to)` | does this literal's value fit in `to`? | | +| `coerce(v, want)` | **implicit** conversion into a known slot, or error | `convert()` is **explicit** `T(x)` and allows lossy casts; `unify()` converts **two** operands toward each other | +| `unify(l, r)` | bring binary operands to a common type | used by `arithOp()` and `compare()` | +| `intCast()` / `intToFloat()` | emit the signedness-correct cast | low-level helpers used by the three above | + +### Codegen: expressions + +| Function | Does | Differs from | +|---|---|---| +| `expr(e)` | r-value: emits code, returns `{value, type, literal}` | `place()` returns an **address** instead of loading | +| `place(e)` | l-value address of a variable, C global or `p.value` | used by `expr()` (then loads), `assign()` (then stores), `addrOf()` (returns as is) | +| `binary()` | dispatch by `BinOp` | | +| `arith()` vs `arithOp()` | `arith()` wraps `arithOp()` and computes the result's `literal` flag | `arithOp()` does `unify` and emits the instruction | +| `compare()` | all comparisons, pointer/str/float/int variants | always returns `bool` and is never a literal | +| `logic()` | `&&`/`\|\|` with branches and a φ | the only place that builds a φ by hand | +| `call()` | resolves a call name to one of the five kinds | | +| `callFinch()` vs `callC()` | see section 8.10 | | +| `print()` | builds a printf format from the types | | +| `addrOf()` | `addr(x)` → `place(x).addr` | | +| `convert()` | `T(x)` | | +| `libc()` | `getOrInsertFunction`: declare-or-reuse an external function | `declare()` creates **Finch** functions with `Function::Create` | +| `cGlobal()` | declare-or-reuse an `extern` global | | + +### Codegen: statements and control + +| Function | Does | Differs from | +|---|---|---| +| `declare()` vs `define()` | signature pass / body pass | | +| `block()` vs `blockBody()` | `block()` pushes and pops a scope around `blockBody()`; `blockBody()` emits statements and rejects unreachable ones | `define()` calls `blockBody()` directly, because parameters and the outer body share one scope | +| `stmt()` | dispatch by `StmtKind`; also handles `break`/`continue` | | +| `varDecl()` / `assign()` | declaration with optional inference / store to an existing place | | +| `addVar()` | create slot, store initial value, register name (with no-shadowing check) | `slot()` only creates the `alloca` | +| `lookup()` | search all scopes, innermost first | | +| `condition(e)` | `coerce(expr(e), bool)` with the message "a condition must be bool" | | +| `ifStmt()` / `whileStmt()` / `forStmt()` / `returnStmt()` | section 8.5 | | +| `terminated()` | does the current block already end in a terminator? | | +| `continueAt(bb, deadEnd)` | move to the merge block, or delete it if unreachable | | +| `newBlock()` | create a basic block in the current function | | + +### Runtime checks + +| Function | Does | +|---|---| +| `panicFn()` | get or create `finch.panic` | +| `panicIf(cond, msg, pos)` | conditional branch to a panic block | +| `checkDivisor(l, r, pos)` | zero and `MIN/-1` checks, or a compile-time error | + +### Driver (`main.cpp`) + +| Function | Does | +|---|---| +| `hostMachine()` | create the `TargetMachine` | +| `optimize()` | O2 pipeline | +| `emitObject()` | write the `.o` | +| `capture()` | run a shell command, collect its output and status | +| `libFlags()` | pkg-config or `-l` for one library | +| `link()` | run the linker, delete the `.o`, explain failures | +| `linkFailed()` / `guessLib()` | translate linker errors into Finch advice | + +--- + +## 17. Extending the compiler + +### A new built-in function (example: `len(str) -> int`) + +1. Add the name to `Codegen::isBuiltin()`, so it cannot be redefined. +2. In `Codegen::call()`, dispatch `if (c.callee == "len") return len(c);`. +3. Implement it: check the argument count, `coerce(expr(arg), Str, …)`, emit `strlen` via `libc()`, + return `{call, I64}`. `strlen` returns `u64`; convert with `intCast` if `int` is wanted. +4. Add `tests/run/len.fn` and its `.out`, plus a `tests/fail/` case for a wrong argument. + +### A new binary operator + +1. Lexer: add a `Tok`, its spelling in the operator `switch`, and `tokName()`. +2. AST: add a `BinOp`. +3. Parser: put it in the right precedence function. +4. Codegen: `opName()`, then emit it in `arithOp()` or `compare()`, and explain bad operands in `badOperands()`. + +### A new statement + +Add a `StmtKind` and node in `ast.h`, recognise it in `Parser::statement()`, and handle it in +`Codegen::stmt()`. If it can transfer control, make sure the result respects `terminated()` and +uses `continueAt()` for its merge block. + +### Coding conventions + +Match the existing style: early `fail()` with a fixing hint, comments only where the *why* is not +obvious, and one test per behavior. + +--- + +## 18. Known limitations and roadmap + +- **No aggregates yet:** `struct`, arrays/slices with `.len`, and C structs by value. These are the next + milestones, in that order: arrays build on structs (pointer + length), and the memory model builds on both. +- **Memory model** (agreed design): values that own heap memory are freed automatically at the end of + the block that owns them, `defer` runs code at block exit, and `free` remains for manual control. + Implementation sketch: a per-block cleanup list, emitted at every exit edge (normal end, `return`, + `break`, `continue`). `blockBody()` and the `loops` stack already know every exit edge. +- **Strings:** concatenation, `len`, `str(x)`. These depend on the memory model. +- **Input:** `input()`. +- **Modules:** `import name` for Finch code. One `.fn` = one program today. +- **Unchecked UB:** shift amounts, float→int range, dangling pointers. Shift and float→int could be + checked like division at small cost. +- **Debug info:** no DWARF yet. `DIBuilder` would hook into `define()` and each statement's `pos`. +- **Targets:** only the host triple, with Linux/x86-64 tested. The code is target-neutral except for + the `cc -E -v` include discovery and `dprintf`. +- **Bootstrapping:** rewriting the compiler in Finch is the long-term goal. Calling the LLVM-C API + already works today: LLVM-C uses opaque handles (`LLVMModuleRef` and so on), which map to Finch's + `ptr`. `examples/llvm.fn` builds and prints a module from Finch (`import "llvm-c/Core.h"`, + `link "LLVM"`). What is still missing for a self-hosted compiler is structs, arrays, strings and file I/O. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md new file mode 100644 index 0000000..12eaaeb --- /dev/null +++ b/docs/en/for-technicians.md @@ -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 compile and run right away +finch build [-o name] compile into a program (default name: the file's name) +finch ir print the LLVM IR (for the curious) +finch version show the version + +options: + -l 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). diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md new file mode 100644 index 0000000..263e39a --- /dev/null +++ b/docs/pl/dla-inzynierow.md @@ -0,0 +1,1136 @@ +# Finch dla inżynierów + +**Kompletny opis wnętrza kompilatora Finch 1.0.** Opisuje, jak kod źródłowy staje się tokenami, +tokeny drzewem AST, a AST kodem LLVM IR, który potem jest optymalizowany, zapisywany do pliku +obiektowego i linkowany. Zawiera dokładne reguły typów, sposób tłumaczenia każdej konstrukcji na +IR (z prawdziwym wyjściem kompilatora), import nagłówków C przez libclang, szczegóły ABI +oraz opis każdej funkcji kompilatora wraz z tym, czym różni się od sąsiednich. + +Zakładam, że znasz C/C++ i widziałeś już LLVM IR. +Język z perspektywy użytkownika opisuje [przewodnik dla techników](dla-technikow.md). + +> 🇬🇧 English version: [for-engineers.md](../en/for-engineers.md) + +--- + +## Spis treści + +1. [Architektura](#1-architektura) +2. [Pliki źródłowe](#2-pliki-źródłowe) +3. [Lekser](#3-lekser) +4. [Gramatyka i parser](#4-gramatyka-i-parser) +5. [AST](#5-ast) +6. [Model semantyczny](#6-model-semantyczny) +7. [System typów](#7-system-typów) +8. [Generowanie kodu](#8-generowanie-kodu) +9. [Kontrole w czasie działania](#9-kontrole-w-czasie-działania) +10. [Współpraca z C przez libclang](#10-współpraca-z-c-przez-libclang) +11. [Optymalizacja, emisja, linkowanie](#11-optymalizacja-emisja-linkowanie) +12. [Diagnostyka](#12-diagnostyka) +13. [System budowania](#13-system-budowania) +14. [Testy](#14-testy) +15. [Semantyka a C: zachowania zdefiniowane i niezdefiniowane](#15-semantyka-a-c-zachowania-zdefiniowane-i-niezdefiniowane) +16. [Spis funkcji: kto co robi](#16-spis-funkcji-kto-co-robi) +17. [Rozbudowa kompilatora](#17-rozbudowa-kompilatora) +18. [Znane ograniczenia i plan](#18-znane-ograniczenia-i-plan) + +--- + +## 1. Architektura + +Finch to klasyczny jednoprzebiegowy front-end przed LLVM. Sprawdzanie typów i generowanie IR +odbywają się w jednym przejściu po AST. Nie ma osobnego przebiegu semantycznego ani własnej +reprezentacji pośredniej. + +``` + plik.fn + │ readFile() src/main.cpp + ▼ + lex() ──────────► std::vector src/lexer.cpp + ▼ + parse() ─────────► Program (AST) src/parser.cpp, src/ast.h + │ ├─ imports ──► importHeaders() ──► CImports src/cimport.cpp (libclang) + │ └─ links + ▼ + hostMachine() ───► llvm::TargetMachine (triple, data layout) + ▼ + generate() ──────► llvm::Module (typy + IRBuilder, potem verifyModule) src/codegen.cpp + ▼ + optimize() ──────► potok PassBuilder default (pomijany przy -O0) + ▼ + emitObject() ────► plik.o (legacy::PassManager + addPassesToEmitFile) + ▼ + link() ──────────► cc plik.o -o plik -lm [-l… | pkg-config --libs …] + ▼ + plik wykonywalny (finch run: uruchamiany z pliku tymczasowego, potem usuwany) +``` + +Decyzje, które kształtują całą resztę: + +- **Szybka porażka.** Pierwszy błąd wypisuje diagnostykę i wywołuje `exit(1)` (`fail()` w `src/error.h`). + Nie ma odtwarzania po błędach, więc AST i codegen nigdy nie widzą częściowo poprawnego wejścia. +- **Jedno przejście.** `Codegen` niesie stan zasięgów, pętli i bieżącej funkcji. Każde wyrażenie + zwraca jednocześnie wartość IR i swój typ Fincha (`Value_`). +- **Niech LLVM robi robotę.** Zmienne lokalne to `alloca`, a `mem2reg`/SROA zamieniają je na SSA. + Finch nigdy sam nie buduje SSA ani węzłów φ, z wyjątkiem `&&`/`||`. +- **Systemowy toolchain C jest środowiskiem uruchomieniowym.** `printf`, `strcmp`, `dprintf`, `exit` + pochodzą z libc. Finch nie ma własnej biblioteki uruchomieniowej. + +--- + +## 2. Pliki źródłowe + +| Plik | Linie | Odpowiedzialność | +|---|---|---| +| `src/error.h` | ~25 | `fail(line, col, msg)`: wypisuje `plik:linia:kol: error:` i fragment kodu z `^`, kończy proces. Trzyma `g_file`, `g_source`. | +| `src/lexer.h/.cpp` | ~240 | enum `Tok`, `Token`, `lex()`, `tokName()` | +| `src/ast.h` | ~235 | `Type`, wszystkie węzły `Expr`/`Stmt`, `FnDecl`, `Import`, `Link`, `Program` | +| `src/parser.h/.cpp` | ~430 | Parser zstępujący `Parser`, `typeFromName()` | +| `src/cimport.h/.cpp` | ~280 | Import nagłówków przez libclang: `importHeaders()` → `CImports` (funkcje, stałe, zmienne globalne) | +| `src/codegen.h/.cpp` | ~900 | `Codegen`: sprawdzanie typów + generowanie LLVM IR, `generate()` | +| `src/main.cpp` | ~260 | Sterownik: CLI, maszyna docelowa, optymalizacja, emisja, linkowanie, analiza błędów linkera, `run` | +| `tests/run.sh` | | Testy wzorcowego wyjścia i oczekiwanych błędów | + +--- + +## 3. Lekser + +`lex(src, file)` przechodzi raz, liniowo, przez źródło i zwraca `std::vector` +zakończony `Tok::End`. + +```cpp +struct Token { + Tok kind; + std::string text; // treść identyfikatora / literału (sekwencje \ już zdekodowane) + int line, col; // od 1 + bool newlineBefore; // między poprzednim tokenem a tym był znak nowej linii +}; +``` + +### Nowe linie jako koniec instrukcji + +Finch nie ma średników. Zamiast emitować tokeny `NEWLINE`, jak robią to wewnętrznie Python i Go, +lekser **oznacza** każdy token flagą `newlineBefore`. Co ona znaczy, decyduje parser (rozdział 4). +Dzięki temu gramatyka nie zawiera tokenów nowej linii, które trzeba by wszędzie pomijać: +w listach argumentów, między `}` a `else` i tak dalej. + +### Tokeny + +- **Pozycje:** `line` i `col` liczone są od 1, a `col` liczy **znaki**, nie bajty. + Bajty kontynuacji UTF-8 jej nie zwiększają, więc `^` trafia we właściwe miejsce także w linijkach z `"Błąd"`. + Nieoczekiwany znak spoza ASCII jest pokazywany w całości (`'ż'`), z podpowiedzią, że nazwy są tylko ASCII. +- **Identyfikatory / słowa kluczowe:** `[A-Za-z_][A-Za-z0-9_]*`. Słowa kluczowe: `fn return if else while for in break continue true false null import link`. + **Nazwy typów nie są słowami kluczowymi.** `int`, `u8`, `ptr`… są leksowane jako `Ident`, a parser rozpoznaje je przez `typeFromName()`. + Lekser nie zależy więc od listy typów, a `u8(x)` parsuje się jak zwykłe wywołanie. +- **Liczby całkowite:** dziesiętne albo szesnastkowe `0x`, z separatorami `_` (usuwanymi z `text`). + Zera wiodące nic nie zmieniają: Finch nie ma ósemkowych, `010` to 10. Wartość liczy parser (`strtoull`, podstawa 10 lub 16). +- **Liczby zmiennoprzecinkowe:** `cyfry.cyfry`. Kropka należy do liczby tylko wtedy, gdy **po niej jest cyfra**, + więc `0..10` to `Int DotDot Int`, a nie `Float(0.) Dot …`. +- **Teksty / znaki:** `"…"` i `'…'`. Sekwencje `\n \t \r \0 \\ \" \'` są dekodowane do `text`. + Nieznana sekwencja, niezamknięty literał albo nowa linia w środku literału to błąd. Literał znakowy musi dać dokładnie jeden bajt. +- **Operatory:** zasada najdłuższego dopasowania: `->` przed `-`, `<<` i `<=` przed `<`, `&&` przed `&`, `..` przed `.`, `:=` (samotny `:` to błąd z podpowiedzią `:=`). +- **Komentarze:** `//` do końca linii, `/* … */` (bez zagnieżdżania; niezamknięty to błąd). + +--- + +## 4. Gramatyka i parser + +`Parser` (`src/parser.cpp`) to ręcznie pisany parser zstępujący z jednym tokenem wyprzedzenia +(`cur()`), a w dwóch miejscach z drugim (`peekTok()`). + +### Gramatyka (EBNF) + +```ebnf +program = { import | link | function } ; +import = "import" STRING ; +link = "link" STRING ; +function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ; +param = type IDENT ; +type = TYPENAME | "ptr" "[" type "]" ; (* TYPENAME: int float bool char str ptr i8..u64 f32 f64 *) + +block = "{" { statement TERMINATOR } "}" ; +statement = if | while | for | return | "break" | "continue" | block + | type IDENT [ "=" expr ] (* deklaracja *) + | IDENT ":=" expr (* deklaracja z wnioskowaniem *) + | target assignop expr (* target: IDENT albo postfix kończący się .value *) + | call ; +if = "if" expr block [ "else" ( if | block ) ] ; +while = "while" expr block ; +for = "for" IDENT "in" expr ".." expr block ; +return = "return" [ expr ] ; (* expr tylko w tej samej linii *) +assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; + +expr = or ; +or = and { "||" and } ; +and = cmp { "&&" cmp } ; +cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ; +add = mul { ( "+" | "-" | "|" | "^" ) mul } ; +mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ; +unary = ( "-" | "!" | "~" ) unary | postfix ; +postfix = primary { "." IDENT } ; +primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null" + | IDENT [ "(" [ expr { "," expr } ] ")" ] + | "(" expr ")" ; +``` + +### Reguła końca instrukcji + +`TERMINATOR` nie jest tokenem. Po każdej instrukcji `endOfStatement()` wymaga, żeby następny +token był `}`, końcem pliku albo miał `newlineBefore == true`. W przeciwnym razie zgłasza błąd +`put each statement on its own line`. + +Wewnątrz wyrażeń `sameLine()` decyduje, czy operator binarny, `.` albo `(` wywołania +**kontynuuje** bieżące wyrażenie: + +```cpp +bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; } +``` + +Czyli: + +```c +a := 1 + // '+' jest w tej linii → wyrażenie przechodzi do następnej + 2 +b := (1 // w nawiasach (parenDepth > 0) nowe linie są ignorowane + + 2) +c := 1 +-5 // '-' zaczyna nową linię → instrukcja skończyła się na '1'; "-5" to wtedy błąd +``` + +`parenDepth` rośnie wokół grup `( … )`, list argumentów i list parametrów. Przy wejściu do bloku +`{ }` jest **zapamiętywany i zerowany**, więc blok wewnątrz nawiasów (dziś to się nie zdarza) +i tak używałby reguł linii. + +`(` wywołania musi być w tej samej linii co nazwa funkcji. Inaczej `f⏎(x)` po cichu stałoby się wywołaniem. + +### Priorytety: jak w Go, nie jak w C + +| Poziom | Operatory | +|---|---| +| 5 | `*` `/` `%` `<<` `>>` `&` | +| 4 | `+` `-` `\|` `^` | +| 3 | `==` `!=` `<` `<=` `>` `>=` | +| 2 | `&&` | +| 1 | `\|\|` | + +Wszystkie operatory binarne są lewostronnie łączne. Porównania też (`a < b < c` się sparsuje), +ale sprawdzanie typów odrzuci potem porównanie `bool < int`. C stawia `&`, `^`, `|` **poniżej** `==`, +przez co `x & MASKA == 0` znaczy `x & (MASKA == 0)`. Finch przyjmuje tabelę z Go, co usuwa całą tę klasę błędów. + +### Jak rozpoznawane są instrukcje + +`statement()` rozgałęzia się po pierwszym tokenie: + +1. Słowa kluczowe (`if`, `while`, `for`, `return`, `break`, `continue`, `{`) są obsługiwane wprost. + `fn`, `import` i `link` wewnątrz bloku dają celowane komunikaty. +2. **Deklaracja z typem:** `atDeclaration()` zwraca prawdę, gdy bieżący token jest nazwą typu, + a następny identyfikatorem (`int x`), albo gdy to `ptr`, a po nim `[` (`ptr[int] p`). + To jedyne miejsce wymagające dwóch tokenów wyprzedzenia. +3. **Deklaracja z wnioskowaniem:** `IDENT`, a po nim `:=`. +4. W pozostałych przypadkach parser **najpierw parsuje całe wyrażenie**, a potem patrzy na następny + token. Jeśli to operator przypisania w tej samej linii, wyrażenie staje się celem przypisania. + Musi być `VarExpr` albo `MemberExpr`, inaczej: `only a variable or p.value can be assigned to`. + Takie podejście („najpierw wyrażenie, potem decyzja”) obsługuje dowolne formy l-wartości + (`pp.value.value = 1`) bez osobnej gramatyki l-wartości. +5. Samodzielne wyrażenie jako instrukcja musi być `CallExpr`. Wszystko inne daje + `this value is computed but never used`. + +### Poziom pliku + +`program()` przechodzi przez `import`, `link` i `fn`. Element zaczynający się nazwą typu dostaje +podpowiedź `functions start with 'fn', like: fn add(int a, int b) -> int { ... }`, co łapie +definicje funkcji pisane jak w C. Argument `link` jest walidowany: `"libfoo.so"` albo ścieżka +są odrzucane z podpowiedzią samej nazwy. + +--- + +## 5. AST + +Wszystkie węzły są w `src/ast.h`. Wyrażenia i instrukcje to małe hierarchie klas z jawnym +znacznikiem `kind`. Kod je przetwarzający robi `switch` po `kind` i `static_cast`, co pozwala +obejść się bez RTTI i `dynamic_cast` i sprawia, że rozgałęzienia są oczywiste. + +```cpp +struct Expr { ExprKind kind; Pos pos; virtual ~Expr(); }; +using ExprPtr = std::unique_ptr; +``` + +| ExprKind | Węzeł | Pola | +|---|---|---| +| `Int` | `IntExpr` | `long long value` | +| `Float` | `FloatExpr` | `double value` | +| `Bool` / `Char` / `Str` | `BoolExpr` / `CharExpr` / `StrExpr` | `value` | +| `Null` | `NullExpr` | | +| `Var` | `VarExpr` | `name` | +| `Unary` | `UnaryExpr` | `char op` (`-` `!` `~`), `operand` | +| `Binary` | `BinaryExpr` | `BinOp op`, `lhs`, `rhs` | +| `Call` | `CallExpr` | `std::string callee`, `args` | +| `Member` | `MemberExpr` | `obj`, `field` (w 1.0 ma sens tylko `value`) | + +| StmtKind | Węzeł | Pola | +|---|---|---| +| `Block` | `BlockStmt` | `std::vector body` | +| `VarDecl` | `VarDeclStmt` | `hasType`, `type`, `name`, `init` (może być null, gdy `hasType`) | +| `Assign` | `AssignStmt` | `target` (Var/Member), `char op` (`= + - * / %`), `value` | +| `Expr` | `ExprStmt` | `expr` (zawsze wywołanie) | +| `If` | `IfStmt` | `cond`, `then`, `otherwise` (null, `BlockStmt` albo zagnieżdżony `IfStmt` dla `else if`) | +| `While` | `WhileStmt` | `cond`, `body` | +| `For` | `ForStmt` | `var`, `from`, `to`, `body` | +| `Return` | `ReturnStmt` | `value` (może być null) | +| `Break` / `Continue` | | | + +Wywołania przechowują funkcję **po nazwie**, a nie jako wyrażenie. Finch nie ma funkcji jako +wartości, a rozwiązywanie nazw dopiero w codegenie pozwala, żeby jeden `CallExpr` oznaczał +funkcję wbudowaną (`print`, `addr`), konwersję (`u8`), funkcję Fincha albo funkcję C (rozdział 8.10). + +### Przykład + +```c +x += add(x, 2) +``` + +parsuje się do: + +``` +AssignStmt (op '+') +├─ target: VarExpr "x" +└─ value: CallExpr "add" + ├─ VarExpr "x" + └─ IntExpr 2 +``` + +**W AST nie ma węzła „load”.** To, czy `VarExpr` znaczy „adres x”, czy „wartość w x”, zależy +od miejsca, w którym występuje. Decyduje o tym codegen (`place()` kontra `expr()`, rozdział 8.2). + +### `Type` + +```cpp +struct Type { + enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null }; + Kind kind; + std::shared_ptr elem; // tylko Ptr: typ wskazywany; null = `ptr` bez typu +}; +``` + +- `int` i `i64` to **ten sam typ** (`I64`), a `name()` wypisuje go jako `int`. Analogicznie `float` to `F64`. +- `Null` to typ wyłącznie literału `null`. Żadna zmienna nie może go mieć. +- `Void` to „typ” wywołań, które nic nie zwracają, i funkcji bez `->`. +- Równość jest strukturalna (`ptr[ptr[int]] == ptr[ptr[int]]`). +- Rodzaje są ułożone tak, że `isInt()` to `I8..U64`, `isSigned()` to `I8..I64`, a `isUnsigned()` to `U8..U64`. + +--- + +## 6. Model semantyczny + +Całe sprawdzanie semantyczne odbywa się w `Codegen` podczas emisji IR. + +### Funkcje + +`Codegen::run()` robi dwa przejścia po `Program::fns`: + +1. `declare()` tworzy każdą `llvm::Function` (tylko sygnaturę), więc **kolejność definicji nie ma + znaczenia** i działa rekurencja wzajemna. Sprawdza też: duplikaty nazw, nazwy wbudowane i sygnaturę + `main` (bez parametrów; nic nie zwraca, zwraca `int` albo `i32`). +2. `define()` generuje ciała. + +Bez `main` kompilacja kończy się błędem `there is no main function`. + +### Zasięgi + +`scopes` to `std::vector>`. Nowa mapa jest wkładana dla +parametrów funkcji, dla każdego bloku `{ }` i dla każdej pętli `for` (trzyma zmienną pętli). + +```cpp +struct Var { AllocaInst *slot; FType type; bool readonly; }; +``` + +**Bez przesłaniania:** `addVar()` odrzuca nazwę, którą `lookup()` znajdzie w *dowolnym* +zewnętrznym zasięgu, a nie tylko w bieżącym. Odrzuca też nazwy funkcji i wbudowanych. +To świadoma reguła prostoty: w całym ciele funkcji nazwa znaczy jedną rzecz. + +**Tylko do odczytu:** zmienne `for` mają `readonly`. Przypisanie do nich albo `addr()` na nich +to błąd, więc licznika pętli nie da się zmienić za jej plecami. + +### Rozwiązywanie identyfikatorów + +Dla `VarExpr name` w pozycji wartości kolejność jest taka: + +1. zmienna lokalna (`lookup`), +2. stała C (`cimports.consts`: wartości enum, liczby z `#define`), +3. zmienna globalna C (`cimports.globals`: `stdout`, …), +4. błąd `there is no variable named 'name'`. + +Dla wywołań: `print` → `addr` → nazwa typu (konwersja) → funkcja Fincha → funkcja C → błąd. +Ponieważ funkcje Fincha są sprawdzane przed funkcjami C, **funkcja Fincha przesłania funkcję C +o tej samej nazwie**. Funkcja Fincha jest emitowana jako `finch.`, więc oba symbole +współistnieją przy linkowaniu. + +--- + +## 7. System typów + +### Reprezentacja w LLVM + +| Finch | LLVM | Uwagi | +|---|---|---| +| `bool` | `i1` | | +| `char`, `i8`, `u8` | `i8` | znakowość jest w typie Fincha, nie w LLVM | +| `i16`, `u16` | `i16` | | +| `i32`, `u32` | `i32` | | +| `int`/`i64`, `u64` | `i64` | | +| `f32` | `float` | | +| `float`/`f64` | `double` | | +| `str` | `ptr` | napis C zakończony zerem | +| `ptr`, `ptr[T]` | `ptr` | LLVM 21 ma nieprzezroczyste wskaźniki; typ wskazywany istnieje tylko w typie Fincha | +| `null` | `ptr` | `ConstantPointerNull` | +| nic | `void` | | + +Liczby całkowite w LLVM nie mają znaku. Każda operacja, w której znak ma znaczenie +(`sdiv`/`udiv`, `srem`/`urem`, `ashr`/`lshr`, `icmp slt`/`ult`, `sext`/`zext`, +`sitofp`/`uitofp`, `fptosi`/`fptoui`), wybiera instrukcję na podstawie typu **Fincha**. + +### Wartości w codegenie + +```cpp +struct Value_ { + Value *v; // wartość IR + FType type; // jej typ w Finchu + bool literal; // liczba wpisana w kodzie (albo zaimportowana stała C) +}; +``` + +### Literały („stałe bez typu”) + +`literal` jest ustawiane dla `IntExpr`, `FloatExpr` i zaimportowanych stałych C. Przechodzi +przez minus jednoargumentowy i przez arytmetykę **między dwoma literałami**, która zwija się do +stałej (`arith()` ustawia `literal = l.literal && r.literal && isa(wynik)`). **Jawna +konwersja je kasuje:** `u8(250)` to stała typu `u8`, a nie literał. + +Literał całkowity może stać się **dowolnym typem całkowitym, w którym się mieści** (`fits()` +sprawdza zakres, uwzględniając znakowość źródłowego literału). Dzięki temu `u8 b = 200`, +`malloc(4 * 10)` (do `u64`) i `glClear(GL_COLOR_BUFFER_BIT)` (makro `i64` do parametru `u32`) +działają bez rzutowań, a `u8 b = 300` jest odrzucane komunikatem `the number 300 doesn't fit in u8`. +Literał `f64` może stać się `f32` (`f32 h = 0.5`). + +Czemu nie wystarczy sprawdzać `isa`? `IRBuilder` zwija stałe, więc `u8(250)` też jest +`ConstantInt`. Gdyby regułą było „jest stałą”, `u8(250) + 10` po cichu rozszerzyłoby się do +`int` i dało 260, a ten sam kod ze zmienną `u8` zawinąłby się do 4. Jawna flaga sprawia, że +zachowanie zależy od tego, co *napisałeś*, a nie od tego, co zdołał zobaczyć optymalizator. + +### Konwersje niejawne: `coerce(value, want, pos, what)` + +Używane wszędzie, gdzie wartość trafia do miejsca o znanym typie: inicjalizatory, przypisania, +argumenty, wartości zwracane, warunki (`want = bool`), granice zakresu (`want = int`). +Dozwolone, po kolei: + +1. identyczne typy, +2. `null` → dowolny `ptr…` albo `str`, +3. dowolny `ptr…` albo `str` → `ptr` bez typu, a `ptr` bez typu → dowolny `ptr[T]` (reguły `void*`; tak działa `ptr[i32] n = malloc(4)`), +4. int → int, gdy wartość jest mieszczącym się literałem **albo** `widens(from, to)`: + - ta sama znakowość i ściśle więcej bitów, **albo** + - bez znaku → ze znakiem o ściśle większej liczbie bitów, + - (ze znakiem → bez znaku nigdy się nie rozszerza), +5. dowolna całkowita → dowolna zmiennoprzecinkowa (`sitofp`/`uitofp`), +6. `f32` → `f64` (`fpext`), +7. literał `f64` → `f32` (`fptrunc`, zwijane do stałej). + +Wszystko inne to błąd. Komunikat podpowiada jawną konwersję (`use i32(...) to convert`) +i ostrzega, gdy obetnie wartość (`it cuts off the fraction`). + +### Argumenty operatorów binarnych: `unify(l, r)` + +Przed arytmetyką i porównaniem obie strony są sprowadzane do jednego typu: + +1. ten sam typ → gotowe, +2. obie całkowite: strona-literał dopasowuje się do drugiej, jeśli się mieści; w przeciwnym razie + rozszerzana jest strona, która `widens()` do drugiej; w przeciwnym razie **błąd** (`i32` + `u32`: + żadna nie rozszerza się do drugiej, a Finch nie zgaduje), +3. całkowita i zmiennoprzecinkowa → całkowita zamieniana na ten typ zmiennoprzecinkowy, +4. `f32` i `f64` → obie `f64`. + +Typ wyniku arytmetyki to ten wspólny typ. **Nie ma promocji całkowitych jak w C:** +`u8 + u8` liczy się w `u8` i się zawija. To celowe: tego wymaga typ z rozmiarem, i tak samo działają Rust i Zig. + +### Konwersje jawne: `convert(call, to)` + +`T(x)` parsuje się jako wywołanie o nazwie typu i jest tłumaczone tutaj: + +| Z → Na | Instrukcja | +|---|---| +| int/char → int/char | `CreateIntCast(signed = from.isSigned())` → `trunc` / `sext` / `zext` | +| bool → int/char | `zext` | +| float → int | `fptosi` / `fptoui` (wg znakowości celu) | +| int → float | `sitofp` / `uitofp` (wg znakowości źródła) | +| float → float | `fpext` / `fptrunc` (`CreateFPCast`) | +| int → bool | `icmp ne x, 0` | +| ptr/str/null → ptr…, ptr → str | brak instrukcji (wszystko to `ptr`) | + +`fptosi` dla wartości spoza zakresu to w LLVM poison (w C: UB). Finch 1.0 tego nie sprawdza, zobacz rozdział 15. + +### Porównania: `compare()` + +- **Wskaźniki:** jeśli którakolwiek strona jest `ptr…` albo `null`, dozwolone są tylko `==`/`!=`, + kompilowane do `icmp eq/ne ptr`. `str` porównywany z `null` też porównuje adresy. +- **`str == str`:** wywołanie `strcmp`, a potem `icmp eq/ne i32 %r, 0` (porównanie treści). `<`/`>` na `str` są odrzucane. +- **Liczby zmiennoprzecinkowe:** `oeq olt ole ogt oge` (uporządkowane: fałsz dla NaN) oraz `une` dla `!=` + (nieuporządkowane: `NaN != NaN` to prawda). Tak samo jak w C. +- **Całkowite / char:** `eq ne` oraz `slt sle sgt sge` albo `ult ule ugt uge` wg znakowości. `char` porównywany jest bez znaku. +- **bool:** tylko `==`/`!=`. + +--- + +## 8. Generowanie kodu + +`Codegen` posiada `llvm::IRBuilder<> b`, `Module`, tablicę funkcji `fns` +(`nazwa → {Function*, const FnDecl*}`), `scopes`, stos `loops` i `curFn`. + +### 8.1 Zmienne to miejsca na stosie + +Każda zmienna, **łącznie z parametrami**, dostaje `alloca` tworzoną przez `slot()`. +`slot()` zawsze wstawia ją **na początek bloku wejściowego**, drugim `IRBuilder` ustawionym na +`entry.begin()`, niezależnie od miejsca deklaracji. Tylko takie `alloca` `mem2reg`/`SROA` umieją +zamienić na rejestry SSA, więc wyjście O2 nie ma ruchu na stosie dla wartości skalarnych. +Ponieważ każde nowe miejsce trafia na sam początek, w IR z `-O0` `alloca` są w **odwrotnej kolejności +deklaracji** (`%b2` przed `%a1` niżej). + +Parametry są kopiowane do swoich miejsc na wejściu do funkcji (`store %a, %a1`), dzięki czemu +parametry są zwykłymi, modyfikowalnymi zmiennymi lokalnymi. + +### 8.2 Load i store: `expr()` kontra `place()` + +To jest serce generowania dostępów do pamięci: + +- `place(e)` zwraca **`Place {Value *addr; FType type}`**, czyli *adres* l-wartości. + - `VarExpr` → `alloca` zmiennej (albo `GlobalVariable` zmiennej globalnej C). + - `MemberExpr` `p.value` → `expr(p)` (które **ładuje wskaźnik** z miejsca p) plus kontrola null + (rozdział 9). Adresem jest sam załadowany wskaźnik, a typem `*p.type.elem`. +- `expr(e)` zwraca **`Value_`**, czyli r-wartość. Dla `Var` i `Member` woła `place()`, a potem + emituje **jeden `load`** typu `ty(place.type)` z `place.addr`. + +Instrukcja `load` dla identyfikatora powstaje więc w `Codegen::expr()`, w przypadku `ExprKind::Var`, +jako `b.CreateLoad(ty(pl.type), pl.addr, v.name)`. Nazwa wzięta od zmiennej sprawia, że IR z `-O0` +pokazuje `%x1`, `%x2` itd. (LLVM dokleja numery do powtórzonych nazw). `p.value` kosztuje **dwa** +loady: wskaźnika i wartości wskazywanej. `pp.value.value` kosztuje trzy. + +Instrukcje store powstają w dokładnie trzech miejscach: `addVar()` (inicjalizacja), `assign()` i krok pętli `for`. + +### 8.3 Przypisanie i kolejność obliczeń + +```c +x += add(x, 2) +``` + +`assign()` **najpierw** liczy `place(target)`. Dla operatorów złożonych potem **ładuje bieżącą +wartość**, następnie oblicza prawą stronę, wywołuje `arith()`, robi `coerce()` z powrotem do typu +celu i zapisuje. Prawdziwe wyjście z `-O0`: + +```llvm +define i32 @main() { +entry: + %x = alloca i64, align 8 + store i64 5, ptr %x, align 8 ; x := 5 + %0 = load i64, ptr %x, align 8 ; bieżące x (dla +=) + %x1 = load i64, ptr %x, align 8 ; argument x + %1 = call i64 @finch.add(i64 %x1, i64 2) + %2 = add i64 %0, %1 + store i64 %2, ptr %x, align 8 + %x2 = load i64, ptr %x, align 8 + %3 = call i32 (ptr, ...) @printf(ptr @fmt, i64 %x2) + ret i32 0 +} +``` + +Kolejność obliczeń w Finchu jest **zawsze od lewej do prawej**: cel, potem argumenty operatora +binarnego od lewej, potem argumenty wywołania od lewej. C zostawia większość tego nieokreśloną. +Zauważ, że w `x += f()` stara wartość `x` jest czytana **przed** wywołaniem `f()`. Gdyby `f` +zmieniła `x` przez wskaźnik, ta zmiana zostałaby nadpisana. To to samo co `x = x + f()` +czytane od lewej do prawej. + +Po O2 cały program powyżej zwija się do `printf(@fmt, i64 12)`. + +### 8.4 Funkcje, linkowanie i `main` + +```llvm +define internal i64 @finch.add(i64 %a, i64 %b) { +entry: + %b2 = alloca i64, align 8 + %a1 = alloca i64, align 8 + store i64 %a, ptr %a1, align 8 + store i64 %b, ptr %b2, align 8 + %a3 = load i64, ptr %a1, align 8 + %b4 = load i64, ptr %b2, align 8 + %0 = add i64 %a3, %b4 + ret i64 %0 +} +``` + +- Funkcje użytkownika nazywają się **`finch.`** i mają **wiązanie wewnętrzne** (internal linkage). + Kropka nie może wystąpić w identyfikatorze C, więc funkcja Fincha nigdy nie zderzy się z symbolem + libc ani biblioteki, a wiązanie wewnętrzne pozwala LLVM swobodnie je wstawiać (inline), + specjalizować albo usuwać. W C funkcja bez `static` jest zewnętrzna i musi zostać zachowana + tak, jak ją napisano. To prawdopodobnie jeden z powodów, dla których rekurencyjne `fib(40)` + działa odrobinę szybciej niż ten sam kod C pod `clang -O2` (ok. 0,19 s wobec 0,23 s na maszynie autora). +- `main` jest emitowane jako **`i32 @main()` z wiązaniem zewnętrznym**. `fn main()` zwraca `i32 0` + na każdej ścieżce wyjścia, także przy jawnym `return`. `fn main() -> int` przycina swój wynik + `i64` przez `CreateIntCast(…, i32, signed)`. +- Brakujący return: jeśli po ciele funkcji bieżący blok nie ma terminatora, funkcje `void` dostają + `ret void` (albo `ret i32 0` dla `main`). Funkcje zwracające wartość dostają błąd kompilacji: + `function 'f' can reach its end without returning int`. Sprawdza się to na faktycznie zbudowanym + CFG (rozdział 8.5), więc `if/else` zwracające wartość w obu gałęziach jest akceptowane bez dodatkowej analizy. +- Arytmetyka jest emitowana **bez flag `nsw`/`nuw`**, więc przepełnienie liczb całkowitych to + zdefiniowane zawinięcie w kodzie uzupełnień do dwóch (rozdział 15). + +### 8.5 Sterowanie przebiegiem i stan „zakończony” + +Blok, do którego wstawia builder, jest jedynym źródłem prawdy o osiągalności: + +```cpp +bool terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; } +``` + +Po `return`, `break` albo `continue` bieżący blok ma terminator. +`blockBody()` sprawdza `terminated()` **przed każdą instrukcją**. Jeśli jest ustawiony, instrukcja +jest nieosiągalna i to błąd kompilacji (`this code can never run`). Martwe bloki dla kodu po skoku nigdy nie powstają. + +`continueAt(bb, deadEnd)` jest używane po `if` i `while`. Jeśli blok łączący `bb` **nie ma +poprzedników** (wszystkie ścieżki wróciły albo przerwały), zostaje usunięty, a builder zostaje +w `deadEnd`, który jest zakończony. Stan „zakończony” propaguje się więc na zewnątrz przez +zagnieżdżone konstrukcje i widzi go sprawdzanie brakującego returna. + +**`if / else if / else`:** bloki `then`, `else` (jeśli jest) i `endif`. `else if` to `IfStmt` +w `otherwise`, generowany rekurencyjnie wewnątrz bloku `else`. + +**`while`:** bloki `while.cond`, `while.body`, `while.end`. Jeśli warunek zwinie się do stałej +`true`, emitowany jest bezwarunkowy `br`, więc `while.end` ma poprzedników tylko przez `break`. +`while true` bez `break` zostawia więc funkcję „zakończoną”, a funkcja, której ostatnią instrukcją +jest taka pętla, nie potrzebuje końcowego `return`. + +**`for i in a..b`:** `a` i `b` są obliczane **raz**, przed pętlą, i zamieniane na `int`. +Bloki: `for.cond` (`icmp slt i, end`), `for.body`, `for.step` (`i + 1`), `for.end`. `continue` +skacze do `for.step`, a nie do `for.cond`, więc inkrementacja nigdy nie jest pomijana. +Zmienna pętli żyje we własnym zasięgu wokół pętli. + +```llvm + store i64 0, ptr %i, align 8 + br label %for.cond +for.cond: + %3 = load i64, ptr %i, align 8 + %4 = icmp slt i64 %3, 2 + br i1 %4, label %for.body, label %for.end +for.body: + ... + br label %for.step +for.step: + %6 = load i64, ptr %i, align 8 + %7 = add i64 %6, 1 + store i64 %7, ptr %i, align 8 + br label %for.cond +for.end: +``` + +**`break` / `continue`:** `loops` to stos par `{continueTo, breakTo}`. `while` odkłada +`{while.cond, while.end}`, a `for` odkłada `{for.step, for.end}`. Poza pętlą to błąd. + +### 8.6 Skrócone `&&` / `||`: jedyne ręcznie budowane φ + +```c +fn check(int a, int b) -> bool { + return a > 0 && b > 0 +} +``` + +```llvm + %a3 = load i64, ptr %a1, align 8 + %0 = icmp sgt i64 %a3, 0 + br i1 %0, label %and.rhs, label %and.end +and.rhs: + %b4 = load i64, ptr %b2, align 8 + %1 = icmp sgt i64 %b4, 0 + br label %and.end +and.end: + %2 = phi i1 [ false, %entry ], [ %1, %and.rhs ] +``` + +`logic()` zapamiętuje blok, w którym lewa strona się **skończyła** (`from`), i blok, w którym +skończyła się prawa (`rhsEnd`). To niekoniecznie bloki, w których obliczanie się zaczęło, bo +zagnieżdżone `&&` tworzy własne bloki. φ używa `!isAnd` jako stałej skrótu (`false` dla `&&`, `true` dla `||`). + +### 8.7 `print` + +`print(a, b, …)` kompiluje się do **jednego wywołania `printf`**. Format jest budowany w czasie +kompilacji z typów argumentów, łączony spacjami i zakończony `\n`: + +| Typ Fincha | Format | Konwersja argumentu | +|---|---|---| +| całkowite ze znakiem | `%lld` | `sext` → `i64` | +| całkowite bez znaku | `%llu` | `zext` → `i64` | +| `f32`, `f64` | `%g` | `fpext` → `double` | +| `char` | `%c` | `zext` → `i32` | +| `str` | `%s` | bez zmian | +| `bool` | `%s` | `select i1, "true", "false"` | +| `ptr…` | `%p` | bez zmian (glibc wypisuje `(nil)` dla null) | +| `null` | dosłowny tekst `null` | brak | + +Format jest generowany, więc teksty użytkownika zawsze idą jako argumenty `%s`, nigdy jako +format. `%` w tekście użytkownika jest bezpieczne. O2 często zamienia `printf("%s\n", s)` na `puts(s)`. + +### 8.8 Teksty + +Literał tekstowy to prywatna stała globalna `unnamed_addr` (`CreateGlobalString`), a jego +wartością jest `ptr` na nią. Optymalizator może scalać identyczne literały. Zmienna `str` +zadeklarowana bez wartości wskazuje na wspólną stałą `""`. W 1.0 teksty są **niezmienne +i statyczne**. Nie ma sklejania ani alokacji (rozdział 18). + +### 8.9 `addr()` i wskaźniki + +`addrOf()` wymaga argumentu `Var` albo `Member` i zwraca `place(arg).addr` z typem `ptr[T]`. +Nie ma żadnego load: `addr(x)` *to jest* `alloca` zmiennej x. Takiej `alloca` mem2reg nie zamieni +na rejestr, co jest spodziewanym kosztem pobrania adresu. `addr(p.value)` daje wskaźnik już +zapisany w `p`, po kontroli null. `addr` zmiennej `for` jest odrzucane (jest tylko do odczytu). + +### 8.10 Wywołania: `callFinch()` kontra `callC()` + +Obie sprawdzają liczbę argumentów i robią `coerce()` każdego argumentu do typu parametru. Różnice: + +| | `callFinch()` | `callC()` | +|---|---|---| +| Wołana funkcja | `finch.`, utworzona w `declare()` | `getOrInsertFunction(nazwa, …)`: deklarowana leniwie przy pierwszym użyciu, potem używana ponownie | +| Zmienna liczba argumentów | nigdy | obsługiwana; dodatkowe argumenty dostają domyślne promocje C | +| Atrybuty parametrów | niepotrzebne | `signext`/`zeroext` dla parametrów i wartości zwracanych węższych niż 32 bity, na deklaracji i w miejscu wywołania | +| Nieobsługiwane sygnatury | niemożliwe | odrzucane z powodem zapisanym przez cimport (`passes a C struct by value`, …) | +| Wywołanie `main` | odrzucane | nie dotyczy | + +Domyślne promocje dla części zmiennej: `f32` → `double`; `bool`, `char` i liczby całkowite +węższe niż 32 bity → `i32` (z rozszerzeniem znaku albo zerami). Wskaźniki, `str` i liczby +32/64-bitowe przechodzą bez zmian. To odwzorowuje reguły C, których oczekuje `va_arg` w `printf`. + +Po co `signext`/`zeroext`: na x86-64 System V clang przekazuje `char`/`short`/`_Bool` już +rozszerzone do 32 bitów i tak je oznacza. Niektóre funkcje na tym polegają. Bez atrybutu LLVM +zostawiłby górne bity nieokreślone, co byłoby niezgodnością ABI z kodem C skompilowanym clangiem. + +--- + +## 9. Kontrole w czasie działania + +Finch usuwa dwa najczęstsze źródła niezdefiniowanego zachowania w C, które da się tanio sprawdzić. +Wszystkie kontrole korzystają z jednej funkcji pomocniczej na moduł: + +```llvm +; Function Attrs: cold noinline noreturn +define internal void @finch.panic(ptr %0, i64 %1) #0 { +entry: + %2 = call i32 (i32, ptr, ...) @dprintf(i32 2, ptr @panic.fmt, ptr @panic.file, i64 %1, ptr %0) + call void @exit(i32 1) + unreachable +} +``` + +`panicFn()` tworzy ją przy pierwszym użyciu. `panicIf(cond, msg, pos)` emituje +`br cond, %panic, %ok`, wywołanie `finch.panic(msg, linia)` i `unreachable` w `%panic`, +a dalej kontynuuje w `%ok`. Pisze na deskryptor 2 przez `dprintf`, więc nie zależy od symbolu +`stderr` z libc. Funkcja jest `cold` i `noinline`, więc układ skoków faworyzuje ścieżkę bez +błędu, a kontrola kosztuje jedno porównanie i dobrze przewidywany skok. + +### Dzielenie i reszta całkowita: `checkDivisor()` + +```llvm +define internal i64 @finch.div(i64 %a, i64 %b) { + ... + %0 = icmp eq i64 %b4, 0 + br i1 %0, label %panic, label %ok +panic: + call void @finch.panic(ptr @panic.msg, i64 2) + unreachable +ok: + %1 = icmp eq i64 %b4, -1 + %2 = icmp eq i64 %a3, -9223372036854775808 + %3 = and i1 %2, %1 + br i1 %3, label %panic5, label %ok6 +panic5: + call void @finch.panic(ptr @panic.msg.1, i64 2) + unreachable +ok6: + %4 = sdiv i64 %a3, %b4 +``` + +- Dzielnik będący **stałym zerem** to błąd **kompilacji**. +- Stały dzielnik różny od zera (i od −1 dla typów ze znakiem) nie generuje żadnej kontroli. +- Przy dzieleniu ze znakiem `MIN / -1` się przepełnia (na x86 kończy się SIGFPE). To też jest sprawdzane. +- Gdy wszystko jest znane, O2 usuwa kontrole całkowicie: `div(10, 2)` powyżej staje się `5` w `main`. + +### `.value` na null: w `place()` + +Każde `p.value`, którego wskaźnik nie jest oczywiście niezerowy (nie jest wprost `alloca` ani +zmienną globalną), dostaje `panicIf(icmp eq p, null, "used .value on a null pointer")`. +Bez tego LLVM traktuje load z null jako UB i może go **usunąć**. Zanim dodano tę kontrolę, +`print(p.value)` przy `p` równym null wypisywało śmieciową wartość pod O2 zamiast się wysypać. + +--- + +## 10. Współpraca z C przez libclang + +`importHeaders(imports, dir)` (`src/cimport.cpp`) działa **przed** codegenem i tworzy: + +```cpp +struct CImports { + std::unordered_map fns; // nazwa, nagłówek, wynik, parametry, variadic, powód braku obsługi + std::unordered_map consts; // wartości enum + liczbowe #define + std::unordered_map globals; // zmienne extern +}; +``` + +### Parsowanie + +Dla **każdego** `import "h"` parsowana jest osobna jednostka translacji z pliku w pamięci +`finch_import.c` zawierającego `#include "h"`. Osobne jednostki dają dokładne przypisanie błędów: +brakujący nagłówek wskazuje swoją linijkę `import`. Flagi: + +``` +-x c -std=gnu11 -I -isystem +CXTranslationUnit_DetailedPreprocessingRecord (żeby widzieć makra) +CXTranslationUnit_SkipFunctionBodies (szybkość: ciała funkcji inline są zbędne) +``` + +- **Systemowe katalogi nagłówków** pochodzą z uruchomienia `$CC -E -v -x c /dev/null` i odczytania + bloku między `#include <...> search starts here:` a `End of search list.`. Dzięki temu `stdio.h` + rozwiązuje się tak samo jak dla systemowego kompilatora. Ma to znaczenie na NixOS, gdzie wrapper + cc dokłada ścieżki ze store, o których „goły” libclang nic nie wie. +- **`gnu11`** zamiast `c11`: glibc w trybie ścisłym ukrywa `M_PI` i inne nazwy spoza ISO. +- **Diagnostyka** o wadze ≥ error przerywa import. `file not found` zamienia się w + `can't find the C header 'h'`. + +### Odwiedzający + +`visit()` przechodzi po kursorach najwyższego poziomu. Wygrywa pierwsza deklaracja danej nazwy. + +| Kursor | Działanie | +|---|---| +| `FunctionDecl` | Mapuje kanoniczny **typ funkcji** (`clang_getArgType`, więc parametry tablicowe są już zamienione na wskaźniki). Funkcje `static` są zapisywane jako nieobsługiwane (`static inline` nie ma symbolu do zlinkowania). `FunctionNoProto` (K&R `f()`) jest nieobsługiwane. Zmienna liczba argumentów pochodzi z `clang_isFunctionTypeVariadic`. | +| `VarDecl` z `extern` | → `CGlobal`, jeśli typ się mapuje | +| `EnumDecl` | rekurencja do dzieci | +| `EnumConstantDecl` | → `CConst` (`i64`, literał) | +| `MacroDefinition` | pomija wbudowane i makra-funkcje; `macroValue()` próbuje odczytać liczbę | + +### Mapowanie typów: `mapType()` + +Działa na typie **kanonicznym**, więc typedefy (`size_t`, `uint32_t`, `GLuint`, `FILE`) są rozwijane. +Typy całkowite są mapowane **według rozmiaru** (`clang_Type_getSizeOf`), a nie nazwy, więc +`long` staje się `i64` na LP64. + +| C (kanoniczny) | Finch | +|---|---| +| `void` | nic | +| `_Bool` | `bool` | +| `char` (dowolna znakowość) | `char` | +| `signed char`, `short`, `int`, `long`, `long long`, `enum` | `i8`/`i16`/`i32`/`i64` wg rozmiaru | +| warianty `unsigned` | `u8`…`u64` wg rozmiaru | +| `float` / `double` | `f32` / `f64` | +| `char *` (dowolne kwalifikatory) | `str` | +| `T *`, gdzie T mapuje się na typ niebędący void | `ptr[T]` | +| `void *`, wskaźnik na struct/union/funkcję/typ niemapowalny | `ptr` (nieprzezroczysty) | +| struct/union przez wartość | **nieobsługiwane** („passes a C struct by value”) | +| `long double`, `__int128`, wektory, … | **nieobsługiwane** | + +Funkcje nieobsługiwane zostają w tablicy, więc próba ich wywołania daje precyzyjny błąd zamiast +„nie ma takiej funkcji”. + +### Makra: `macroValue()` + +Tokenizuje zakres makra, odrzuca token nazwy i komentarze, zdejmuje **zrównoważone nawiasy +zewnętrzne**, akceptuje jeden wiodący `-` i wymaga **dokładnie jednego** literału liczbowego. +Liczby całkowite są czytane przez `strtoull(…, 0)`, czyli według **reguł C**: `0x` szesnastkowo +i zero wiodące jako **ósemkowe**, inaczej niż literały samego Fincha. Zmiennoprzecinkowe to +wszystko, co zawiera `.`, `e` lub `E` i nie jest szesnastkowe. Akceptowane są przyrostki +`u U l L f F`. To obejmuje `M_PI`, `EOF` (`(-1)`), `RAND_MAX`, `INT_MAX` oraz stałe `GL_*` i `GLFW_*`. +Wyrażenia (`(1 << 4)`), odwołania do innych makr i teksty są pomijane. + +### Wywoływanie + +Zaimportowane funkcje są deklarowane w module **dopiero przy wywołaniu**, przez +`getOrInsertFunction`. To używa istniejącej deklaracji, jeśli `print` już utworzył `printf`. +Sygnatury się zgadzają (`i32 (ptr, ...)`), więc nie ma konfliktu. + +### Błędy linkowania + +Gdy linkowanie się nie uda, `linkFailed()` w `main.cpp` przeszukuje wyjście linkera pod kątem +`undefined reference to \`sym'` (GNU ld) i `undefined symbol: sym` (lld). Grupuje symbole według +`CFunc::header` i wypisuje, z którego nagłówka pochodzą, razem z odgadniętą linijką `link` +(`guessLib()`: nazwa pliku bez rozszerzenia i końcowych cyfr, małymi literami: +`GLFW/glfw3.h` → `glfw`). `cannot find -lfoo` zamienia się w `the library 'foo' wasn't found`. + +--- + +## 11. Optymalizacja, emisja, linkowanie + +### Cel + +`hostMachine()` tworzy `TargetMachine` dla `sys::getDefaultTargetTriple()` z CPU `"generic"`, +bez dodatkowych cech i z `Reloc::PIC_`, które jest wymagane dla domyślnych plików PIE we +współczesnych dystrybucjach. Jej **data layout jest ustawiany na module, zanim powstanie +jakikolwiek IR**, w konstruktorze `Codegen`. To ważne: `IRBuilder` bierze wyrównanie loadów +i store'ów z data layoutu i bez niego dostępy do `i64` byłyby emitowane z `align 4`. + +`generic` znaczy, że plik zadziała na każdym x86-64. Zachowanie w stylu `-march=native` wymagałoby +zmiany nazwy CPU i cech w tym miejscu. + +### Optymalizacja + +`optimize()` buduje potok nowego menedżera przebiegów: + +```cpp +PassBuilder pb(tm); +pb.register{Module,CGSCC,Function,Loop}Analyses(...); +pb.crossRegisterProxies(lam, fam, cgam, mam); +pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam); +``` + +To jest środkowa część `-O2` z clanga: SROA/mem2reg, instcombine, inlining, GVN, LICM, +rozwijanie pętli, wektoryzacja i tak dalej. `-O0` pomija to wywołanie, co przydaje się do +czytania IR wyprodukowanego przez `Codegen` (`finch ir plik.fn -O0`). Generator kodu maszynowego +i tak działa na domyślnym poziomie TargetMachine. + +### Emisja + +`emitObject()` używa **starego** (legacy) menedżera przebiegów, bo potok kodu maszynowego LLVM +jest wciąż dostępny tylko przez niego: `tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)`. + +### Linkowanie + +``` +$CC plik.o -o plik -lm [pkg-config --libs NAZWA | -lNAZWA]... +``` + +Jako linker służy systemowy sterownik kompilatora C (domyślnie `cc`). Zna pliki crt platformy, +ścieżkę dynamicznego loadera, libc i domyślne ścieżki bibliotek, a odtwarzanie tego byłoby +pracochłonne. `-lm` jest dodawane zawsze. Dla każdego `link "NAZWA"` i `-l NAZWA` najpierw +próbowane jest `pkg-config --libs NAZWA`, żeby dołączyć ścieżki `-L` i biblioteki zależne. +Jeśli to się nie uda, używane jest `-lNAZWA`. Na Niksie wrapper cc zamienia ścieżki `-L` +w wpisy `RPATH`, więc wynik działa bez `LD_LIBRARY_PATH`. Plik obiektowy jest usuwany po +linkowaniu, niezależnie od wyniku. + +### `finch run` + +Kompiluje do pliku tymczasowego (`sys::fs::createTemporaryFile`), uruchamia go przez `system()`, +usuwa i przekazuje dalej kod wyjścia. Jeśli proces potomny zginął od sygnału, wypisuje +`the program crashed: ` i zwraca `128 + numer_sygnału`, jak powłoka. + +--- + +## 12. Diagnostyka + +`fail(line, col, msg)` jest `[[noreturn]]`: wypisuje + +``` +plik:linia:kol: error: msg + linia | tekst źródła + | ^ +``` + +i kończy proces z kodem 1. Fragment jest brany z `g_source`, a tabulatory w prefiksie są +zachowywane, żeby `^` trafiło w miejsce. Konwencje w całym kodzie: + +- **Pozycje:** operatory binarne raportują pozycję **operatora**, wywołania pozycję nazwy funkcji, + a błędy typów w argumentach pozycję **argumentu**. +- **Komunikaty** mówią prostymi słowami, co jest źle, i **jak to naprawić**, gdy naprawa jest + oczywista (`use int(...) to convert`, `did you mean ':='?`, `add import "math.h"`). +- `headerHint()` mapuje około dwóch tuzinów popularnych nazw z libc/libm na ich nagłówki, na + potrzeby wywołań nieznanych funkcji. +- Wewnętrzna niespójność, jeśli `verifyModule` kiedykolwiek zawiedzie, wypisuje + `internal compiler error (please report)` z wyjściem weryfikatora i kończy proces z kodem 2. + +Kompilator nie ma ostrzeżeń. Wszystko, co byłoby ostrzeżeniem, jest albo w porządku, albo błędem. + +--- + +## 13. System budowania + +`CMakeLists.txt`: + +- `find_package(LLVM CONFIG)`. **Jeśli istnieje cel `LLVM` (współdzielone `libLLVM.so`), Finch linkuje + tę jedną bibliotekę.** To nie kosmetyka, tylko konieczność: `libclang.so` sam linkuje `libLLVM.so`. + Linkowanie Fincha ze statycznymi bibliotekami komponentów (`libLLVMCore.a`, …) wprowadza do procesu + **drugą kopię LLVM**. Obie kopie rejestrują te same obiekty globalne i program wywracał się przy + wyjściu w globalnym destruktorze (`std::vector::~vector`, podwójne zwolnienie). + Do tego binarka miała 82 MB zamiast 11 MB. +- libclang jest szukany przez `find_path(clang-c/Index.h)` i `find_library(clang)`, z podpowiedzią katalogów LLVM. +- `FINCH_VERSION` pochodzi z `project(VERSION …)` i jest wypisywane przez `finch version` razem z `LLVM_VERSION_STRING`. +- C++17, `-Wall -Wextra`, a budowanie przechodzi bez ostrzeżeń. + +--- + +## 14. Testy + +`tests/run.sh` (ścieżkę kompilatora nadpisuje `FINCH=…`): + +- `tests/run/*.fn`: uruchamiane przez `finch run`. Połączone stdout+stderr musi być dokładnie równe `*.out`. +- `tests/fail/*.fn`: muszą zakończyć się kodem różnym od zera, a wyjście musi zawierać tekst po + `// expect: ` z pierwszej linii. To obejmuje błędy kompilacji, błędy w czasie działania (z numerami + linii) oraz problemy z nagłówkami i linkowaniem. `tests/fail/lib_hint.h` deklaruje funkcję, której + nie ma w żadnej bibliotece, żeby przetestować podpowiedź `link`. + +Stan obecny: **34 passed, 0 failed.** Każdy nowy komunikat błędu powinien dostać test w `fail/`, +a każda nowa funkcjonalność test w `run/`. + +--- + +## 15. Semantyka a C: zachowania zdefiniowane i niezdefiniowane + +| Sytuacja | C | Finch 1.0 | +|---|---|---| +| Przepełnienie liczby ze znakiem | UB | **Zdefiniowane zawinięcie** (bez flag `nsw`) | +| Przepełnienie liczby bez znaku | zawija się | zawija się | +| Dzielenie całkowite przez zero | UB | **Błąd kompilacji**, jeśli stałe, w przeciwnym razie **błąd w czasie działania** | +| `INT_MIN / -1` | UB | **Błąd w czasie działania** | +| Load/store przez null | UB | **Błąd w czasie działania** dla `.value` | +| `x & 1 == 0` | parsuje się jako `x & (1 == 0)` | parsuje się jako `(x & 1) == 0` | +| Niejawne zwężenie (`int` → `char`) | po cichu | **Błąd kompilacji** | +| Mieszanie ze znakiem / bez znaku | cicha konwersja | **Błąd kompilacji**, chyba że jedna strona się rozszerza | +| Użycie zmiennej przed nadaniem wartości | UB | Niemożliwe: każda deklaracja inicjalizuje (domyślnie zerem) | +| Brak `return` w funkcji z wynikiem | UB, jeśli wynik jest użyty | **Błąd kompilacji** | +| Przesunięcie o ≥ szerokość bitową | UB | **Nadal poison** (w 1.0 niesprawdzane) | +| `float` → int poza zakresem | UB | **Nadal poison** (w 1.0 niesprawdzane) | +| Wiszący wskaźnik na martwą zmienną lokalną | UB | **Nadal UB** (brak kontroli czasu życia) | +| Kolejność obliczeń | w większości nieokreślona | Od lewej do prawej | + +--- + +## 16. Spis funkcji: kto co robi + +Grupy funkcji o podobnych nazwach i czym się różnią. + +### Leksowanie i parsowanie + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `lex()` | tekst → tokeny, ustawia `newlineBefore` | nic nie wie o gramatyce ani typach | +| `tokName()` | rodzaj tokenu → nazwa czytelna dla człowieka (do błędów) | | +| `Parser::cur()` / `peekTok()` / `next()` | bieżący token / podgląd bez konsumowania / konsumpcja | | +| `accept(k)` kontra `expect(k)` | konsumuje, jeśli pasuje, i zwraca bool / konsumuje albo **zgłasza błąd** „expected …, found …” | | +| `unexpected(ctx)` | błąd „unexpected X (ctx)” na bieżącym tokenie | `expect` mówi, czego chciał; `unexpected` tłumaczy kontekst | +| `sameLine()` | czy bieżący token może kontynuować wyrażenie? | `endOfStatement()` to odpowiednik na poziomie instrukcji (wymaga nowej linii albo `}`) | +| `atDeclaration()` | podgląd `int x` / `ptr[...] p` | `type()` faktycznie konsumuje typ | +| `typeFromName()` kontra `isTypeName()` | nazwa → `Type` / nazwa → bool | | +| `statement()` kontra `block()` | jedna instrukcja / `{ instrukcja* }` z kontrolą końców i zerowaniem `parenDepth` | | +| `callArgs()` | wspólne dla wywołań i konwersji: `( expr, … )` | | +| `primary()` / `postfix()` / `unary()` / `multiplicative()` / `additive()` / `compare()` / `andExpr()` / `orExpr()` | po jednej funkcji na poziom priorytetu | | + +### Codegen: typy i wartości + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `ty(FType)` | typ Fincha → typ LLVM | | +| `zero(FType)` | wartość domyślna dla deklaracji bez inicjalizatora | | +| `widens(from, to)` | czy każdą wartość `from` da się przedstawić w `to`? (czysta reguła typów) | `fits()` pyta o to samo dla jednej stałej | +| `fits(c, from, to)` | czy wartość tego literału mieści się w `to`? | | +| `coerce(v, want)` | **niejawna** konwersja do miejsca o znanym typie albo błąd | `convert()` to **jawne** `T(x)` i pozwala na stratne rzutowania; `unify()` zbliża do siebie **dwa** argumenty | +| `unify(l, r)` | sprowadza argumenty binarne do wspólnego typu | używane przez `arithOp()` i `compare()` | +| `intCast()` / `intToFloat()` | emitują rzutowanie z poprawną znakowością | niskopoziomowe pomocniki trzech powyższych | + +### Codegen: wyrażenia + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `expr(e)` | r-wartość: emituje kod, zwraca `{wartość, typ, literal}` | `place()` zwraca **adres**, zamiast ładować | +| `place(e)` | adres l-wartości: zmiennej, zmiennej globalnej C albo `p.value` | używane przez `expr()` (potem load), `assign()` (potem store) i `addrOf()` (zwraca bez zmian) | +| `binary()` | rozgałęzienie po `BinOp` | | +| `arith()` kontra `arithOp()` | `arith()` opakowuje `arithOp()` i liczy flagę `literal` wyniku | `arithOp()` robi `unify` i emituje instrukcję | +| `compare()` | wszystkie porównania: wskaźniki/str/float/int | zawsze zwraca `bool` i nigdy nie jest literałem | +| `logic()` | `&&`/`\|\|` ze skokami i φ | jedyne miejsce budujące φ ręcznie | +| `call()` | zamienia nazwę wywołania na jeden z pięciu rodzajów | | +| `callFinch()` kontra `callC()` | zobacz rozdział 8.10 | | +| `print()` | buduje format printf z typów | | +| `addrOf()` | `addr(x)` → `place(x).addr` | | +| `convert()` | `T(x)` | | +| `libc()` | `getOrInsertFunction`: zadeklaruj albo użyj istniejącej funkcji zewnętrznej | `declare()` tworzy funkcje **Fincha** przez `Function::Create` | +| `cGlobal()` | zadeklaruj albo użyj istniejącej zmiennej `extern` | | + +### Codegen: instrukcje i sterowanie + +| Funkcja | Robi | Różni się od | +|---|---|---| +| `declare()` kontra `define()` | przejście sygnatur / przejście ciał | | +| `block()` kontra `blockBody()` | `block()` otwiera i zamyka zasięg wokół `blockBody()`; `blockBody()` emituje instrukcje i odrzuca nieosiągalne | `define()` woła `blockBody()` bezpośrednio, bo parametry i zewnętrzne ciało dzielą jeden zasięg | +| `stmt()` | rozgałęzienie po `StmtKind`; obsługuje też `break`/`continue` | | +| `varDecl()` / `assign()` | deklaracja z opcjonalnym wnioskowaniem / zapis do istniejącego miejsca | | +| `addVar()` | tworzy miejsce, zapisuje wartość początkową, rejestruje nazwę (z kontrolą przesłaniania) | `slot()` tylko tworzy `alloca` | +| `lookup()` | szuka we wszystkich zasięgach, od najbardziej wewnętrznego | | +| `condition(e)` | `coerce(expr(e), bool)` z komunikatem „a condition must be bool” | | +| `ifStmt()` / `whileStmt()` / `forStmt()` / `returnStmt()` | rozdział 8.5 | | +| `terminated()` | czy bieżący blok kończy się już terminatorem? | | +| `continueAt(bb, deadEnd)` | przejdź do bloku łączącego albo usuń go, jeśli jest nieosiągalny | | +| `newBlock()` | tworzy blok podstawowy w bieżącej funkcji | | + +### Kontrole w czasie działania + +| Funkcja | Robi | +|---|---| +| `panicFn()` | pobiera albo tworzy `finch.panic` | +| `panicIf(cond, msg, pos)` | warunkowy skok do bloku z panic | +| `checkDivisor(l, r, pos)` | kontrole zera i `MIN/-1` albo błąd kompilacji | + +### Sterownik (`main.cpp`) + +| Funkcja | Robi | +|---|---| +| `hostMachine()` | tworzy `TargetMachine` | +| `optimize()` | potok O2 | +| `emitObject()` | zapisuje `.o` | +| `capture()` | uruchamia polecenie powłoki, zbiera wyjście i kod wyjścia | +| `libFlags()` | pkg-config albo `-l` dla jednej biblioteki | +| `link()` | uruchamia linker, usuwa `.o`, tłumaczy porażki | +| `linkFailed()` / `guessLib()` | zamieniają błędy linkera na porady w języku Fincha | + +--- + +## 17. Rozbudowa kompilatora + +### Nowa funkcja wbudowana (przykład: `len(str) -> int`) + +1. Dodaj nazwę do `Codegen::isBuiltin()`, żeby nie dało się jej zdefiniować ponownie. +2. W `Codegen::call()` dodaj rozgałęzienie `if (c.callee == "len") return len(c);`. +3. Zaimplementuj: sprawdź liczbę argumentów, `coerce(expr(arg), Str, …)`, wyemituj `strlen` przez `libc()`, + zwróć `{call, I64}`. `strlen` zwraca `u64`; jeśli chcesz `int`, przekonwertuj przez `intCast`. +4. Dodaj `tests/run/len.fn` z plikiem `.out` i przypadek w `tests/fail/` dla złego argumentu. + +### Nowy operator binarny + +1. Lekser: nowy `Tok`, jego zapis w `switch` operatorów i w `tokName()`. +2. AST: nowy `BinOp`. +3. Parser: umieść go w funkcji właściwego poziomu priorytetu. +4. Codegen: `opName()`, emisja w `arithOp()` albo `compare()` i wyjaśnienie złych argumentów w `badOperands()`. + +### Nowa instrukcja + +Dodaj `StmtKind` i węzeł w `ast.h`, rozpoznaj ją w `Parser::statement()` i obsłuż w `Codegen::stmt()`. +Jeśli może przenosić sterowanie, upewnij się, że wynik respektuje `terminated()` i używa `continueAt()` +dla swojego bloku łączącego. + +### Konwencje + +Trzymaj się istniejącego stylu: wczesne `fail()` z podpowiedzią naprawy, komentarze tylko tam, +gdzie *dlaczego* nie jest oczywiste, i jeden test na jedno zachowanie. + +--- + +## 18. Znane ograniczenia i plan + +- **Brak typów złożonych:** `struct`, tablice/wycinki z `.len` i struktury C przez wartość. To kolejne + kamienie milowe, w tej kolejności: tablice opierają się na strukturach (wskaźnik + długość), + a model pamięci na obu. +- **Model pamięci** (uzgodniony projekt): wartości posiadające pamięć na stercie są zwalniane + automatycznie na końcu bloku, który je posiada, `defer` uruchamia kod przy wyjściu z bloku, + a `free` zostaje do ręcznej kontroli. Szkic implementacji: lista sprzątania na blok, emitowana na + każdej krawędzi wyjścia (normalny koniec, `return`, `break`, `continue`). `blockBody()` i stos + `loops` już znają każdą krawędź wyjścia. +- **Teksty:** sklejanie, `len`, `str(x)`. Zależą od modelu pamięci. +- **Wejście:** `input()`. +- **Moduły:** `import nazwa` dla kodu w Finchu. Dziś jeden `.fn` = jeden program. +- **Niesprawdzane UB:** wielkość przesunięcia, zakres float→int, wiszące wskaźniki. Przesunięcia + i float→int dałoby się sprawdzać jak dzielenie, niewielkim kosztem. +- **Informacje dla debuggera:** brak DWARF. `DIBuilder` podpiąłby się w `define()` i przy `pos` każdej instrukcji. +- **Platformy:** tylko triple hosta, testowane na Linux/x86-64. Kod jest niezależny od platformy + z wyjątkiem wykrywania nagłówków przez `cc -E -v` i użycia `dprintf`. +- **Bootstrap:** długoterminowym celem jest przepisanie kompilatora w Finchu. Wołanie API LLVM-C już + działa: LLVM-C używa nieprzezroczystych uchwytów (`LLVMModuleRef` itd.), które mapują się na `ptr` + Fincha. `examples/llvm.fn` buduje i wypisuje moduł z poziomu Fincha (`import "llvm-c/Core.h"`, + `link "LLVM"`). Do samodzielnego kompilatora brakuje jeszcze struktur, tablic, tekstów i operacji na plikach. diff --git a/docs/pl/dla-poczatkujacych.md b/docs/pl/dla-poczatkujacych.md new file mode 100644 index 0000000..bd2659d --- /dev/null +++ b/docs/pl/dla-poczatkujacych.md @@ -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. + +
Rozwiązanie + +```c +fn main() { + for i in 1..11 { + print("7 x", i, "=", 7 * i) + } +} +``` +
+ +### Projekt 2: Parzysta czy nieparzysta + +Dla każdej liczby od 1 do 10 napisz, czy jest parzysta, czy nieparzysta. + +
Rozwiązanie + +```c +fn main() { + for n in 1..11 { + if n % 2 == 0 { + print(n, "jest parzysta") + } else { + print(n, "jest nieparzysta") + } + } +} +``` +
+ +### Projekt 3: Stopnie Celsjusza na Fahrenheita + +Pokaż tabelkę od 0 °C do 100 °C co 10 stopni. Wzór: `F = C * 9 / 5 + 32`. + +
Rozwiązanie + +```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. +
+ +### 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ę. + +
Rozwiązanie + +```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) + } + } +} +``` +
+ +### Projekt 5: Największa z trzech + +Napisz funkcję `najwieksza(int a, int b, int c) -> int`, która oddaje największą z trzech liczb. + +
Rozwiązanie + +```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 +} +``` +
+ +--- + +## 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. diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md new file mode 100644 index 0000000..69ac141 --- /dev/null +++ b/docs/pl/dla-technikow.md @@ -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 skompiluj i od razu uruchom +finch build [-o nazwa] skompiluj do programu (domyślna nazwa: nazwa pliku) +finch ir pokaż LLVM IR (dla ciekawskich) +finch version pokaż wersję + +opcje: + -l 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). diff --git a/examples/c_import.fn b/examples/c_import.fn new file mode 100644 index 0000000..df1ad30 --- /dev/null +++ b/examples/c_import.fn @@ -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") +} diff --git a/examples/hello.fn b/examples/hello.fn new file mode 100644 index 0000000..0c7d90a --- /dev/null +++ b/examples/hello.fn @@ -0,0 +1,3 @@ +fn main() { + print("Hello, Finch!") +} diff --git a/examples/llvm.fn b/examples/llvm.fn new file mode 100644 index 0000000..970e50b --- /dev/null +++ b/examples/llvm.fn @@ -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) +} diff --git a/examples/tour.fn b/examples/tour.fn new file mode 100644 index 0000000..514c605 --- /dev/null +++ b/examples/tour.fn @@ -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)) +} diff --git a/examples/types.fn b/examples/types.fn new file mode 100644 index 0000000..5964b60 --- /dev/null +++ b/examples/types.fn @@ -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") + } +} diff --git a/examples/window.fn b/examples/window.fn new file mode 100644 index 0000000..47dea72 --- /dev/null +++ b/examples/window.fn @@ -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() +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b4c65fe --- /dev/null +++ b/shell.nix @@ -0,0 +1,17 @@ +{ pkgs ? import {} }: + +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 + ]; +} diff --git a/src/ast.h b/src/ast.h new file mode 100644 index 0000000..c779cb2 --- /dev/null +++ b/src/ast.h @@ -0,0 +1,235 @@ +#pragma once + +#include +#include +#include + +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 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(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; + +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 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; + +struct BlockStmt : Stmt { + std::vector body; + explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {} +}; +using BlockPtr = std::unique_ptr; + +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 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 imports; + std::vector links; + std::vector fns; +}; diff --git a/src/cimport.cpp b/src/cimport.cpp new file mode 100644 index 0000000..99fa255 --- /dev/null +++ b/src/cimport.cpp @@ -0,0 +1,246 @@ +#include "cimport.h" +#include "error.h" + +#include + +#include +#include +#include +#include + +namespace { + +// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would. +std::vector 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 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 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(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 &imports, const std::string &dir) { + CImports out; + if (imports.empty()) return out; + + std::vector args = {"-x", "c", "-std=gnu11", "-I" + dir}; + for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d); + std::vector 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; +} diff --git a/src/cimport.h b/src/cimport.h new file mode 100644 index 0000000..d977d6d --- /dev/null +++ b/src/cimport.h @@ -0,0 +1,37 @@ +#pragma once + +#include "ast.h" + +#include +#include +#include + +// What Finch learned from `import "something.h"`. + +struct CFunc { + std::string name; + std::string header; // the import it came from + Type ret; + std::vector 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 fns; + std::unordered_map consts; + std::unordered_map globals; +}; + +// `dir` is the folder of the .fn file, searched first for "local.h" headers. +CImports importHeaders(const std::vector &imports, const std::string &dir); diff --git a/src/codegen.cpp b/src/codegen.cpp new file mode 100644 index 0000000..38ea2c7 --- /dev/null +++ b/src/codegen.cpp @@ -0,0 +1,887 @@ +#include "codegen.h" +#include "error.h" + +#include +#include +#include +#include + +#include + +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("finch", c)), cimports(ci) { + mod->setTargetTriple(tm.getTargetTriple()); + mod->setDataLayout(tm.createDataLayout()); + } + + std::unique_ptr 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 mod; + const CImports &cimports; + + std::unordered_map fns; + std::vector> scopes; + std::vector 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(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(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 params, bool vararg = false) { + return cast(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 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(s)); break; + case StmtKind::VarDecl: varDecl(static_cast(s)); break; + case StmtKind::Assign: assign(static_cast(s)); break; + case StmtKind::Expr: expr(*static_cast(s).expr); break; + case StmtKind::If: ifStmt(static_cast(s)); break; + case StmtKind::While: whileStmt(static_cast(s)); break; + case StmtKind::For: forStmt(static_cast(s)); break; + case StmtKind::Return: returnStmt(static_cast(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(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(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(obj.v) && !isa(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(*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(*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(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(e).value), FType::I64, true}; + case ExprKind::Float: return {ConstantFP::get(b.getDoubleTy(), static_cast(e).value), FType::F64, true}; + case ExprKind::Bool: return {b.getInt1(static_cast(e).value), FType::Bool}; + case ExprKind::Char: return {b.getInt8(static_cast(e).value), FType::Char}; + case ExprKind::Str: return {b.CreateGlobalString(static_cast(e).value, "str"), FType::Str}; + case ExprKind::Null: return {ConstantPointerNull::get(b.getPtrTy()), FType::Null}; + case ExprKind::Var: { + auto &v = static_cast(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(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(e)); + case ExprKind::Call: return call(static_cast(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(l.v) : nullptr; + auto *rc = r.literal ? dyn_cast(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(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(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 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 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 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 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(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 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 generate(const Program &prog, const CImports &c, LLVMContext &ctx, TargetMachine &tm) { + return Codegen(ctx, c, tm).run(prog); +} diff --git a/src/codegen.h b/src/codegen.h new file mode 100644 index 0000000..be2e51b --- /dev/null +++ b/src/codegen.h @@ -0,0 +1,14 @@ +#pragma once + +#include "ast.h" +#include "cimport.h" + +#include +#include +#include + +#include + +// `tm` describes the machine we compile for (sizes, alignment), which shapes the IR. +std::unique_ptr generate(const Program &prog, const CImports &c, llvm::LLVMContext &ctx, + llvm::TargetMachine &tm); diff --git a/src/error.h b/src/error.h new file mode 100644 index 0000000..3fcf29e --- /dev/null +++ b/src/error.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +// 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); +} diff --git a/src/lexer.cpp b/src/lexer.cpp new file mode 100644 index 0000000..9222167 --- /dev/null +++ b/src/lexer.cpp @@ -0,0 +1,211 @@ +#include "lexer.h" +#include "error.h" + +#include +#include + +static const std::unordered_map 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 lex(const std::string &src, const std::string &file) { + g_file = file; + g_source = src; + std::vector 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 "?"; +} diff --git a/src/lexer.h b/src/lexer.h new file mode 100644 index 0000000..02220fc --- /dev/null +++ b/src/lexer.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +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 lex(const std::string &src, const std::string &file); +const char *tokName(Tok t); diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..86ce3e5 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,258 @@ +#include "cimport.h" +#include "codegen.h" +#include "lexer.h" +#include "parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace llvm; + +static void usage() { + std::fprintf(stderr, + "Finch " FINCH_VERSION " - small, quick, sharp.\n\n" + "usage:\n" + " finch run compile and run\n" + " finch build [-o name] compile to a program\n" + "\n" + " -l link a C library, same as link \"lib\" in the file\n" + " -O0 skip optimizations (to read the raw IR)\n" + " finch ir 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. +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 missing, missingLibs; + auto addOnce = [](std::vector &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>> 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 &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 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 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; +} diff --git a/src/parser.cpp b/src/parser.cpp new file mode 100644 index 0000000..a513a3c --- /dev/null +++ b/src/parser.cpp @@ -0,0 +1,425 @@ +#include "parser.h" +#include "error.h" + +#include +#include +#include +#include +#include + +bool typeFromName(const std::string &name, Type &out) { + static const std::unordered_map 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 &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 &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(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(p); + s->cond = expr(); + s->body = block(); + return s; + } + case Tok::For: { + next(); + auto s = std::make_unique(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(p); + if (!at(Tok::RBrace) && !cur().newlineBefore) s->value = expr(); + return s; + } + case Tok::Break: next(); return std::make_unique(p); + case Tok::Continue: next(); return std::make_unique(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(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(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(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(p, std::move(e)); + } + + StmtPtr ifStmt() { + auto s = std::make_unique(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(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(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(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(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(p, op, std::move(l), unary()); + } + return l; + } + + ExprPtr unary() { + Pos p = pos(); + if (accept(Tok::Minus)) return std::make_unique(p, '-', unary()); + if (accept(Tok::Not)) return std::make_unique(p, '!', unary()); + if (accept(Tok::Tilde)) return std::make_unique(p, '~', unary()); + return postfix(); + } + + // p.value + ExprPtr postfix() { + ExprPtr e = primary(); + while (sameLine() && at(Tok::Dot)) { + Pos p = pos(); + next(); + e = std::make_unique(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(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(p, (long long)v); + } + case Tok::Float: next(); return std::make_unique(p, std::strtod(t.text.c_str(), nullptr)); + case Tok::True: next(); return std::make_unique(p, true); + case Tok::False: next(); return std::make_unique(p, false); + case Tok::Null: next(); return std::make_unique(p); + case Tok::Char: next(); return std::make_unique(p, t.text[0]); + case Tok::String: next(); return std::make_unique(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(p, t.text); + } + default: unexpected("(expected a value)"); + } + } +}; + +} // namespace + +Program parse(const std::vector &tokens) { + return Parser(tokens).program(); +} diff --git a/src/parser.h b/src/parser.h new file mode 100644 index 0000000..4daab16 --- /dev/null +++ b/src/parser.h @@ -0,0 +1,6 @@ +#pragma once + +#include "ast.h" +#include "lexer.h" + +Program parse(const std::vector &tokens); diff --git a/tests/fail/bad_link_name.fn b/tests/fail/bad_link_name.fn new file mode 100644 index 0000000..f6898bd --- /dev/null +++ b/tests/fail/bad_link_name.fn @@ -0,0 +1,3 @@ +// expect: write just the library's name +link "libm.so" +fn main() {} diff --git a/tests/fail/bool_bitand.fn b/tests/fail/bool_bitand.fn new file mode 100644 index 0000000..53240ca --- /dev/null +++ b/tests/fail/bool_bitand.fn @@ -0,0 +1,4 @@ +// expect: for true/false use '&&' +fn main() { + print(true & false) +} diff --git a/tests/fail/condition_not_bool.fn b/tests/fail/condition_not_bool.fn new file mode 100644 index 0000000..cdaa1b4 --- /dev/null +++ b/tests/fail/condition_not_bool.fn @@ -0,0 +1,5 @@ +// expect: a condition must be bool +fn main() { + if 1 { + } +} diff --git a/tests/fail/div_zero_const.fn b/tests/fail/div_zero_const.fn new file mode 100644 index 0000000..abe0a2b --- /dev/null +++ b/tests/fail/div_zero_const.fn @@ -0,0 +1,4 @@ +// expect: division by zero +fn main() { + print(1 / 0) +} diff --git a/tests/fail/div_zero_runtime.fn b/tests/fail/div_zero_runtime.fn new file mode 100644 index 0000000..ff5fc0b --- /dev/null +++ b/tests/fail/div_zero_runtime.fn @@ -0,0 +1,5 @@ +// expect: 4: runtime error: division by zero +fn main() { + zero := 0 + print(10 / zero) +} diff --git a/tests/fail/float_to_int.fn b/tests/fail/float_to_int.fn new file mode 100644 index 0000000..d301294 --- /dev/null +++ b/tests/fail/float_to_int.fn @@ -0,0 +1,5 @@ +// expect: cuts off the fraction +fn main() { + float f = 2.5 + int n = f +} diff --git a/tests/fail/forgot_import.fn b/tests/fail/forgot_import.fn new file mode 100644 index 0000000..f467048 --- /dev/null +++ b/tests/fail/forgot_import.fn @@ -0,0 +1,4 @@ +// expect: import "math.h" +fn main() { + print(sqrt(2)) +} diff --git a/tests/fail/lib_hint.fn b/tests/fail/lib_hint.fn new file mode 100644 index 0000000..2de6273 --- /dev/null +++ b/tests/fail/lib_hint.fn @@ -0,0 +1,5 @@ +// expect: link "lib_hint" +import "lib_hint.h" +fn main() { + print(finch_test_missing(1)) +} diff --git a/tests/fail/lib_hint.h b/tests/fail/lib_hint.h new file mode 100644 index 0000000..6fc638a --- /dev/null +++ b/tests/fail/lib_hint.h @@ -0,0 +1 @@ +int finch_test_missing(int x); diff --git a/tests/fail/literal_too_big.fn b/tests/fail/literal_too_big.fn new file mode 100644 index 0000000..0691338 --- /dev/null +++ b/tests/fail/literal_too_big.fn @@ -0,0 +1,4 @@ +// expect: doesn't fit in u8 +fn main() { + u8 b = 300 +} diff --git a/tests/fail/loop_var_readonly.fn b/tests/fail/loop_var_readonly.fn new file mode 100644 index 0000000..d762baf --- /dev/null +++ b/tests/fail/loop_var_readonly.fn @@ -0,0 +1,6 @@ +// expect: can't be changed +fn main() { + for i in 0..3 { + i = 5 + } +} diff --git a/tests/fail/missing_header.fn b/tests/fail/missing_header.fn new file mode 100644 index 0000000..f015b4e --- /dev/null +++ b/tests/fail/missing_header.fn @@ -0,0 +1,3 @@ +// expect: can't find the C header +import "nope_not_here.h" +fn main() {} diff --git a/tests/fail/missing_lib.fn b/tests/fail/missing_lib.fn new file mode 100644 index 0000000..2e47fe4 --- /dev/null +++ b/tests/fail/missing_lib.fn @@ -0,0 +1,3 @@ +// expect: the library 'definitely_not_a_lib' wasn't found +link "definitely_not_a_lib" +fn main() {} diff --git a/tests/fail/missing_return.fn b/tests/fail/missing_return.fn new file mode 100644 index 0000000..10898eb --- /dev/null +++ b/tests/fail/missing_return.fn @@ -0,0 +1,5 @@ +// expect: can reach its end without returning int +fn f() -> int { + print(1) +} +fn main() {} diff --git a/tests/fail/mixed_signs.fn b/tests/fail/mixed_signs.fn new file mode 100644 index 0000000..223b354 --- /dev/null +++ b/tests/fail/mixed_signs.fn @@ -0,0 +1,6 @@ +// expect: can't use '+' on i32 and u32 +fn main() { + i32 a = 1 + u32 b = 2 + print(a + b) +} diff --git a/tests/fail/no_main.fn b/tests/fail/no_main.fn new file mode 100644 index 0000000..9a19f2c --- /dev/null +++ b/tests/fail/no_main.fn @@ -0,0 +1,2 @@ +// expect: there is no main function +fn helper() {} diff --git a/tests/fail/non_ascii_name.fn b/tests/fail/non_ascii_name.fn new file mode 100644 index 0000000..c5a5b28 --- /dev/null +++ b/tests/fail/non_ascii_name.fn @@ -0,0 +1,4 @@ +// expect: unexpected character 'ż' (names use only +fn main() { + żółw := 1 +} diff --git a/tests/fail/null_value.fn b/tests/fail/null_value.fn new file mode 100644 index 0000000..b5f78d7 --- /dev/null +++ b/tests/fail/null_value.fn @@ -0,0 +1,5 @@ +// expect: 4: runtime error: used .value on a null pointer +fn main() { + ptr[int] p = null + print(p.value) +} diff --git a/tests/fail/old_function_syntax.fn b/tests/fail/old_function_syntax.fn new file mode 100644 index 0000000..2065a06 --- /dev/null +++ b/tests/fail/old_function_syntax.fn @@ -0,0 +1,4 @@ +// expect: functions start with 'fn' +int add(int a) { + return a +} diff --git a/tests/fail/redeclare.fn b/tests/fail/redeclare.fn new file mode 100644 index 0000000..316a61e --- /dev/null +++ b/tests/fail/redeclare.fn @@ -0,0 +1,5 @@ +// expect: already exists +fn main() { + x := 1 + x := 2 +} diff --git a/tests/fail/two_statements.fn b/tests/fail/two_statements.fn new file mode 100644 index 0000000..1c525b4 --- /dev/null +++ b/tests/fail/two_statements.fn @@ -0,0 +1,4 @@ +// expect: put each statement on its own line +fn main() { + x := 1 print(x) +} diff --git a/tests/fail/type_mismatch.fn b/tests/fail/type_mismatch.fn new file mode 100644 index 0000000..d052328 --- /dev/null +++ b/tests/fail/type_mismatch.fn @@ -0,0 +1,4 @@ +// expect: 'x' must be int, but this is str +fn main() { + int x = "hi" +} diff --git a/tests/fail/unclosed_brace.fn b/tests/fail/unclosed_brace.fn new file mode 100644 index 0000000..16647cd --- /dev/null +++ b/tests/fail/unclosed_brace.fn @@ -0,0 +1,3 @@ +// expect: never closed +fn main() { + print(1) diff --git a/tests/fail/undefined_var.fn b/tests/fail/undefined_var.fn new file mode 100644 index 0000000..6e9c910 --- /dev/null +++ b/tests/fail/undefined_var.fn @@ -0,0 +1,4 @@ +// expect: there is no variable named 'y' +fn main() { + print(y) +} diff --git a/tests/fail/unreachable.fn b/tests/fail/unreachable.fn new file mode 100644 index 0000000..44b672d --- /dev/null +++ b/tests/fail/unreachable.fn @@ -0,0 +1,5 @@ +// expect: this code can never run +fn main() { + return + print(1) +} diff --git a/tests/fail/untyped_null.fn b/tests/fail/untyped_null.fn new file mode 100644 index 0000000..fdb2319 --- /dev/null +++ b/tests/fail/untyped_null.fn @@ -0,0 +1,4 @@ +// expect: null needs a type +fn main() { + p := null +} diff --git a/tests/fail/untyped_ptr.fn b/tests/fail/untyped_ptr.fn new file mode 100644 index 0000000..fc99168 --- /dev/null +++ b/tests/fail/untyped_ptr.fn @@ -0,0 +1,6 @@ +// expect: this ptr has no type +import "stdlib.h" +fn main() { + p := malloc(8) + print(p.value) +} diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..528783a --- /dev/null +++ b/tests/run.sh @@ -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: " +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 ] diff --git a/tests/run/basics.fn b/tests/run/basics.fn new file mode 100644 index 0000000..86b3738 --- /dev/null +++ b/tests/run/basics.fn @@ -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") +} diff --git a/tests/run/basics.out b/tests/run/basics.out new file mode 100644 index 0000000..bfbf2fd --- /dev/null +++ b/tests/run/basics.out @@ -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 diff --git a/tests/run/bits.fn b/tests/run/bits.fn new file mode 100644 index 0000000..68a9c26 --- /dev/null +++ b/tests/run/bits.fn @@ -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) +} diff --git a/tests/run/bits.out b/tests/run/bits.out new file mode 100644 index 0000000..7917ddc --- /dev/null +++ b/tests/run/bits.out @@ -0,0 +1,4 @@ +2 7 5 16 64 -1 +true +5 true false +-4 15 diff --git a/tests/run/cimport.fn b/tests/run/cimport.fn new file mode 100644 index 0000000..ac66d48 --- /dev/null +++ b/tests/run/cimport.fn @@ -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) +} diff --git a/tests/run/cimport.out b/tests/run/cimport.out new file mode 100644 index 0000000..90792d5 --- /dev/null +++ b/tests/run/cimport.out @@ -0,0 +1,5 @@ +42-printf +4 1024 2 +5 5 123 +-1 true true +7 diff --git a/tests/run/control.fn b/tests/run/control.fn new file mode 100644 index 0000000..95370e8 --- /dev/null +++ b/tests/run/control.fn @@ -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) + } +} diff --git a/tests/run/control.out b/tests/run/control.out new file mode 100644 index 0000000..ddf3541 --- /dev/null +++ b/tests/run/control.out @@ -0,0 +1,4 @@ +negative zero positive +odd sum below 10: 25 +n: 5 +blocks have their own scope diff --git a/tests/run/exitcode.fn b/tests/run/exitcode.fn new file mode 100644 index 0000000..fa61e12 --- /dev/null +++ b/tests/run/exitcode.fn @@ -0,0 +1,4 @@ +fn main() -> int { + print("exiting") + return 0 +} diff --git a/tests/run/exitcode.out b/tests/run/exitcode.out new file mode 100644 index 0000000..26b03a5 --- /dev/null +++ b/tests/run/exitcode.out @@ -0,0 +1 @@ +exiting diff --git a/tests/run/functions.fn b/tests/run/functions.fn new file mode 100644 index 0000000..1ee4237 --- /dev/null +++ b/tests/run/functions.fn @@ -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") +} diff --git a/tests/run/functions.out b/tests/run/functions.out new file mode 100644 index 0000000..2e5af6a --- /dev/null +++ b/tests/run/functions.out @@ -0,0 +1,3 @@ +6765 +42 +hi finch diff --git a/tests/run/pointers.fn b/tests/run/pointers.fn new file mode 100644 index 0000000..40b9055 --- /dev/null +++ b/tests/run/pointers.fn @@ -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) +} diff --git a/tests/run/pointers.out b/tests/run/pointers.out new file mode 100644 index 0000000..f51c9b2 --- /dev/null +++ b/tests/run/pointers.out @@ -0,0 +1,4 @@ +42 +2 1 +99 +true true (nil) diff --git a/tests/run/types.fn b/tests/run/types.fn new file mode 100644 index 0000000..f5d6df5 --- /dev/null +++ b/tests/run/types.fn @@ -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) +} diff --git a/tests/run/types.out b/tests/run/types.out new file mode 100644 index 0000000..909e054 --- /dev/null +++ b/tests/run/types.out @@ -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