Kompilator umożliwia generacje kodu LLVM jak i x86 w wersji 64-bitowej.
Kompilator został napisany w języku Rust, więc aby go uruchomić na maszynie musi zostać zainstalowany ten język. Do poprawnego działania projektu wystarczy, aby na maszynie działały narzędzia cargo
oraz rustc
.
W przypadku braku Rusta na maszynie, najłatwiej go zainstalować używając komendy:
curl https://sh.rustup.rs -sSf | sh
A następnie wykonując przelogowanie w celu poprawnego załadowania środowiska Rust. Niecierpliwi, zamiast tego mogą skorzystać z komendy:
source $HOME/.cargo/env
Bo wykonaniu powyższych kroków Rust powinien zostać zainstalowany na maszynie, wraz z jego podstawowymi narzędziami
- rustup - instalator
- rustc - kompilator
- cargo - manager pakietów
W celu odinstalowania Rusta z maszyny należy skorzystać z komendy:
rustup self uninstall
make
Buduje cały projekt (z pominięciem generowania parsera). W wyniku powstają pliki latc
, latc_x86_64
oraz latc_llvm
.
make x86
Buduje plik latc_x86_64
oraz latc
make llvm
Buduje plik latc_llvm
oraz latc
make generate
Generuje pliki statyczne parsera (src/parser/src/grammar.rs
). Wywołanie make
nie wykonuje tej operacji z racji na to, że parser musi zostać wygenerowany na nowo tylko w przypadku zmian w gramatyce, a sam proces jest dosyć czasochłonny.
Dodatkowo make posiada kilka konfigurowalnych flag:
SILENT
- jeśli ustawione na1
to kompilacja jest wykonywana w trybie "cichym" (domyślnie:1
)ANALYSIS
- jeśli ustawione na0
to analiza semantyczna jest pomijana (domyślnie:1
)CODEGEN
- jeśli ustawione na0
to generowanie kodu jest pomijane (domyślnie:1
)MEM2REG
- jeśli ustawione na0
to generowany kod nie jest w postaci SSA (domyślnie:1
)OPTIMIZE
- jeśli ustawione na0
to kod generowany jest bez optymalizacji (domyślnie:1
)REGALLOC
- jeśli ustawione na0
to kod x86 generowany jest bez alokacji rejestrów (domyślnie:1
)
UWAGA: kompilacja x86 aktualnie nie jest możliwa przy wyłączonym MEM2REG
, a kompilacja z REGALLOC
nie będzie działała jeśli wyłączymy optymalizacji, gdyż wyłącza to MEM2REG
.
Kompilacja plików Latte za pomocą latc
, latc_llvm
, latc_x86_64` działa w sposób opisany w treści zadania.
Projekt, prócz bibliotek systemów języka Rust, wykorzystuje pakiet lalrpop, umożliwiający generacje plików parsera dla danej gramatyki.
Gramatyka języka (src/parser/src/grammar.lalrpop
) jest przeniesieniem gramatyki BNF z treści zadania na format zdefiniowany przez lalrpop. Gramatyka jest w 100% autorska.
Projekt posiada następujące zależności:
codespan
,codespan-reporting
- wypisywanie błędów w estetycznym formacieansi_term
- używanie kolorów/formatowania w konsolilalrpop-util
, regex- biblioteki wymagane przez wygenerowany plik parsera (
src/parser/src/grammar.rs`), nie są w żadnej sposób używane przez sam kod kompilatora
Ponadto, do poprawnego działania kompilator wymaga narzędzia clang używanego do kompilacji funkcji bibliotecznych oraz linkowania jej z wygenerowanym kodem.
Kompilator posiada zaimplementowane następujące optymalizacje:
- generowanie kodu LLVM w postaci SSA (mem2reg)
- optymalizacje stałych (constants folding & propagationa - bez typu
string
) - redukcja ścieżek prostych w CFG
- popragacja skoków w blokach prostych złożonych jedynie z instrukcji skoku (zamiast skakać do takiego bloku, kod skacze do bloku, który on wskazuje)
- eliminacja martwego kodu (unreachable code & unused assignments elimination)
- eliminacja trywialnych funkcji phi
- eliminacja wspólnych podwyrażeń
Kompilator wspiera tablice wielowymiarowe z pętlą for-each w postaci:
for (int x: t) {
...
}
gdzie x
jest referencją na kolejne elementy tablicy t
. Dzięki temu, możliwa jest zmiana wartości tablicy w pęli, bez używania indeksowania, co myślę że jest bardzo przydatne.
Dla typów prostych (nietablicowych) zmienna x
jest również copy-on-write.
Kompilator posiada zaimplementowaną alokacje rejestrów dla binarki x86-64. Wykorzystuje tu wszystkie rejestry poza RAX
, RBP
, RSP
, RCX
, RDX
. Pierwsze 3 rejestry są pomijane z oczywistych względów. RCX
oraz RDX
nie alokuje, gdyż wykorzystywane są przy dzieleniu oraz czasem do offloadowania wartości przy operacjach tablicowych.
Alokacja RCX
, RDX
prawdopodobnie byłaby możliwa przy przemyślanym odkładaniu ich wartości na stos (biorąc poprawkę na rejestr w którym powinien znaleźć się końcowy wynik).
Mimo to alokacja działa dobrze. We wszystkich publicznych testach alokacja nie spilluje żadnych zmiennych tj. całość wykonania programów odbywa się tylko na rejestrach.
Całość kodu źródłowego kompilatora znajduje się w folderze src
i składa się z następujących pakietów:
cli/src/main.rs
- entrypoint kompilatorabase
- kod wspólnyast.rs
- definicja drzewa składniowegosymbol_table.rs
- implementacja generycznej tablicy symbolitypes.rs
- definicja ogólnie używanych typów
parser
- kod parseragrammar.lalrpop
- składnia językagrammar.rs
- wygenerowany kod parsera języka
analyzer
- analiza semantyczna kompilatoraanalyzer.rs
- implementacja analizy semantycznejerrors.rs
- definicja błędów dla analizy i pochodnychtypes.rs
- definicja typów używanych przy analizie semantycznejevaluation.rs
- kod odpowiedzialny za upraszczanie stałych w drzewie AST
llvm
- kompilacja do LLVMcompiler.rs
- translacja z AST do kodu pośrednio bazowanego na LLVMinstructions.rs
- defincja kodu pośredniego, typów itp.operators.rs
- definicja operatorówcontrol_flow_graph.rs
- obliczanie grafu przepływu sterowaniamem2reg
- konwersja kodu do postaci SSA -dominance.rs
- obliczanie dominance frontiers i drzewa dominatorów -transform.rs
- właściwy kod konwersji do SSAoptimizations
- zawiera wszelakie zaimplementowane optymalizacje
x86
- kompilacja do x86compiler.rs
- translacja z LLVM do x86instructions.rs
- defincja kodu pośredniego, typów itp.operators.rs
- definicja operatorówregister_allocation
- kod alokacji rejestrów -liveness.rs
- analiza żywotności zmiennych -interference_graph.rs
- obliczanie grafu interferencji zmiennych -colouring.rs
- kolorowanie grafu interferencji
W projekcie znajdują się pliki lib.rs
oraz main.rs
, które w Ruscie odpowiedzialne są kolejno za implementacje interfejsu danego pakietu oraz samej binarki.
Ponadto w folderze lib
można znaleźć plik runtime.c
, zawierający implementacje funkcji bibliotecznych.