.fn is already used by other tools (e.g. Fennel). Modules are now looked up as name.fch; a leftover name.fn gets a clear message telling you to rename it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 KiB
Finch dla inżynierów
Kompletny opis wnętrza Fincha 2.0. Jak kod źródłowy staje się tokenami, tokeny drzewem AST,
a AST kodem LLVM IR, który potem jest optymalizowany, zapisywany i linkowany z małym runtime'em w C.
Dokładne reguły typów; model własności i to, jak tłumaczy się na kopie i zwolnienia; jak każda
konstrukcja staje się IR (z prawdziwym wyjściem kompilatora); obsługa nagłówków C, struktur C i ABI
System V; moduły, informacje dla debuggera i kompilator samohostujący z boot/. Każda funkcja
kompilatora jest opisana razem 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.
🇬🇧 English version: for-engineers.md
Spis treści
- Architektura
- Pliki źródłowe
- Lekser
- Gramatyka i parser
- AST
- Wczytywanie: pliki i moduły
- Model semantyczny
- System typów
- Własność i pamięć
- Generowanie kodu
- Kontrole w czasie działania
- Runtime w C
- Współpraca z C: nagłówki, struktury, ABI
- Optymalizacja, emisja, linkowanie
- Informacje dla debuggera
- Diagnostyka
- Kompilator samohostujący (boot/)
- System budowania
- Testy
- Semantyka a C
- Spis funkcji
- Rozbudowa kompilatora
- Znane ograniczenia
1. Architektura
Finch to jednoprzebiegowy front-end przed LLVM: sprawdzanie typów i generowanie IR to jedno przejście po AST, bez własnej reprezentacji pośredniej.
main.fch ──► Loader::load() ── lex() ── parse() ──► Program (+ każdy importowany moduł, rekurencyjnie)
│
wszystkie `import "x.h"` ──► importHeaders() (libclang) ──► CImports
│
hostMachine() ──► TargetMachine (triple + data layout)│
▼
generate(progs, cimports, tm, debug) ──► Codegen::run()
declareStructs() wszystkie struktury Fincha i C, ciała typów LLVM, kontrola cykli
declareFns() sygnatura każdej funkcji (+ analiza „pożyczania” parametrów)
define() każde ciało: typy + IRBuilder, zasięgi, zwalnianie, kontrole
defineMainWrapper C-owe `main(argc, argv)`
DIBuilder::finalize (z -g) ; verifyModule
▼
optimize() PassBuilder default<O2> (pomijane przy -O0)
emitObject() legacy PM + addPassesToEmitFile ──► prog.o
link() cc prog.o ~/.cache/finch/rt-<hash>.o [twoje .c/.o/.a] -lm [pkg-config/-l…]
▼
plik wykonywalny (finch run: uruchamiany z pliku tymczasowego z resztą argumentów, potem usuwany)
Decyzje, które kształtują resztę:
- Szybka porażka. Pierwszy błąd wypisuje diagnostykę i kończy proces (
failAt()wsrc/error.h). Bez odtwarzania po błędach, więc dalsze fazy nigdy nie widzą częściowo poprawnego wejścia. - Jedno przejście.
Codegenniesie zasięgi, pętle, bieżącą funkcję i moduł. Każde wyrażenie zwraca wartość IR, typ Fincha i dwie flagi (Value_, §8). - Niech LLVM robi robotę. Zmienne lokalne to
alloca, któremem2reg/SROA zamieniają na rejestry. Węzły φ Finch buduje ręcznie tylko dla&&/||. - Wartości, nie referencje. Tablice, teksty i struktury mają semantykę wartości: przypisanie kopiuje, koniec bloku zwalnia. Własność rozstrzyga się w czasie kompilacji, bez liczenia referencji (§9).
- Malutki runtime w C (
runtime/finch_rt.c, ok. 430 linii) robi to, co w IR byłoby żmudne: operacje na tekstach, wzrost tablic, wejście, pliki, panic. Jest wbudowany w kompilator i trzymany w cache.
2. Pliki źródłowe
| Plik | Linie | Odpowiedzialność |
|---|---|---|
src/error.h |
40 | g_files (wszystkie źródła), failAt(file, line, col, msg), fail(line, col, msg) |
src/lexer.h/.cpp |
250 | Tok, Token, lex(indeksPliku), tokName() |
src/ast.h |
310 | Type (z Array, Fixed, Struct, Named), wszystkie węzły Expr/Stmt, FnDecl, StructDecl, Import, Link, Program |
src/parser.h/.cpp |
540 | Parser zstępujący Parser, typeFromName() |
src/cimport.h/.cpp |
420 | Import przez libclang: funkcje, struktury C (pola, przesunięcia), stałe, zmienne globalne |
src/codegen.h |
15 | generate() |
src/codegen_impl.h |
265 | Klasa Codegen i jej struktury pomocnicze, wspólne dla czterech plików poniżej |
src/codegen.cpp |
1090 | Program, deklaracje, zasięgi i sprzątanie, instrukcje, wyrażenia, miejsca (ref), operatory, analiza modyfikacji |
src/codegen_types.cpp |
500 | Typy LLVM, rozwiązywanie typów, układ struktur, konwersje, własność (helpery kopiowania/zwalniania), wypisywanie, typy DWARF |
src/codegen_builtins.cpp |
710 | Wywołania, konstruktory, funkcje wbudowane, konwersje, metody tablic i tekstów, deklaracje runtime'u, panic |
src/abi.cpp |
310 | Klasyfikacja System V x86-64, wywołania C ze strukturami przez wartość, wrappery wywoływalne z C |
src/main.cpp |
390 | Sterownik: CLI, ładowanie, maszyna docelowa, O2, emisja, cache runtime'u, linkowanie, porady przy błędach linkera, run |
runtime/finch_rt.c |
440 | Biblioteka uruchomieniowa (§12) |
boot/*.fch |
3080 | Kompilator samohostujący (§17) |
3. Lekser
lex(file) przechodzi raz przez g_files[file].text i zwraca std::vector<Token> zakończony Tok::End.
struct Token { Tok kind; std::string text; int line, col; bool newlineBefore; };
- Nowe linie nie są tokenami. Każdy token zapamiętuje
newlineBefore, a o znaczeniu decyduje parser (§4). Dzięki temu obsługa nowych linii nie zaśmieca każdej reguły listowej w gramatyce. - Pozycje: od 1;
colliczy znaki (bajty kontynuacji UTF-8 jej nie zwiększają), więc^trafia pod"Błąd". Zabłąkany 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 struct defer. Nazwy typów nie są słowami kluczowymi:int,u8,ptr… toIdent, rozpoznawane przez parser za pomocątypeFromName(), więcu8(x)jest zwykłym wywołaniem, a lekser nie zależy od listy typów. - Liczby: dziesiętne albo
0xszesnastkowe z separatorami_. Bez ósemkowych (010to 10)..należy do liczby tylko wtedy, gdy po niej jest cyfra, więc0..10toInt DotDot Int. - Teksty / znaki: sekwencje
\n \t \r \0 \\ \" \'. Bez nowej linii w literale. - Operatory: najdłuższe dopasowanie (
->przed-,<</<=przed<,&&przed&,..przed.,:=przed:). Samotny:to token (argumenty nazwane). - Komentarze:
//i niezagnieżdżane/* */.
4. Gramatyka i parser
Parser (src/parser.cpp) to ręcznie pisany parser zstępujący z jednym tokenem wyprzedzenia, a w
atDeclaration() z maksymalnie trzema.
program = { import | link | struct | function } ;
import = "import" ( STRING | IDENT ) ; (* "x.h" = nagłówek C, nazwa = moduł Fincha *)
link = "link" STRING ; (* "glfw" | "plik.c" | "plik.o" | "lib.a" *)
struct = "struct" IDENT "{" { type IDENT [ "=" expr ] NOWA_LINIA } "}" ;
function = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ;
param = type IDENT ;
type = "[" "]" type | "ptr" [ "[" type "]" ] | TYPENAME | IDENT [ "." IDENT ] ;
block = "{" { statement TERMINATOR } "}" ;
statement = if | while | for | return | "break" | "continue" | "defer" statement | block
| type IDENT [ "=" expr ] (* deklaracja, zobacz atDeclaration *)
| IDENT ":=" expr
| target assignop expr (* cel: zmienna, pole, element, p.value *)
| call | method ;
for = "for" IDENT "in" expr ( ".." expr block | block ) ; (* zakres | po elementach *)
assignop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
expr = or ; or = and { "||" and } ; and = cmp { "&&" cmp } ;
cmp = add { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add } ;
add = mul { ( "+" | "-" | "|" | "^" ) mul } ;
mul = unary { ( "*" | "/" | "%" | "<<" | ">>" | "&" ) unary } ;
unary = ( "-" | "!" | "~" ) unary | postfix ;
postfix = primary { "." IDENT [ args ] | "[" expr "]" } ;
primary = INT | FLOAT | STRING | CHAR | "true" | "false" | "null"
| IDENT [ args ] | "(" expr ")" | "[" [ expr { "," expr } [","] ] "]" ;
args = "(" [ arg { "," arg } ] ")" ; arg = [ IDENT ":" ] expr ;
Koniec instrukcji. Po instrukcji endOfStatement() wymaga }, końca pliku albo tokenu
z newlineBefore. Wewnątrz wyrażeń sameLine() (parenDepth > 0 || !newlineBefore) decyduje, czy
operator, ., [ albo ( wywołania kontynuuje wyrażenie. parenDepth rośnie w ( ) i [ ],
a przy wejściu do { } jest zapamiętywany i zerowany.
Priorytety są jak w Go, nie jak w C: & wiąże jak *, a |/^ jak +, wszystkie powyżej porównań,
więc x & MASKA == 0 znaczy (x & MASKA) == 0.
Rozpoznawanie instrukcji. atDeclaration() zwraca prawdę dla [ ] (typ tablicowy), TYP IDENT,
ptr [ i IDENT . IDENT IDENT (typ z modułu). W pozostałych przypadkach parser parsuje całe
wyrażenie, a potem patrzy na następny token: operator przypisania robi z niego cel (musi być Var,
Member albo Index), a inaczej musi to być wywołanie funkcji lub metody, w przeciwnym razie
this value is computed but never used.
Literały struktur to wywołania: Punkt(1, 2) albo Punkt(x: 1, y: 2). Literały w klamrach
(Punkt{…}) byłyby niejednoznaczne z blokami (if gotowe {); wywołanie nie potrzebuje specjalnej
gramatyki, a argumenty nazwane (IDENT ":" w args()) są akceptowane tylko przez konstruktory na etapie generowania kodu.
5. AST
Wszystkie węzły są w src/ast.h: małe hierarchie klas z jawnym kind, rozgałęziane przez
switch + static_cast.
| ExprKind | Węzeł | Pola |
|---|---|---|
Int Float Bool Char Str |
literały | value |
Null |
NullExpr |
|
Var |
VarExpr |
name |
Unary |
UnaryExpr |
op (- ! ~), operand |
Binary |
BinaryExpr |
BinOp op, lhs, rhs |
Call |
CallExpr |
callee (nazwa), args, argNames |
Member |
MemberExpr |
obj, field (.x, .len, .ptr, .value) |
Index |
IndexExpr |
obj, index |
ArrayLit |
ArrayLitExpr |
elems |
Method |
MethodExpr |
obj, name, args, argNames; także modul.fch(...) |
| StmtKind | Węzeł |
|---|---|
Block VarDecl Assign Expr If While For ForEach Return Break Continue Defer |
jak w nazwie; ForEachStmt ma var, list, body; DeferStmt trzyma jedną instrukcję |
math.sqrt(x) parsuje się jako MethodExpr na VarExpr("math"); to, czy math jest modułem, czy zmienną,
rozstrzyga się przy generowaniu kodu. Wywołania nazywają funkcję, zamiast trzymać wyrażenie: Finch nie ma
funkcji jako wartości, a późne rozwiązywanie nazwy pozwala, by jeden CallExpr był funkcją wbudowaną,
konwersją (u8(x)), konstruktorem struktury, funkcją Fincha albo funkcją C.
Nie ma węzła „load”: to, czy x znaczy adres, czy wartość, zależy od kontekstu (ref() kontra expr(), §10.2).
Type
struct Type {
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null, Array, Fixed, Struct, Named };
Kind kind;
std::shared_ptr<Type> elem; // Ptr (null = `ptr` bez typu), Array, Fixed
long long count; // Fixed
std::string name, module; // Named (jak napisano) / Struct (rozwiązany)
StructInfo *info; // Struct
};
Parser tworzy Named dla nazw struktur; Codegen::resolve() zamienia je na Struct ze wskaźnikiem
info. Fixed ([N]T) pochodzi tylko z pól struktur C. int to I64, a float to F64.
Równość jest strukturalna; dwa Struct są równe, gdy mają to samo info.
6. Wczytywanie: pliki i moduły
Loader::load(path, module, from) w main.cpp czyta plik, dopisuje go do g_files (pozycje niosą
indeks pliku w Pos::file), leksuje go i parsuje, a potem wczytuje każdy jeszcze niewidziany import nazwa:
nazwa.fch obok importującego pliku albo w folderze z FINCH_PATH. Wynik to std::vector<Program>,
z głównym plikiem na początku. Cykle nie przeszkadzają: moduł jest oznaczany jako wczytany, zanim
zaczniemy podążać za jego importami.
Wszystkie pliki trafiają do jednego modułu LLVM. Każdy moduł Fincha ma ModuleScope (funkcje,
struktury, zbiór modułów, które importuje). Nazwy szuka się w bieżącym module; nazwy innego modułu
są dostępne tylko z kwalifikatorem (geometry.length, geometry.Vec) i tylko tam, gdzie moduł
zaimportowano. Funkcje są emitowane jako finch.<moduł>.<nazwa> (finch.<nazwa> w głównym pliku),
typy struktur jako finch.<moduł>.<Nazwa>.
7. Model semantyczny
Kolejność deklaracji
Codegen::run() najpierw deklaruje wszystkie struktury wszystkich modułów i zaimportowane struktury C,
rozwiązuje typy pól i ustawia ciała typów LLVM (declareStructs → resolveStruct), potem deklaruje
wszystkie sygnatury funkcji (declare), a dopiero potem definiuje ciała. Kolejność w pliku i między
modułami nigdy więc nie ma znaczenia, a rekurencja (także wzajemna i między modułami) działa.
resolveStruct wykrywa cykle przez wartość trójstanową flagą (struct A { A inner } to błąd z radą,
by użyć ptr[A] albo []A). Wskaźniki i tablice struktury nie potrzebują jej układu, więc
struct Wezel { []Wezel dzieci; ptr[Wezel] rodzic } jest w porządku: rozwiązują nazwę „płytko”
(resolveT(…, deep=false)).
Zasięgi i zmienne
struct Var { Value *slot; FType type; bool readonly; std::string readonlyWhy;
bool owned; int order; bool moved; };
struct Cleanup { bool isDefer; std::string var; const Stmt *body; int order; };
struct Scope { unordered_map<string, Var> vars; vector<Cleanup> cleanups; };
Zasięg jest otwierany dla parametrów funkcji, każdego { }, każdego for (trzyma i), każdego for … in
(ukryta tymczasowa lista) i każdej jego iteracji (trzyma x).
- Bez przesłaniania:
addVar()odrzuca nazwę znalezioną w dowolnym zewnętrznym zasięgu, funkcję modułu, nazwę zaimportowanego modułu i funkcję wbudowaną. - Tylko do odczytu: zmienne pętli zakresowej i pętli po elementach (
readonlyWhydaje konkretny komunikat). ordernumeruje deklaracje;deferużywa go, by ukryć zmienne zadeklarowane po nim (§9.5).
Rozwiązywanie nazw
x w pozycji wartości: zmienna lokalna → stała C → zmienna globalna C → (nazwa modułu: błąd z podpowiedzią) → błąd.
Wywołanie n(...): funkcje wbudowane (print addr input new free exit shell read_file write_file file_exists) →
nazwy typów (konwersja) → funkcja bieżącego modułu → struktura bieżącego modułu / struktura C
(konstruktor) → funkcja C → błąd (z podpowiedzią, jeśli funkcja istnieje w innym module, albo z
headerHint dla znanych nazw z libc).
8. System typów
Reprezentacja w LLVM
| Finch | LLVM |
|---|---|
bool |
i1 |
char i8 u8 / i16 u16 / i32 u32 / int u64 |
i8 / i16 / i32 / i64 |
f32 / float |
float / double |
str, []T |
{ ptr, i64, i64 } (wskaźnik, długość, pojemność) |
ptr, ptr[T], null |
ptr (nieprzezroczysty) |
[N]T |
[N x T] |
| struktura Fincha | nazwany typ struct, naturalny układ |
| struktura C | nazwany upakowany (packed) struct z jawnymi bajtami wypełnienia (§13.2) |
Znakowość istnieje tylko w typie Fincha i wybiera sdiv/udiv, ashr/lshr, icmp s*/u*,
sext/zext, sitofp/uitofp, fptosi/fptoui.
Wartości
struct Value_ { Value *v; FType type; bool literal; bool fresh; };
literal: liczba wpisana w kod albo zaimportowana stała C. Może stać się dowolnym typem całkowitym, w którym się mieści (fits()), albof32. Przechodzi przez minus jednoargumentowy i arytmetykę samych literałów zwijaną do stałej; jawne konwersje ją kasują (u8(250)to stała z typem, więcu8(250) + 10zawija się do 4).fresh: wartość posiadająca pamięć (str/tablica/struktura z takimi polami), której nikt jeszcze nie trzyma: wynik wywołania, literał, sklejenie, konstruktor. Trzeba ją zapisać (przenieść) albo zwolnić (§9).
Konwersje niejawne: coerce(v, want)
Identyczne typy; null → dowolny wskaźnik; wskaźnik z typem ↔ ptr bez typu; całkowita → całkowita,
jeśli wartość to mieszczący się literał albo typ się rozszerza (widens(): ta sama znakowość i więcej bitów
albo bez znaku → ściśle większa ze znakiem); całkowita → zmiennoprzecinkowa; f32 → f64; literał f64 → f32.
Nic więcej: błąd podpowiada jawną konwersję. Typy posiadające pamięć konwertują się tylko na siebie.
Literały tablic biorą typ elementu z miejsca, do którego trafiają (exprWant()), więc []f32 xs = [1, 2.5] działa.
Argumenty operatorów: unify(l, r)
Strona-literał dopasowuje się do drugiej; w przeciwnym razie rośnie strona, która się rozszerza;
i32 + u32 to błąd; całkowita + zmiennoprzecinkowa → zmiennoprzecinkowa; f32 + f64 → f64.
Nie ma promocji całkowitych z C: u8 + u8 zostaje u8.
Konwersje jawne: convert()
| Z → Na | IR |
|---|---|
| int/char ↔ int/char | trunc/sext/zext wg znakowości źródła |
| bool → int | zext |
| float ↔ int | fptosi/fptoui, sitofp/uitofp |
| float ↔ float | fpext/fptrunc |
| int → bool | icmp ne 0 |
| liczba/bool/char → str | finch_str_from_int/uint/float/char/bool (fresh) |
| str → int/float | finch_str_to_int/float (panic przy złym tekście) |
[]u8/[]char → str |
finch_str_from_bytes |
| ptr → str | finch_str_from_c (pożyczony, cap = -1) |
| ptr ↔ ptr | nic |
Porównania
Wskaźniki/null: tylko icmp eq/ne ptr. str: finch_str_eq (długość + memcmp) dla ==/!=,
finch_str_cmp dla porządku. Liczby zmiennoprzecinkowe: oeq olt ole ogt oge i une dla !=.
Całkowite i znaki wg znakowości (char bez znaku). Bool: tylko równość. Tablice/struktury: błąd.
9. Własność i pamięć
9.1 Model
Każda wartość typu posiadającego pamięć (str, []T albo struktura Fincha z takim polem) ma
dokładnie jednego właściciela: zmienną, element tablicy, pole struktury albo komórkę na stercie z new().
Właściciel zwalnia wartość, gdy sam znika. Nie ma liczenia referencji ani garbage collectora;
każda decyzja zapada statycznie.
| Sytuacja | Co się dzieje |
|---|---|
x := expr / T x = expr / x = expr / pole, element, push |
own(v): wartość fresh jest przenoszona; pożyczona jest kopiowana w głąb |
x = … (typ posiadający) |
najpierw liczona jest nowa wartość, stara zwalniana po zapisie |
koniec bloku, return, break, continue |
posiadane zmienne każdego opuszczanego zasięgu są zwalniane (od najnowszej), uruchamiają się defery |
return lokalna |
zmienna jest przenoszona na zewnątrz (oznaczana jako moved na czas sprzątania tego returna) |
| przekazanie argumentu | pożyczka: funkcja dostaje płytką kopię i nigdy jej nie zwalnia; argumenty fresh zwalnia wywołujący po wywołaniu |
| parametr, który funkcja zmienia | kopiowany na wejściu i posiadany przez funkcję (decyduje mutates(), niżej) |
wartość tymczasowa, której nikt nie trzyma (print(a + b), f(g()), instrukcja-wyrażenie) |
release(v): zwalniana zaraz po użyciu |
a.pop(), a.remove(i) |
element jest przenoszony na zewnątrz (fresh) |
for x in lista |
x pożycza kolejne elementy; jeśli ciało może zmieniać lista, elementy są kopiowane |
Literał tekstowy to wartość fresh z cap = 0: przeniesienie nic nie kosztuje, zwalnianie jest pomijane
(release() ignoruje stałe), a zapis do niego (s[i] = c) najpierw robi kopię na stercie (finch_str_own).
Teksty z C są pożyczane z cap = -1 i zawsze kopiowane, zanim zostaną zatrzymane.
9.2 Pożyczanie parametrów: mutates()
Przy deklarowaniu funkcji każdy parametr posiadający pamięć jest sprawdzany przez mutates(nazwa, ciało):
przypisanie, którego zmienna-korzeń (rootVar(): przez .pole i [i]) jest tym parametrem, metoda
zmieniająca (push pop insert remove clear resize sort reverse) albo addr(…) na nim. Niezmieniane
parametry są pożyczane (Fn::borrowParam), zmieniane kopiowane raz na wejściu. Analiza jest składniowa
i zachowawcza: wszystko, co może zmienić parametr, powoduje kopię, co zawsze jest poprawne.
Ta sama analiza decyduje, czy for x in lista musi kopiować elementy.
9.3 Kopiowanie i zwalnianie
dropAt(addr, T) / copyAt(dst, src, T) działają na adresach:
str:finch_str_drop(zwalnia, gdycap > 0) /finch_str_copy(literały współdzieli, resztę duplikuje).[]Ti struktury posiadające: generowane helperyfinch.drop.<klucz>ifinch.copy.<klucz>, tworzone raz na typ (dropFn,copyFn, klucz ztypeKey(), np.arr_S_main_Gracz). Tablice zwalniają każdy posiadający element, a potemfinch_arr_free; kopie wołająfinch_arr_clone_raw, a potem kopiują w głąb posiadające elementy na surowe bity. Struktury zwalniają/kopiują tylko pola posiadające.
Helpery są emitowane we własnych funkcjach (HelperScope zapamiętuje pozycję buildera i wyłącza lokacje
debugowe), mogą wołać się rekurencyjnie (struct Wezel { []Wezel dzieci }) i są wstawiane (inline) przez O2.
9.4 Jak to wygląda
fn greet(str name) -> str {
msg := "Hi, " + name
return msg
}
fn main() {
a := ["x"]
b := a
b.push(greet("Ola"))
print(b[1])
}
IR funkcji main z -O0 (skrócony):
%3 = call ptr @finch_alloc(i64 24) ; ["x"]: jeden element na stercie
store { ptr, i64, i64 } { ptr @str.1, i64 1, i64 0 }, ptr %4 ; literał, cap 0
...
store { ptr, i64, i64 } %7, ptr %a ; przeniesione do a (fresh)
%a1 = load { ptr, i64, i64 }, ptr %a
store { ptr, i64, i64 } %a1, ptr %1
call void @finch.copy.arr_str(ptr %2, ptr %1) ; b := a kopiuje
%8 = load { ptr, i64, i64 }, ptr %2
store { ptr, i64, i64 } %8, ptr %b
%9 = call { ptr, i64, i64 } @finch.greet({ ptr, i64, i64 } { ptr @str.2, i64 3, i64 0 })
...
call void @finch_arr_reserve(ptr %b, i64 %12, i64 24) ; push: powiększ, jeśli trzeba
store { ptr, i64, i64 } %9, ptr %15 ; wynik greet przeniesiony, bez kopii
...
%20 = icmp uge i64 1, %19 ; b[1]: kontrola zakresu
br i1 %20, label %out.of.range, label %in.range
...
call void @finch.drop.arr_str(ptr %b) ; koniec main: od najnowszej
call void @finch.drop.arr_str(ptr %a)
ret void
a greet zwraca msg bez kopiowania (ret … %5 po wczytaniu slotu: zmienna została przeniesiona).
9.5 defer
defer instrukcja dokłada do bieżącego zasięgu Cleanup{isDefer, body, order = bieżące varOrder}
i niczego w tym miejscu nie emituje. Za każdym razem, gdy emitowane jest sprzątanie zasięgu (dojście do
końca bloku, return, wyjście przez break/continue), ciało jest generowane w tym wyjściu, z
deferLimit = order, żeby lookup() ukrył zmienne zadeklarowane po defer: te są wtedy już zwolnione
(sprzątanie idzie w odwrotnej kolejności). return/break/continue w instrukcji defer to błędy (inDefer).
Ciała defer są więc powielane na każdą ścieżkę wyjścia, przez co na zwykłej ścieżce nic nie kosztują.
9.6 Pamięć ręczna
new(v) alokuje sizeof(T) przez finch_alloc, zapisuje own(v) i zwraca ptr[T]. free(p) zwalnia
wskazywaną wartość, jeśli T posiada pamięć (pomijając null), i woła free z libc. Wskaźniki nigdy
niczego nie posiadają: addr(x) może wisieć po końcu bloku x; nie jest to sprawdzane.
9.7 Weryfikacja
MEMCHECK=1 tests/run.sh buduje każdy program testowy i uruchamia go pod valgrindem z
--leak-check=full --errors-for-leak-kinds=all: zero wycieków i zero nieprawidłowych dostępów we wszystkich,
w tym przy returnach z wnętrza pętli, continue/break z posiadanymi zmiennymi, deferach, modyfikacji
podczas iteracji, zagnieżdżonych tablicach struktur i przekazywaniu struktur C.
10. Generowanie kodu
10.1 Zmienne
Każda zmienna, łącznie z parametrami, dostaje alloca na początku bloku wejściowego (slot();
struktury C dostają wyrównanie z C). addVar() zapisuje wartość początkową, rejestruje zmienną,
dokłada sprzątanie, jeśli zmienna posiada pamięć, i (z -g) zgłasza ją debuggerowi.
10.2 Miejsca: ref(), place(), expr()
struct LRef { bool isPlace; Place pl; Value_ val; }; // adres albo (jeśli go nie ma) wartość
ref(e, forWrite) przechodzi łańcuch Var/Member/Index i zwraca adres, jeśli istnieje:
Var→ jejalloca(albo zmienna globalna C)..polena miejscu-strukturze →getelementptrdo pola (llvmIndex; struktury C pomijają elementy wypełnienia).- przez wskaźnik (
p.value,p.pole,p[i]) → wczytanie wskaźnika, kontrola null, adres celu.p.valuena wskaźniku do struktury z własnym polemvalueoznacza to pole. a[i]na miejscu-tablicy/tekście → wczytanie{ptr,len,cap},boundsCheck(i, len),getelementptr. PrzyforWritenastrnajpierwfinch_str_own(kopiowanie przy zapisie dla literałów).[N]Tsprawdza względemN..len/.ptr→ wartości.- wartość tymczasowa (wynik wywołania…) nie ma adresu: jej pole/element jest wyciągany, kopiowany (jeśli posiada pamięć), a sama wartość zwalniana.
expr() dla tych rodzajów woła ref() i wczytuje spod adresu: stąd bierze się każdy load zmiennej,
pola albo elementu. place() wymaga adresu (cele przypisań, addr()).
10.3 Przypisanie
x = v: najpierw liczone jest v (może przenieść pamięć, w której żyje cel: a[0] = f(), gdzie f
robi push do a), potem place(cel, forWrite), coerce, own, wczytanie starej wartości, zapis i
zwolnienie starej. Złożone x += v: liczenie v, wczytanie celu, arith(), zapis, zwolnienie starej
wartości (dla str +=).
10.4 Funkcje i main
Funkcje są internal z naturalną sygnaturą LLVM (agregaty przez wartość). Parametry posiadające
pamięć są kopiowane na wejściu tylko wtedy, gdy borrowParam jest fałszywe. fn main to wewnętrzna
finch.main; osobna zewnętrzna i32 main(i32, ptr) (defineMainWrapper) buduje []str args przez
finch_args, jeśli trzeba, woła ją, zwalnia argumenty i zwraca kod wyjścia. Funkcja, która dochodzi do
swojego końca, dostaje sprzątanie zasięgów i ret void albo błąd can reach its end without returning T.
10.5 Sterowanie przebiegiem
Bieżący blok buildera jest źródłem prawdy o osiągalności (terminated()): instrukcje po terminatorze to
błąd (this code can never run). continueAt() usuwa blok łączący, do którego nikt nie skacze.
if/while/for mają klasyczny układ bloków; while true bez break zostawia funkcję zakończoną
(nie trzeba po nim return). for i in a..b liczy oba końce raz; continue idzie do for.step.
for x in lista czyta długość listy z jej adresu w każdej rundzie (push w środku pętli jest bezpieczny)
i trzyma indeks w slocie na stosie. break/continue emitują sprzątanie zasięgów otwartych wewnątrz pętli
(Loop::scopeDepth) przed skokiem.
10.6 Skrócone && / ||
Jedyne ręcznie budowane φ: lewa strona kończy się w bloku from, prawa w rhsEnd;
phi i1 [!isAnd, from], [rhs, rhsEnd].
10.7 Wywołania
callFinch() konwertuje argumenty na typy parametrów (literały tablic przez exprWant), przekazuje
posiadające płytko, woła, a potem zwalnia argumenty fresh. Wyniki typów posiadających są fresh.
construct() buduje strukturę od zeroinitializer przez insertvalue: pozycyjnie (wszystkie pola)
albo po nazwie (brakujące biorą wyrażenie domyślne pola, liczone w module struktury z podmienionymi
zasięgami wywołującego, albo zero). Wywołania C opisuje §13.
10.8 Wypisywanie
print emituje printf dla każdego argumentu z formatem wybranym wg typu (%lld, %llu, %g, %c,
%.*s dla str z jego długością, %p), oddzielając spacjami, a na końcu \n. Tablice, tablice o stałym
rozmiarze i struktury wołają generowane helpery finch.print.<klucz>, które wypisują [1, 2] /
Punkt(x: 1, y: 2), biorąc teksty i znaki w środku w cudzysłów.
10.9 Metody tablic i tekstów
Tablice: push (finch_arr_reserve + zapis + len++), pop/remove (przeniesienie na zewnątrz,
remove_gap), insert (insert_gap), clear/resize (zwolnienie znikających elementów,
finch_arr_resize), find/contains (pętla z compare(Eq)), slice (nowa tablica, copyAt na element),
reverse (pętla zamian), sort (qsort z libc z generowaną porównywarką finch.cmp.<klucz>), join (runtime).
Metody zmieniające tablicę wymagają adresu (zmienna/pole/element).
Teksty: sub find contains starts_with ends_with split trim upper lower replace repeat bytes, wszystkie w runtime.
11. Kontrole w czasie działania
Wszystkie błędy w czasie działania idą przez finch_panic(plik, linia, msg) /
finch_panic_index(plik, linia, i, len) (zadeklarowane noreturn cold), więc gałąź błędu jest układana
jako zimna, a kontrola kosztuje porównanie i dobrze przewidywany skok. LLVM usuwa kontrole, które potrafi
udowodnić (stałe indeksy w zakresie, kontrole wyciągnięte z pętli).
| Kontrola | Gdzie |
|---|---|
| dzielenie/reszta przez zero (stałe zero: błąd kompilacji) | checkDivisor() |
MIN / -1 dla typów ze znakiem |
checkDivisor() |
indeks tablicy/tekstu/tablicy stałej poza zakresem (icmp uge, więc także ujemne) |
boundsCheck() |
.value, .pole, [i] przez null |
member(), index() |
pop() na pustej, slice()/sub() poza zakresem |
metody, runtime |
zły tekst w int(s)/float(s), nieczytelny plik w read_file |
runtime |
Sito na 20 milionów elementów z kontrolą zakresu przy każdym dostępie działa tak szybko jak to samo w C (ok. 0,09 s): kontrole są wyciągane z pętli albo zwijane.
12. Runtime w C
runtime/finch_rt.c to zwykłe C bez stanu globalnego. CMake wbudowuje go w kompilator jako surowy
napis (rt_source.inc); przy pierwszym użyciu runtimeObject() kompiluje go przez $CC -O2 -fPIC -c
do ~/.cache/finch/rt-<hash>.o, gdzie hash obejmuje źródło, wersję Fincha i kompilator C.
Jest linkowany statycznie do każdego programu.
Wspólny układ:
typedef struct { char *ptr; int64_t len; int64_t cap; } FStr; // ptr zawsze zakończony zerem
typedef struct { void *ptr; int64_t len; int64_t cap; } FArr;
// cap tekstu: > 0 sterta (posiadany), 0 literał (statyczny), -1 pożyczony z C (skopiuj, zanim zatrzymasz)
Funkcje przyjmują i zwracają te struktury przez wskaźnik, więc ich ABI jest trywialne (bez klasyfikacji
struktur): budowanie tekstów (concat, from_int/uint/float/char/bool, sub, trim, upper, lower,
replace, split, join, repeat, from_bytes), porównania (eq, cmp, find, starts, ends),
własność (copy, own, drop, from_c), tablice (reserve, make, resize, clone_raw, free,
insert_gap, remove_gap), args, input, pliki, shell i panic. Brak pamięci wypisuje
out of memory i kończy proces.
13. Współpraca z C: nagłówki, struktury, ABI
13.1 Import nagłówków
importHeaders(imports, dirs) parsuje każdy import "x.h" jako osobną jednostkę translacji z pliku
w pamięci #include "x.h", z -x c -std=gnu11, -I dla folderu każdego pliku .fch i -isystem dla
każdego katalogu przeszukiwanego przez systemowy kompilator C (odczytanego z $CC -E -v -x c /dev/null,
dzięki czemu działa to na NixOS). gnu11, bo glibc w trybie ścisłego ISO ukrywa M_PI i podobne.
Importer odwiedza kursory najwyższego poziomu:
| Kursor | Wynik |
|---|---|
FunctionDecl |
CFunc (sygnatura mapowana w kontekście sygnatury; funkcje static i K&R zapisywane jako nieobsługiwane) |
TypedefDecl rekordu |
nazywa rekord (typedef struct {…} CXCursor → CXCursor) |
StructDecl (definicja) |
nazywa rekord jego tagiem |
VarDecl extern |
CGlobal (mapowany w kontekście danych) |
EnumConstantDecl, liczbowe MacroDefinition |
CConst (literał) |
mapType() działa na typach kanonicznych i mapuje liczby całkowite wg rozmiaru. Kontekst decyduje o
char *: w sygnaturach funkcji to str (Finch konwertuje przy wywołaniu), ale w strukturach, zmiennych
globalnych i za wskaźnikami to ptr[char] (tam układ pamięci musi zostać pojedynczym wskaźnikiem).
Rekordy przez wartość stają się Named{nazwa, "C"} i są definiowane przez defineRecord(): każde pole
(clang_Type_visitFields) z przesunięciem w bajtach, rozmiarem i wyrównaniem, zagnieżdżone rekordy,
tablice stałe (Fixed). Unie i pola bitowe zostają jako ukryte bajty i oznaczają strukturę jako
nieobsługiwaną przy wywołaniach przez wartość; wskaźniki na wszystko inne (void, funkcje, niekompletne
struktury) stają się ptr.
13.2 Układ struktur C
resolveStruct() buduje upakowany struct LLVM, który odwzorowuje C bajt w bajt: wypełnienie
[n x i8] przed każdym polem, którego przesunięcie wyprzedza bieżącą pozycję, samo pole (tylko jeśli jego
rozmiar w LLVM zgadza się z C) i wypełnienie końcowe do sizeof z C. Dostęp do pól używa zapisanego
llvmIndex. Upakowanie czyni układ dokładnym niezależnie od reguł wyrównania LLVM; alokacje i wartości
tymczasowe dostają wyrównanie z C jawnie.
13.3 Konwencja wywołań System V x86-64
classify(T) (w abi.cpp) spłaszcza strukturę do skalarnych liści z przesunięciami w bajtach (przez
zagnieżdżone struktury i tablice stałe) i:
- MEMORY, jeśli jest większa niż 16 bajtów albo ma niewyrównany liść: przekazywana jako wskaźnik z
byval(T) align ≥8, zwracana przez ukryty pierwszy wskaźniksret(T). - w przeciwnym razie każdy eightbyte jest INTEGER, jeśli którykolwiek liść w nim jest liczbą
całkowitą/wskaźnikiem, a inaczej SSE. Części: INTEGER →
iNdla pozostałych bajtów (i32dla 4-bajtowegoColor), SSE →double,<2 x float>albofloat. Jedna część idzie wprost; dwie jako dwa argumenty albo zwracane jako{a, b}.
makePlan() przechodzi parametry, licząc 6 rejestrów całkowitych i 8 SSE; struktura, której części już się
nie mieszczą, idzie w całości na stos (MEMORY), tak jak robi to clang. Małe parametry całkowite dostają
signext/zeroext. emitCCall() zapisuje każdy argument-strukturę do slotu wystarczająco dużego dla
struktury i jej postaci rejestrowej, wczytuje części z przesunięć 0 i 8, woła z zaplanowanym typem funkcji
i atrybutami, a wyniki-struktury odbudowuje w ten sam sposób.
cThunk(fn) to odwrotność dla addr(fn): wewnętrzna funkcja z sygnaturą C, która składa parametry-struktury
z rejestrów albo pamięci byval, woła funkcję Fincha i obniża jej wynik. Akceptowane są tylko sygnatury zgodne z C.
tests/run/c_structs.fch sprawdza {float,float}, {float,float,float}, {u8×4}, {double,int},
{i64,i64}, strukturę 24-bajtową, strukturę z char[8], zagnieżdżone struktury, wyczerpanie rejestrów
przy 13 argumentach-strukturach oraz wywołania zwrotne przyjmujące i zwracające struktury, na bibliotece C
kompilowanej przez link "abi.c". Wyniki zgadzają się z tymi samymi wywołaniami zrobionymi z C.
13.4 Linkowanie kodu C
link "nazwa" próbuje pkg-config --libs nazwa, a potem -lnazwa. link "plik.c" kompiluje plik
(względem pliku .fch) przez $CC -O2 -fPIC -c do tymczasowego obiektu; pliki .o i .a przechodzą
bez zmian. Gdy linkowanie się nie uda, linkFailed() mapuje niezdefiniowane symbole na nagłówek, który
je zadeklarował, i wypisuje linijkę link do dopisania.
14. Optymalizacja, emisja, linkowanie
- Cel:
sys::getDefaultTargetTriple(), CPUgeneric,Reloc::PIC_. Data layout jest ustawiany na module, zanim powstanie jakikolwiek IR (od niego zależy wyrównanie loadów i store'ów). - Optymalizacja:
PassBuilder::buildPerModuleDefaultPipeline(O2): środkowa część-O2z clanga.-O0ją pomija (finch ir plik.fch -O0pokazuje surowe wyjścieCodegen). - Emisja: stary
PassManager+addPassesToEmitFile(ObjectFile). - Linkowanie:
$CC prog.o rt.o [obiekty użytkownika] -lm [biblioteki]. Na Niksie wrapper cc zamienia ścieżki-LnaRPATH, więc programy działają bezLD_LIBRARY_PATH. run: tymczasowy plik wykonywalny uruchamiany z resztą argumentów i potem usuwany; sygnał jest zgłaszany jakothe program crashed: <nazwa>z kodem wyjścia128 + n.
Uwagi o wydajności: funkcje użytkownika są internal, więc LLVM swobodnie je wstawia i specjalizuje
(rekurencyjne fib(40): ok. 0,19 s wobec ok. 0,23 s dla clang -O2 na tym samym kodzie C na maszynie
autora); kod tablicowy z kontrolą zakresu dorównuje C na sicie.
15. Informacje dla debuggera
Z -g Codegen tworzy DIBuilder, jednostkę kompilacji (DW_LANG_C, więc składnia wyrażeń C w gdb
działa dla print p.x), DIFile na plik źródłowy, DISubprogram na funkcję, zmienne-parametry
(createParameterVariable, więc bt pokazuje length (v=…)), zmienne lokalne (insertDeclare na ich
alloca) i DILocation na każdą instrukcję i wywołanie (setLoc). Typy: podstawowe z nazwami Fincha
(int, u8, float), str/tablice jako struktury {ptr, len, cap}, wskaźniki, tablice stałe i struktury
z przesunięciami pól z StructLayout LLVM. Struktury rekurencyjne używają zastępowalnej deklaracji
wyprzedzającej. Generowane helpery nie mają lokacji. Najlepiej działa -g -O0; przy O2 część zmiennych jest usuwana.
16. Diagnostyka
failAt(plik, linia, kol, msg) wypisuje ścieżka:linia:kol: error: msg i linijkę źródła ze znakiem ^
(tabulatory zachowane, kolumny liczone w znakach), a potem kończy proces z kodem 1. Konwencje: operatory
binarne raportują operator, wywołania nazwę funkcji, błędy argumentów pozycję argumentu. Komunikaty mówią,
co jest źle, a gdy naprawa jest oczywista, także jak to naprawić. Ostrzeżeń nie ma. Błąd weryfikatora LLVM
wypisuje internal compiler error (please report) i kończy proces z kodem 2.
17. Kompilator samohostujący (boot/)
boot/ to drugi kompilator Fincha napisany w Finchu (ok. 3100 linii):
| Plik | Zawartość |
|---|---|
boot/lexer.fch |
lekser (tokeny to {kind, text, line, col, nl}; rodzaje to słowa, a słowa kluczowe i operatory są swoim własnym rodzajem) |
boot/ast.fch |
Type (kind, elem jako tablica 0/1 elementów, name, module), jeden ogólny Node na wszystko, Program |
boot/parser.fch |
ta sama gramatyka i reguły nowych linii co parser C++, wspinanie się po priorytetach dla operatorów binarnych |
boot/gen.fch |
sprawdzanie typów + generator tekstowego LLVM IR: zasięgi, miejsca, konwersje, kopie, helpery, wypisywanie, kontrole, metody tablic i tekstów |
boot/main.fch |
ładowanie modułów i sterownik: zapisuje .ll, uruchamia clang -O2 plik.ll runtime/finch_rt.c |
Obsługuje rdzeń języka: int float bool char str, []T, struktury (wartości domyślne, konstruktory po
nazwie i po kolei), ptr[T]/addr/new/free/null, wszystkie instrukcje poza defer, moduły oraz
funkcje wbudowane i metody kompilatora C++, bez importu C, liczb z rozmiarem i defer. Ma tę samą
semantykę wartości i analizę pożyczania (kopiuje tam, gdzie główny kompilator), ale nigdy nie zwalnia:
jako kompilator bootstrapowy zostawia pamięć systemowi operacyjnemu.
Stan kompilatora to jedna struktura Gen przekazywana jako ptr[Gen]: Finch nie ma zmiennych globalnych,
a przekazanie wskaźnika to sposób, w jaki funkcja zmienia wartość wywołującego. Bez defer i typu map zasięgi
to tablice tablic przeszukiwane liniowo; funkcje pomocnicze (kopiowanie/wypisywanie/porównywanie na typ)
są generowane przez tymczasową podmianę buforów wyjścia (beginHelper/endHelper).
Punkt stały. tests/boot.sh:
build/finch(C++) kompilujeboot/→ stage 1,- stage 1 kompiluje
boot/→ stage 2 (i jego IR), - stage 2 kompiluje
boot/→ IR stage 3,
i wymaga, żeby IR ze stage 2 i stage 3 był identyczny co do bajtu (ok. 32 000 linii). Potem kompiluje
kompilatorem samohostującym każdy test z tests/run, który mieści się w podzbiorze języka, i porównuje
wynik z oczekiwanym. Kompilacja samego siebie zajmuje kompilatorowi samohostującemu ok. 2,3 s.
18. System budowania
find_package(LLVM CONFIG); jeśli istnieje współdzielony celLLVM, Finch linkuje tę jedną bibliotekę.libclang.sosam linkujelibLLVM.so; linkowanie dodatkowo statycznych bibliotek komponentów wprowadzało do procesu dwie kopie LLVM, a zduplikowane obiekty globalne wywracały program przy wyjściu (podwójne zwolnienie w globalnym destruktorze). Do tego binarka miała 82 MB zamiast ok. 20 MB.- libclang:
find_path(clang-c/Index.h)+find_library(clang). runtime/finch_rt.cjest wczytywany przy konfiguracji dobuild/rt_source.incjako surowy literał napisowy;CMAKE_CONFIGURE_DEPENDSponawia konfigurację, gdy plik się zmieni.FINCH_VERSIONzproject(VERSION 2.0.0). C++17,-Wall -Wextra, bez ostrzeżeń.shell.nixwymieniallvmPackages.clangprzedllvmPackages.libclang: ten drugi dostarcza też „gołego”clang, który nie widzi nagłówków systemowych.
19. Testy
tests/run.sh: każdytests/run/*.fchzfn mainmusi wypisać dokładnie swój.out(stdin z.in, jeśli jest); pliki bezmainto moduły albo pliki pomocnicze. Każdytests/fail/*.fchmusi się nie udać z tekstem z linijki// expect:. Obecnie 59 passed, 0 failed.MEMCHECK=1 tests/run.sh: to samo plus valgrind na każdym programie (bez wycieków i złych dostępów).tests/boot.sh: punkt stały samohostowania i przebieg na podzbiorze (§17).
20. Semantyka a C
| Sytuacja | C | Finch 2.0 |
|---|---|---|
| Przepełnienie liczby ze znakiem | UB | zdefiniowane zawinięcie (bez nsw) |
Dzielenie całkowite przez zero, INT_MIN / -1 |
UB | błąd kompilacji, jeśli stałe, inaczej błąd w czasie działania |
| Indeks tablicy poza zakresem | UB | błąd w czasie działania (także dla pól [N]T struktur C) |
| Dereferencja null | UB | błąd w czasie działania dla .value, .pole, [i] |
| Użycie po zwolnieniu / podwójne zwolnienie tablic, tekstów, struktur | częste błędy | niemożliwe: tych nie zwalnia się ręcznie |
| Wyciek tablic, tekstów, struktur | częsty błąd | niemożliwy bez new |
x & 1 == 0 |
x & (1 == 0) |
(x & 1) == 0 |
| Niejawne zwężanie, mieszanie ze znakiem / bez znaku | po cichu | błędy kompilacji |
| Niezainicjalizowane zmienne | UB | niemożliwe (zero albo wartości domyślne) |
| Brak return | UB, jeśli użyty | błąd kompilacji |
| Kolejność obliczeń | w większości nieokreślona | od lewej do prawej; w przypisaniu najpierw prawa strona |
| Przesunięcie ≥ szerokość, float→int poza zakresem | UB | nadal poison (jeszcze niesprawdzane) |
Wiszący wskaźnik z addr() / new + free |
UB | nadal UB |
21. Spis funkcji
Sterownik (main.cpp)
| Funkcja | Robi |
|---|---|
Loader::load / findModule |
czyta, leksuje i parsuje plik oraz rekurencyjnie jego moduły / szuka nazwa.fch |
hostMachine, optimize, emitObject |
maszyna docelowa, potok O2, plik obiektowy |
runtimeObject |
kompiluje runtime i trzyma go w cache |
capture, libFlags, link |
uruchamia polecenie / pkg-config albo -l / linkowanie, z kompilacją plików z link "x.c" |
linkFailed, guessLib |
zamieniają wyjście linkera na porady |
Lekser i parser
| Funkcja | Robi | Różni się od |
|---|---|---|
lex / tokName |
tekst → tokeny / nazwy do komunikatów | |
accept / expect / unexpected |
konsumuje, jeśli pasuje / konsumuje albo zgłasza błąd / błąd z kontekstem | |
sameLine / endOfStatement |
czy ten token może kontynuować wyrażenie? / czy instrukcja się skończyła? | |
atDeclaration / type |
podgląd deklaracji / parsowanie typu | |
args |
( [nazwa:] expr, … ) dla wywołań, metod i konstruktorów |
|
postfix |
łańcuchy .pole, .metoda(…), [i] |
primary parsuje początek łańcucha |
Codegen: program i instrukcje (codegen.cpp)
| Funkcja | Robi | Różni się od |
|---|---|---|
declareStructs / declareFns / declare |
wszystkie typy / wszystkie sygnatury / jedna sygnatura + analiza pożyczania | |
define / defineMainWrapper |
jedno ciało / punkt wejścia C | |
pushScope / popScope |
otwiera / zamyka zasięg (popScope emituje jego sprzątanie, jeśli osiągalne) |
|
emitCleanups(downTo) / emitScopeCleanups(i) |
sprzątanie kilku zasięgów bez zamykania (return/break) / jednego zasięgu | |
slot / tmpOf / tmp |
alloca zmiennej / anonimowa / anonimowa z wartością |
|
lookup / addVar |
szuka widocznej zmiennej (respektuje deferLimit) / deklaruje zmienną |
|
stmt, varDecl, assign, ifStmt, whileStmt, forStmt, forEachStmt, returnStmt, jump |
instrukcje | |
rootVar / mutates / mutatesExpr |
zmienna, od której zaczyna się l-wartość / czy instrukcja albo wyrażenie ją zmienia? |
Codegen: wyrażenia
| Funkcja | Robi | Różni się od |
|---|---|---|
expr / exprWant |
r-wartość / to samo z oczekiwanym typem dla literałów tablic | |
ref / place |
adres, jeśli jest, inaczej wartość / adres albo błąd | expr wczytuje z ref |
member / index |
.x, .len, .ptr, .value, auto-dereferencja / [i] z kontrolą zakresu i kopiowaniem przy zapisie |
|
arrayLit |
[…] → tablica na stercie |
|
unary, binary, arith/arithOp, compare, logic |
operatory (arith dokłada flagę literału) |
|
unify / coerce / convert |
dwa argumenty do siebie / jedna wartość do miejsca / jawne T(x) |
Codegen: typy i własność (codegen_types.cpp)
| Funkcja | Robi |
|---|---|
ty, resolve/resolveT, findStruct, resolveStruct |
typ LLVM; Named → Struct (głęboko albo płytko); szukanie struktury; układ + kontrola cykli |
owning, zero, defaultValue |
czy typ posiada pamięć; stała zerowa; zero albo wartości domyślne struktury |
own / release |
coś do zapisania (przeniesienie albo kopia) / zwolnienie niezatrzymanej wartości fresh |
copyValue, dropValue, copyAt, dropAt, copyFn, dropFn, typeKey |
kopiowanie i zwalnianie przez wartość, przez adres i helpery na typ |
forN |
wewnętrzna pętla for i in 0..n używana przez helpery |
strConst, cstr, strFromC, elemSize |
stała-literał str; str → char*; char* → pożyczony str; rozmiar elementu |
emitPrint, printFn, printf_ |
wypisywanie |
diType, setLoc |
informacje dla debuggera |
Codegen: wywołania (codegen_builtins.cpp) i ABI (abi.cpp)
| Funkcja | Robi |
|---|---|
call / method |
rozwiązuje wywołanie / metodę albo wywołanie z modułu |
callFinch / construct / callC |
funkcja Fincha / konstruktor struktury / funkcja C |
print, addrOf, convert, arrayMethod |
funkcje wbudowane |
rt / libc |
deklaruje funkcję runtime'u / libc |
fileName, panicIf, boundsCheck, checkDivisor |
kontrole w czasie działania |
classify, makePlan, declareC, emitCCall, cThunk |
klasyfikacja System V, plan rejestrów, deklaracja, wywołanie, wrapper dla callbacków |
22. Rozbudowa kompilatora
Funkcja wbudowana: dodaj nazwę do isBuiltin(), obsłuż ją w call(), zaimplementuj (w IR albo jako
funkcję runtime'u zadeklarowaną w tabeli rt()), dodaj przypadki w tests/run/ i tests/fail/. Jeśli ma
istnieć też w kompilatorze samohostującym, dodaj ją do call() i runtimeDecls() w boot/gen.fch.
Metoda tablicy albo tekstu: dodaj gałąź w arrayMethod() albo w części str w method(); jeśli
zmienia tablicę, dopisz jej nazwę do zbiorów changing w method() i mutatesExpr(), żeby pożyczanie
dalej było poprawne.
Operator: token leksera + tokName, BinOp, właściwa funkcja priorytetu w parserze, opName, emisja
w arithOp/compare i wyjaśnienie w badOperands.
Instrukcja: StmtKind + węzeł, Parser::statement, Codegen::stmt; jeśli przenosi sterowanie,
respektuj terminated(), używaj continueAt() dla bloków łączących i emituj sprzątanie (emitCleanups)
przed wyskokiem z zasięgów.
Nowy typ posiadający pamięć: rozszerz owning(), dropAt/copyAt (i ich helpery), emitPrint, diType.
23. Znane ograniczenia
- Brak metod w strukturach, typów generycznych, map,
match, domknięć i błędów jako wartości (int("x"),read_filerobią panic). - Teksty to bajty:
.len,s[i],upper()nie znają Unicode. - Wiszące wskaźniki (
addrzmiennej, która zniknęła, użycie pofree) nie są wykrywane. - Wielkość przesunięć i konwersje float→int nie są sprawdzane (poison w LLVM, jak w C).
- C: unie i struktury z polami bitowymi przez wartość, makra-funkcje,
long double. - Testowana jedna platforma: Linux x86-64 (kod ABI jest specyficzny dla System V x86-64).
- Kompilator samohostujący nie ma importu C, liczb z rozmiarem ani
deferi nie zwalnia pamięci.