Kompiler pro Universal Scripting Language |  |
Začneme obrázkem: Na obrázku vidíte z jakých částí se bude náš kompiler skládat. Kompiler se skládá z Front-end a Back-end části. Front-end část pracuje se vstupem, který má za úkol zkontrolovat a upozornit na všechny chyby které by mohly znemožnit vykonání skriptu a které jsou při kompilaci zjistitelné. Například nezachytí dělení nulou pokud nula jako dělitel je v proměnné a dostane se sem jako výsledek nějakého předchozího výpočtu. Na to se přijde až během provádění skriptu. Jestliže Front-end část zpracovává vstup, u Back-end části to bude naopak. Ta zase ze zkontrolovaného vstupního skriptu vygeneruje nějaký výstup. Front-end část: ### Lexical Scanner - slovní skener je část kompileru, která má za úkol vzít vstupní soubor, tedy nějaký skript a postupně v něm vyhledávat symboly které skript smí obsahovat. Pokud se podíváme na příklad skriptu z úvodu, kde se volá funkce MessageBoxA: `MessageBoxA(Nula,Zprava,Titulek,Nula);` Lexikální skener tento výraz rozdělí na symboly: `MessageBoxA ( Nula , Zprava , Titulek , Nula )` Asi si teď říkáte, že napsat modul který bude tohle dělat je dost časově náročné. Chvíli by trvalo takový modul vyladit a přesvědčit se o tom zda funguje bezchybně. Museli bychom zřejmě konstruovat složité podmínky které by zachytily všechny potřebné případy nemluvě o tom, že člověk který skript píše nemusí dodržovat krasopis a vynechávat mezery mezi příkazy které dělají kód přehledný. Na to že budou slova oddělena mezerou bychom se nemohli spolehnout, což je další věc co by nám tvorbu takového modulu trochu zkomplikovala. Jako v příkladu výše. Jak máme vědět že jedno slovo má být `MessageBoxA` a ne třeba `MessageBoxA(Nula,` když to od sebe není nijak oddělené. Naštěstí lidé tvoří kompilery už nějakou dobu a lexikální skenery se nevyužívají jen v kompilerech a proto vznikly utility které pro nás lexikální skener vygenerují a ušetří nám tak mnoho času. ### Parser - Parser pracuje nad Lexikálním skenerem a jeho hlavním úkolem je zkontrolovat zda symboly které přečetl lexikální skener jdou za sebou ve správném pořadí. Opět z příkladu výše, zjistí že MessageBoxA je volání funkce. Takový příkaz se musí skládat ze jména funkce, levé závorky, žádného, jednoho či více parametrů, pravé závorky a středníku. Parser zjistí jestli to tak je a pokud ne, nahlásí chybu v syntaxi. Jak to parser dělá a jak mu řekneme jak má například volání funkce vypadat se dozvíte až budeme tento modul vytvářet. Napsat tento modul od začátku mi připadá ještě šílenější než napsat lexikální skener. V jádru to sice těžké není, ale opět je to spousta podmínek a času stráveného s hrnkem kafe. Naštěstí s tímto modulem nám pomůže také utilita stvořená přesně k tomu účelu. ### Weeding - českých překladů pro toto slovo je více, ale mě se nejvíc líbilo plení. Tento modul vytrhá plevel z kontrolovaného skriptu. Jedná se o jednoduchý modul což sami později poznáte. U našeho jazyka kterému nezáleží na typu proměnné bude zase o něco jednodušší. V našem jazyce také nezapisujeme jestli funkce vrací hodnotu nebo ne. Dokonce je povolené v jedné funkci na jednom místě hodnotu vrátit a na jiném zase ne. Nemusíme proto kontrolovat zda funkce která má vracet hodnotu ji vrací, což by tento modul jinak dělal. My v tomto modulu budeme sice také zjišťovat jestli funkce vrací hodnotu, ale z trochu jiného důvodu. Dále zde odchytíme pokusy o dělení nulou, tedy takové kdy bude jako dělitel napsána přímo nula a také najdeme kód který se nikdy neprovede, pokud takový kód bude ve skriptu obsažen. ### Symbol Checking - Tento modul má běžne za úkol zkontrolovat zda všechny symboly použité v programu jsou nadefinovány. Symboly se myslí názvy funkcí a proměnných. Náš jazyk ale nepotřebuje znát typ proměnné a proměnnou deklarujeme prostě tak že ji napíšeme. Tento modul bude tedy vkládat automaticky deklarace proměnných před příkazy kde se proměnná poprvé vyskytuje. Dále bude ověřovat zda jsou definovány všechny volané a použité funkce. ### Type Checking - V kompileru pro jazyk jako je C, kde je potřeba znát datový typ proměnných by tento modul kontroloval zda sčítáte proměnné se stejným datovým typem a pokud ne, zda může jednu z proměnných přetypovat aby byl výraz platný. Pokud proměnnou nelze přetypovat, jde o neplatný výraz a kompiler nahlásí chybu. To ale není náš případ. Náš jazyk nepotřebuje znát datové typy proměnných. V tomto modulu jen zkontrolujeme zda se v nějakém výrazu nevyskytuje volání funkce která nevrací hodnotu. Například `a = min(2,3)` je chybný pokud funkce min nevrací hodnotu. Back-end část: ### Resource Calculations - Toto je také jednoduchý modul. Jeho úkolem je spočítat indexy použitých proměnných a návěští. Proč se to dělá by vám mělo být jasnější až si přečtete o tom jak funguje Virtuální stroj a jak vypadá skript vyjádřený v assembleru jazyka. ### Coding - Tento modul má za úkol pro každou funkci vygenerovat kód v assembleru jazyka. Pokud jste se s nějakým assemblerem již potkali, jistě víte jak vypadá. Pokud ne, zjistíte to v tomto kurzu :) ### Emitting - Tento modul zapíše kód který byl vygenrován v předchozím kroku do assembly souboru. Assembly soubor je jen soubor ve kterém je skript zapsán v podobě assembleru a tento soubor se bude ještě zpracovávat. Aby jste si udělali představu o tom jak takový soubor vypadá, tak tady máte assembly soubor který kompiler vygeneroval pro skript Ahoj světe, který jsem ukazoval v úvodu. `.import "user32.dll" int MessageBoxA(4) .function main(0) .locals_limit 3 .stack_limit 7 ldc_int 0 store 0 ldc_string "Ahoj světe" store 1 ldc_string "Universal Script Language" store 2 ldc_int 0 dup store 0 pop load 0 load 1 load 2 load 0 ecall MessageBoxA(4) 4 pop .endf` Všechny řádky které nezačínají tečkou představují každý jednu instrukci. Tak teď víte jak vypadá program v podobě instrukcí a co může být výstupem tohoto modulu. K detailům se opět dostaneme až budeme tento modul programovat. Nemá smysl psát to dvakrát. ### Assembling - Poslední modul pracuje se souborem který vygeneroval předchozí modul a udělá z něj binární soubor, binární reprezentaci skriptu. Nebudu ho tady ukazovat, protože většina bajtů/znaků je stejně netisknutelných a jevilo by se to jako bílé prázdné místo. Jde o to že všechny instrukce (jako ty z příkladu výše) jsou uloženy jako číslo od 0 do 255. Každá instrukce má přiřazený svůj jedinečný kód. Může se zdát že je to malý rozsah, ale zjistíte že je to víc než dost a že bychom si bohatě vystačili se čtvrtinou. Universal Scripting Language rozeznává celkem 33 instrukcí.