Kompiler - Část 4 , Symbol Checking |  |
Pokrok :) Hotový projekt Vítejte v další kapitole. Pro ty kdo přeskočili teorii nebo ji už pozapoměli, zopakuji o co jde. V tomto modulu musíme zkontrolovat zda všechny symboly použité ve skriptu (symboly jsou názvy funkcí a proměnných, tj. proměnné a funkce) jsou správně definovány a zda jsou viditelné na místě kde jsou použity. Náš modul se ale bude trochu lišit. V jazyce kde je nutné zadávat datové typy proměnných se proměnné které jsou použity bez toho aby byly nadefinované najdou snadno. V Universal Scriptu to ale zadávat nemusíme, čili proměnnou uživatel definuje prostě tím, že ji do skriptu napíše nebo ji použije v nějakém výrazu. V AST stromu pak pro některé proměnné nemusí být vůbec struktury DECLARATION, které by představovaly jejich deklaraci, ale mohou být třeba jen v jediné struktuře LVALUE, která může být součástí nějakého výrazu (EXPRESSION). Budeme tedy muset zajistit deklaraci těch proměnných které deklarovány nejsou. Další věc je, že pokud ve skriptu napíšete deklaraci proměnné `a = 10;` a později do proměnné a přiřadíte třeba 25: `a = 25;`. Vidíte, že je to stejné jako deklarace proměnné a v AST stromu to taky tak bude vypadat. Taková situace by normálně byla považována za duplicitní deklaraci proměnné. V našem modulu to ale tak nechat nemůžeme a proto každou takovou definici proměnné převedeme na přiřazení hodnoty do proměnné. Na obrázku níže můžete vidět jak vypadá v AST stromu deklarace proměnné a na co ji musíme upravit aby z toho bylo přiřazení. Nyní si objasníme co se myslí tím, zda je proměnná vidět na místě kde je použita. S tím souvisí i pojem scope (má to hooodně významů), což je i druh příkazu (struktury STATEMENT) v AST stromu. Scope říkáme skupině příkazů mezi závorkami {...). Třeba tělo příkazu if představuje nový scope: `a = "Ahoj"; while (1) {/*novy scope*/ b = 48; a += " světe"; }` Vůbec nepřemýšlejte o smyslu ukázkového kódu. Slouží jen k vysvětlení viditelnosti proměnných. Proměnná: a je definovaná mimo tělo cyklu while. Když ji v cyklu používáme, znamená to tuto proměnnou. Proměnná: b je definovaná v těle cyklu, znamená to že může být použita jen tam a nikde jinde. Nemůžeme ji tedy použít na té úrovni kde do proměnná a přiřazujeme text "Ahoj". Z povahy našeho skriptovacího jazyka ale není například jako v C++ možné překrýt proměnnou: a novou deklarací proměnné: a v těle cyklu. Tento modul totiž zjistí že proměnná: a je definovaná nad tělem cyklu a bude všechny identifikátory a v těle cyklu považovat za tuto proměnnou. To bychom měli nějakou teorii k problému proměnných v Universal Scriptu. Teď si povíme něco o způsobu jakým to naprogramujeme. K tomu abychom mohli v každém místě skriptu zjišťovat, zda případný symbol už byl dříve definován, nebo ne, musíme si pokaždé když najdeme nějaký symbol zapamatovat, že jsme ho vůbec našli a také kde jsme ho našli. K tomu se používá tzv. Hash tabulka. Takovou tabulku má struktura SCRIPT. Do této tabulky jsou ukládány symboly které jsou vidět v celém skriptu. Jelikož náš skript se může sestávat jen z funkcí, mohou v této tabulce být jen symboly představující názvy funkcí. Svoji vlastní hashovací tabulku má také struktura FUNCTION, která reprezentuje funkci. Tam se zase dostanou symboly představující názvy proměnných, které jsou vidět v celé funkci. Poslední struktura která má hashovací tabulku je struktura STATEMENT ale jen v případě že jde o příkaz scope. Tam se budou ukládat názvy proměnných které jsou vidět jen v dané skupině příkazů (scope). Když si projdete ostatní struktury stromu, zjistíte že jinde už hashovací tabulka není potřeba. Pro grafické zobrazení toho co se pokoušel vysvětlit tento odstavec tu máme další obrázek. Pojem tabulka nemusíme myslím vysvětlovat. Raději se podíváme na to jak se hashovací tabulka liší od jiných tabulek. Odlišnost je v tom jak se používá. Hashovací tabulku budeme používat k ukládání nalezených symbolů. Aby nám sloužila k účelu k jakému ji potřebujeme, tedy zaznamenání toho že jsme nějaký symbol našli a pozdějšímu zjištění jestli už byl definován, tedy jestli už je v tabulce. Asi by nebylo optimální ukládat záznamy za sebou a při každém testu nějakého symbolu tabulku celou procházet a hledat symbol. Lepší by bylo hned vědět na jaké pozici v tabulce symbol má být. Pokud tam bude, znamená to že už definován byl a pokud ne, tak definován nebyl. Pozice symbolu v tabulce (v našem případě je symbol nějaký řetězec, jméno proměnné nebo funkce) se počítá ze symbolu. Každé jméno proměnné nebu funkce musí být unikátní, proto i pozice v tabulce kterou získáme na základě nějakého výpočtu založeného na symbolu, bude unikátní a my budeme pokaždé schopni pomocí výpočtu zjistit pozici symbolu v tabulce a podívat se jestli už tam symbol je nebo není. Každá hashovací tabulka také bude vědět, která hashovací tabulka je jejím rodičem. To znamená že například hashovací tabulka funkce bude vědět že její rodičovskou tabulkou je hashovací tabulka celého skriptu. Stejně tak hashovací tabulka nějaké skupiny příkazů (scope) bude znát ukazatel na hashovací tabulku funkce, která je její rodičovskou tabulkou. Ukazatele na rodičovské tabulky potřebujeme znát abychom mohli hledat symbol i v těchto tabulkách. Například ona proměnná a do které v cyklu přičítáme " světe" (`a += " světe";`). Když budeme tento symbol hledat v hashovací tabulce skupiny příkazů těla cyklu while (scope, nejsvětlejší obdélník na obrázku), nenajdeme ho tam. To ale neznamená že a není ve skriptu definováno. Musíme se proto podívat ještě do rodičovské tabulky. Z příkladu je to hashovací tabulka funkce. Tam už symbol a najdeme, protože tuto část jsme prošli předtím než jsme se dostali do těla cyklu a symbol a byl přidán do této tabulky, protože tam nebyl a nemohl být ani v tabulce celého skriptu. Zjistíme tedy že symbol a je definován a že v cyklu se jedná o proměnnou která byla definována o úroveň výše. Hashovací tabulku napíšeme jako třídu, bude to tedy objekt. Kromě operace pro vložení symbolu bude mít ještě funkce pro nalezení symbolu a to dvojí verze. Jedna verze bude hledat symbol jen ve vlastní tabulce. Druhá verze bude symbol hledat také ve vlastní tabulce, ale pokud neuspěje, požádá svoji rodičovskou tabulku aby se podívala jestli symbol není definován v ní. Ta bude pokračovat stejně. Tedy pokud symbol obsahova nebude, předá žádost dál. Poslední z důležitých funkcí hashovací tabulky bude funkce která vytvoří novou hashovací tabulku pro novou skupinu příkazů (scope) a její ukazatel na rodičovskou tabulku nastaví na sebe sama. Pochopíte to určitě z kódu. Kód hashovací tabulky bude předmětem samostudia. Dále tady budeme rozebírat jen funkce spojené s používáním hashovací tabulky a s úpravou AST stromu. Nyní se podíváme na definici třídy pro hashovací tabulku a třídy pro modul Symbol Checking ze souboru symbol.h. `/*Symbol checking (kontrola symbolu) modul pro Universal Script Language*/ #include "tree.h" #define HashTableSize 512 //velikost hash tabulky //**************************************************************************************************************** //Hashovaci tabulka symbolu //**************************************************************************************************************** class CHashedSymbolTable { private: SYMBOL** m_Table; //tabulka,tvorena polem ukazatelu na symboly int m_TableSize; //velikost tabulky CHashedSymbolTable* m_ParentTable; //ukazatel na rodicovskou tabulku private: unsigned int GetHash(char* str); //vrati hash cislo odpovidajici zadanemu retezci SYMBOL* Put(char* key,SymbolKind kind); //vloz symbol do tabulky public: HRESULT Init(int size = HashTableSize); //inicializace tabulky SYMBOL* Get(char* key); //pokud je nejaky szmbol definovan v tehle nebo nejake rodicovske tabulce, vrati na nej ukazatel SYMBOL* PutVariable(char* variable); //vlozi symbol promenne do tabulky SYMBOL* PutFunction(char* function); //vlozi symbol fce do tabulky bool Defined(char* name); //zjisti zda je symbol definovan v teto tabulce, rodicovske uz neprohledava CHashedSymbolTable* Scope(); //vytvori novou hashovaci tabulku a jeji ukazatel na rodicovskou tabulku nastavi na sebe void Parent(CHashedSymbolTable* pTable) {m_ParentTable = pTable;} //nastavi ukazatel na rodicovskou tabulku CHashedSymbolTable* Parent() {return m_ParentTable;} //vrati ukazatel na rodicovskou tabulku CHashedSymbolTable(); //konstruktor ~CHashedSymbolTable(); //destruktor }; /**************************************************************************************************************** Trida zapouzdrujici kontrolu symbolu pro nas skriptovaci jazyk Diky produkcnim pravidlum take parser vsechny prikazy, vyrazy pritrazeni identifikuje jako novou deklaraci, protoze se vlastne prirazeni od deklarace v nasem skriptovacim jazyce vubec nelisi. Proto vsechny deklarace, ktere obsahuji vlevo , tj. lvalue, jiz deklarovanou promennou, prevedeme na prikazy prirazeni. Pri kontrole vyrazu jako +=, -= muzeme take narazit na lvalue, tj. promenne ktere este deklarovane nebyly. Musime tedy zajistit jejich deklaraci, a tu vlozime pred prikaz, vyarz, kde se vyskytuji. To mame zajistenou automatickou deklaraci promennych. Dale take nas jazyk podporuje automatickou inicializaci. Inicilializace promenne pri jeji deklaraci vyzadovana neni. Pokud tedy narazime na deklaraci promenne bez inicializace, inicializujeme ji na nula. Dale najdeme duplicitni deklarace promennych. Ty se mohou vyskytnout v deklaraci parametru funkce nebo v inicializaci for cyklu. Jinde to prakticky neni mozne, ostatni dekllarace jsou deklarace promennych a duplicitni vyskyt je jak jsem jiz psal nahrazen prirazenim. V druhem pruchodu skriptem se kontroluji jmena funkci, zda jsou všechny definovány. Tím, že strom procházíme 2x máme možnost definovat funkce až po jejich použití ve skriptu, což třeba C++ neumožňuje. Tam musí být vždy deklarace funkce předtím než ji někde použijeme, nebo aspoň musí být deklarován prototyp funkce. */ class CSymbolChecking { private: /*prvni pruchod*/ void process1PassSCRIPT(SCRIPT* theScript); void process1PassTOPLEVEL(TOPLEVEL* toplevel, CHashedSymbolTable* symbolTable); void process1PassFUNCTION(FUNCTION* functio, CHashedSymbolTable* symbolTable); void process1PassDECLARATION(DECLARATION* declaration, CHashedSymbolTable* symbolTable); void process1PassFORINIT(FORINIT* forinit, CHashedSymbolTable* symbolTable); void process1PassSTATEMENT(STATEMENT* statement, CHashedSymbolTable* symbolTable); void process1PassEXPRESSION(EXPRESSION* expression, CHashedSymbolTable* symbolTable); void process1PassLVALUE(LVALUE* lvalue, CHashedSymbolTable* symbolTable); /*druhy pruchod*/ void process2PassSCRIPT(SCRIPT* theScript); void process2PassTOPLEVEL(TOPLEVEL* toplevel, CHashedSymbolTable* symbolTable); void process2PassFUNCTION(FUNCTION* function); void process2PassDECLARATION(DECLARATION* declaration, CHashedSymbolTable* symbolTable); void process2PassFORINIT(FORINIT* forinit, CHashedSymbolTable* symbolTable); void process2PassSTATEMENT(STATEMENT* statement, CHashedSymbolTable* symbolTable); void process2PassEXPRESSION(EXPRESSION* expression, CHashedSymbolTable* symbolTable); public: //zpracovani skriptu void Process(SCRIPT* theScript); };` K hashovací tabulce by to bylo asi vše. Třída CSymbolChecking take není složitá. Stejně jako trida CWeeding v předchozi kapitole obsahuje jen funkce process... které zpracovávaji příslušné struktury/uzly stromu. Jen při kontrole symbolů musíme celý strom projít dvakrát. Proč? To je kvůli kontrole funkcí. Kvůli tomu abychom zjistili zda jsou volané funkce definované. Šlo by to dělat v jednom průchodu, ale pak by se definice funkce musela vždy nacházet před místem kde ji voláme. Tak jak to musí být v C, nebo v C++. Ale tím, že přidáme druhý průchod umožníme našemu jazyku definovat funkce i za místem kde jsou poprvé volány. To v C++ není možné, pokud někde v kódu před voláním funkce není aspoň její prototyp. V prvním průchodu stromem si do hashovací tabulky uložíme názvy definovaných funkcí a v průchodu druhém zkontrolujeme všechny volání funkcí. Následující část kódu je z funkce process1PassFUNCTION a ukazuje hlavně vytvoření hashovací symbol tabulky pro skript: `if (function->kind == localT) //dalsi veci maj cenu jen v pripade lokalni funkce { function->symbolTable = symbolTable->Scope(); //vytvoreni nove tabulky symbolu pro samostatnou funkci if (function->declaration) process1PassDECLARATION(function->declaration,function->symbolTable); //kontrola argumentu if (function->statements) { ParentStm = &(function->statements); process1PassSTATEMENT(function->statements,function->symbolTable); //kontrola tela funkce } }` Jak můžete vidět, je to jednoduché. symbolTable je ukazatel na rodičovskou tabulku, ve funkci process1PassFUNCTION tedy ukazuje na symbol tabulku celého skriptu. Ve volání dalších funkcí pro zpracování dalších uzlů stromu je vidět, že jim předáváme i ukazatel na symbol tabulku kterou jsme právě vytvořili pro funkci, protože to je rodičovská tabulka pro tabulku která bude vytvořena v těle funkce pro příkaz typu scope, pokud tam takový bude. Nyní se podíváme na první funkci kde se dá něco zkoumat: `. . case declstmT: /*protoze jazyk podporuje nezadavani datovych typu, vlastne zadavani datovych typu nepodporuje vsechny prirazeni budou ve stromu reprezentovany jako deklarace promenne. Takze co musime udelat. Podivame se pokud je symbol definovan a pokud ano, zmenime prikaz deklarace na prirazeni, protoze promenna uz byla definovana. V opacnem pripade promennou nadefinujeme a ponechame prirazeni. To plati jen v pripade ze promenna je i inicializovana. Pokud neni inicializovana, ale je to treba jen g; bereme to jako deklaraci a zavolame process1PassDECLARATION a ta rozhodne jestli jde o duplicitni deklaraci */ if (statement->val.declaration->val.variableD.initialization) { //podivame se jestli je symbol definovan SYMBOL* tmpSymbol = symbolTable->Get(statement->val.declaration->val.variableD.identifiers->name); //pokud ano if (tmpSymbol) { theLog.TraceF("Variable '%s' is already defined. Converting current declaration to assignment. Line %i<br>",statement->val.declaration->val.variableD.identifiers->name,statement->line_number); LVALUE* lvalue = makeLVALUEidentifier(statement->val.declaration->val.variableD.identifiers->name); EXPRESSION* expassign = makeEXPRESSIONassignment(lvalue,statement->val.declaration->val.variableD.initialization); //expassign->val.assignmentE.left->symbol = tmpSymbol; *ParentStm = makeSTATEMENTexpression(expassign); (*ParentStm)->line_number = statement->line_number; (*ParentStm)->val.expression->val.assignmentE.left->symbol = tmpSymbol; process1PassEXPRESSION((*ParentStm)->val.expression->val.assignmentE.right,symbolTable); } else { //symbol neni definovan nebo definovan je ale neni inizializovan ParentExpr = (void*)statement; process1PassDECLARATION(statement->val.declaration,symbolTable); } } else { //symbol neni definovan nebo definovan je ale neni inizializovan ParentExpr = (void*)statement; process1PassDECLARATION(statement->val.declaration,symbolTable); } break; . .` To je část funkce process1PassStatement. Tato část se zabývá kontrolou příkazu-deklarace proměnné. V kódu vidíte, že kontrolujeme zda je deklarovaná proměnná inicializována. Tzn. jestli může jít o případ splynutí deklarace s přiřazením hodnoty proměnné. Pokud proměnná inicicializována je, pokusíme se ji najít v hashovací tabulce nebo v rodičovských hashovacích tabulkách. Pokud ji tam najdeme, nahradíme strukturu reprezentující deklaraci proměnné strukturou definující že jde o přiřazení hodnoty proměnné. Co změníme na co můžete vidět na prvním obrázku z této lekce. Abychom ale do stromu mohli vložit strukturu STATEMENT typu výraz která bude ukazovat na EXPRESSION s přiřazováním hodnoty do proměnné, potřebujeme se ve stromu posunout o jeden příkaz zpět. Protože nyní se nacházíme ve struktuře STATEMENT která definuje příkaz deklarace proměnné. Kdybychom použili ukazatel statement, který ukazuje na aktuální zpracovávanou strukturu, přepíšeme tak tuto strukturu, což by vlastně bylo správně. Ale v příkazu který původně ukazoval na příkaz deklaarce proměnné by stále zůstala stará hodnota a ukazoval by stále na starou strukturu. Musíme proto získat adresu ukazatele který ukazuje na právě zpracovávanou strukturu. Nesnažte se předstírat, že jste té větě rozuměli :) Příklad: Ve stromu bude STATEMENT který bude reprezentovat sekvenci dvou příkazů. Jeho ukazatel first který ukazuje na první příkaz ze sekvence, bude ukazovat na STATEMENT ve kterám bude deklarace proměnné která se má převést na přiřazení. Na tuto druhou strukturu ukazuje ukazatel statement když se struktura začne zpracovávat a zjistíme, že je potřeba to tady změnit. Když vytvoříme novou strukturu STATEMENT představující výraz na ukazateli statement, tak bude ale ukazatel first pořád ukazovat na starou strukturu STATEMENT. Musíme proto novou strukturu vytvořit na ukazateli first aby se to projevilo ve stromu. Adresu tohoto ukazatele máme uloženou v proměnné ParentStm a to je účel za kterým tam tato proměnná je. Pokud proměnná inicializována není nebo není její symbol nalezen v tabulce, necháme deklaraci zpracovat funkcí process1PassDECLARATION. Doufám že s pomocí tohoto šíleného obrázku a prvního obrázku z lekce se dalo pochopit jak tahle část pracuje a jdeme dál. Když už jsme u té funkce process1PassSTATEMENT, všimňete si ještě vytvoření nové hashovací tabulky pro novou skupinu příkazů (scope): `. . case scopeT: //vytvoreni tabulky pro novy blok {} statement->val.scopeS.symbolTable = symbolTable->Scope(); ParentStm = &(statement->val.scopeS.statement); process1PassSTATEMENT(statement->val.scopeS.statement, statement->val.scopeS.symbolTable); break; . .` Další je funkce pro zpracování deklarací proměnných: `//**************************************************************************************************************** //fce pro kontrolu deklaraci //**************************************************************************************************************** void CSymbolChecking::process1PassDECLARATION(DECLARATION* declaration, CHashedSymbolTable* symbolTable) { SYMBOL* symbol; switch (declaration->kind) { case formalT: //opet pokud je argument definovan v tabulce, je tady deklarovan podruhe a to je chyba. Pokud ne, //ulozime ho do tabulky. Nemuzeme mit v deklaraci funkce dve promenne se stejnym nazvem if (symbolTable->Defined(declaration->val.formalD.name)) theLog.ReportError(declaration->line_number,"Duplicate declaration of formal '%s'",declaration->val.formalD.name); else { //ulozeni symbolu do tabulky theLog.TraceF("Declared formal '%s'. Line: %i<br>",declaration->val.formalD.name,declaration->line_number); symbol = symbolTable->PutVariable(declaration->val.formalD.name); //ulozeni ukazatele na uzel deklarace, tj na misto kde je v programu promenna definovana a ke ktere symbol patri symbol->val.declarationS = declaration; } break; case variableT: //vlozeni inicializace pokud promenna inicializovana neni if (declaration->val.variableD.initialization) { ptrType = 1; process1PassEXPRESSION(declaration->val.variableD.initialization,symbolTable); } //zjistime jestli je symbol v tabulce //symbol = symbolTable->Get(declaration->val.variableD.identifiers->name); theLog.TraceF("Declared variable '%s'. Line: %i ",declaration->val.variableD.identifiers->name,declaration->line_number); symbol = symbolTable->PutVariable(declaration->val.variableD.identifiers->name); symbol->val.declarationS = declaration; declaration->val.variableD.symbol = symbol; break; case simplevarT: if (declaration->val.simplevarD.initialization != NULL) //kontrola inicializace { ptrType = 1; process1PassEXPRESSION(declaration->val.simplevarD.initialization, symbolTable); } //opet pokud je symbol definovan, je promenna definovana podruhe a to nemuzeme pripustit, meli bychom v tom bordel //a nemohli bychom se rozmyslet jakou promennou kdo kde pouziva symbol = symbolTable->Get(declaration->val.simplevarD.name); if (symbol) theLog.ReportError(declaration->line_number,"Duplicate declaration of variable ´%s'",declaration->val.simplevarD.name); else { //ulozeni symbolu do tabulky theLog.TraceF("Declared simplevar '%s'. Line: %i<br>",declaration->val.simplevarD.name,declaration->line_number); symbol = symbolTable->PutVariable(declaration->val.simplevarD.name); //ulozeni ukazatele na uzel deklarace, tj na misto kde je v programu promenna definovana a ke ktere symbol patri symbol->val.declarationS = declaration; } break; } if (declaration->next) process1PassDECLARATION(declaration->next, symbolTable); }` Vidíte že můžeme mít tři typy deklarací proměnné. První z nich se rozpozná konstantou formalT a označuje deklaraci parametrů funkce. Ve funkci: `fce (a, b)` jsou dvě takové deklarace. Jedna pro proměnnou a a druhá pro proměnnou b. Podívame se jestli je symbol v symbol tabulce. Pokud ano, znamená to, že je parametr deklarován podruhe, což nelze a nahlásíme chybu. Pokud ne, je to vpořádku a symbol do tabulky vložíme. Všiměnte si že pro zjištění jestli je symbol v tabulce používáme funkci Defined. Ta hledá symbol jen v tabulce která je zrovna používána. Při kontrole deklarací parametrů to může být jen hash tabulka funkce a žádná jiná. Je také nesmysl hledat symboly představující parametry funkce v jíné tabulce, například tabulce celého skriptu. V části simplevarT je to naopak. Tam používáme k nalezení symbolu funkci Get, která prohledává i rodičovské tabulky (volá jejich funkci Get). Část simplevarT totiž představuje deklarace proměnné/ných v inicializaci cyklu for. Jinde na tento typ deklarace také nenarazíme. Prostřední část variableT představuje proměnné definované kdekoliv v těle funkce kromě inicializace cyklu for. Tady se nám prakticky nemůže stát, tedy nemělo by, abychom narazili na duplicitní deklaraci, protože všechny duplicitní deklarace byly ve funkci processSTATEMENT nahrazeny přiřazením ještě dřív než je mohla tato funkce zkontrolovat. V této částí nám tedy zbývá jen přidat symbol do tabulky. Ve částech variableT a simplevarT se také kontroluje zda je proměnná inicializována a pokud ne, je inicializována na nulu. Jednoduše tím že vytvoříme strukturu EXPRESSION s celočíselnou konstantou nula a uložíme ukazatel do proměnné declaration dané deklarace. Symbol v hashovací tabulce je reprezentován strukturou SYMBOL. Jedním z jejích členů je jednak řetězcová hodnota uchovávající samotný symbol (název proměnné nebo funkce), za druhé konstanta určující typ symbolu (zda jde o název proměnné nebo funkce) a za třetí také ukazatel na strukturu DECLARATION, který ukazuje na strukturu kde je symbol definován (řádek: `symbol->val.declarationS = declaration;`). Že to není tak strašné jak to na první pohled vypadá? Nyní tu máme funkci process1PassLVALUE: `//**************************************************************************************************************** //kontrola lvalue //**************************************************************************************************************** void CSymbolChecking::process1PassLVALUE(LVALUE* lvalue, CHashedSymbolTable* symbolTable) { SYMBOL* symbol; switch (lvalue->kind) { case identifierT: /*protoze jazyk zabezpecuje automatickou deklaraci promennych, promenne ktere nejsou definovane nadefinujeme a k zadnemu erroru tak nemuze dojit */ symbol = symbolTable->Get(lvalue->val.idL); if (!symbol) { theLog.TraceF("Variable '%s' is not declared. Inserting automatic declaration. Line: %i<br>",lvalue->val.idL,lvalue->line_number); symbol = symbolTable->PutVariable(lvalue->val.idL); //musime pred vyraz kde se promenna vyskytuje vlozit deklaraci promenne //vytvor deklaraci DECLARATION* varDeclaration = makeDECLARATIONvariable(makeIDENTIFIER(lvalue->val.idL),makeEXPRESSIONintconst(0)); varDeclaration->val.variableD.symbol = symbol; symbol->val.declarationS = varDeclaration; //vytvor prikaz deklarace STATEMENT* StmDeclaration = makeSTATEMENTdeclaration(varDeclaration); STATEMENT* StmExpression = NULL; if (ptrType == 0) { //vytvor prikaz vyraz //ParentExpr ukayoval na strukturu EXPRESSION, abychom mohli udelat sekvenci prikazu, potrebujeme //STATEMENT ukazujici na EXPRESSION StmExpression = makeSTATEMENTexpression((EXPRESSION*)ParentExpr); } else if (ptrType == 1) { StmExpression = (STATEMENT*)ParentExpr; } ptrType = 0; //vytvor sekvenci prikazu deklarace a puvodniho vyrazu STATEMENT* StmSequence = makeSTATEMENTsequence(StmDeclaration,StmExpression); StmSequence->line_number = (*ParentStm)->line_number; *(ParentStm) = StmSequence; //nastav adresu ParentStm na druhy prikaz, coz je puvodne proverovany prikaz ParentStm = &((*ParentStm)->val.sequenceS.second); //process1PassSTATEMENT(*ParentStm,symbolTable); } lvalue->symbol = symbol; break; } }` Tato funkce také vypadá hrozivě, ale tak silné to nebude. Struktura LVALUE v sobě obsahuje informace o identifikátoru na levé straně nějakého výrazu. Třeba ve výrazu `a = 4 + 5;` je lvalue a. Protože jazyk má disponovat funkcí automatické deklarace proměnných, můžeme ve struktuře LVALUE narazit na symbol který není v žádné symbol tabulce, tudíž taková proměnná kterou představuje není deklarovaná. To znamená že musíme symbol do tabulky vložit, aby se o něm příště vědělo. To samo o sobě ale nestačí, protože to není deklarace proměnné. Musíme tuto nedeklarovanou proměnnou deklarovat. Přidáním symbolu do tabulky jen ukládáme informaci o tom jaký symbol a kde jsme ho našli. Vyvstává otázka, jak nadeklarovat tuto nedeklarovanou proměnnou a kam deklaraci vložit. Nadeklarujeme ji prostě tak že vytvoříme příslušné struktury a vložíme je do stromu. Ale kam? Nic nezkazíme tím, když deklaraci vložíme před místo kde se tato proměnná poprvé vyskytla, to jest před výraz nebo příkaz. Tady to ale není tak jednoduché. Musíme zapojit do hry proměnnou ptrType. Ve funkci process1PassSTATEMENT se v každé možnosti příkazu switch ukladá adresa ukazatele (do proměnné ParentExpr) který ukazuje na příkaz nebo výraz který se bude zkoumat a podle toho zda jde o výraz nebo příkaz se i nastavuje proměnná ptrType. To souvisí s touto funkcí,s vkládáním deklarace před výraz kde se poprvé vyskytne. `a += 4;` - řekněme že v tomto příkladu zjistíme, že lvalue a není deklarovaná. Je nutné a nadeklarovat, což by mělo vypadat takhle: `a = 0; a += 4;`. Jak je to ale uloženo v AST stromu? První musel být uzel STATEMENT obsahující výraz a ten ukazoval na EXPRESSION obsahující daný výraz. Před tohle potřebujeme vložit deklaraci proměnné. To znamená že první strukturu STATEMENT potřebujeme změnit z typu výraz na sekvenci příkazů, kde první bude příkaz deklarace proměnné a a druhý příkaz bude zmíněný výraz. Z obrázku to snad bude zase jasnější: Teď druhý případ. `if (a) ...`. Je nutné vložit deklaraci před příkaz if. Uvedu zde už jen obrázek: Doufám že obrázky pomohly objasnit jaké dva případy mohou nastat když chceme přidat deklaraci proměnné, pomohly pochopit použití pomocných proměnných ptrType,ParentStm a ParentExpr i jak se deklarace do stromu přidává. Nejdříve si ve funkci vytvoříme deklaraci proměnné (struktura DECLARATION). V druhém kroku vytvoříme příkaz (STATEMENT) deklaarce proměnné (kind=declstmT), protože deklaraci budeme vkládat do první větve sekvence příkazů a ta musí ukazovat na STATEMENT. V posledním kroku vytvoříme sekvenci příkazů, přičemž do první větve dáme deklaraci proměnné a do druhé původní prověřovaný příkaz nebo výraz. Pokud byl prověřován výraz (ptrType=0 a ParentExpr ukazuje na nějakou strukturu EXPRESSION), musíme z původního výrazu udělat příkaz výraz, protože i druhý ukazatel ze sekvence příkazů musí ukazovat na STATEMENT. To je řádek `StmExpression = makeSTATEMENTexpression((EXPRESSION*)ParentExpr);`. Pokud ptrType=1 tak ParentExpr ukazuje na stejné místo jako ParentStm. Žádný příkaz před původní dávat nemusíme (`StmExpression = (STATEMENT*)ParentExpr;`). Ukazatel jehož adresu máme v ParentStm nastavíme na vytvořenou sekvenci příkazů a vložení deklarace je tak hotové. Už jsme skoro na konci této lekce. Poslední důležitý kód je ve funkci process2PassEXPRESSION, kde se kontroluje volání funkcí: `. . case callT: /*Budeme jen zjistovat zda je funkce definovana. Pokud je, musi se nachazet v symbol tabulce celeho skriptu pouzivani promenne jako funkce neni vzhledem k automaticke deklaraci promennych mozne. Muzeme mit promennou min a funkci min */ //nejdriv vytvorime docasny podpis char* signature = stringConcat(expression->val.callE.name,makeSignature(expression->val.callE.arguments),NULL); char* signature2 = NULL; //pak zkusime jestli je symbol v tabulce symbol = theScriptSymbolTable->Get(signature); if (!symbol) { //pokud tam neni, zkusime jestli nejde o pretizenou fci s neznamym poctem parametru signature2 = stringConcat(expression->val.callE.name,"(-1)",NULL); symbol = theScriptSymbolTable->Get(signature2); if (!symbol) //Pokud nejde ani o funkci s neznamym poctem parametru, jde o chybu theLog.ReportError(expression->line_number,"Error: Function '%s' is referenced but not defined\n",expression->val.callE.name); else { //funkce je definovana, uloz si ukazatel na symbol a jmeno nastav na podpis expression->val.callE.symbol = symbol; expression->val.callE.name = signature2; } } else { //funkce je definovana, uloz si ukazatel na symbol a jmeno nastav na podpis expression->val.callE.symbol = symbol; expression->val.callE.name = signature; } if (expression->val.callE.arguments) process2PassEXPRESSION(expression->val.callE.arguments, symbolTable); break; . .` Aby jazyk podporoval přetěžování funkcí, ukládají se do symbol tabulky podpisy funkcí. Když chceme zkontrolovat volání funkce, musíme sestavit podpis funkce a zjistit zda je hash tabulce. Pokud ano, je to vpořádku. pokud ne, uživatel se pokouší volat nedefinovanou funkci. Symboly funkcí hledáme jen v hash tabulce pro celý skript, protože jinde být ani nemohou. Nemůžete napsat deklaraci funkce uvnitř těla nějaké jiné funkce. Symboly funkcí které jsou definované byly do tabulky vloženy v prvním průchodu. Aby jste neměli nejasnosti týkající se tohoto kódu, musíme si povědět ještě o externích funkcích. Externí funkci můžete ve skriptu definovat například takto: `extern <knihovna.dll> int fce(3)`. Taková funkce pak bude mít podpis fce(3) a takový symbol bude i v symbol tabulce. Deklarace externí funkce může být i následující: `extern <knihovna.dll> int fce(N/A)`. Ta bude mít podpis fce(-1). N/A i -1 znamen to samé, a totiž že funkce má proměnlivý počet parametrů. Když tedy narazíme v kódu na volání funkce, spočítáme kolik má parametrů a na základě toho a jejího jména sestavíme podpis. Koukneme do tabulky symbolů jestli je funkce definovaná. Pokud zjistíme že v tabulce symbol není, nemusí to ještě znamenat, že funkce není definovaná. Zkusíme proto ještě možnost že má neznámý počet argumentů, tj. podpis s -1 v závorce. Pokud ani poté symbol nenajdeme, víme že funkce není deklarovaná. Pokud symbol najdeme, je vše vpořádku a k volání funkce uložíme informaci o jejím symbolu a jméno změníme na podpis funkce. Proč změníme jméno na podpis? To souvisí s pozdějším převodem skriptu do assembly a binární formy. Pro tuto lekci to je vše. Až se vzpamatuje z uvedených, na první pohled šílených konstrukcí a ukazatelů, pokračujte pátou částí :). Část 3 Část 5