This is an old revision of the document!


Tema de casă 2 - Generarea codului obiect

Tema de casă 2 - Generarea de cod

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

Informații organizatorice

  • Deadline: Termenul limită până când se pot trimite temele fără depunctări de întârziere este vineri, 2 decembrie 2016, ora 23:59. Pentru mai multe detalii, consultaţi regulamentul aferent temelor de casă.
  • Colaborare: Tema va fi rezolvată individual.
  • Punctare:
    • TODO (250p)

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:

  • Semantica și comportamentul programelor LCPL
  • LLVM IR / IRBuilder API pentru generarea de LLVM IR
  • Suportul pentru runtime

Arhiva de pornire

Arhiva de pornire conține:

  • Directoarele include și src, în care găsiți implementarea analizei semantice și un schelet de cod pentru generarea de IR
  • Directorul lcpl-AST, în care se află AST-ul limbajului LCPL
  • Directorul runtime, î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ătoare
  • Fișierul lcpl-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 rezultat
  • Fișierul CMakeLists.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.

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

  • întregii sunt reprezentați pe 4 bytes (i32)
  • Reprezentarea unui obiect în memorie arată astfel:
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.

  • Reprezentarea RTTI în memorie arată astfel
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:

  • Obiectul de tip 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) }
  • Informația de runtime pentru clasa 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.

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.

  • Constructorul clasei
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.

  • Metodele clasei
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
  • Obiectele de tip String care reprezintă numele claselor din programul de intrare vor avea forma N<NumeClasă>.
  • Structurile care definesc layoutul obiectelor in memorie vor avea forma T<NumeClasă>
  • Informația de runtime va avea forma R<NumeClasă>
  • Metodele de inițializare vor avea forma <NumeClasă>_init
  • Restul metodelor se vor genera pe principiul M<N>_<NumeClasă>_<NumeMetodă>, unde N este numărul de caractere al numelui clasei
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:

  • să creeze un obiect de tip Main (pentru asta trebuie apelată funcția ajutătoare __lcpl_new, apoi pe obiectul nou obținut trebuie apelat constructorul clasei Main)
  • să apeleze metoda main a acestui obiect

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:

  • Structurile de date corespunzătoare
  • Obiectele de tip __lcpl_rtti corespunzătoare
  • Constructorii
  • Metodele definite în manual:
    • Pentru clasa Object: M6_Object_abort, M6_Object_typeName și M6_Object_copy
    • Pentru clasa IO: M2_IO_in și M2_IO_out
    • Pentru clasa String: M6_String_length și M6_String_toInt
  • Funcții ajutătoare pentru clasa 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)
Alte funcții ajutătoare
  • __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ă

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:

  • un TypeTable(ASTTypes) și un SymbolMap(DefinitionsMap) în care se găsesc informațiile de la analiza semantică
  • un obiect de tip 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)
  • un map care asociază fiecărui nod din AST o valoare din IR (NodeMap)
  • un IRBuilder și alte obiecte care pot fi utile (modulul curent etc)

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:

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

Modul în care este distribuit punctajul pentru această temă este următorul:

  • Testele publice (TODO)
    • Testele simple (TODO)
    • Testele advanced (TODO)
    • Testele complex (TODO)
  • Calitatea implementării (TODO)
    • Organizarea codului sursă
    • Comentariile din cod
    • Explicațiile din README - acestea trebuie să conţină o prezentare extinsă a modului de implementare a temei şi a problemelor întâmpinate pe parcurs.

Instrucţiuni de predare a temei

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

  • CMakeLists.txt în rădăcină
  • să producă un executabil lcpl-irgen
  • nu puneți în arhivă fișiere obiect/executabile

Resurse

F A Q

Change Log

cpl/teme/tema2.1478674052.txt.gz · Last modified: 2016/11/09 08:47 by bogdan.nitulescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0