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
|