This is an old revision of the document!
În cadrul acestei teme veti implementa generarea de cod pentru limbajul LCPL.
simple
(120p)advanced
(75p)complex
(30p) Va trebui să realizaţi, în limbajul C++, porţiunea responsabilă cu generarea de cod LLVM IR, preferabil folosind IRBuilder. Programul vostru va trebui să primească la intrare output-ul temei 1 şi să genereze un fișier cu cod corect LLVM IR. Rezultatul programului realizat de voi poate folosi tool-urile LLVM pentru a genera cod pentru x86 și pentru a se executa.
Documentaţia principală în cadrul acestei teme va fi LLVM IR și API-ul clasei IRBuilder.
Pentru generarea de cod e recomandat să folosiți clasa IRBuilder, similar cu ceea ce ați învățat în cadrul laboratorului. De asemenea, puteți să studiați și tutorialul de generare de cod de aici.
Primul pas ar trebui să fie înțelegerea documentației:
Arhiva de pornire conține:
include
și src
, în care găsiți implementarea analizei semantice și un schelet de cod pentru generarea de IRlcpl-AST
, în care se află AST-ul limbajului LCPLruntime
, în care găsiți biblioteca de runtime pentru limbajul LCPL, alcătuită din clasele speciale definite în limbaj, precum și câteva funcții ajutătoarelcpl-driver.sh.in
, care este configurat de CMake cu path-urile necesare pentru a servi ca driver pentru un compilator de LCPL ce ia ca input un fișier JSON (output-ul temei 1), îl trece prin generatorul de cod scris de voi, linkează cu biblioteca de runtime și în final rulează interpretorul LLVM (lli
) pe fișierul rezultatCMakeLists.txt
Pentru a compila proiectul, va trebui să creați un director de build și să rulați
cmake -DPATH_TO_LLVM_CONFIG="/path/to/the/llvm-config/tool" -DCMAKE_BUILD_TYPE=Debug /path/to/skel/
Opțional puteți adăuga și un generator, ca de exemplu -G Ninja
.
în momentul de față, scheletul de cod pus la dispoziție este hardcodat pentru a genera IR-ul pentru testul hello.lcpl.json
. Va trebui să generalizați pentru a putea procesa AST-uri din ce în ce mai complexe (ordinea recomandată de implementare este cea urmată de teste - încercați să faceți câte un test să treacă și refactorizați pe parcurs).
Scheletul de cod este descris în detaliu mai jos.
După apelul către CMake, directorul de build va conține un fișier lcpl_runtime.ll
. Acesta este IR-ul corespunzător claselor speciale din limbajul LCPL. Puteți folosi acest fișier ca referință pentru reprezentarea datelor în limbajul LCPL. Aceasta va fi descrisă în cadrul acestei secțiuni.
i32
)Offset | Descriere |
---|---|
+0 | Pointer către RTTI-ul clasei din care face parte obiectul |
+4 | Atributele obiectului - întregi pe 4 bytes sau referinţe către alte obiecte. |
Referinţa la un obiect este adresa din memorie a acelui obiect.
Offset | Descriere |
---|---|
+0 | Pointer către obiectul String care reprezintă numele clasei |
+4 | Dimensiunea în bytes a obiectului, incluzând informația de runtime |
+8 | Pointer către informația de runtime a clasei părinte, NULL pentru Object |
+12 | Tabela de metode |
în tabela de metode se găsesc adresele metodelor din clasa părinte, urmate de metodele clasei. Atunci când o metodă a clasei ascunde metoda din clasa părinte, îi va ocupa locul în tabela de metode.
Pentru:
class Main inherits IO main : [out "Hello world!"]; end; end;
se generează următoarele:
String
care reprezintă numele clasei@0 = global [5 x i8] c"Main\00" @NMain = global %struct.TString { %struct.__lcpl_rtti* @RString, i32 4, i8* getelementptr inbounds ([5 x i8], [5 x i8]* @0, i32 0, i32 0) }
Main
@RMain = global { %struct.TString*, i32, %struct.__lcpl_rtti*, [6 x i8*] } { %struct.TString* @NMain, i32 8, %struct.__lcpl_rtti* @RIO, [6 x i8*] [ i8* bitcast (void (%struct.TObject*)* @M6_Object_abort to i8*), i8* bitcast (%struct.TString* (%struct.TObject*)* @M6_Object_typeName to i8*), i8* bitcast (%struct.TObject* (%struct.TObject*)* @M6_Object_copy to i8*), i8* bitcast (%struct.TString* (%struct.TIO*)* @M2_IO_in to i8*), i8* bitcast (%struct.TIO* (%struct.TIO*, %struct.TString*)* @M2_IO_out to i8*), i8* bitcast (void (%struct.TMain*)* @M4_Main_main to i8*) ]}
Clasa Main
nu are atribute proprii, prin urmare dimensiunea ei este de 8 bytes, dimensiunea pointerului la RTTI. Main
este derivată din clasa IO
, deci informația de parent va arăta către RTTI-ul clasei IO
(@RIO). În tabela de funcții a clasei Main
se află metodele clasei părinte IO
și metoda main
a clasei Main
.
DataLayout::getTypeAllocSize()
. Puteți obține DataLayout-ul cu metoda getDataLayout
a modulului LLVM pe care îl generați.
define void @Main_init(%struct.TMain*) { entry: %1 = bitcast %struct.TMain* %0 to %struct.TIO* call void @IO_init(%struct.TIO* %1) ret void }
Din constructorul clasei Main
se apelează constructorul clasei părinte, pentru a se inițializa atributele clasei părinte. Pentru clase care au atribute declarate cu inițializări, constructorul trebuie să conțină și codul necesar acestor inițializări.
define void @M4_Main_main(%struct.TMain* %self) { ; Prologue - save parameters %1 = alloca %struct.TMain* store %struct.TMain* %self, %struct.TMain** %1 ... }
Primul parametru pentru orice funcție generată va conține adresa obiectului de care aceasta aparține (self). Variabilele LCPL pot fi pe stivă sau în registre. Structurile precum tabelele virtuale sau informațiile de runtime sunt variabile globale LLVM IR.
N<NumeClasă>
. T<NumeClasă>
R<NumeClasă>
<NumeClasă>_init
M<N>_<NumeClasă>_<NumeMetodă>
, unde N este numărul de caractere al numelui claseiBiblioteca de runtime LCPL implementează funcţionalitatea claselor de bază aşa cum este descrisă în manualul limbajului LCPL. În codul generat metodele din bibliotecă pot fi folosite respectând convenţia ca obiectul apelant să fie primul parametru al apelului. Este foarte recomandat să folosiţi această convenţie pentru toate clasele şi metodele, nu doar pentru clasele de bază.
Punctul de intrare în program este funcția main
generată de scheletul de cod. Aceasta trebuie să facă următorii pași:
Main
(pentru asta trebuie apelată funcția ajutătoare __lcpl_new
, apoi pe obiectul nou obținut trebuie apelat constructorul clasei Main
)main
a acestui obiect
Runtime-ul limbajului LCPL conține definițiile claselor speciale din limbaj, precum și câteva funcții / structuri de date ajutătoare (vedeți fișierul runtime/lcpl_runtime.h
din arhiva de pornire):
Reprezintă informația de RTTI asociată fiecărei clase: numele clasei, dimensiunea obiectelor din clasa respectivă, pointer către RTTI-ul clasei părinte și tabela de metode virtuale. Fiecare obiect din program (în afară de întregi, desigur) va conține ca primul membru un pointer către structura __lcpl_rtti
corespunzătoare clasei din care face parte.
Definiții pentru clasele Object
, String
și IO
:
__lcpl_rtti
corespunzătoareObject
: M6_Object_abort
, M6_Object_typeName
și M6_Object_copy
IO
: M2_IO_in
și M2_IO_out
String
: M6_String_length
și M6_String_toInt
String
:M6_String_substring
: folosită pentru implementarea funcționalității de substring (primește ca parametri un obiect de tip String
și 2 indici)M6_String_concat
: folosită pentru concatenarea stringurilor (primește ca parametri 2 obiecte de tip String
)M6_String_equal
: folosită pentru verificarea egalității a 2 obiecte de tip String
(primite ca parametri)__lcpl_new
: alocă memorie pentru un obiect dintr-o clasă al cărei RTTI îl primește ca parametru; după apelarea acestei funcții trebuie invocat constructorul respectiv pe zona de memorie obținută.__lcpl_checkNull
: verifică dacă un obiect este null
și aruncă o eroare dacă da; această funcție ar trebui apelată înainte de a face dispatch pe un obiect.__lcpl_cast
: realizează un cast explicit sau aruncă o eroare dacă nu se poate realiza castul; primește ca parametri obiectul care trebuie castat și RTTI-ul clasei către care se face cast.__lcpl_intToString
: realizează conversia implicită de la un întreg la String
.
Analiza semantică se asigură de corectitudinea semantică a programului, și în același timp adună informații utile pentru generarea de IR. Este implementată ca un vizitator la nivelul AST-ului (ASTVisitor
). în cadrul temei nu va fi nevoie să modificați partea de analiză semantică, dar vă veți folosi de câteva informații puse la dispoziție de aceasta.
Menține informații legate de tipuri; de exemplu, pentru a afla tipul corespunzător unui nod din AST folosiți metoda getType(TreeNode *)
.
Există câteva tipuri speciale: cele corepunzătoare claselor speciale (Object
, IO
, String
), tipul corespunzător întregilor (Int
), tipul corespunzător constantelor null
și tipul vid (Void
). Aveți acces la aceste tipuri speciale prin intermediul metodelor getIntType
, getStringType
etc.
Pentru a compara tipuri, puteți compara direct pointerii întorși de TypeTable
.
Alte metode care pot fi utile sunt getParentClass
, getAttribute
, getMethod
.
Reține pentru fiecare nod de tip Symbol
nodul din AST corespunzător definiției acestuia (poate fi un LocalDefinition
, Attribute
, FormalParam
sau simbolul special pentru self (vedeți metoda isSelf
din nodul Class
).
Generatorul de cod (IRGenerator
) pus la dispoziție este unul naiv, care știe să genereze cod pentru un singur lucru: “Hello world”. îl găsiți în fișierele include/IRGenerator.h
și src/IRGenerator.cpp
. Va trebui să organizați și să generalizați codul respectiv pentru a putea genera IR valid pentru orice program LCPL.
Generatorul de cod este la rândul său un ASTVisitor
, cu o metodă visit
pentru fiecare tip de nod (în scheletul de cod sunt adăugate doar cele necesare pentru “Hello world”, va trebui să le adăugați voi pe celelalte). în plus, conține:
TypeTable
(ASTTypes
) și un SymbolMap
(DefinitionsMap
) în care se găsesc informațiile de la analiza semanticăRuntimeInterface
care conține informații legate de runtime-ul LCPL (tipuri, declarații de funcții etc - doar declarații, întrucât definițiile sunt în biblioteca de runtime)NodeMap
)
Puteți modifica structura generatorului de cod oricum doriți și puteți adăuga oricâte clase ajutătoare. De exemplu, scheletul de cod conține și un ClassVisitor
, care vă permite să vizitați clasele din program într-o ordine topologică (clasele părinte sunt vizitate întotdeauna înaintea celor care le moștenesc). Puteți moșteni din aceast vizitator oricând aveți nevoie de prelucrări pentru fiecare clasă (la generarea layout-ului, la generarea constructorilor etc).
Toate clasele de mai sus sunt puse cap la cap în main.cpp
: AST-ul primit ca input este deserializat, apoi este trecut prin analiza semantică și prin generatorul de cod. Modulul obținut este scris în fișierul de output și apoi verificat (astfel, aveți acces la codul generat chiar daca nu este valid). Partea de verificare presupune doar faptul că IR-ul generat respectă constrângerile LLVM IR-ului (ex: fiecare BasicBlock se termină cu o instrucțiune de tip terminator - branch, return etc).
în principiu fișierul main.cpp
nu ar trebui modificat - conține deja tot ce vă trebuie, inclusiv legătura între analiza semantică și generatorul de cod.
Pentru crearea de tipuri puteți folosi metodele statice ale claselor llvm::StructType
, llvm::FunctionType
, llvm::ArrayType
etc. Uneori poate fi util să creați structuri goale și apoi să apelați setBody
. Pentru tipuri simple puteți folosi metodele din IRBuilder
: getInt32Ty
, getVoidTy
, getInt8PtrTy
(acesta din urmă e util pentru a reprezenta void *
din C) etc. Tipurile LLVM au o metodă getPointerTo
pe care o puteți folosi pentru a crea tipuri pointer.
IRBuilder
conține metode convenabile și pentru adăugarea de constante: getInt32
, CreateGlobalStringPtr
.
Cea mai substanțială parte a IRBuilder
este crearea de instrucțiuni: CreateCall
, CreateLoad
, CreateRetVoid
, CreateGEP
, CreateBitCast
etc. Va trebui însă să aveți grijă unde sunt create aceste instrucțiuni: IRBuilder
are un InsertPoint care trebuie setat în locul unde doriți să inserați. Pentru asta puteți folosi GetInsertPoint
/ SetInsertPoint
.
Nu uitați de forma SSA - o valoare poate fi scrisă o singură dată. De aceea, poate fi util să alocați spațiu pe stivă (CreateAlloca
) pentru valori care pot fi scrise mai mult de o dată (de exemplu, vedeți ce generează clang
pentru parametri la -O0). Nu încercați să generați cod optim, important e să fie corect (puteți avea oricâte perechi load/store redundante etc).
Este util să folosiți un build de debug al LLVM pentru rezolvarea temei. Buildurile de debug conțin assert-uri care vă pot ajuta să faceți debug atunci când generați IR greșit. De asemenea, clasele derivate din llvm::Value
au o metodă dump
care poate fi invocată dintr-un debugger atunci când vreți să vedeți care valori duc la declanșarea assert-urilor.
Testarea temei de casă va folosi o serie de teste ce vor fi disponibile pe vmchecker. Aceleași teste sunt disponibile în arhiva de teste.
Testele pot fi rulate prin intermediul scriptului eval.sh
, care ia ca parametri calea către directorul de teste și calea către lcpl-driver.sh
(care se află în directorul de build după ce rulați CMake). Dacă doriți să rulați un singur test, puteți pasa numele acestuia ca al treilea argument pentru eval.sh
:
eval.sh /path/to/tests /path/to/lcpl-driver.sh advanced/stack
Dacă un test pică, eval.sh
va păstra următoarele fișiere:
<test>.out
și <test>.err
- conțin stream-urile de output și error pentru compilarea și rularea testului.<test>.lcpl.json.ll
- modulul IR generat de programul vostru.<test>.lcpl.json.bc
- bitcode-ul rezultat după linkarea cu runtime-ul LCPL. Pentru a vedea IR-ul corespunzător, folosiți llvm-dis
.Arhiva trebuie să aibă structura din arhiva de start: