Table of Contents

Tema de casă 2 - Generarea de cod

În cadrul acestei teme veti implementa generarea de cod pentru limbajul LCPL.

Informații organizatorice

Enunţ

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.

Generarea de cod

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

Arhiva de pornire conține:

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.

Suportul de execuție pentru LCPL

Reprezentarea datelor

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.

Deși scheletul de cod pus la dispoziție generează IR care funcționează, acesta nu respectă întru totul convențiile LCPL. Cade în sarcina voastră să îl reparați (vedeți TODO-urile din scheletul de cod).

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.

Exemple de reprezentare

Pentru:

class Main inherits IO
 main : 
  [out "Hello world!"];
 end;
end;

se generează următoarele:

@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) }
@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.

Dimensiunile tipurilor de date depind de platforma pe care rulați (de exemplu pointerii pot avea 8 sau 4 bytes). De aceea, este indicat să folosiți metoda 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
 ...
}
Convenţia de apel

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.

Convenţia de nume
Funcţii şi date predefinite pentru LCPL

Biblioteca 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ă.

Secvenţa de iniţializare

Punctul de intrare în program este funcția main generată de scheletul de cod. Aceasta trebuie să facă următorii pași:

Scheletul de cod

Runtime-ul limbajului LCPL

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):

Structura __lcpl_rtti

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țiile claselor speciale

Definiții pentru clasele Object, String și IO:

Alte funcții ajutătoare

Analiza semantică

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.

TypeTable

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.

SymbolMap

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

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:

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).

Programul principal

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.

API LLVM pe scurt

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.

Testare automată și punctaj

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:

Instrucţiuni de predare a temei

Arhiva trebuie să aibă structura din arhiva de start:

Resurse

F A Q

Change Log