Ciele
- Definovať zásobníkový automat ako ekvivalentný spôsob určenia bezkontextového jazyka.
- Uviesť základné pojmy analýzy bezkontextových gramatík. Množiny $\mathit{FIRST},$ $\mathit{FOLLOW},$ $\mathit{PREDICT}$ a definovať $LL(1)$ gramatiku.
- Predstaviť princíp nerekurzívnej syntaktickej analýzy zhora nadol postavenej na zásobníkovom automate.
- Implementovať syntaktický analyzátor jednoduchého jazyka aritmetických výrazov na báze zásobníkového automatu.
Úvod
V predchádzajúcich cičeniach sme venovali základom bezkontextových gramatík a metódam syntaktickej analýzy. V tomto cvičení si rozšírime vedomosti o zásobníkové automaty, ktoré predstavujú formálny model schopný rozpoznať všetky bezkontextové jazyky. Zásobníkové automaty preto v tomto zmysle predsatvujú plnohodnotnou alternatívou k bezkontextovým gramatikám pri definovaní jazykov tohto typu.
Následne sa zameriame na kľúčové pojmy súvisiace s analýzou bezkontextových gramatík, predovšetkým na množiny $\mathit{FIRST}$, $\mathit{FOLLOW}$ a definíciu $LL(1)$ gramatiky, ktoré zohrávajú kľúčovú úlohu pri tvorbe deterministických parsovacích algoritmov.
Osobitnú pozornosť budeme venovať princípom nerekurzívnej syntaktickej analýzy zhora nadol. Táto metóda využíva zásobník na riadenie parsovacieho procesu namiesto rekurzívnych volaní, čo z nej robí efektívnu a praktickú metódu pre implementáciu syntaktických analyzátorov. V závere kapitoly si ukážeme implementáciu jednoduchého nerekurzívneho syntaktického analyzátora zhora nadol, ktorý bude schopný spracovať vstupný reťazec podľa LL(1) gramatiky.
Postup
Krok 1
Zásobníkové automaty (ZA) zohrávajú pri akceptácii bezkontextových jazykov rovnakú úlohu ako konečné automaty pri akceptácii regulárnych jazykov. To znamená, že každý jazyk, pre ktorý existuje bezkontextová gramatika, ktorá ho generuje, je akceptovateľný nejakým ZA a naopak, každý jazyk akceptovaný nejakým ZA je zároveň generovateľný nejakou bezkontextovou gramatikou.
Schému ZA prezentuje nasledujúci Obrázok 1.
Zásobníkový automat (angl. pushdown automata) je usporiadana sedmica: $$M=(Q,T,\Gamma,\delta,q_0,Z_0,F),$$ kde:
- $Q$ je konečná množina stavov automatu,
- $T$ je konečná množina prípustných vstupných symbolov (alebo vstupná abeceda) automatu,
- $\Gamma$ je konečná množina zásobníkových symbolov (tzv. zásobníková abeceda),
- $\delta$ je prechodová funkcia zásobníkového automatu, $\delta: Q \times T_\varepsilon\times\Gamma_\varepsilon \to 2^{Q\times\Gamma^*}$,
- $q_0$ je počiatočný alebo iniciálny stav automatu, $q_0 \in Q$,
- $Z_0$ je zásobníkový symbol nachádzajajúci sa na začiatku každého výpočtu na dne zásobníka, $Z_0 \in \Gamma$,
- $F$ je množina akceptujúcich (alebo koncových) stavov automatu, $F\subseteq Q$.
Poznámka
Na základe vyššie uvedenej definície je zrejmé, že ZA predstavuje rozšírenie NKA o pamäťový prvok – zásobník.
Poznámka
Nad zásobníkom možno vykonávať dve základné operácie, a to:
- $\mathit{POP}()$, ktorá odstráni jeden symbol z vrchola zásobníka a zároveň ho poskytne ako návratovú hodnotu,
- $\mathit{PUSH}(s)$, ktorá postupne pridá na vrchol zásobníka symboly slova $s\in \Gamma^*$ (zľava doprava).
Všimnime si, že špecifikácia prechodovej funkcie $\delta$ zodpovedá nedeterministickému ZA. Uprednostnenie tohto variantu vyplýva z jeho univerzálnosti a schopnosti popísať taktiež deterministický variant ZA jednoduchým obmedzením kodomény jeho prechodovej funkcie $\delta$.
Konfigurácia zásobníkového automatu $M=(Q, T, \Gamma, \delta, q_0, Z_0, F)$ je usporiadaná trojica $(q, w, s)\in Q\times T^*\times \Gamma^*$, kde $q$ je aktuálny stav automatu, $w$ je ešte nespracovaná časť vstupu a $s$ je obsah zásobníka. Vo výpočte ZA sa vyskytujú osobitné konfigurácie, ktoré zohrávajú špecifickú úlohu pri akceptácii vstupu:
- Začiatočná konfigurácia ZA $M$ – je konfigurácia v tvare $(q_0, w, Z_0)$, kde $w$ je celý vstupný reťazec.
- Koncová konfigurácia ZA $M$ – je konfigurácia tvaru $(q, \varepsilon, s)$, teda taká, pri ktorej bol celý vstup spracovaný.
- Akceptujúca konfigurácia ZA $M$ – je koncová konfigurácia, v ktorej $q \in F$ alebo $s=\varepsilon$.
Na množine konfigurácií $Q \times T^* \times \Gamma^* $ definujeme binárnu reláciu prechodu (alebo krok výpočtu) ZA označovaného $\vdash$ nasledovne. Nech $q, p \in Q, w \in T^*, x \in T_\varepsilon, Z \in \Gamma, s,t \in \Gamma^*$. Potom
$(q, xw, Zs) \vdash (p, w, ts)$ vtedy a len vtedy, keď $(p,t) \in \delta(q, x, Z).$
Postupnosť (sekvencia) konfigurácií $ (q_0, w_0, s_0), (q_1, w_1, s_1), \ldots, (q_k, w_k, s_k) $, pre $ k \in \mathbb{N}$ takých, že $$ (q_0, w_0, s_0) \vdash (q_1, w_1, s_1) \vdash \dots \vdash (q_k, w_k, s_k), $$ sa nazýva výpočet ZA $M$ z $(q_0, w_0, s_0)$ do $(q_k, w_k, s_k)$ a vyjadrovať ho budeme v tvare $$ (q_0, w_0, s_0) \vdash^k (q_k, w_k, s_k). $$
Poznámka
Číslo $k$ predstavuje počet krokov výpočtu.
Pri zásobníkových automatoch existujú dva spôsoby akceptácie reťazcov. Slovo môže byť akceptované:
- akceptačným stavom – automat prečíta celý vstup a ukončí výpočet v akceptačnom stave, obsah zásobníka je v tomto prípade irelevantný, alebo
- prázdnym zásobníkom – automat prečíta celý vstup a ukončí výpočet s prázdnym zásobníkom, stav je v tomto prípade irelevantný.
Oba spôsoby akceptácie slov sú ekvivalentné. Vždy sa dá automat pracujúci jedným spôsobom previesť na automat pracujúci druhým spôsobom a naopak.
Jazyk $L(M)$ rozpoznávaný (akceptovaný) zásobníkovým automatom $M$ je teda množina:
$L(M) = \{ w \mid (q_0, w, Z_0) \vdash^* (q, \varepsilon, s) \land w \in T^*,$ $s \in \Gamma^* \land (q \in F \lor s=\varepsilon) \}.$
Príklad
Navrhnite zásobníkový automat $M$ akceptujúci jazyk $L = \{ a^{n}b^{n}~|~n \geq 1\}$, $n \in \mathbb{N}$. Na automate $M$ vykonajte výpočty nad slovami $aabb$, $aab$.
Riešenie
V 4. cvičení sme dokázali, že tento jazyk nie je regulárny, preto neexistuje KSA, ktorý by ho akceptoval. Problém spočíva v tom, že pri postupnom spracovávaní reťazca musíme zabezpečiť taktiež počítanie výskytov jeho jednotlivých symbolov. Keďže jediným kvázi-pamäťovým prvkom KSA je jeho vnútorny stav, túto informáciu nemáme ako zaznamenať. V prípade ZA však máme k dispozícii skutočnú pamäťovú jednotku – zásobník, nad ktorým možeme vykonávať základné operácie $\mathit{PUSH}$ a $\mathit{POP}$. Uveďme teda princíp fungovania ZA akceptujúceho jazyk $L$.
- Vždy keď sa na vstupe spracuje symbol $a$, na vrchol zásobníka sa pridá nejaký (konkrétny) symbol zásobníkovej abecedy, napr. symbol $a$.
- Po spracovaní prvého symbolu $b$ zo vstupu ZA zmení svoj stav a vykoná operáciu $\mathit{POP}$ nad zásobníkom.
- Po spracovaní každého ďalšieho symbolu $b$ zo vstupu ZA vykoná operáciu $\mathit{POP}$ nad zásobníkom.
- Ak po spracovaní celého reťazca zo vstupu v zásobníku ostane len počiatočný zásobníkový symbol, ZA zmení svoj stav na akceptačný.
Výsledný automat má tvar $M=(\{q_0,q_1,q_2\},\{a,b\},\{a,Z_0\},\delta,q_0,Z_0,\{q_2\})$, kde prechodová funkcia $\delta$ je určená nasledujúcim diagramom.
Výpočet nad slovom $aabb$:
$(q_0,aabb,$$\phantom{a}Z_0$$)\vdash(q_0,abb,$$\phantom{a}aZ_0$$)\vdash(q_0,bb,$$\phantom{a}aaZ_0$$)\vdash(q_1,b,$$\phantom{a}aZ_0$$)\vdash(q_1,\varepsilon,$$\phantom{a}Z_0$$)\vdash(q_2,\varepsilon,$$\phantom{a}Z_0$$)\nvdash$
Slovo bolo akceptované akceptačným stavom.
Výpočet nad slovom $aab$:
$(q_0,aab,$$\phantom{a}Z_0$$)\vdash(q_0,ab,$$\phantom{a}aZ_0$$)\vdash(q_0,b,$$\phantom{a}aaZ_0$$)\vdash(q_1,\varepsilon,$$\phantom{a}aZ_0$$)\nvdash$
Slovo nebolo akceptované, keďže výpočet neskončil v konečnom stave ani s prázdnym zásobníkom.
Poznámka
Označenie prechodu $a,b/s$ v diagrame znamená, že zo vstupu sa prečíta symbol $a\in T$, na vrchole zásobníka sa musí nachádzať symbol $b\in \Gamma$, ktorý sa zo zásobníka vyberie ($\mathit{POP}$) a následne sa do zásobníka vloží ($\mathit{PUSH}$) postupnosť symbolov zásobníka $s\in\Gamma^*$
Poznámka
Na vyjadrenie obsahu zásobníka využívame riadkovu notáciu, v rámci ktorej je vrchol zásobníka najviac vľavo. Pre jednoznačnosť však pridáme do notácie taktiež grafické vyznačenie, napríklad $\phantom{a}aZ_0$ reprezentuje zásobník, na vrchole ktorého je zásobníkový symbol $a$ a na dne symbol $Z_0$.
Príklad
Transformujte ZA $M$ z predchádajúcého príkladu na ZA $M'$, ktorý bude akceptovať slová prázdnym zásobníkom. Vykonajte na ňom výpočty nad slovami $aabb$, $aab$.
Riešenie
$M=(\{q_0,q_1\},\{a,b\},\{a,Z_0\},\delta,q_0,Z_0,\emptyset)$, kde prechodová funkcia $\delta$ je určená nasledujúcim diagramom.
Výpočet nad slovom $aabb$:
$(q_0,aabb,$$\phantom{a}Z_0$$)\vdash(q_0,abb,$$\phantom{a}a$$)\vdash(q_0,bb,$$\phantom{a}aa$$)\vdash(q_1,b,$$\phantom{a}a$$)\vdash(q_1,\varepsilon,\varepsilon)\nvdash$
Slovo bolo akceptované prázdnym zásobníkom.
Výpočet nad slovom $aab$:
$(q_0,aab,$$\phantom{a}Z_0$$)\vdash(q_0,ab,$$\phantom{a}a$$)\vdash(q_0,b,$$\phantom{a}aa$$)\vdash(q_1,\varepsilon,$$\phantom{a}a$$)\nvdash$
Slovo nebolo akceptované, keďže výpočet neskončil s prázdnym zásobníkom ani v konečnom stave.
Úloha 1.1
Navrhnite zásobníkový automat $M$ akceptujúci jazyk $L = \{ a^{n}b^{2n}~|~n \in \mathbb{N} \;\land\;n \geq 1\}$.
Úloha 1.2
Navrhnite zásobníkový automat $M$ akceptujúci jazyk $L = \{ ww^{R}~|~w \in \{0,1\}^{*}\}$.
Krok 2
Základnou požiadavkou na v praxi použiteľný syntaktický analyzátor je jeho schopnosť deterministicky spracovať vstupné reťazce s lineárnou časovou zložitosťou. V kontexte syntaktickej analýzy zhora nadol to znamená, že v každom kroku (najľavejšieho) odvodenia je možné jednoznačne (deterministicky) určiť, ktoré pravidlo gramatiky je potrebné aplikovať. Takéto sytaktické analyzátory potom označujeme ako prediktívne. Aby sme mohli rozhodnúť o tom, či je daná gramatika deterministická potrebujeme ju najprv analyzovavať, a to prostredníctvom konštukcie nasledujúcich analytických množín bezkontextových gramatík.
$\mathit{FIRST}(\alpha)$ je množina takých terminálnych symbolov, ktoré môžu stáť na začiatku reťazcov odvodených z $\alpha$. Ak možno z $\alpha$ odvodiť prázdny reťazec, $\varepsilon$ taktiež patrí do tejto množiny.
$\mathit{FIRST}(\alpha)=\{a \mid \alpha \Rightarrow^{*} a\beta \;\land$ $a\in T \;\land\; \beta\in (N\cup T)^{*}\}\cup\{\varepsilon\ \mathit{ak}\ \alpha \Rightarrow^{*} \varepsilon\}$
Výpočet množiny $\mathit{FIRST}(\alpha)$ pre ľubovoľný reťazec $\alpha \in (N \cup T)^{*}$ je praktické rozdeliť na dve časti. Najprv sa pre každý neterminálny symbol gramatiky $A$ vypočíta množina $\mathit{FST}_A$ – teda množina terminálnych symbolov, ktoré sa môžu objaviť na začiatku reťazcov odvodených z tohto neterminálneho symbolu $A$. Keďže tieto pomocné množiny $\mathit{FST}_A$ závisia iba od pravidiel gramatiky, stačí ich vytvoriť iba raz. Tento krok predstavuje akési predspracovanie gramatiky, ktoré umožní neskôr jednoduchšie a efektívne určovať množinu $\mathit{FIRST}(\alpha)$ aj pre ľubovoľne dlhé reťazce.
Tento dvojkrokový postup môžeme formalizovať nasledujúcou dvojicou algoritmov.
Algoritmus 1. konštrukcia množín $\mathit{FST}_A$ pre všetky $A \in N$.
Vstup: Bezkontextová gramatika $G=(N,T,P,S)$ a množina $N_\varepsilon$.
Výstup: Množiny $\mathit{FST}_A$ pre všetky $A \in N$.
- for all $a \in T$ do
- $\mathit{FST}_a \leftarrow \{a\}$;
- end for
- for all $A \in N$ do
- $\mathit{FST}_A \leftarrow \emptyset$;
- end for
- for all $A \in N_\varepsilon$ do
- $\mathit{FST}_A \leftarrow \{\varepsilon\}$;
- end for
- do
- for all $A\to\beta_1\beta_2\ldots\beta_k \in P$, kde $\beta_1\beta_2\ldots\beta_k \in (N\cup T)$ do
- for all $\beta_i \in \{\beta_1\beta_2\ldots\beta_k\}$ do
- $\mathit{FST}_A \leftarrow \mathit{FST}_A \cup \mathit{FST}_{\beta_i}$;
- if $\beta_i \notin N_\varepsilon$ then
- break;
- end if
- end for
- end for
- while nastala zmena niektorej z množín $\mathit{FST}_A$ pre všetky $A\in N$
Slovný opis:
Algoritmus 1 iteratívnej konštrukcie pomocných množín $\mathit{FST}_A$ pre všetky neterminálne symboly $A \in N$ možno rozdeliť do troch fáz:
- Pre každý terminálny symbol $a \in T$ sa vytvorí pomocná množina $\mathit{FST}_a = \{a\}$.
- Pomocné množiny $\mathit{FST}_A$ pre všetky neterminálne symboly $A \in N$ sa inicializujú prázdnou množinou. Následne sa do pomocných množín $\mathit{FST}_A$ vloží $\varepsilon$ pre neterminálne symboly $A \in N_\varepsilon$, z ktorých možno odvodiť prázdny reťazec pridá $\varepsilon$.
- Iteratívne sa prechádzajú všetky pravidlá $A \to \beta_1 \beta_2 \dots \beta_k$, kde $\beta_1,\beta_2,\ldots,\beta_k$ predstavujú jednotlivé terminálne a neterminálne symboly pravej strany pravidla. Do množiny $\mathit{FST}_A$ sa postupne pridáva obsah množín $\mathit{FST}_{\beta_i}$, až kým nenarazíme na symbol $\beta_i$ z ktorého nemožno odvodiť prázdny reťazec a spracovávanie daného pravidla sa v tomto okamihu preruší. Tento proces sa opakuje, až kým sa všetky množiny $\mathit{FST}_A$ nestabilizujú.
Algoritmus 2. konštrukcia množiny $\mathit{FIRST}(\alpha)$ pre $\alpha \in (N \cup T)^{*}$.
Vstup: Bezkontextová gramatika $G=(N,T,P,S)$ a vetná forma $\alpha\in(N \cup T)^{*}$.
Výstup: Množina $\mathit{FIRST}(\alpha)$.
- function $\mathit{FIRST}(\alpha)$
- if $\alpha=\varepsilon$ then
- return $\{\varepsilon\}$;
- end if
- if $\alpha=a\beta$, kde $a\in T\;\land\;\beta\in(N\cup T)^{*}$ then
- return $\{a\}$;
- end if
- if $\alpha=A\beta$, kde $A\in N\;\land\;\beta\in(N\cup T)^{*}$ then
- if $A\in N_\varepsilon$ then
- return $(\mathit{FST}_A - \{\varepsilon\})\cup \mathit{FIRST}(\beta)$;
- else
- return $\mathit{FST}_A$;
- end if
- end if
- end function
Poznámka
Množiny $\mathit{FIRST}(A)$ pre všetky $A \in N$ zodpovedajú množinám $\mathit{FST}_A$, čo vyplýva z Algoritmu 2, ktorý pri spracovaní reťazca pozostávajúceho z jediného neterminálneho symbolu $A$ vracia práve množinu $\mathit{FST}_A$.
Množina $\mathit{FOLLOW}(A)$ je množina všetkých terminálnych symbolov, ktoré môžu stáť bezprostredne za neterminálom A v nejakej vetnej forme (reťazci odvoditeľnom zo začiatočného symbolu gramatiky). Ak neterminál A môže stáť na konci niektorej vetnej formy, symbol $ patrí taktiež do tejto množiny.
Poznámka
$ je špeciálny symbol signalizujúci koniec reťazca. Nie je súčasťou samotného reťazca, ale používa sa na označenie jeho konca. V tomto zmysle sa preto podobá na ukončovací znak \0 v jazyku C.
$\mathit{FOLLOW}(A)=\{a \mid S \Rightarrow^{+} \beta_{1} Aa\beta_{2} \;\land\; a\in T\;\land$ $\beta_{1},\beta_{2}\in(N \cup T)^{*}\}\cup$
$\{$$$\ \mathit{ak}\ S \Rightarrow^{*} \beta A \;\land\; \beta\in(N \cup T)^{*}\}$
Algoritmus 3. konštrukcia množiny $\mathit{FOLLOW}(A)$ pre všetky $A \in N$.
Vstup: Bezkontextová gramatika $G=(N,T,P,S)$ a množina neterminálnych symbol $N$.
Výstup: Množiny $\mathit{FOLLOW}(A)$ pre všetky $A \in N$.
- pre všetky $A \in N$ vykonaj
- $\mathit{FLW}_A\leftarrow \emptyset$;
- koniec pre
- $\mathit{FLW}_S\leftarrow\{$$$\};$
- opakuj
- pre všetky $A \in N$ vykonaj
- pre všetky $B\to\alpha A\beta \in P$, kde $\alpha,\beta\in (N\cup T)^{*}$ vykonaj
- $\mathit{FLW}_A \leftarrow \mathit{FLW}_A \cup (\mathit{FIRST}(\beta)-\{\varepsilon\});$
- ak $\varepsilon \in \mathit{FIRST}(\beta)$ potom
- $\mathit{FLW}_A\leftarrow \mathit{FLW}_A \cup \mathit{FLW}_B;$
- koniec ak
- koniec pre
- koniec pre
- pokiaľ nenastala zmena niektorej z množín $\mathit{FLW}_A$ pre všetky $A \in N$.
- pre všetky $A \in N$, $\mathit{FOLLOW}(A)=\mathit{FLW}_A$
Slovny opis: Algoritmus 3 iteratívne konštruuje množiny $\mathit{FOLLOW}(A)$ pre všetky neterminálne symboly $A \in N$. Jeho činnosť možno opísať prostredníctvom troch základných pravidiel, ktoré zodpovedajú jednotlivým častiam tohto algoritmu:
- Na začiatku sa všetky pomocné množiny $\mathit{FLW}_A$ pre všetky neterminálne symboly $A \in N$ inicializujú prázdnou množinou.
- Do pomocnej množiny $\mathit{FLW}_S$ pre začiatočný symbol $S$ gramatiky sa pridá špeciálny koncový symbol $. Tento symbol indikuje, že existuje taká vetná forma, v ktorej $S$ stojí na jej konci. Je pritom zrejmé, že tento symbol bude vždy súčasťou množiny $\mathit{FOLLOW}$ pre začiatočný symbol gramatiky.
- Ak neterminálny symbol $A$ stojí na pravej strane pravidla pre neterminálny symbol $B$ a je nasledovaný zvyškom pravej strany pravidla $\beta$,
- do $\mathit{FLW}_A$ sa pridá $\mathit{FIRST}(\beta)$ okrem prázdneho reťazca $\varepsilon$,
- ak $\mathit{FIRST}(\beta)$ obsahuje prázdný reťazec, do $\mathit{FLW}_A$ sa pridá taktiež obsah $\mathit{FLW}_B.$
Táto fáza sa opakuje, až kým sa všetky množiny $\mathit{FLW}_A$ nestabilizujú.
Na záver platí, že $\mathit{FOLLOW}(A)=\mathit{FLW}_A\text{ pre všetky }A\in N.$
Aby sme zistili, či gramatika povoľuje konštrukciu prediktívneho syntaktického analyzátora, je nutné konštruovať množiny $\mathit{PREDICT}(A\to\alpha)$ pre každé pravidlo danej gramatiky a následne konštruovať tzv. rozkladovú tabuľku $RT$ (z angl. parse table). Táto tabuľka na základe aktuálne odvodzovaného (expandovaného) netermináleho symbolu $A$ a terminálneho symbolu zo vstupu $t$ špecifikuje, ktoré z pravidiel gramatiky možno aplikovať, ide teda o zobrazenie:
$RT: N\times (T\cup\{$$$\})\to 2^P$.
V prípade, že vstupný symbol neurčuje použitie žiadneho pravidla pre expanziu neterminálu $A$, signalizuje to syntaktickú chybu.
Najprv teda definujme množinu $\mathit{PREDICT}(A\to\alpha)$, ktorá predstavuje množinu všetkých takých terminálnych symbolov (prípadne symbolu $), ktoré pokiaľ sa nachádzajú na vstupe, signalizujú možnosť použiť na expanziu neterminálu $A$ pravidlo $A\to\alpha$. Formálne túto množinu definujeme nasledovne:
$$ \mathit{PREDICT}(A\to\alpha)=\left\{\begin{array}{ll} \mathit{FIRST}(\alpha), & \text{ak } \varepsilon \notin \mathit{FIRST}(\alpha),\\ (\mathit{FIRST}(\alpha)-\{\varepsilon\})\cup \mathit{FOLLOW}(A), & \text{ak } \varepsilon \in \mathit{FIRST}(\alpha). \end{array}\color{transparent}\right\} $$
Rozkladovú tabuľku potom možno konštruovať pomocou nasledujúceho pravidla:
Ak platí, že rozkladová tabuľka danej gramatiky jazyka neobsahuje konflikty, teda pre jeden neterminálny symbol $A$ a jeden terminálny symbol $t$ poskytuje nanajvýš jedno pravidlo, túto gramatiku označujeme ako $LL(1)$ gramatiku. Formálna špecifikácia zobrazenia RT v prípade $LL(1)$ gramatík má preto tvar:
$RT: N\times (T\cup\{$$$\}) \rightharpoonup P.$
Na základe takejto gramatiky možno zostaviť deterministický (prediktívny) syntaktický analyzátor, ktorého princíp fungovania predstavíme v ďalšom kroku.
Príklad
Daná je gramatika $G = (\{$E,F,T$\}$,$\{$+,*,(,),cislo$\}$,$P$,E$)$, kde $P$ je množina nasledujúcich pravidiel:
| ${}$ |
|---|
E $\to$ E + T | T |
T $\to$ T * F | F |
F $\to$ cislo | ( E ). |
Riešenie
Aby sme odpovedali na uvedenú otázku, potrebujeme zostrojiť $RT$ pre definovanú gramatiku $G$. V prvom kroku to vyžaduje konštrukciu množín $\mathit{FIRST}(A)$ pre každý neterminálny symbol $A\in N$. Ako sme už uviedli tieto množiny sa zhodujú s množinami $\mathit{FTS}_A$.
Pred samotnou konštrukciou týchto množín využitím Algoritmu 1 však najprv musíme určiť množinu neterminálnych symbolov $N_\varepsilon$, ktoré možu generovať prázdny reťazec (pozri Algoritmus 3 v predchádzajúcom cvičení). V prípade danej gramatiky $G$ platí, že $N_\varepsilon=\emptyset$, keďže táto gramatika neobsahuje ani jedno $\varepsilon$-pravidlo.
Následne inicializujeme všetky pomocné premenné $\mathit{FST_A}$ prázdnou množinou a pristúpime k vykonaniu logického cyklu Algoritmu 1, ktorého jednotlivé kroky zachytáva nasledujúca Tabuľka. Doplníme, že pre prehľadnosť v tabuľke uvádzame priebežné hodnoty množín $\mathit{FST}_A$ iba v tých iteráciách, v ktorých dochádza k ich zmene.
| Iterácia stredného cyklu pre pravidlo $A\to\beta_1\beta_2\ldots\beta_k$ | Iterácia vnútorného cyklu pre $\beta_i$ | $\mathit{FST}_\texttt{E}\phantom{halo}$ | $\mathit{FST}_\texttt{T}\phantom{halo}$ | $\mathit{FST}_\texttt{F}\phantom{halo}$ |
|---|---|---|---|---|
1. E$\to$ E+T |
1. E |
|||
2. E$\to$ T |
1. T |
|||
3. T$\to$ T*F |
1. T |
|||
4. T$\to$ F |
1. F |
|||
5. F$\to$ num |
1. num |
$\{$num$\}$ |
||
6. F$\to$ (E) |
1. ( |
$\{$num,($\}$ |
||
1. E$\to$ E+T |
1. E |
|||
2. E$\to$ T |
1. T |
|||
3. T$\to$ T*F |
1. T |
|||
4. T$\to$ F |
1. F |
$\{$ num,($\}$ |
||
5. F$\to$ num |
1. num |
|||
6. F$\to$ (E) |
1. ( |
|||
1. E$\to$ E+T |
1. E |
|||
2. E$\to$ T |
1. T |
$\{$num,($\}$ |
||
3. T$\to$ T*F |
1. T |
|||
4. T$\to$ F |
1. F |
|||
5. F$\to$ num |
1. num |
|||
6. F$\to$ (E) |
1. ( |
Keďže po tretej iterácii vonkajšieho logického cyklu došlo k zmene obsahu množiny $\mathit{FST}_\texttt{E}$, mali by sme pristúpiť k ďalšej iteráciu tohto cyklu. V rámci nej však nedôjde k zneme obsahu žiadnej z množín $\mathit{FST}_\texttt{E}$, $\mathit{FST}_\texttt{T}$ a $\mathit{FST}_\texttt{F}$, z tohto dôvodu ju neuvádzame.
Na základe vyššie uvedeného výpočtu je zrejmé, že:
- $\mathit{FIRST}($
E$)=\{$cislo,($\}$, - $\mathit{FIRST}($
T$)=\{$cislo,($\}$ a - $\mathit{FIRST}($
F$)=\{$cislo,($\}$.
Následne pristúpime ku konštrukcii množín $\mathit{FOLLOW}(A)$ pre každý neterminál $A\in N$. Najprv inicializujeme všetky pomocné premenné $\mathit{FLW_A}$ pre každý neterminál $A\in N$ práznou množinou a do množiny $\mathit{FLW_E}$ pridáme symbol $, keďže neterminálny symbol E je počiatočným neterminálom. Následne pristúpime k vykonaniu logického cyklu, a to nasledovne.
| Iterácia vonkajšieho logického cyklu | Iterácia stredného cyklu | $A$ | Iterácia vnútorného cyklu | Pravidlo $B\to \alpha A\textcolor{red}{\beta}\phantom{\varepsilon}$ | $\mathit{FIRST}(\textcolor{red}{\beta})$ | $\mathit{FLW_E}\phantom{\varepsilon}$ | $\mathit{FLW_T}\phantom{\varepsilon\varepsilon}$ | $\mathit{FLW_F}\phantom{\varepsilon\varepsilon}$ |
|---|---|---|---|---|---|---|---|---|
| 1. | 1. | E |
1. | E$\to$ E + T |
$\{$+$\}$ |
$\{$$,+$\}$ |
$\emptyset$ | $\emptyset$ |
| ${}$ | ${}$ | ${}$ | 2. | F$\to$ ( E ) |
$\{$)$\}$ |
$\{$$,+,)$\}$ |
${}$ | ${}$ |
| ${}$ | 2. | T |
1. | E$\to$ E + T$\textcolor{red}{\varepsilon}$ |
$\{\varepsilon\}$ | ${}$ | $\{$$,+,)$\}$ |
${}$ |
| ${}$ | ${}$ | ${}$ | 2. | E$\to$ T$\textcolor{red}{\varepsilon}$ |
$\{\varepsilon\}$ | ${}$ | $\{$$,+,)$\}$ |
${}$ |
| ${}$ | ${}$ | ${}$ | 3. | T$\to$ T * F |
$\{$*$\}$ |
${}$ | $\{$$,+,),*$\}$ |
${}$ |
| ${}$ | 3. | F |
1. | T$\to$ T * F$\textcolor{red}{\varepsilon}$ |
$\{\varepsilon\}$ | ${}$ | ${}$ | $\{$$,+,),*$\}$ |
| ${}$ | ${}$ | ${}$ | 2. | T$\to$ F$\textcolor{red}{\varepsilon}$ |
$\{\varepsilon\}$ | ${}$ | ${}$ | $\{$$,+,),*$\}$ |
Keďže po prvej iterácii vonkajšieho logického cyklu došlo k zmene obsahu všetkých troch pomocných množín $\mathit{FLW_E}, \mathit{FLW_T} a \mathit{FLW_F}$, mali by sme pristúpiť k druhej iterácii tohto cyklu. V rámci nej však už nedôjde k žiadnej zmene obsahu týchto pomocných množín a z tohto dôvodu ju neuvádzame.
Na základe vyššie uvedeného výpočtu je zrejmé, že:
- $\mathit{FOLLOW}($
E$)=\{$$,+,)$\}$, - $\mathit{FOLLOW}($
T$)=\{$$,+,),*$\}$ a - $\mathit{FOLLOW}($
F$)=\{$$,+,),*$\}$.
Následne pristúpime ku konštrukcii množín $\mathit{PREDICT}(A\to\alpha)$ pre každé pravidlo $A\to\alpha\in P$.
- $\mathit{PREDICT}($
E$\to$E + T$)$
$=\mathit{FIRST}($
E + T$)=\{$cislo,($\}$, keďže $\mathit{FIRST}($E + T$)$ neobsahuje $\varepsilon$.
- $\mathit{PREDICT}($
E$\to$T$)$
$=\mathit{FIRST}($
T$)=\{$cislo,($\}$, keďže $\mathit{FIRST}($T$)$ neobsahuje $\varepsilon$.
- $\mathit{PREDICT}($
T$\to$T * F$)$
$=\mathit{FIRST}($
T * F$)=\{$cislo,($\}$, keďže $\mathit{FIRST}($T * F$)$ neobsahuje $\varepsilon$.
- $\mathit{PREDICT}($
T$\to$F$)$
$=\mathit{FIRST}($
F$)=\{$cislo,($\}$, keďže $\mathit{FIRST}($F$)$ neobsahuje $\varepsilon$.
- $\mathit{PREDICT}($
cislo$)$
$=\mathit{FIRST}($
cislo$)=\{$cislo$\}$, keďže $\mathit{FIRST}($cislo$)$ neobsahuje $\varepsilon$.
- $\mathit{PREDICT}($
( E )$)$
$=\mathit{FIRST}($
( E )$)=\{$($\}$, keďže $\mathit{FIRST}($( E )$)$ neobsahuje $\varepsilon$.
Na základe vyššie uvedeného teraz možno pristúpiť k zostrojeniu rozkladovej nasledujúcej tabuľky.
| $RT$ | + |
* |
( |
) |
cislo |
$ |
|---|---|---|---|---|---|---|
E |
${}$ | ${}$ | $\textcolor{red}{1,2}$ | ${}$ | $\textcolor{red}{1,2}$ | |
T |
${}$ | ${}$ | $\textcolor{red}{3,4}$ | ${}$ | $\textcolor{red}{3,4}$ | |
F |
${}$ | ${}$ | $\textcolor{green}{6}$ | ${}$ | $\textcolor{green}{5}$ |
Keďže rozkladová tabuľa obsahuje konflikty (zvýraznené červenou farbou), daná gramatika nie je $LL(1)$ gramatikou.
Konflikty v rozkladovej tabuľke pre gramatiku z vyššie uvedeného príkladu sú spôsobené ľavo rekurzívnymi prepisovacími pravidlami. Gramatika s ľavo rekurzívnymi neterminálnymi symbolmi preto nikdy nebude $LL(1)$ gramatikou.
Ďalším zdrojom nedeterminizmu v gramatike je prítomnosť viacerých pravidiel pre neterminálny symbol $A,$ ktorých pravé strany začínajú rovnakou vetnou formou $\alpha$. Keďže princípy eliminácie oboch týchto problémov sme vysvetlili a precvičili na predchádzajúcom cvičení, v nasledujúcom príklade sa už len presvedčíme, že odstránením týchto zdrojov nedeterminizmu možno gramatiku determinizovať.
Príklad
Overte, či transformovaná gramatika jazyka aritmetických výrazov $G^T=(\{$E,E'',T,T'',F$\}$,$\{$+,*,(,)$\}$,$P^T$,E$)$, kde $P^T$ je množina nasledujúcich pravidiel:
| ${}$ |
|---|
E $\to$ T E'' |
E'' $\to$ + T E''$\;\;$|$\;\;\varepsilon$ |
T $\to$ F T'' |
T'' $\to$ * F T''$\;\;$|$\;\;\varepsilon$ |
F $\to$ cislo | ( E ) |
je $LL(1)$ gramatikou.
Riešenie
Zostavme množiny $\mathit{FIRST}(A)$ a $\mathit{FOLLOW}(A)$, pre všetky neterminály $A \in N^T$ a množiny $\mathit{PREDICT}(A\to\alpha)$ pre všetky pravidlá $A\to\alpha \in P^T$.
| ${}$ | ${}$ | ${}$ |
|---|---|---|
$\mathit{FIRST}($E$)=\{$cislo,($\}$ |
${}$ | $\mathit{FOLLOW}($E$)=\{$$,)$\}$ |
$\mathit{FIRST}($E''$)=\{$+,$\varepsilon\}$ |
${}$ | $\mathit{FOLLOW}($E''$)=\{$$, )$\}$ |
$\mathit{FIRST}($T$)=\{$cislo,($\}$ |
${}$ | $\mathit{FOLLOW}($T$)=\{$$,),+$\}$ |
$\mathit{FIRST}($T''$)=\{$*,$\varepsilon\}$ |
${}$ | $\mathit{FOLLOW}($T''$)=\{$$,),+$\}$ |
$\mathit{FIRST}($F$)=\{$cislo,($\}$ |
${}$ | $\mathit{FOLLOW}($F$)=\{$$,),*,+$\}$ |
| ${}$ |
|---|
$\mathit{PREDICT}($E$\to$ T E''$)=\{$cislo,($\}$ |
$\mathit{PREDICT}($E''$\to$ + T E''$)=\{$+$\}$ |
$\mathit{PREDICT}($E''$\to \varepsilon)=\{$$,)$\}$ |
$\mathit{PREDICT}($T$\to$ F T''$)=\{$cislo,($\}$ |
$\mathit{PREDICT}($T''$\to$ * F T''$)=\{$*$\}$ |
$\mathit{PREDICT}($T''$\to \varepsilon)=\{$$,),+$\}$ |
$\mathit{PREDICT}($F$\to$ cislo$)=\{$cislo$\}$ |
$\mathit{PREDICT}($F$\to$ ( E )$)=\{$($\}$ |
Následne možno zostaviť rozkladovú tabuľku.
| $RT$ | + |
* |
( |
) |
cislo |
$ |
|---|---|---|---|---|---|---|
E |
${}$ | ${}$ | $\textcolor{green}{1}$ | ${}$ | $\textcolor{green}{1}$ | ${}$ |
E'' |
$\textcolor{green}{2}$ | ${}$ | ${}$ | $\textcolor{green}{3}$ | ${}$ | $\textcolor{green}{3}$ |
T |
${}$ | ${}$ | $\textcolor{green}{4}$ | ${}$ | $\textcolor{green}{4}$ | ${}$ |
T'' |
$\textcolor{green}{6}$ | $\textcolor{green}{5}$ | ${}$ | $\textcolor{green}{6}$ | ${}$ | $\textcolor{green}{6}$ |
F |
${}$ | ${}$ | $\textcolor{green}{8}$ | ${}$ | $\textcolor{green}{7}$ | ${}$ |
Gramatika $G^T$ je $LL(1)$ gramatikou, to znamená, že na základe danej gramatiky možno zostaviť jednoduchý prediktívny nerekurzívny syntaktický analyzátor.
Úloha 2.1
Daná je nasledujúca gramatika:
| ${}$ |
|---|
$(1)$ S $\to$ A |
$(2)$ A $\to$ xAx |
$(3)$ A $\to$ C |
$(4)$ B $\to$ yBy |
$(5)$ B $\to$ C |
$(6)$ C $\to$ zBz |
$(7)$ C $\to$ wAw |
$(8)$ C $\to$ $\varepsilon$ |
a) Určte množiny $N$, $T$.
b) Skonštruujte deriváciu reťazca xzzx začínajúc v S. Vyznačte, ktoré odvodzovacie pravidlá ste použili a nakreslite odvodzovací strom podľa Vašej derivácie.
c) Nájdite množiny $\mathit{FIRST}$ a $\mathit{FOLLOW}$ pre každý neterminálny symbol gramatiky.
d) Pre každé pravidlo gramatiky skonštruujte množinu $\mathit{PREDICT}$.
e) Zostavte rozkladovú tabuľku pre danú gramatiku. Zistite, či daná gramatika je $LL(1)$ gramatikou. Svoju odpoveď odôvodnite.
f) Ukážte kroky, ktoré by váš analyzátor realizoval na analýzu vstupu xzyyzx.
Riešenie
a) $T = \{$x, y, z, w$\}$, $N=\{$ S, A, B, C$\}$.
b)
| ${}$ | ${}$ |
|---|---|
$\phantom{\overset{(2)}{\Rightarrow}}$S |
$\overset{(1)}{\Rightarrow}$ A |
| ${}$ | $\overset{(2)}{\Rightarrow}$ xAx |
| ${}$ | $\overset{(3)}{\Rightarrow}$ xCx |
| ${}$ | $\overset{(6)}{\Rightarrow}$ xzBzx |
| ${}$ | $\overset{(5)}{\Rightarrow}$ xzCzx |
| ${}$ | $\overset{(8)}{\Rightarrow}$ xzzx |
Strom odvodenia vyplýva priamo z derivácie.
xzzx
c)
| ${}$ | ${}$ | ${}$ |
|---|---|---|
$\mathit{FIRST}($S$) = \{$ x, z, w $\varepsilon\}$ |
${}$ | $\mathit{FOLLOW}($S$) = \{$$$\}$ |
$\mathit{FIRST}($A$) = \{$ x, z, w, $\varepsilon\}$ |
${}$ | $\mathit{FOLLOW}($A$) = \{$$, x, w $\}$ |
$\mathit{FIRST}($B$) = \{$ y, z, w, $\varepsilon\}$ |
${}$ | $\mathit{FOLLOW}($B$) = \{$ y, z $\}$ |
$\mathit{FIRST}($C$) = \{$ z, w, $\varepsilon\}$ |
${}$ | $\mathit{FOLLOW}($C$) = \{$$, x, y, z, w$\}$ |
d)
| ${}$ |
|---|
$\mathit{PREDICT}(1) = \{$ x, z, w, $$\}$ |
$\mathit{PREDICT}(2) = \{$ x $\}$ |
$\mathit{PREDICT}(3) = \{$ z, w, x, $$\}$ |
$\mathit{PREDICT}(4) = \{$ y $\}$ |
$\mathit{PREDICT}(5) = \{$ z, w, y $\}$ |
$\mathit{PREDICT}(6) = \{$ z $\}$ |
$\mathit{PREDICT}(7) = \{$ w $\}$ |
$\mathit{PREDICT}(8) = \{$$ , x, y, z, w $\}$ |
e)
| $RT$ | x |
y |
z |
w |
$ |
|---|---|---|---|---|---|
S |
$\textcolor{green}{1}$ | $\textcolor{green}{1}$ | $\textcolor{green}{1}$ | $\textcolor{green}{1}$ | |
A |
$\textcolor{red}{2, 3}$ | $\textcolor{green}{3}$ | $\textcolor{green}{3}$ | $\textcolor{green}{3}$ | |
B |
$\textcolor{red}{4, 5}$ | $\textcolor{green}{5}$ | $\textcolor{green}{5}$ | ||
C |
$\textcolor{green}{8}$ | $\textcolor{green}{8}$ | $\textcolor{red}{6, 8}$ | $\textcolor{red}{7, 8}$ | $\textcolor{green}{8}$ |
Uvedená gramatika nie je $LL(1)$ gramatikou, pretože v rozkladovej tabuľke sú konflikty.
f) Analyzátor by nemohol analyzovať daný reťazec, pretože v tabuľke analýzy sú konflikty.
Úloha 2.2
Daná je nasledujúca gramatika:
| ${}$ |
|---|
$(1)$ S $\to$ AB |
$(2)$ A $\to$ xA |
$(3)$ A $\to$ B |
$(4)$ B $\to$ yzB |
$(5)$ B $\to$ z |
a) Určte množiny $N$, $T$.
b) Skonštruujte strom odvodenia pre vstupný reťazec xyzzz
c) Nájdite množiny $\mathit{FIRST}$ a $\mathit{FOLLOW}$ pre každý neterminálny symbol gramatiky.
d) Pre každé pravidlo gramatiky skonštruujte množinu $\mathit{PREDICT}$.
e) Zostavte rozkladovú tabuľku pre danú gramatiku. Zistite, či daná gramatika je $LL(1)$ gramatikou. Svoju odpoveď odôvodnite.
f) Ak do danej gramatiky pridáme pravidlo A $\to$ $\varepsilon$, zmení sa jej klasifikácia? Svoju odpoveď odôvodnite.
Riešenie
a) $T = \{$ x, y, z, $\}$, $N=\{$ S, A, B $\}$.
b)
xyzzz
c)
| ${}$ | ${}$ | ${}$ |
|---|---|---|
$\mathit{FIRST}($S$) = \{$ x, y, z $\}$ |
${}$ | $ \mathit{FOLLOW}($S$) = \{$$$\}$ |
$\mathit{FIRST}($A$) = \{$ x, y, z $\}$ |
${}$ | $ \mathit{FOLLOW}($A$) = \{$ y, z $\}$ |
$\mathit{FIRST}($B$) = \{$ y, z $\}$ |
${}$ | $ \mathit{FOLLOW}($B$) = \{$ y, z, $$\}$ |
d)
| ${}$ |
|---|
$\mathit{PREDICT}(1) = \{$ x, y, z $\}$ |
$\mathit{PREDICT}(2) = \{$ x $\}$ |
$\mathit{PREDICT}(3) = \{$ y, z $\}$ |
$\mathit{PREDICT}(4) = \{$ y $\}$ |
$\mathit{PREDICT}(5) = \{$ z $\}$ |
e)
| $RT$ | x |
y |
z |
$ |
|---|---|---|---|---|
S |
$\textcolor{green}{1}$ | $\textcolor{green}{1}$ | $\textcolor{green}{1}$ | |
A |
$\textcolor{green}{2}$ | $\textcolor{green}{3}$ | $\textcolor{green}{3}$ | |
B |
$\textcolor{green}{4}$ | $\textcolor{green}{5}$ |
Uvedená gramatika je $LL(1)$ gramatikou, pretože v rozkladovej tabuľke nie sú konflikty.
f) Označme nové pravidlo $A \rightarrow \varepsilon$ poradovým číslom $(6)$. Množiny $\mathit{FIRST}$ a $\mathit{FOLLOW}$ sa zmenia nasledovne:
| ${}$ | ${}$ | ${}$ |
|---|---|---|
$\mathit{FIRST}($S$) = \{$ x, y, z $\}$ |
${}$ | $ \mathit{FOLLOW}($S$) = \{$$$\}$ |
$\mathit{FIRST}($A$) = \{$ x, y, z, $\varepsilon \}$ |
${}$ | $ \mathit{FOLLOW}($A$) = \{$ y, z $\}$ |
$\mathit{FIRST}($B$) = \{$ y, z $\}$ |
${}$ | $ \mathit{FOLLOW}($B$) = \{$ y, z, $$\}$ |
Pozn. Žiadna z množín $\mathit{FOLLOW}$ sa nezmenila.
Množiny $\mathit{PREDICT}$
| ${}$ |
|---|
$\mathit{PREDICT}(1) = \{$ x, y, z $\}$ |
$\mathit{PREDICT}(2) = \{$ x $\}$ |
$\mathit{PREDICT}(3) = \{$ y, z $\}$ |
$\mathit{PREDICT}(4) = \{$ y $\}$ |
$\mathit{PREDICT}(5) = \{$ z $\}$ |
$\mathit{PREDICT}(6) = \{$ y, z $\}$ |
Pozn. Množina $\mathit{PREDICT}(6)$ je rovnaká ako $\mathit{FOLLOW}(A)$.
Ak by sme zostavili rozkladovú tabuľku pre túto novú gramatiku, dostaneme:
| $RT$ | x |
y |
z |
$ |
|---|---|---|---|---|
S |
$\textcolor{green}{1}$ | $\textcolor{green}{1}$ | $\textcolor{green}{1}$ | |
A |
$\textcolor{green}{2}$ | $\textcolor{red}{3,6}$ | $\textcolor{red}{3,6}$ | |
B |
$\textcolor{green}{4}$ | $\textcolor{green}{5}$ |
Gramatika teda nie je $LL(1)$ gramatikou.
Krok 3
Teoretický model nerekurzívneho sytaktického analyzátora jazyka generovaného $LL(1)$ gramatikou je zásobníkový automat, ktorý namiesto rekuzívneho volania procedúr (pozri cvičenie 6.) používa zásobník pre ukladanie terminálnych a neterminálnych symbolov, resp. jednotlivých vetných foriem odvodenia. Začiatočný symbol pritom predstavuje počiatočný zásobníkový symbol nachádzajúci sa na dne jeho zásobníka.
Keďže väčšina bezkontextových gramatík obsahuje viacero pravidiel pre jeden neterminálny symbol, efektívne spracovanie vstupu vyžaduje, aby syntaktický analyzátor jednoznačne vybral správny variant pravidla a zabránil tak nedeterministickému vetveniu výpočtu. Z tohto dôvodu sa syntaktický analyzátor na báze zásobníkového automatu často implementuje so schopnosťou nahliadať na niekoľko nasledujúcich symbolov vstupného reťazca, tzv. "look-ahead" symbolov, bez posunu čítacej hlavy. Je pritom zrejmé, že pre gramatiky typu $\mathit{LL(1)}$ postačuje jeden takýto symbol. Tieto symboly následne umožňujú predikciu vhodného pravidla pomocou $RT$, pričom výsledkom činnosti analyzátora je odvodenie, konkrétne postupnosť pravidiel použitých pri najľavejšom odvodení vstupného reťazca. Z tohto dôvodu sa tento prístup označuje ako prediktívna syntaktická analýza. Schéma nerekurzívneho prediktívneho syntaktického analyzátora je znázornená na nasledujúcom obrázku
Samotný princíp nerekuzívnej prediktívnej syntaktickej analýzy definujeme prostredníctvom nasledujúceho algoritmu.
Algoritmus 4. Algoritmus nerekurzívnej prediktívnej syntaktickej analýzy.
Vstup: Bezkontextová gramatika $G=(N,T,P,S)$, k nej zostrojená rozkladová tabuľka syntaktického analyzátora $LL(1)$, vstupný reťazec (slovo) $w$.
Výstup: Ak $w\in L(G)$ výstupom je najľavejšie odvodenie reťazca $w$, v opačnom prípade je výstupom informácia o chybe.
- $\mathit{PUSH}(S)$;
- $t\leftarrow$ prvý symbol $w$;
- while zásobnik nie je prázdny do
- $X\leftarrow \mathit{POP}()$;
- if $X\in N$ then
- if $RT[X,t]=X\to\alpha$ then
- $\mathit{PUSH}(\alpha^R)$;
- write $X\to\alpha$;
- else
- write $\mathit{SYNTAX\ ERROR}$;
- halt;
- end if
- else
- if $X\in T\;\land\;X=t$ then
- $t\leftarrow$ nasledujúci symbol $w$;
- else
- write $\mathit{SYNTAX\ ERROR}$;
- halt;
- end if
- end if
- end while
Algoritmus 4 rozlišuje iba úspešné spracovanie vstupného reťazca a výskyt syntaktickej chyby. Pre praktické použitie syntaktického analyzátora je však žiaduce poskytovať detailnejšiu informáciu o povahe chyby, najmä o očakávaných a skutočne získaných symboloch zo vstupu.
Úloha 3.1
Zamyslite sa nad implementáciou mechanizmu hlásenia chýb do Algoritmu 4, ktorý by poskytoval informácie o očakávaných a skutočne načítaných vstupných symboloch.
Príklad
Vykonajte nerekurzívnu syntaktickú analýzu jazykového výrazu 3 * ( 2 + 1 ) na báze gramatiky $G^T$ konštruovanej v predchádzajúcom príklade.
Riešenie
| Iterácia | Vstup | Zásobník $\phantom{aaaaa}$ | Výstup |
|---|---|---|---|
| 1. | 3 * ( 2 + 1 ) |
E |
$(1)$ |
| 2. | 3 * ( 2 + 1 ) |
T E'' |
$(4)$ |
| 3. | 3 * ( 2 + 1 ) |
F T'' E'' |
$(7)$ |
| 4. | 3 * ( 2 + 1 ) |
3 T'' E'' |
|
| 5. | * ( 2 + 1 ) |
T'' E'' |
$(5)$ |
| 6. | * ( 2 + 1 ) |
* F T'' E'' |
|
| 7. | ( 2 + 1 ) |
F T'' E'' |
$(8)$ |
| 8. | ( 2 + 1 ) |
( E ) T'' E'' |
|
| 9. | 2 + 1 ) |
E ) T'' E'' |
$(1)$ |
| 10. | 2 + 1 ) |
T E'' ) T'' E'' |
$(4)$ |
| 11. | 2 + 1 ) |
F T'' E'' ) T'' E'' |
$(7)$ |
| 12. | 2 + 1 ) |
2 T'' E'' ) T'' E'' |
|
| 13. | + 1 ) |
T'' E'' ) T'' E'' |
$(6)$ |
| 14. | + 1 ) |
E'' ) T'' E'' |
$(2)$ |
| 15. | + 1 ) |
+ T E'' ) T'' E'' |
|
| 16. | 1 ) |
T E'' ) T'' E'' |
$(4)$ |
| 17. | 1 ) |
F T'' E'' ) T'' E'' |
$(7)$ |
| 18. | 1 ) |
1 T'' E'' ) T'' E'' |
|
| 19. | ) |
T'' E'' ) T'' E'' |
$(6)$ |
| 20. | ) |
E'' ) T'' E'' |
$(3)$ |
| 21. | ) |
) T'' E'' |
|
| 22. | $\varepsilon$ | T'' E'' |
$(6)$ |
| 23. | $\varepsilon$ | E'' |
$(3)$ |
| 24. | $\varepsilon$ | $\varepsilon$ |
Príklad
Nakreslite diagram ZA z predchádzajúceho príkladu. Z koľkých stavov a koľkých prechodov bude pozostávať?
Riešenie
Keďže všetky výpočty výsledného ZA sú realizované výlučne na báze zásobníka tento automat obsahuje len len jeden jediný stav s počtom prechodov 13.
Úloha 3.2
Analyticky určte počet prechodov jednostavového ZA vykonavajuceho akceptáciu ľubovoľného jazyka definovaného $LL(1)$ gramatikou prázdnym zásobníkom.
Krok 4
V tomto kroku pristúpime ku konštrukcii nerekurzívneho syntaktického analyzátora jazyka aritmetických výrazov, ktorý je definovaný transformovanou $LL(1)$ gramatikou $G^T$, prezentovanou v predchádzajúcom kroku. Samotný analyzátor bude realizovaný na báze zásobníkového automatu, ktorého zásobník bude v priebehu analýzy slúžiť na uchovávanie terminálnych a neterminálnych symbolov gramatiky. Z tohto dôvodu je nevyhnutné, aby boli tieto symboly reprezentované jednoznačným a programovo spracovateľným spôsobom. Keďže terminálne symboly možno reprezentovať prostredníctvom enumeračného typu TokenType, pre zabezpečenie analogickej reprezentácie neterminálnych symbolov zavedieme vlastný enumeračný typ Nonterminal.
# Definícia enumeračného typu neterminálov
class Nonterminal(Enum):
E = auto()
E_PRIME = auto()
T = auto()
T_PRIME = auto()
F = auto()
Inicializácia syntaktického analyzátora: Do internej premennej syntaktického analyzátora sa uloží referencia na zvolený lexikálny analyzátor.
class Parser:
def __init__(self, scanner):
self.scanner = scanner # Lexikálny analyzátor na získavanie tokenov
self.current_token = None # Aktuálne rozpoznaný token zo vstupu
self.stack = None # Zásobník
Súčasťou inicializácie je taktiež definícia gramatiky a rozkladovej tabuľky syntaktického analyzátora. Obe sú uložené v dátovej štruktúre typu slovník, ktorá predstavuje kolekciu dvojíc kľúč–hodnota. V prípade gramatiky je kľúčom číselný identifikátor pravidla a hodnotou jeho ďalej štrukturovaná reprezentácia. Rozkladová tabuľka využíva ako kľúč kombináciu neterminálneho a vstupného symbolu a ako hodnotu číslo pravidla, ktoré je treba aplikovať. Takto navrhnutá reprezentácia umožňuje efektívne a jednoznačné vyhľadávanie pravidiel počas analýzy vstupného reťazca.
self.rules = {
1: {"left_side": Nonterminal.E, "right_side": [Nonterminal.T,Nonterminal.E_PRIME]}, # E -> T E'
2: {"left_side": Nonterminal.E_PRIME, "right_side": [TokenType.PLUS,Nonterminal.T,Nonterminal.E_PRIME]}, # E' -> + T E'
3: {"left_side": Nonterminal.E_PRIME, "right_side": []}, # E' -> ε
4: {"left_side": Nonterminal.T, "right_side": [Nonterminal.F,Nonterminal.T_PRIME]}, # T -> F T'
5: {"left_side": Nonterminal.T_PRIME, "right_side": [TokenType.MULTIPLY,Nonterminal.F,Nonterminal.T_PRIME]}, # T' -> * F T'
6: {"left_side": Nonterminal.T_PRIME, "right_side": []}, # T' -> ε
7: {"left_side": Nonterminal.F, "right_side": [TokenType.NUM_CONST]}, # F -> cislo
8: {"left_side": Nonterminal.F, "right_side": [TokenType.LPAREN,Nonterminal.E,TokenType.RPAREN]}, # F -> ( E )
}
self.parsing_table = {}
# Riadky rozkladovej tabuľky
# Riadok E
self.parsing_table[(Nonterminal.E, TokenType.LPAREN)] = 1
self.parsing_table[(Nonterminal.E, TokenType.NUM_CONST)] = 1
# Riadok E'
self.parsing_table[(Nonterminal.E_PRIME, TokenType.PLUS)] = 2
self.parsing_table[(Nonterminal.E_PRIME, TokenType.RPAREN)] = 3
self.parsing_table[(Nonterminal.E_PRIME, TokenType.EOF)] = 3
# Riadok T
self.parsing_table[(Nonterminal.T, TokenType.LPAREN)] = 4
self.parsing_table[(Nonterminal.T, TokenType.NUM_CONST)] = 4
# Riadok T'
self.parsing_table[(Nonterminal.T_PRIME, TokenType.MULTIPLY)] = 5
self.parsing_table[(Nonterminal.T_PRIME, TokenType.PLUS)] = 6
self.parsing_table[(Nonterminal.T_PRIME, TokenType.RPAREN)] = 6
self.parsing_table[(Nonterminal.T_PRIME, TokenType.EOF)] = 6
# Riadok F
self.parsing_table[(Nonterminal.F, TokenType.NUM_CONST)] = 7
self.parsing_table[(Nonterminal.F, TokenType.LPAREN)] = 8
Metóda parse: Na začiatku analýzy sa inicializuje zásobník, na vrchol zásobníka sa vloží počiatočný neterminálny symbol E a načíta sa prvý token zo vstupného reťazca.
def parse(self):
self.stack=[TokenType.EOF]
self.stack.append(Nonterminal.E)
self.current_token = self.scanner.get_next_token()
Hlavný cyklus analýzy: Postupne spracováva symboly z vrchola zásobníka. Ak je na vrchole neterminálny symbol, použije rozkladovú tabuľku na vyhľadanie príslušného pravidla, ktorým neterminál expanduje (vloží do zásobníka symboly pravej strany tohto pravidla v opačnom poradí). Ak je na vrchole terminál, porovná sa s aktuálnym tokenom zo vstupu; ak sa zhodujú, parser načíta ďalší token. V prípade nezrovnalosti ohlási syntaktickú chybu. Proces pokračuje, kým nie je obsah zásobníka úplne spracovaný.
while self.stack:
# Vrchol zásobníka
X = self.stack.pop()
# Ak je X neterminálny symbol
if isinstance(X, Nonterminal):
# Ak existuje pravidlo v rozkladovej tabuľke pre kombináciu (neterminál, token)
if (X, self.current_token.type) in self.parsing_table:
# Získame číslo pravidla a pravú stranu pravidla
rule_number = self.parsing_table[(X, self.current_token.type)]
right_side = self.rules[rule_number]["right_side"]
# Postupné vloženie symbolov pravej strany pravidla do zásobníka v opačnom poradí
for symbol in reversed(right_side):
self.stack.append(symbol)
print(f"({rule_number})",end='=>' if len(self.stack)>1 else '\n')
else:
# Syntaktická chyba vo vstupnom reťazci
raise SyntaxError("Syntaktická chyba")
# Ak je X terminál alebo koncový znak a zhoduje sa s aktuálnym tokenom zo vstupu
elif isinstance(X,TokenType) and X==self.current_token.type:
# Posun na ďalší token
self.current_token = self.scanner.get_next_token()
else:
# Syntaktická chyba vo vstupnom reťazci
raise SyntaxError("Syntaktická chyba")
Úloha 4.1
Upravte implementáciu syntaktického analyzátora tak, aby na výstupe negeneroval iba postupnosť aplikovaných pravidiel, ale aj úplné postupné odvodenie vstupného slova.
Úloha 4.2
Doplňte implementáciu syntaktického analyzátora o vami navrhnutý mechanizmus hlásenia chýb.
Úloha 4.3
Doplňte zdrojový kód syntaktického analyzátora tak, aby počas parsovania výrazu konštruoval taktiež odvodzovací strom výrazu, ktorého grafická reprezentácia sa zobrazí po úspešnom ukončení syntaktickej analýzy. Pre vykreslenie stromu môžete v jazyku Python použiť napríklad externú knižnicu PrettyPrintTree.
Doplňujúce materialy
Pomocný nastroj pre náplň tohto cvičenia nájdete na tejto stránke prístupnej len z TUKE siete.
Dotazník na ohodnotenie učiva
Vážení študenti,
týmto by sme Vás chceli požiadať o poskytnutie spätnej väzby k formátu a obsahu materiálov na cvičenia vo forme vyplnenie nasledujúceho dotazníka .
Tento dotazník slúži na ohodnotenie vašich skúseností s pripravenými materiálmi z ôsmeho cvičenia. Na vylepšovaní materiálov na základe vašej spätnej väzby aktívne pracujeme, a to s cieľom zvýšiť kvalitu, zrozumiteľnosť a prínosnosť výučby.