VOJENSKÁ AKADÉMIA V LIPTOVSKOM MIKULÁŠI Fakulta zabezpečenia velenia Katedra informatiky a výpočtovej techniky RNDr. Ľubomír Dedera, PhD. PREKLADAČE Prvá kniha Skriptá Liptovský Mikuláš 2002
RNDr. Ľubomír Dedera, PhD. Recenzent: pplk. doc. RNDr. Milan Lehotský, CSc. Za odbornú a jazykovú stránku tohto vysokoškolského učebného textu zodpovedá autor. Rukopis neprešiel redakčnou ani jazykovou úpravou. ISBN 80-8040-175-6
3 PRVÁ KNIHA Obsah OBSAH... 3 PRVÁ KNIHA... 3 ZOZNAM POUŢITÝCH SKRATIEK A SYMBOLOV... 6 PREDSLOV... 7 PRVÁ ČASŤ: TEORETICKÉ VÝCHODISKÁ... 9 1. FORMÁLNE JAZYKY A GRAMATIKY... 9 1.1. ZÁKLADNÉ POJMY... 9 1.2. KLASIFIKÁCIA GRAMATÍK... 17 Cvičenia... 18 2. REGULÁRNE JAZYKY A KONEČNÉ AUTOMATY... 20 2.1. REGULÁRNE JAZYKY... 20 2.2. DETERMINISTICKÉ KONEČNÉ AUTOMATY... 20 2.3. NEDETERMINISTICKÉ KONEČNÉ AUTOMATY... 24 2.4. VZŤAH MEDZI DETERMINISTICKÝMI A NEDETERMINISTICKÝMI KONEČNÝMI AUTOMATMI... 26 2.5. MINIMALIZÁCIA POČTU STAVOV DETERMINISTICKÝCH KONEČNÝCH AUTOMATOV... 31 2.6. VZŤAH MEDZI KONEČNÝMI AUTOMATMI A REGULÁRNYMI GRAMATIKAMI... 35 Cvičenia... 37 3. BEZKONTEXTOVÉ JAZYKY A ZÁSOBNÍKOVÉ AUTOMATY... 39 3.1. BEZKONTEXTOVÉ JAZYKY... 39 3.2. TRANSFORMÁCIE BEZKONTEXTOVÝCH GRAMATÍK... 43 3.2.1. Odstránenie nadbytočných symbolov gramatiky... 43 3.2.2. Odstránenie -pravidiel... 47 3.2.3. Odstránenie jednoduchých pravidiel... 49 3.2.4. Odstránenie ľavej rekurzie... 53 3.2.5. Greibachovej normálny tvar gramatiky... 57 3.3. ZÁSOBNÍKOVÉ AUTOMATY... 59 3.4. VZŤAH MEDZI ZÁSOBNÍKOVÝMI AUTOMATMI A BEZKONTEXTOVÝMI GRAMATIKAMI... 63 Cvičenia... 64 DRUHÁ ČASŤ: VŠEOBECNÉ ASPEKTY TVORBY KOMPILÁTOROV... 67 4. ŠTRUKTÚRA KOMPILÁTORA A LEXIKÁLNA ANALÝZA... 67 4.1. ŠTRUKTÚRA KOMPILÁTORA... 68 4.2. LEXIKÁLNY ANALYZÁTOR... 70 4.3. GENERÁTOR LEXIKÁLNEHO ANALYZÁTORA LEX... 80 Cvičenia... 85 5. SYNTAKTICKÁ ANALÝZA DETERMINISTICKÝCH BEZKONTEXTOVÝCH JAZYKOV... 86 5.1. DETERMINISTICKÉ BEZKONTEXTOVÉ JAZYKY A DETERMINISTICKÉ ZÁSOBNÍKOVÉ AUTOMATY 86 5.2. ALGORITMY PRE ANALÝZU GRAMATÍK... 88 5.3. CIELE SYNTAKTICKEJ ANALÝZY... 93 5.4. SYNTAKTICKÁ ANALÝZA ZHORA-NADOL... 94
4 5.4.1. Teoretický model... 94 5.4.2. LL(1) syntaktické analyzátory... 97 5.4.3. Úpravy bezkontextových gramatík na LL(1) gramatiky... 101 5.4.4. Problém if-then-else pri LL syntaktickej analýze... 104 5.5. SYNTAKTICKÁ ANALÝZA ZDOLA-NAHOR... 105 5.5.1. Teoretický model... 106 5.5.2. Všeobecný algoritmus LR syntaktických analyzátorov... 109 5.5.3. LR(0) syntaktické analyzátory... 113 5.5.4. LR(1) syntaktické analyzátory... 118 5.5.5. SLR(1) syntaktické analyzátory... 126 5.5.6. LALR(1) syntaktické analyzátory... 128 5.5.7. Problém if-then-else pri LR syntaktickej analýze... 133 Cvičenia... 134 6. SPRACOVANIE SÉMANTIKY... 139 6.1. MEDZIKÓD... 141 6.2. SÉMANTICKÉ PODPROGRAMY... 144 6.3. VOLANIE SÉMANTICKÝCH PODPROGRAMOV V LL SYNTAKTICKÝCH ANALYZÁTOROCH... 146 6.4. VOLANIE SÉMANTICKÝCH PODPROGRAMOV V LR SYNTAKTICKÝCH ANALYZÁTOROCH... 153 6.5. GENERÁTOR LALR(1) SYNTAKTICKÉHO ANALYZÁTORA YACC... 157 Cvičenia... 163 7. TABUĽKY SYMBOLOV... 166 7.1. ZÁKLADNÉ IMPLEMENTAČNÉ TECHNIKY... 167 7.2. ŠPECIÁLNE PRÍPADY... 170 Cvičenia... 173 8. ORGANIZÁCIA PAMÄTI POČAS BEHU PROGRAMU... 174 8.1. PRIDEĽOVANIE PAMÄTI PREMENNÝM JEDNOTLIVÝCH ÚDAJOVÝCH TYPOV... 174 8.2. ORGANIZÁCIA PAMÄTI PRIDELENEJ PROGRAMU... 176 Cvičenia... 185 OBSAH... 191 DRUHÁ KNIHA... 191 TRETIA ČASŤ: SPRACOVANIE JAZYKOVÝCH KONŠTRUKCIÍ A GENEROVANIE KÓDU 194 9. SPRACOVANIE DEKLARÁCIÍ... 194 9.1. ZÁKLADNÉ TECHNIKY A ÚDAJOVÉ ŠTRUKTÚRY... 196 9.2. DEKLARÁCIE PREMENNÝCH... 201 9.3. DEKLARÁCIE TYPOV... 205 9.4. TYP ZÁZNAM... 206 9.5. TYP POLE... 209 Cvičenia... 212 10. SPRACOVANIE VÝRAZOV A ÚDAJOVÝCH ŠTRUKTÚR... 214 10.1. SPRACOVANIE JEDNODUCHÝCH IDENTIFIKÁTOROV A KONŠTÁNT... 214 10.2. PREKLAD VÝRAZOV... 216 10.3. PREKLAD ODKAZOV NA POLOŢKY ZÁZNAMOV... 223 10.4. PREKLAD ODKAZOV NA PRVKY POĽA S KONŠTANTNÝMI HRANICAMI... 225 Cvičenia... 228 11. SPRACOVANIE RIADIACICH ŠTRUKTÚR... 230 11.1. SPRACOVANIE ZLOŢENÉHO PRÍKAZU, PRÁZDNEHO PRÍKAZU A SEKVENCIE PRÍKAZOV... 230 11.2. PREKLAD PRIRAĎOVACÍCH PRÍKAZOV... 231 11.3. PREKLAD PODMIENENÝCH PRÍKAZOV... 232 11.3.1. Preklad príkazu if... 232 11.3.1. Preklad príkazu case... 234
5 11.4. PREKLAD CYKLOV... 241 11.4.1. Preklad príkazu while... 241 11.4.2. Preklad príkazu for... 243 11.5. SKRÁTENÉ VYHODNOCOVANIE LOGICKÝCH VÝRAZOV... 246 Cvičenia... 257 12. SPRACOVANIE PROCEDÚR A FUNKCIÍ... 259 12.1. SPRACOVANIE DEFINÍCIÍ JEDNODUCHÝCH PODPROGRAMOV BEZ PARAMETROV... 259 12.2. SPRACOVANIE FORMÁLNYCH A SKUTOČNÝCH PARAMETROV... 265 12.3. SPRACOVANIE HLAVNÉHO PROGRAMU... 276 Cvičenia... 277 13. GENEROVANIE A OPTIMALIZÁCIA KÓDU... 279 13.1. ZÁKLADNÉ ÚLOHY... 279 13.2. JEDNODUCHÝ GENERÁTOR KÓDU... 281 13.3. INTERPRETATÍVNY PRÍSTUP KU GENEROVANIU KÓDU A LOKÁLNA OPTIMALIZÁCIA... 286 13.3.1. Optimalizácia výpočtu adresy... 287 13.3.2. Eliminácia redundantných výpočtov... 288 13.3.3. Prideľovanie registrov... 291 13.4. GLOBÁLNA OPTIMALIZÁCIA... 298 Cvičenia... 299 LITERATÚRA... 301 PRÍLOHA SYNTAX POUŢITÉHO JAZYKA... I
6 Zoznam použitých skratiek a symbolov AZ aktivačný záznam podprogramu AP ukazovateľ na aktivačný záznam (activation pointer) AST abstraktný syntaktický strom (abstract syntax tree) CFSM charakteristický automat syntaktického analyzátora (characteristic finite state machine) CSE spoločný podvýraz (common subexpression) DKA deterministický konečný automat DZA deterministický zásobníkový automat GNT Greibachovej normálny tvar (bezkontextovej gramatiky) KA konečný automat (deterministický alebo nedeterministický) NKA nedeterministický konečný automat OP podľa kontextu buď operačná pamäť alebo operátor PT prechodová tabuľka konečného automatu RT rozkladová tabuľka LL(1) syntaktického analyzátora SP ukazovateľ na vrchol zásobníka (stack pointer) SZ sémantický záznam ST skoková tabuľka TS tabuľka symbolov ZA zásobníkový automat (nedeterministický) # a (w) počet výskytov symbolu a v slove w Q počet prvkov mnoţiny Q w dĺţka reťazca w prázdne slovo prázdna mnoţina relácia derivácie (v gramatike) x R relácia prechodu automatu obrátený reťazec k reťazcu x prechodová funkcia (konečného alebo zásobníkového) automatu
7 Predslov Problematika prekladačov a kompilátorov je súčasťou študijných programov inţinierskych resp. magisterských odborov zameraných na informatiku na elektrotechnických resp. matematicko-fyzikálnych fakultách v ČR a SR. Hlavným cieľom tejto publikácie je preklenúť medzeru v dostupnej literatúre a poskytnúť pedagógom a študentom, ako aj odbornej verejnosti prakticky orientovanú učebnicu zameranú na túto problematiku. U čitateľa sa na začiatku predpokladá znalosť základov algebry (pojmy ako sú mnoţina, binárna relácia, relácia ekvivalencie, zobrazenie apod.), dobrá znalosť nejakého vyššieho procedurálneho programovacieho jazyka (najlepšie PASCALu, na príklade riadiacich a údajových štruktúr ktorého sa problematika vysvetľuje), znalosť základných údajových štruktúr a algoritmov (zásobník, front, lineárny zoznam resp. binárny vyhľadávací strom) a aspoň pasívna znalosť prog. jazyka C, pretoţe algoritmy v tejto publikácii sú zostavené v pseudokóde zaloţenom na tomto jazyku. Pre čitateľa je tieţ výhodou, ak aspoň pasívne ovláda nejaký strojovo orientovaný jazyk. Znalosť problematiky formálnych jazykov a automatov nie je na začiatku potrebná, nakoľko tie aspekty, ktoré sa priamo dotýkajú prekladačov, sú prebraté v kap. 1-3 a čiastočne 5. Na druhej strane, táto publikácia si nerobí nárok na to, aby bola plnohodnotnou učebnicou teórie formálnych jazykov a automatov, pokiaľ sa táto problematika vyučuje ako samostatný predmet. Pri výklade problematiky sa učebnica snaţí byť algoritmicky orientovanou, čím by mala významne pomôcť pri tvorbe vlastného kompilátora, či uţ ako semestrálnej práce alebo aj komerčne orientovaného. Do tohto rámca spadá aj orientácia na prostriedky pre podporu tvorby kompilátorov programy lex a yacc, ktoré sú súčasťou vývojových prostredí unixovských operačných systémov. Keďţe tieto prostriedky sú úzko viazané na prog. jazyk C, v záujme jednotného spôsobu vyjadrenia sú algoritmy v tejto učebnici prezentované v pseudokóde zaloţenom na prog. jazyku C, resp. priamo v ňom. Pre dobré zvládnutie tejto problematiky je preto potrebné mať k dispozícii nejaký počítač s UNIXom plne postačuje bezplatný LINUX. Na druhej strane, pri výklade problematiky kompilátorov sa v prevaţnej miere opierame o jazykové konštrukcie vychádzajúce z prog. jazyka PASCAL, a to pre ich jednoduchosť a všeobecnú známosť ich syntaxe a sémantiky.
8 Publikáciu moţno logicky rozčleniť na tri časti. Prvá časť nazvaná Teoretické východiská je zameraná teoreticky a jej cieľom je zoznámiť čitateľa so základmi teórie formálnych jazykov a automatov, ktorá je fundamentálna pre konštrukciu praktických prekladačov. V rámci toho je kap. 1 venovaná základným pojmom teórie formálnych jazykov a gramatík, kap. 2 sa zaoberá regulárnymi jazykmi a konečnými automatmi a kap. 3 sa venuje bezkontextovým jazykom a zásobníkovým automatom. Druhá časť nazvaná Všeobecné aspekty tvorby kompilátorov logicky predstavuje rozhranie medzi teoretickou prvou časťou a praktickejšou treťou časťou. V rámci toho sa kap. 4 venuje štruktúre kompilátora a lexikálnej analýze, kap. 5 syntaktickej analýze deterministických bezkontextových jazykov, kap. 6 spracovaniu sémantiky na všeobecnej úrovni, kap. 7 tabuľkám symbolov a kap. 8 organizácii pamäti počas behu programu. Tretia časť nazvaná Spracovanie jazykových konštrukcií a generovanie kódu je obsahovo zameraná na popis spracovania sémantiky najdôleţitejších údajových a riadiacich štruktúr procedurálne orientovaných programovacích jazykov (PASCAL): spracovanie deklarácií (kap. 9), výrazov a údajových štruktúr (kap. 10), riadiacich štruktúr (kap. 11), procedúr a funkcií (kap. 12). Záverom sa v kap. 13 venujeme základom generovania a optimalizácie cieľového kódu. Všetky kapitoly obsahujú na záver cvičenia, ktorých vyriešenie resp. programová realizácia napomáha úspešnému zvládnutiu problematiky. Príloha publikácie obsahuje syntax pouţitej podmnoţiny prog. jazyka PASCAL, pomocou ktorej je vedený výklad. Prílohu moţno vyuţiť aj pri zadaní semestrálnej práce. Záverom by som chcel poďakovať recenzentovi pplk. doc. RNDr. Milanovi Lehotskému, CSc. za jeho ochotu a cenné pripomienky, ktoré prispeli ku obsahovej kvalite tejto učebnice. Autor
9 Prvá časť: TEORETICKÉ VÝCHODISKÁ Cieľom tejto časti je zoznámiť čitateľa so základmi teórie formálnych jazykov a automatov, na ktorej je zaloţená konštrukcia moderných kompilátorov. V rámci toho je kap. 1 venovaná základným pojmom teórie formálnych jazykov a gramatík, kap. 2 sa zaoberá regulárnymi jazykmi a konečnými automatmi a kap. 3 sa venuje bezkontextovým jazykom a zásobníkovým automatom. 1. Formálne jazyky a gramatiky 1.1. Základné pojmy Formálne jazyky a gramatiky predstavujú teoretické prostriedky, pomocou ktorých je moţné popísať syntax programovacích jazykov a ako uvidíme neskôr, zohrávajú kľúčovú úlohu aj pri konštrukcii praktických prekladačov. Najprv si ale musíme vybudovať nevyhnutný pojmový aparát. Podobne ako u prirodzených jazykov aj tu je potrebné začať s prvotnými pojmami ako sú písmeno (symbol) a abeceda, z ktorých sa vytvárajú vyššie jednotky slová resp. vety [2]. Definícia 1.1 [2] Abeceda je konečná mnoţina prvkov, ktoré sa nazývajú symboly. Príklad 1.1 Abecedou môţe byť: {A, B,..., Z latinka; {0, 1,..., 9 abeceda desiatkových číslic; {and, array, begin, end, case, div, do,... mnoţina kľúčových slov jazyka PASCAL. Zo symbolov sa vytvárajú vyššie jednotky reťazce (resp. slová alebo vety všetky tri pojmy predstavujú v tomto kontexte synonymá) ako vhodné postupnosti prípustných symbolov. Definícia 1.2 Nech A je abeceda. Potom ľubovoľnú konečnú postupnosť pozostávajúcu zo symbolov z A nazývame reťazec (slovo, veta) nad abecedou A. Počet symbolov v reťazci x nazývame dĺţka reťazca a označujeme x. Ďalej si zavedieme niekoľko pojmov, ktoré priamo súvisia s reťazcami.
10 Definícia 1.3 Nech A je abeceda. Potom: Reťazec s nulovou dĺţkou sa nazýva prázdny reťazec a označuje sa. Ak x = a 1...a n je reťazec nad abecedou A, potom obrátený reťazec k reťazcu x je reťazec x R = a n...a 1. Ak u = a 1...a k a v = b 1...b l sú reťazce nad abecedou A, potom zreťazenie dvoch reťazcov u, v je reťazec u.v (alebo skrátene uv), u.v = a 1...a k b 1...b l. Ak x, y, z sú tri reťazce, potom x je predponou, y je podreťazcom a z je príponou reťazca xyz. Symbolom A * označujeme mnoţinu všetkých reťazcov nad abecedou A a symbolom A + = A * { mnoţinu všetkých neprázdnych reťazcov nad abecedou A. Pre prázdny reťazec je charakteristické, ţe nezávisí od abecedy. Z reťazcov môţeme ďalej vytvárať formálne jazyky, ktoré budú ďalej predmetom hlbšieho štúdia. Definícia 1.4 Ak je daná abeceda A, tak ľubovoľnú podmnoţinu L mnoţiny A * nazývame formálny jazyk nad abecedou A. Ak L =, tak L nazývame prázdny jazyk, ak L obsahuje konečný počet slov, tak L nazývame konečný jazyk. Podobne ako v prípade reťazcov môţeme definovať nad formálnymi jazykmi niektoré operácie. Definícia 1.5 Ak L 1, L 2 sú jazyky, potom ich zreťazením je jazyk L = L 1 L 2 = {xy, x L 1 y L 2. Ak L je jazyk, potom jeho n-tá mocnina L n je jazyk definovaný takto: a) L 0 = { ; b) L k+1 = LL k. Iteráciou jazyka L nazývame mnoţinu L * n L n 0 a pozitívnou iteráciou mnoţinu L 1 n n L. V tomto prípade platí, ţe L * = L + {. Príklad 1.2 Nech A = {a, b, c, d je abeceda, x, y, z sú reťazce, x = aba, y = bcd, z = a, L 1 = {a a L 2 = {ba, bb, bc, bd sú jazyky. Potom a) xy = ababcd; b) x = 3; c) y R = dcb; d) z je predponou (aj príponou) x;
11 e) A * = {, a, b, c, d, aa, ab, ac, ad, ba, bb, bc, bd, ca, cb, cc, cd, da, db, dc, dd, aaa, aab,...; * f) λ, a, aa,,... L1 aaa ; g) L 1 L 2 = {aba, abb, abc, abd. Ďalej sa budeme zaoberať tzv. generatívnym spôsobom špecifikácie jazyka [2], ktorý je reprezentovaný formálnym systémom gramatikou. Okrem generatívneho spôsobu špecifikácie jazyka existuje aj tzv. akceptačný spôsob špecifikácie jazyka, ktorý je zaloţený na rozličných typoch automatov a bude vysvetlený neskôr (kap. 2.2, 2.3, 3.3). Z hľadiska prekladačov je generatívny spôsob vhodný na popis syntaxe programovacích jazykov, akceptačný sa vyuţíva ako teoretický model niektorých súčastí prekladačov (lexikálny a syntaktický analyzátor, kap. 4, 5). V tejto súvislosti je treba zdôrazniť, ţe pri danom formálnom jazyku (zatiaľ chápanom ako mnoţina slov) sa môţeme podľa potreby rozhodnúť buď pre jeho špecifikáciu pomocou gramatiky alebo pomocou automatu, resp. k špecifikácii pomocou gramatiky nájsť ekvivalentnú špecifikáciu vo forme automatu a naopak (kap. 2.6, 3.4). Skôr ako pristúpime k formálnej definícii gramatiky, rozoberieme si spôsob špecifikácie jazyka generatívnym spôsobom na prirodzenom jazyku. Ako abeceda bude slúţiť mnoţina slov a bude nás zaujímať štruktúra vety. Symbolu z predchádzajúcich riadkov bude preto zodpovedať slovo jazyka a reťazcu veta jazyka. Štruktúra vety prirodzeného jazyka je definovaná gramatickými pravidlami. Beţná (zjednodušená) slovenská veta sa skladá z podmetovej časti, po ktorej nasleduje prísudková časť. Vetu teda definujeme pomocou ďalších dvoch jednotiek, ktorých význam zatiaľ nepoznáme, a preto ich musíme definovať. Podmetovú časť moţno definovať ako podstatné meno a prísudkovú časť ako sloveso, za ktorým nasleduje predmetová časť. Aby bol jazyk správne definovaný, musí tento proces byť taký, aby sme v konečnom dôsledku definovali kaţdú jednotku pomocou veličín, ktoré sú známe, prvotné, ktoré sa nakoniec objavia v skutočnej vete. Tak by sme mohli podstatné meno nahradiť skutočným podstatným menom, sloveso konkrétnym slovesom atď. [2]. Celý proces moţno pre vetu Milan píše úlohu graficky zobraziť podľa Obr. 1.1. Ako vidieť z obrázka, prvotné jednotky sú na najniţšej úrovni. Na odlíšenie prvotných jednotiek od jednotiek definovaných pomocou iných jednotiek pouţívame zvyčajne špeciálne ohraničujúce symboly, odlišné typy písma apod. V príklade uvedenom na Obr. 1.1 sú definované jednotky uzatvorené do zátvoriek <, >.
12 <veta> <podmetová časť> <prísudková časť> <podstatné meno> <sloveso> <predmetová časť> Milan píše <podstatné meno> úlohu Obr. 1.1 Gramatická štruktúra vety prirodzeného jazyka Gramatické pravidlá, pomocou ktorých definujeme jednotlivé jednotky, budeme písať v tomto tvare: <jednotka, ktorú definujeme> postupnosť (reťazec) jednotiek, pomocou ktorých definujeme jednotku na ľavej strane Vetu prirodzeného jazyka môţeme teraz definovať nasledujúcimi gramatickými pravidlami: 1. <veta> <podmetová časť> <prísudková časť> 2. <podmetová časť> <podstatné meno> 3. <podstatné meno> Milan 4. <prísudková časť> <sloveso> <predmetová časť> 5. <sloveso> píše 6. <predmetová časť> <podstatné meno> 7. <podstatné meno> úlohu Mnoţina všetkých gramatických pravidiel tvorí gramatiku jazyka. Gramatika jazyka nám umoţňuje generovať vety jazyka, ktoré sú gramaticky (syntakticky) správne. Gramatikou určujeme iba syntax jazyka, tzn. ţe určujeme prípustnú štruktúru viet jazyka a prvotné jednotky, ktoré môţeme na danom mieste pouţiť. Ak ľubovoľná veta spĺňa podmienky definované gramatikou, patrí do daného jazyka. Význam viet určuje sémantika. Syntax a sémantika navzájom súvisia. Syntax definuje štruktúru vety, ktorá je zasa základom pri určovaní jej významu [2]. Syntakticky správna veta ešte ne-
13 musí byť sémanticky správna. Ako uvidíme ďalej, to isté platí aj pre programovacie jazyky a ich prekladače. Podobne pri formálnych jazykoch a gramatikách budeme pracovať s objektmi, ku ktorým môţeme prísť na základe analýzy prirodzeného jazyka: sú to prvotné symboly, ktoré sa nazývajú terminálne symboly (terminály), ďalej jednotky definované pomocou iných jednotiek, ktoré sa nazývajú neterminálne symboly (neterminály) a gramatické pravidlá, ktoré sa nazývajú prepisovacie pravidlá. Okrem uvedených objektov vystupuje ešte v gramatike jeden neterminálny symbol, ktorý má význačné postavenie v tom, ţe sa z neho začína generovanie všetkých viet (v príklade na Obr. 1.1 je to <veta>). Preto sa nazýva začiatočný symbol gramatiky alebo začiatočný neterminál. Definícia 1.6 [2] Gramatika je usporiadaná štvorica G = (N, T, P, S), kde N je konečná mnoţina neterminálnych symbolov, T je konečná mnoţina terminálnych symbolov, pričom N T =, S N je začiatočný symbol gramatiky a P je mnoţina prepisovacích pravidiel, ktorá je konečnou podmnoţinou mnoţiny (N T) * N(N T) * (N T) *. Symboliku (N T) * N(N T) * (N T) * je potrebné čítať takým spôsobom, ţe ľavá strana kaţdého pravidla ((N T) * N(N T) * ) je reťazec obsahujúci aspoň jeden neterminálny symbol a pravá strana je reťazec zloţený z terminálnych aj neterminálnych symbolov, resp. pravou stranou môţe byť aj prázdny reťazec. Z dôvodu zrozumiteľnejšieho čítania ďalšieho textu, pokiaľ nebude vyslovene stanovené inak, bude platiť nasledujúca konvencia v symbolike: neterminály A, B, C, <slovo>,... (veľké písmená zo začiatku abecedy); terminály a, b, c,... (malé písmená zo začiatku abecedy); reťazce zloţené z terminálov w, x, y, z,... (malé písmená z konca abecedy); reťazce zloţené z terminálov aj neterminálov,,,... (malé grécke písmená okrem ); jeden terminálny alebo neterminálny symbol X, Y, Z,... (veľké písmená z konca abecedy); prepisovacie pravidlá ( je ľavá strana a je pravá strana pravidla);
14 niekoľko prepisovacích pravidiel so spoločnou ľavou stranou a jednotlivými pravými stranami 1, 2,..., n 1 2... n (spoločná ľavá strana sa v zápise uvedie len raz). Príklad 1.3 Usporiadaná štvorica G = ({A, B, {a, b, P, A) s mnoţinou pravidiel P: A aab B B bba aab a aa bb b bba bbaaa je v zmysle definície Definícia 1.6 gramatika. Mnoţina {A, B je mnoţina neterminálov, mnoţina {a, b je mnoţina terminálov, mnoţina P obsahuje všetky pravidlá gramatiky a neterminál A je začiatočný symbol gramatiky. Ak si zoberieme, napríklad, pravidlo bba bbaaa, potom v súlade s definíciou pravidiel v definícii Definícia 1.6 jeho ľavú stranu (bba) môţeme formálne vyjadriť napríklad v tvare 1 A 2 ( 1 = bb, 2 = ), pričom reťazec 1 ({A, B {a, b) *, neterminál A {A, B a reťazec 2 ({A, B {a, b) * ; podobne pri pravej strane pravidla (reťazec = bbaaa) je ({A, B {a, b) *. Ďalej nás bude zaujímať, ako môţeme v gramatike odvodzovať vety z nejakého jazyka. K tomu si najprv potrebujeme zaviesť niekoľko ďalších pojmov. Definícia 1.7 Nech G = (N, T, P, S) je gramatika. Na mnoţine reťazcov zloţených z terminálnych aj neterminálnych symbolov gramatiky (N T) * definujeme reláciu derivácie (alebo krok odvodenia) nasledujúcim spôsobom: (čítaj: z reťazca je moţné priamo derivovať reťazec ), ak v P existuje pravidlo, pričom,, (N T) *, (N T) * N(N T) * Inverzná relácia k relácii derivácie sa nazýva redukcia, tzn. ţe reťazec je moţné priamo redukovať (pomocou pravidla ) na reťazec. Keďţe deriváciou reťazca dostávame ďalší reťazec, má význam hovoriť o mocnine (viacnásobnej kompozícii) relácie derivácie; týmto sa formalizuje postupná viacnásobná aplikácia prepisovacích pravidiel na nejaký reťazec. Definícia 1.8 Nech G = (N, T, P, S) je gramatika a i (N T) * pre i = 0, 1,... k. Potom hovoríme, ţe k sa dá derivovať z 0 (na k krokov), ak platí, ţe i i+1 pre
15 i = 0, 1, 2,... k 1. Postupnosť reťazcov 0, 1,..., k sa nazýva derivácia k z 0. Vyjadrovať ju budeme vo forme mocniny relácie v tvare 0 k k. Poznámka. Pre kaţdé (N T) * triviálne platí, ţe sa dá derivovať z (na 0 krokov). Túto skutočnosť vyuţijeme pri zostavovaní reflexívneho a tranzitívneho uzáveru relácie derivácie. Definícia 1.9 Nech G = (N, T, P, S) je gramatika. Potom tranzitívnym uzáverom relácie derivácie nazývame reláciu + definovanú na mnoţine (N T) * nasledovne: + práve vtedy, keď existuje n 1 také, ţe n a reflexívnym a tranzitívnym uzáverom relácie derivácie reláciu * definovanú * práve vtedy, keď existuje n 0 také, ţe n,, (N T) *. V prípade tranzitívneho uzáveru sú dva reťazce, zloţené z terminálnych a neterminálnych symbolov danej gramatiky v relácii (zapisujeme + ) práve vtedy, ak môţeme derivovať z na minimálne jeden krok (tzn. minimálne raz sa pouţije nejaké prepisovacie pravidlo). V prípade reflexívneho a tranzitívneho uzáveru pripúšťame aj deriváciu na 0 krokov, tzn. kaţdý reťazec (N T) * bude naviac vţdy aj v relácii sám so sebou ( * ). Príklad 1.4 Uvaţujme o gramatike z príkladu Príklad 1.3. Potom: a) A aab priama derivácia pomocou pravidla A aab; b) aab bbb priama derivácia pomocou aa bb; c) aab 3 aaaabba derivácia aab aaabb aaaabbb aaaabba; d) A + ab existuje n = 2 a derivácia A aab ab (posledný krok pomocou pouţitia pravidla A ); e) A * aabb existuje n = 3 a derivácia A aab aaabb aabb; f) aab * aab lebo aab 0 aab, avšak neplatí aab + aab, pretoţe v danej gramatike nenájdeme ţiadnu deriváciu aab z aab na minimálne jeden krok (preverte). Definícia 1.10 Nech G = (N, T, P, S) je gramatika. Potom kaţdý reťazec (N T) *, ktorý je moţno derivovať zo začiatočného neterminálu gramatiky G, sa nazýva vetná forma; formálne, je vetná forma práve vtedy, keď S *.
16 Príklad 1.5 V predchádzajúcom príklade sú vetnými formami napr. reťazce A, aab, aaabb, ab, aabb. Teraz uţ konečne môţeme pristúpiť k definícii jazyka špecifikovaného (alebo generovaného) formálnou gramatikou. Je to mnoţina tých reťazcov (zloţených len s terminálnych symbolov), ktoré je moţné derivovať zo začiatočného neterminálu gramatiky. Definícia 1.11 Nech G = (N, T, P, S) je gramatika. Jazykom L(G) generovaným gramatikou G nazývame mnoţinu L(G) = {w S * w, w T *. Reťazec patriaci do daného jazyka sa nazýva slovo alebo veta jazyka. Príklad 1.6 Nájdite gramatiku, ktorá generuje jazyk obsahujúci všetky správne uzátvorkované výrazy, tzn. L = {, (), (()), ()(), ((())), (())(), ()(()), ()()(), (((()))),... (vo výrazoch musí ku kaţdej ľavej zátvorke existovať zodpovedajúca pravá zátvorka). Uvaţujme o gramatike G = {{S, A, {(, ), P, S s pravidlami P: 1. S AS 2. S 3. A (S) Je zrejmé, ţe L(G) L, pretoţe v procese derivácie sa vţdy ku kaţdej ( súčasne vygeneruje zodpovedajúca ) (pravidlo 3), čo znamená, ţe kaţdé slovo, ktoré je moţné odvodiť v gramatike G, je správne uzátvorkovaný výraz. Na druhej strane musíme ešte ukázať, ţe ku kaţdému správne uzátvorkovanému výrazu vieme nájsť jeho deriváciu v gramatike G. Tu si pomôţeme matematickou indukciou vzhľadom na maximálnu úroveň vnorenia l výrazu (napr. výrazy (), ()() majú l = 1, (()()), (())(), l = 2 apod.). Nech l = 0. Potom zodpovedajúci výraz musí byť a zodpovedajúca derivácia v gramatike G je S. Predpokladajme teraz, ţe deriváciu vieme nájsť ku všetkým výrazom s maximálnou úrovňou vnorenia l k (indukčný predpoklad). Nech w je výraz s l = k + 1. w si najprv rozdelíme na segmenty w 1...w n, pričom kaţdý segment predstavuje správne uzátvorkovaný výraz a úroveň vnorenia aspoň jedného segmentu je k + 1. Bez straty na všeobecnosti budeme predpokladať, ţe je to segment w 1. Slovo w potom môţeme vygenerovať nasledujúcou deriváciou:
17 S AS AAS... AA... A (S)A...A... w 1 A...A... w 1...w n n Vyuţili sme pri tom skutočnosť, ţe po odstránení krajných zátvoriek zo segmentu w 1 sa jeho úroveň zmenší o 1, čo znamená, ţe na generovanie jeho vnútra z neterminálu S vo vetnej forme (S)A...A môţeme pouţiť indukčný predpoklad. Ak je úroveň vnorenia ďalšieho segmentu k + 1, pouţijeme rovnaký postup, ak je menšia alebo rovná k, môţeme priamo pouţiť indukčný predpoklad atď. Napríklad, ku výrazu (()())() moţno zostrojiť deriváciu S AS AAS AA (S)A (AS)A (AAS)A (AA)A... ((S)(S))(S)... (()())(). Jeden jazyk nemusí byť generovaný len jedinou gramatikou, preto má zmysel na základe toho zaviesť pojem ekvivalentnej gramatiky. Ekvivalentné gramatiky budeme vyuţívať v kapitolách 3.2 a 5.4.3. Definícia 1.12 Gramatiky G 1, G 2, pre ktoré platí L(G 1 ) = L(G 2 ), sa nazývajú ekvivalentné gramatiky. 1.2. Klasifikácia gramatík Na základe tvaru prepisovacích pravidiel sú definované určité hierarchické triedy gramatík a jazykov. Definícia 1.13 [2] Nech G = (N, T, P, S) je gramatika. Potom hovoríme, ţe G je a) bez ohraničenia alebo typu 0, ak na prepisovacie pravidlá nekladieme ţiadne obmedzenia; b) kontextová alebo typu 1, ak kaţdé prepisovacie pravidlo z P má tvar, kde (N T) * N(N T) *, (N T) + a platí (dĺţka pravej strany pravidla nesmie byť menšia ako dĺţka ľavej strany); Iná definícia pripúšťa pravidlá tvaru A γ,,, γ (N T) *, A N; v tomto prípade reťazce, predstavujú kontext, pri ktorom sa neterminál A môţe prepísať na reťazec γ; c) bezkontextová alebo typu 2, ak kaţdé prepisovacie pravidlo z P má tvar A, kde A N, (N T) * ; názov pramení z toho, ţe pri výbere pravidla pre neterminál A sa neberie do úvahy jeho aktuálny kontext vo vetnej forme;
18 d) regulárna alebo typu 3, ak kaţdé prepisovacie pravidlo z P má jeden z tvarov A bb alebo A b, kde A, B N, b T. Niekedy je potrebné špecifikovať jazyk L {, teda jazyk s prázdnym slovom, aj pre jazyky špecifikované typmi gramatík, v ktorých sme nepripustili pouţitie na pravej strane pravidiel (regulárne a kontextové). Tento problém sa zvykne riešiť pouţitím rozšírenej gramatiky, ktorú k danej gramatike zostrojíme pridaním nového začiatočného symbolu S a prepisovacích pravidiel S S a S. V tomto zmysle, ak je jazyk L kontextový, bezkontextový alebo regulárny, aj jazyk L { je kontextový, bezkontextový alebo regulárny. Gramatika uvedená v príklade Príklad 1.3 je gramatika bez obmedzení, v príklade Príklad 1.6 bezkontextová gramatika. Z hľadiska konštrukcie prekladačov nás budú zaujímať regulárne a bezkontextové gramatiky. Keďţe týmito gramatikami sa budeme podrobne zaoberať v kapitolách 2 a 3, ďalšie príklady na ne na tomto mieste vynechávame, resp. nechávame ako cvičenie. Vzťahy gramatík podľa uvedenej klasifikácie sú nasledovné: a) kaţdá regulárna gramatika je bezkontextová; b) kaţdá bezkontextová gramatika je kontextová; c) kaţdá kontextová gramatika je gramatika bez obmedzení. Definícia 1.14 Jazyk nazývame postupne regulárny, bezkontextový, kontextový alebo bez obmedzení, ak ho moţno generovať regulárnou, bezkontextovou, kontextovou gramatikou alebo gramatikou bez obmedzení. Cvičenia 1. Je daná gramatika G = ({S, A, B, {a, b, c, P, S) s pravidlami P: S BAB ABA A AB aa ab B BA b a) stanovte typ gramatiky; b) nájdite deriváciu slova abbbab v danej gramatike. 2. Nájdite gramatiku, ktorá generuje daný jazyk: a) L 1 = {aw w {a, b * ; b) L 2 = {wba w {a, b * ;
19 c) L 3 = {xaby x, y {a, b * ; d) L 4 = mnoţina všetkých čísel v pohyblivej rádovej čiarke (napr. 0.25, 45.78, 58.12E 5,...); e) L 5 = {ww R w {a, b * ; f) L 6 = {a n b n n {0, 1, 2,...; g) L 7 = {a n b n c k n, k {1, 2,...; h) L 8 = {aba n b n a n {0, 1, 2,...; i) L 9 = {ww w {a, b * ; (pomôcka: hľadajte kontextovú gramatiku); j) L 10 = {a n b n c n n {1, 2,...; (pomôcka: hľadajte kontextovú gramatiku); k) L 11 = {w w {a, b * a zároveň # a (w) = # b (w), tzn. do jazyka budú patriť všetky reťazce zloţené zo symbolov a, b, v ktorých je počet symbolov a rovný počtu symbolov b ; (pomôcka: hľadajte bezkontextovú gramatiku, inšpirujte sa príkladom Príklad 1.6).
20 2. Regulárne jazyky a konečné automaty 2.1. Regulárne jazyky Ako uţ bolo uvedené v definícii Definícia 1.14, jazyk je regulárny, ak existuje nejaká regulárna gramatika, ktorá ho generuje. Hoci je trieda regulárnych jazykov pomerne obmedzená, z hľadiska konštrukcie prekladačov majú regulárne jazyky a gramatiky veľký význam. Pomocou nich moţno popísať základné symboly programovacích jazykov ako sú identifikátory, konštanty, kľúčové slová, operátory a separátory. Uveďme si na tomto mieste dva príklady regulárnych jazykov. Príklad 2.1 Nech je daná gramatika G 1 = ({S, B, C, D, {a, +, P, S), pričom mnoţina P obsahuje pravidlá: S a ad D +B +C B a C ad Potom L(G 1 ) = {a, a+a, a+a+a,... Príklad 2.2 Nech je daná gramatika G 2 = ({S, C, D, {+,, 0, 1, 2,..., 9, P, S), pričom P: S +C C 1D 2D... 9D 0 1 2... 9 C 1D 2D... 9D 0 1 2... 9 D 0D 1D... 9D 0 1 2... 9 Potom jazyk L(G 2 ) bude tvorený všetkými celočíselnými konštantami (prípadne so znamienkom) bez nevýznamných núl zľava. 2.2. Deterministické konečné automaty V predchádzajúcej kapitole sme sa zaoberali gramatikami ako prostriedkami špecifikácie formálneho jazyka generatívnym spôsobom, tzn. ukázali sme ako generovať (syntakticky správne) vety daného jazyka. Pri konštrukcii niektorých súčastí prekladačov sa ale vyuţíva tzv. akceptačný spôsob špecifikácie jazyka, tzn. špecifikácia pomocou automatu. Automat neformálne môţeme popísať ako zariadenie, ktoré do-
21 stáva na vstup reťazce zloţené z terminálnych symbolov. Zariadenie daný vstupný reťazec spracuje a po konečnom čase (počte operácií) oznámi, či tento patrí do jazyka alebo nie (hovoríme, ţe automat slovo akceptuje resp. neakceptuje). Začneme od najjednoduchších zariadení tohoto typu deterministických konečných automatov. Definícia 2.1 [2] Pod deterministickým konečným automatom (DKA) M rozumieme usporiadanú päticu M = (Q, T,, q 0, F), kde Q konečná mnoţina stavov automatu; T konečná mnoţina prípustných vstupných symbolov; prechodová funkcia automatu, : Q T Q; q 0 začiatočný stav automatu, q 0 Q; F mnoţina koncových (alebo akceptujúcich) stavov automatu, F Q. Schému konečného automatu môţeme vidieť na Obr. 2.1. Môţeme si predstaviť, ţe konečný automat je zariadenie, ktoré sa skladá zo vstupnej pásky a konečnostavovej riadiacej jednotky. Na vstupnej páske je zapísané slovo, o ktorom máme rozhodnúť, či patrí do jazyka. Slovo sa musí skladať zo symbolov z mnoţiny T. Konečnostavová riadiaca jednotka sa vţdy nachádza v jednom z mnoţiny stavov Q a vidí jeden aktuálny vstupný symbol na páske. Konečný automat pracuje diskrétne po jednotlivých krokoch. V kaţdom kroku prečíta aktuálny symbol zo vstupnej pásky, presunie čítaciu hlavu na nasledujúci symbol a prejde do nového stavu. Nový stav sa určí na základe pôvodného stavu a prečítaného vstupného symbolu a formálne túto činnosť popisuje prechodová funkcia. vstupná páska a 1 a 2... a n konečnostavová riadiaca jednotka Obr. 2.1 Schéma konečného automatu
22 Na začiatku výpočtu je čítacia hlava nastavená na prvý symbol slova a konečnostavová riadiaca jednotka sa nachádza v stave q 0. Výpočet sa skončí, keď sa spracuje celé vstupné slovo. Automat slovo akceptuje, ak sa mu podarí spracovať celé vstupné slovo, pričom skončí v akceptujúcom stave, tzn. v niektorom stave z mnoţiny F. Poďme si teraz uvedené skutočnosti sformalizovať. Definícia 2.2 [2] Nech M = (Q, T,, q 0, F) je konečný automat. Usporiadanú dvojicu (q, w) Q T * nazývame konfigurácia konečného automatu M. Konfiguráciu (q 0, w), kde w je vstupný reťazec, nazývame začiatočná konfigurácia automatu M, konfiguráciu (q, ), kde q F, koncová (alebo akceptujúca) konfigurácia automatu M. Konfiguráciu konečného automatu môţeme interpretovať ako momentálny stav výpočtu, kde q reprezentuje momentálny stav automatu a w doteraz nespracovanú časť vstupu. Prvý symbol reťazca w zároveň predstavuje symbol, ktorý momentálne vidí konečnostavová riadiaca jednotka. Definícia 2.3 [2] Nech M = (Q, T,, q 0, F) je deterministický konečný automat. Nad mnoţinou konfigurácií Q T * definujeme reláciu prechodu (alebo krok výpočtu) DKA takto: Nech q, p Q, w T *, a T. Potom (q, aw) (p, w) práve vtedy, keď (q, a) = p. Definícia relácie prechodu stanovuje pravidlá, ako moţno pri výpočte prejsť z jednej konfigurácie automatu do druhej. q reprezentuje pôvodný stav, p nový stav po prechode, a aktuálny symbol na vstupe, w zvyšok vstupu a prechodovú funkciu. Keďţe po prechode do novej konfigurácie sa čítacia hlava posunie o jeden symbol doprava, v novej konfigurácii uţ bude ako neprečítaná časť vstupu vystupovať len w. Definícia 2.4 Nech M = (Q, T,, q, F) je konečný automat. Postupnosť konfigurácií (q 0, w 0 ), (q 1, w 1 ),..., (q n, w n ) takých, ţe (q 0, w 0 ) (q 1, w 1 )... (q n, w n ), q i Q, w i T *, i = 0, 1,..., n sa nazýva výpočet konečného automatu M z (q 0, w 0 ) do (q n, w n ). Výpočet budeme podobne ako v prípade derivácie v gramatikách vyjadrovať pomocou mocniny relácie v tvare (q 0, w 0 ) n (q n, w n ).
23 Tranzitívny uzáver relácie budeme označovať symbolom + a reflexívny a tranzitívny uzáver symbolom *. Dve konfigurácie budú v relácii (q, w) + (p, x), ak existuje k 1 také, ţe (q, w) k (p, x) a v relácii (q, w) * (p, x), ak existuje k 0 také, ţe (q, w) k (p, x). Definícia 2.5 [2] Nech M = (Q, T,, q 0, F) je konečný automat. Potom hovoríme, ţe stav q Q je dosiahnuteľný, ak existuje výpočet (q 0, w) n (q, ) pre nejaké n 0, w T *. Ak taký výpočet neexistuje, stav q je nedosiahnuteľný. Definícia 2.6 Nech M = (Q, T,, q 0, F) je konečný automat. Potom jazyk L(M) rozpoznávaný (prijímaný, špecifikovaný, akceptovaný) konečným automatom M je mnoţina L(M) = {w (q 0, w) * (q, ), w T *, q F. Poznámka. Prechodová funkcia môţe predstavovať aj tzv. neúplné zobrazenie, čo v tomto prípade znamená, ţe existuje taká usporiadaná dvojica (q, a), q Q, a T (q predstavuje aktuálny stav automatu a a aktuálny vstupný symbol), pre ktorú nie je určený nasledujúci stav automatu. Jazyk rozpoznávaný daným automatom je teda taká mnoţina reťazcov zo vstupnej abecedy, pre ktoré existuje výpočet končiaci v akceptujúcej konfigurácii. Ak automat neakceptuje reťazec, mohla nastať jedna z dvoch situácií: a) automat spracoval celé slovo, ale neskončil v akceptujúcom stave; b) automat sa pri spracovávaní slova zasekol, tzn. dostal sa do stavu, z ktorého pre aktuálny začiatočný symbol nespracovanej časti reťazca nemá definovaný prechod ( ). K takejto situácii by došlo aj vtedy, keby dostal na vstup symbol, ktorý nepatrí do mnoţiny T. Táto moţnosť môţe nastať len vtedy, ak je neúplné zobrazenie. Príklad 2.3 Je daný konečný automat M = {{q 0, q 1, q 2, q 3, {0, 1, q 0,, {q 0, pričom prechodová funkcia je určená prechodovou tabuľkou (PT): 0 1 q 0 q 2 q 1 q 1 q 0 q 3 q 2 q 3 q 0 q 3 q 1 q 2
24 Akceptujúci výpočet na reťazci 10101111 prebehne nasledovne: (q 0, 10101111) (q 1, 0101111) (q 0, 101111) (q 1, 01111) (q 0, 1111) (q 1, 111) (q 3, 11) (q 2, 1) (q 0, ) Reťazec 001 akceptovaný nebude, pretoţe výpočet (q 0, 001) (q 2, 01) (q 3, 1) (q 2, ) skončí v stave, ktorý nie je akceptujúci; pri spracovaní reťazca 002 sa automat zasekne a preto tento tieţ akceptovaný nebude: (q 0, 002) (q 2, 02) (q 3, 2). Konečný automat môţeme reprezentovať aj tzv. prechodovým diagramom. Prechodový diagram je orientovaný graf, v ktorom sú vrcholy ohodnotené stavmi a hrany symbolmi vstupnej abecedy tak, ţe ak (q, t) = p, tak existuje orientovaná hrana z q do p, ktorá je ohodnotená terminálom t. Začiatočný stav je označený šípkou a koncové stavy sú označené dvojitým krúţkom. Reprezentácia konečného automatu z príkladu Príklad 2.3 je na Obr. 2.2. 1 q 0 0 q 1 0 1 0 1 0 q 2 1 q 3 Obr. 2.2 Prechodový diagram konečného automatu Poznámka. V prípade, ţe prechodová funkcia predstavuje neúplné zobrazenie, budú príslušné poloţky v prechodovej tabuľke prázdne (pozri napr. Príklad 2.4 v nasledujúcej kapitole). 2.3. Nedeterministické konečné automaty Všeobecnejším typom konečného automatu je nedeterministický konečný automat. Jeho základnou vlastnosťou je, ţe momentálny stav a aktuálny symbol vstupného re-
25 ťazca neurčujú jednoznačne stav, do ktorého automat môţe prejsť (automat má na výber viacero stavov, do ktorých môţe prejsť). Druhým rozdielom je, ţe automat môţe vykonávať kroky nielen na terminálne symboly, ale aj na prázdne slovo. Z formálneho hľadiska sa oproti DKA zmení iba prechodová funkcia, ktorá bude usporiadanej dvojici (stav, symbol na vstupe) priraďovať mnoţinu stavov, do ktorých môţe automat prejsť (symbolom na vstupe môţe byť aj, čo predstavuje situáciu, keď automat ponechá čítaciu hlavu na pôvodnom symbole a pri kroku výpočtu nebude brať aktuálny symbol na vstupe do úvahy). Definícia 2.7 Pod nedeterministickým konečným automatom (NKA) M rozumieme usporiadanú päticu M = (Q, T,, q 0, F), kde Q konečná mnoţina stavov automatu; T konečná mnoţina prípustných vstupných symbolov; prechodová funkcia automatu, : Q (T { ) 2 Q, pričom 2 Q označuje mnoţinu všetkých podmnoţín mnoţiny Q; q 0 začiatočný stav automatu, q 0 Q; F mnoţina koncových (alebo akceptujúcich) stavov automatu, F Q. Činnosť NKA budeme aj teraz definovať pomocou konfigurácií. Význam konfigurácie, začiatočná a akceptujúca konfigurácia zostávajú rovnaké ako pri DKA. Keďţe sa však zmenila prechodová funkcia, treba zmeniť aj reláciu prechodu. Definícia 2.8 Nech M = (Q, T,, q 0, F) je nedeterministický konečný automat. Nad mnoţinou konfigurácií Q T * definujeme reláciu prechodu (alebo krok výpočtu) NKA takto: Nech q, p Q, w T *, a T {. Potom (q, aw) (p, w) práve vtedy, keď p (q, a). Z uvedeného vyplýva, ţe ak automat realizuje krok na, prechodom sa zmení len jeho stav a neprečítaná časť vstupu ostane nezmenená ( je predponou kaţdého reťazca). Automat môţe prejsť na symbol na vstupe do ľubovoľného zo stavov, ktorý pre danú kombináciu (stav, symbol na vstupe) udáva prechodová funkcia. Akceptovanie reťazca NKA aj jazyk špecifikovaný NKA sú definované rovnakým spôsobom ako pre DKA, tzn. w je akceptované, ak (q 0, w) * (q, ) pre nejaké q F.
26 Príklad 2.4 Je daný automat M = ({q 0, q 1, q f, {+,, 0, 1, 2,..., 9,, q 0, {q f ), kde je určená PT + 0 1... 9 q 0 {q 1 {q 1 {q 1, q f q 1 {q 1, q f q f (symbolom sú oddelené alternatívne vstupné symboly). Pri spracovávaní reťazca +123 existujú nasledujúce moţnosti prechodov (pri prvom kroku môţeme vidieť prechod z (q 0, +123) do (q 1, +123) na ): (q 1, 3) (q 1, ) (q 1, 23) (q f, ) (q 1, 123) (q f, 3) (q 0, +123) (q f, 23) (q 1, +123) Automat reťazec akceptuje, pretoţe existuje výpočtová cesta, ktorý končí v akceptujúcej konfigurácii (je vyznačená podtrhnutými konfiguráciami). Poznámka. V prípade NKA si moţno výpočet predstaviť ako strom, ktorého koreň tvorí začiatočná konfigurácia a ktorý sa bude vetviť vţdy vtedy, keď automat môţe prejsť do viacerých stavov. Slovo bude akceptované vtedy, ak aspoň jedna zo všetkých moţných výpočtových ciest končí v akceptujúcej konfigurácii, tzn. keď aspoň jeden list stromu obsahuje akceptujúcu konfiguráciu. NKA si môţeme tieţ predstaviť ako zariadenie, ktoré sa z hľadiska prechodu do nového stavu vţdy dokáţe správne rozhodnúť, tzn. rozhodnúť sa pre tú z moţností prechodu do nového stavu (ak existuje), ktorá leţí na ceste k nejakej akceptujúcej konfigurácii. NKA neakceptuje slovo vtedy, ak ţiadna výpočtová cesta nekončí akceptujúcou konfiguráciou. Keďţe máme dva typy konečných automatov, vzniká prirodzená otázka, aký je medzi nimi vzťah z hľadiska akceptovanej triedy jazykov. 2.4. Vzťah medzi deterministickými a nedeterministickými konečnými automatmi Keď si porovnáme definíciu DKA a NKA a relácie prechodu u obidvoch automatov, zistíme, ţe DKA je vlastne špeciálnym prípadom NKA, kde (q, a) 1 a a.
27 Trieda jazykov akceptovaných deterministickými konečnými automatmi musí byť preto podtriedou jazykov akceptovaných nedeterministickými konečnými automatmi. Zaujímavé je, ţe platí aj obrátená veta. Veta 2.1 Nech L je jazyk akceptovaný nejakým NKA. Potom existuje DKA, ktorý akceptuje ten istý jazyk L. Dôkaz. K danému NKA M = (Q, T,, q 0, F) skonštruujeme DKA M = (Q, T,, q 0, F ), ktorý bude akceptovať ten istý jazyk. Hlavná myšlienka pri konštrukcii automatu M je, aby skonštruovaný DKA bol schopný sledovať všetky výpočtové cesty, po ktorých by mohol ísť pôvodný NKA M. Dosiahneme to nasledujúcim spôsobom: Stavy q Q automatu M budeme označovať v tvare {q 1, q 2,..., q i, kde q k Q pre k = 1, 2,..., i. Začiatočný stav q 0 = closure({q 0, M), kde operácia uzáveru closure dodá do mnoţiny stavov všetky tie ďalšie stavy, do ktorých je moţné sa dostať z pôvodnej mnoţiny stavov na. Algoritmus operácie closure() je nasledujúci: typedef struct NKA { množina_stavov Q; množina_terminálov T; prechodová_funkcia_nka ; stav q 0 ; množina_stavov F; NKA; typedef struct DKA { množina_stavov Q; množina_terminálov T; prechodová_funkcia_dka ; stav q 0 ; množina_stavov F; DKA; void closure(množina_stavov& S, NKA M) { do { zmena = FALSE; if (existuje taký stav q M.Q, ţe q S a zároveň q M. (p, ) pre nejaké p S) { Pridaj q do S; zmena = TRUE;
28 while (zmena); Algoritmus 2.1 Uzáver mnoţiny stavov NKA Mnoţinovú symboliku sme zvolili zámerne, pretoţe stavy konštruovaného DKA si môţeme predstaviť ako mnoţiny stavov pôvodného NKA. Mnoţina koncových stavov F je tvorená mnoţinou všetkých takých stavov z Q, ktoré obsahujú aspoň jeden koncový stav automatu M. Tým sa zabezpečí, ţe DKA bude akceptovať reťazec práve vtedy, keď aspoň jedna z výpočtových ciest u NKA viedla ku akceptujúcej konfigurácii. Prechodovú funkciu budeme definovať takto: i, (2.1) ({q 1, q 2,..., q i, a) = closure δ q j, a, M j 1 tzn. DKA M prejde do stavu, ktorý bude reprezentovať všetky stavy pôvodného automatu M, do ktorých by sa automat M dostal prechodom zo stavov q 1, q 2,..., q i na vstupný symbol a. Podobne ako pri vytvorení začiatočného stavu automatu M, aj teraz musíme mnoţinu stavov obohatiť o tie stavy, do ktorých je moţné sa dostať z pôvodných stavov prechodom na (lebo definícia DKA kroky na nepripúšťa). Formálnu verifikáciu toho, ţe takto skonštruovaný DKA M akceptuje nejaké slovo w práve vtedy, keď ho akceptuje pôvodný NKA M, nechávame na čitateľa. Poznámka. Pri praktických úlohách, keď chceme ku NKA nájsť ekvivalentný DKA, nemusíme vţdy pracovať so všetkými stavmi tvaru {q 1, q 2,..., q i, kde q k Q pre k = 1, 2,..., i, ktorých celkový počet je 2 Q, kde Q predstavuje počet stavov v mnoţine Q, ale len s tými, do ktorých sa novokonštruovaný automat môţe dostať. Mnoţinu stavov a prechodovú funkciu ekvivalentného DKA môţeme skonštruovať podľa nasledujúceho algoritmu: void vytvor_deterministický(nka M, DKA& M ) { Zaraď stav closure({m.q 0, M) do PT automatu M a označ ho ako nespracovaný; while (v PT existujú nespracované stavy) { Vyber z PT jeden nespracovaný stav q a označ ho ako spracovaný; for (všetky a T) { Urči stav p = (q, a) podľa (2.1); if (p sa nenachádza v PT)
29 Zaraď p do PT a označ ho ako nespracovaný; Zaznamenaj v PT prechod zo stavu q do stavu p na symbol a; Mnoţina stavov M.Q bude tvorená stavmi vytvorenými v PT; Mnoţiny vstupných symbolov sú totoţné, tzn. M.T = M.T; Prechodová funkcia M. je určená vytvorenou PT; Začiatočný symbol M.q 0 = closure({m.q 0, M); Mnoţina koncových stavov M.F je tvorená mnoţinou všetkých takých stavov z M.Q, ktoré obsahujú aspoň jeden koncový stav automatu M (M.F); Algoritmus 2.2 Vytvorenie ekvivalentného DKA Poznámka. V prípade, ţe východiskový NKA neobsahuje prechody na, môţeme v prechádzajúcom algoritme vynechať aplikáciu uzáverovej operácie closure(). Príklad 2.5 Daný je NKA M = ({q 0, q 1, q 2, q 3, q f, {0, 1,, q 0, {q f ), kde prechodové zobrazenie je definované takto: Zostrojte k nemu ekvivalentný DKA. 0 1 q 0 {q 0, q 1 {q 0, q 2 q 1 {q 1, q 3 {q 1 q 2 {q 2 {q 2, q f q 3 {q f q f Poďme postupne zostavovať mnoţinu stavov DKA a PT podľa algoritmu Algoritmus 2.2. Keďţe pôvodný NKA obsahuje aj prechody na, musíme pri konštrukcii stavov ekvivalentného DKA aplikovať operáciu closure(). Pred vstupom do cyklu bude PT vyzerať takto: spracovaný 0 1 {q 0 n Po prvom prechode cyklu bude PT vyzerať takto: spracovaný 0 1 {q 0 a {q 0, q 1 {q 0, q 2 {q 0, q 1 n {q 0, q 2 n Po druhom prechode cyklom bude PT vyzerať takto (q f sa dostalo do stavu {q 0, q 1, q 3 q f výsledkom uzáverovej operácie closure({q 0, q 1, q 3 )):
30 spracovaný 0 1 {q 0 a {q 0, q 1 {q 0, q 2 {q 0, q 1 a {q 0, q 1, q 3 q f {q 0, q 1, q 2 {q 0, q 2 n {q 0, q 1, q 3, q f n {q 0, q 1, q 2 n Po treťom prechode cyklom bude PT vyzerať takto (pridávaný stav {q 0, q 1, q 2 sa uţ v PT nachádzal): spracovaný 0 1 {q 0 a {q 0, q 1 {q 0, q 2 {q 0, q 1 a {q 0, q 1, q 3, q f {q 0, q 1, q 2 {q 0, q 2 a {q 0, q 1, q 2 {q 0, q 2, q f {q 0, q 1, q 3, q f n {q 0, q 1, q 2 n {q 0, q 2, q f n Na konci celého procesu získame nasledujúcu PT: spracovaný 0 1 {q 0 a {q 0, q 1 {q 0, q 2 {q 0, q 1 a {q 0, q 1, q 3, q f {q 0, q 1, q 2 {q 0, q 2 a {q 0, q 1, q 2 {q 0, q 2, q f {q 0, q 1, q 3, q f a {q 0, q 1, q 3, q f {q 0, q 1, q 2 {q 0, q 1, q 2 a {q 0, q 1, q 2, q 3, q f {q 0, q 1, q 2, q f {q 0, q 2, q f a {q 0, q 1, q 2 {q 0, q 2, q f {q 0, q 1, q 2, q 3, q f a {q 0, q 1, q 2, q 3, q f {q 0, q 1, q 2, q f {q 0, q 1, q 2, q f a {q 0, q 1, q 2, q 3, q f {q 0, q 1, q 2, q f Mnoţina akceptujúcich stavov F = {{q 0, q 1, q 3, q f, {q 0, q 2, q f, {q 0, q 1, q 2, q 3, q f, {q 0, q 1, q 2, q f. Výsledný DKA bude obsahovať 8 stavov, zatiaľ čo pôvodný NKA obsahoval 5 stavov. Túto skutočnosť môţeme zovšeobecniť tvrdením, ţe ekvivalentný DKA vţdy bude obsahovať viacej stavov ako pôvodný NKA, v krajnom prípade tento nárast počtu stavov môţe byť aţ exponenciálny. Výhodou NKA z tohto pohľadu je, ţe pri rovnakom akceptovanom jazyku má jednoduchšiu štruktúru ako ekvivalentný DKA. Nedeterministické konečné automaty budeme pre ich jednoduchšiu štruktúru vyuţívať ako medzistupeň pri konštrukcii lexikálneho analyzátora v kap. 4.2.
31 2.5. Minimalizácia počtu stavov deterministických konečných automatov Ďalším problémom, ktorým sa budeme zaoberať, je otázka minimalizácie počtu stavov DKA. Spomedzi všetkých DKA akceptujúcich nejaký regulárny jazyk L existuje jediný DKA M L, ktorý má najmenší počet stavov zo všetkých automatov akceptujúcich jazyk L [2]. Teraz si ukáţeme, ako môţeme takýto automat zostrojiť. Nech M = (Q, T,, q 0, F) je ľubovoľný DKA taký, ţe L(M) = L. Najprv vylúčime z mnoţiny stavov všetky tie stavy, ktoré sú nedosiahnuteľné zo stavu q 0. Tieto sú z hľadiska jazyka akceptovaného automatom zbytočné, pretoţe sa nemôţu objaviť v ţiadnom výpočte. Definícia 2.9 Nech M = (Q, T,, q 0, F) je konečný automat (deterministický alebo nedeterministický). Stav q Q nazývame dosiahnuteľný zo stavu p Q, ak existuje taký reťazec w T *, ţe (p, w) * (q, ). Ak stav q nie je dosiahnuteľný zo stavu p, nazýva sa nedosiahnuteľný zo stavu p. Mnoţinu všetkých stavov dosiahnuteľných zo stavu q 0 môţeme určiť pomocou algoritmu Algoritmus 2.3. Tento vyhľadá postupne všetky stavy, ktoré sú dosiahnuteľné zo stavu q 0. Platí, ţe po i-tom prechode cyklom bude mnoţina dosiahnuteľné obsahovať tie stavy, ktoré sú dosiahnuteľné z q 0 na maximálne i krokov výpočtu. Algoritmus končí vtedy, keď uţ do mnoţiny dosiahnuteľných stavov nemôţeme pridať ţiadny nový stav. Keďţe mnoţina stavov je konečná, algoritmus musí raz skončiť. void zisti_dosiahnuteľné_z_q 0 ( množina_stavov& dosiahnuteľné, DKA M) { množina_stavov nové; dosiahnuteľné = {q 0 ; do { zmena = TRUE; nové = ; for (všetky a M.T a všetky q dosiahnuteľné) nové = nové M. (p, a); if (mnoţina stavov nové nie je celá obsiahnutá v dosiahnuteľné) dosiahnuteľné = dosiahnuteľné nové; else zmena = FALSE;
32 while (zmena); Algoritmus 2.3 Algoritmus pre zistenie stavov dosiahnuteľných zo stavu q 0 Predpokladajme ďalej, ţe sme z automatu odstránili všetky stavy nedosiahnuteľné zo stavu q 0 (okrem Q je ich potrebné odstrániť aj z a F). Ďalej budeme potrebovať nasledujúcu definíciu: Definícia 2.10 [2] Nech M = (Q, T,, q 0, F) je DKA a q 1, q 2 Q sú jeho dva rôzne stavy. Potom budeme hovoriť, ţe: a) reťazec u T * rozlišuje stavy q 1 a q 2 vtedy, ak (q 1, u) * (q 3, ), (q 2, u) * (q 4, ), pričom jeden zo stavov q 3, q 4 patrí do F a druhý nie; b) stavy q 1 a q 2 sú k-nerozlíšiteľné (zapisujeme q 1 k q 2 ), ak neexistuje reťazec u s dĺţkou u k, ktorý stavy q 1 a q 2 rozlišuje; c) stavy q 1 a q 2 sú nerozlíšiteľné (zapisujeme q 1 q 2 ), ak sú k-nerozlíšiteľné pre ľubovoľné k 0. Ľahko sa moţno presvedčiť o tom, ţe je relácia ekvivalencie na mnoţine stavov Q. Princíp konštrukcie redukovaného automatu je zaloţený na stotoţnení navzájom nerozlíšiteľných stavov pôvodného DKA. Ak sú dva stavy nerozlíšiteľné, potom ak by sa výpočet dostal do ľubovoľného z nich, akceptovanie alebo neakceptovanie v prípade rovnakej neprečítanej časti vstupného reťazca by muselo v obidvoch prípadoch dopadnúť rovnako. Na identifikáciu všetkých vzájomne nerozlíšiteľných stavov automatu rozdeľme najprv stavy automatu M na dve skupiny (tzv. zlúčené stavy): do prvého zlúčeného stavu budú patriť všetky koncové stavy a do druhého všetky ostatné stavy (v obidvoch zlúčených stavoch sa budú nachádzať stavy pôvodného automatu M, ktoré sú navzájom 0-nerozlíšiteľné). V ďalších krokoch sa budú tieto zlúčené stavy ďalej deliť v prípade, ţe po prechode na nejaký symbol a T sa všetky stavy automatu M patriace do nejakého zlúčeného stavu nedostanú do rovnakého zlúčeného stavu (Algoritmus 2.4). Algoritmus skončí vtedy, keď uţ nie je potrebné zlúčené stavy ďalej deliť, tzn. keď zlúčené stavy obsahujú vzájomne nerozlíšiteľné stavy. typedef struct množina_zlúčených_stavov { // pole skupín (mnoţín) stavov konečného automatu (tzv. zlúčených stavov) množina_stavov *S;