TIEA241 Automaatit ja kieliopit, kevät 2011 (IV) Antti-Juhani Kaijanaho TIETOTEKNIIKAN LAITOS 29. huhtikuuta 2011
Sisällys
Chomskyn hierarkia kieli säännöllinen kontekstiton kontekstinen rekursiivisesti lueteltava automaatti äärellinen pino lineaarirajoitettu Turingin kone
Jäsennysongelma Olkoon G = (N, Σ, P, S) kontekstiton kielioppi ja w Σ merkkijono. Jäsennysongelmassa tehtävänä on selvittää kaikki w:n jäsennyspuut. Tunnistusongelmassa tehtävänä on selvittää, päteekö w L(G).
Moniselitteisyys Kontekstiton kielioppi on yksiselitteinen, jos kaikilla merkkijonoilla on enintään yksi jäsennyspuu. Muuten se on moniselitteinen (engl. ambiguous).
Sisällys
Jay Earley: An Efficient Context-Free Parsing Algorithm. Communications of the ACM, 13 (2), pp. 94 102, 1970. Ideana rakentaa jäsennyspuu juuresta alkaen (ns. top down) käyden systemaattisesti rinnakkain läpi kaikki kyseeseen tulevat vaihtoehdot.
Earleyn tunnistin 1 Syötteet G = (N, Σ, P, S) on CFG, jolle Ŝ N Σ w = c 0 c n 1 Σ Tulos Tosi, jos w L(G), ja epätosi muuten. Muuttujat S 0,..., S n P {(Ŝ, S)} N N Merkintä (A ω ω, f ) = ((A, ωω ), ω, f ) 1. Alustetaan S 0 = {(Ŝ S, 0)} ja S i =, i = 1,..., n. 2. Kaikilla i = 0,..., n (tässä järjestyksessä) ja jokaisella s S i tehdään seuraavaa: 2.1 Jos s on muotoa A ω Bω, f : Jokaiselle produktiolle B ω B P lisää S i :hin B ω B, i. 2.2 Jos s on muotoa A ω, f : Jokaiselle (B ω B Aω B, f ) S f, lisää S i :hin B ω B A ω B, f. 2.3 Jos s on muotoa A ω cω, f ja c = c i : Lisää S i+1 :een A ωc ω, f. 3. Palauta, päteekö (Ŝ S, 0) S n. 1 Kalvoa on korjattu; korjaukset on merkitty näin.
Esimerkki taululla E E + E E E (E) c w = c + c c
Earleyn jäsennin Kun askeleessa 2.1 lisätään S i :hin B ω B, i, niin tässä syntyy jokaiselle ω B :n symbolille uusi puiden ulkopuolella oleva lapseton solmu. Kun askeleessa 2.2 joukosta S i löytyy s = A ω, f ja joukosta S f löytyy B ω B Aω B, f ja tämän johdosta joukkoon S i lisätään s = B ω B A ω B, f : Linkitetään s :n :ia ennen olevan A:n lapsisolmuiksi jokainen ω:n symboli. On mahdollista, että A:lla on jo ennestään lapsisolmuja. Tällöin linkitys tehdään niin, että entiset lapsisolmut ja nyt lisättävät lapsisolmut kyetään erottamaan toisistaan. Kyse on tällöin useasta vaihtoehtoisesta jäsennyspuusta. Kun askeleessa 2.3 S i+1 :een lisätään B ωc i ω, f, niin tässä kopioidaan olemassaolevat ω:n, c i :n ja ω :n solmut s:stä, ei luoda uusia.
Esimerkki taululla E E + E E E (E) c w = c + c c
Pro ja contra + Yleinen jäsennysalgoritmi, toimii kaikilla CFG:illä. Aika- ja tilavaativuus on (hyvin koodattuna) O(n 3 ), missä n on syötemerkkijonon pituus. Algoritmi ei ole aivan triviaali koodattava.
Sisällys
engl. recursive descent parsing Tehdään kustakin välikesymbolista aliohjelma, joka kokeilee kutakin produktiota vuorollaan. Päätesymbolin kohdalla katsotaan onko se seuraavana merkkijonossa. Välikesymbolin kohdalla kutsutaan sitä vastaavaa aliohjelmaa. Jos jäsennys ei onnistu, peruutetaan (backtrack) lähimpään tehtyyn valintaan, jossa ei ole vielä kaikki vaihtoehdot käyty läpi.
/* <statement> ::= if ( <expression> ) <statement> if ( <expression> ) <statement> else <statement> return ; return <expression> ; */ public static void parsestatement(lexer l) throws SyntaxException { Lexer.State st = l.savestate(); try { l.getif(); l.getoparen(); parseexpression(l); l.getcparen(); parsestatement(l); if (l.iselse()) { l.getelse(); parsestatement(l); } return; } catch (SyntaxException e) {} l.restorestate(st); try { l.getreturn(); Lexer.State st1 = l.savestate(); try { parseexpression(l); } catch (SyntaxException e) { l.restorestate(st1); } l.getsemicolon(); return; } catch (SyntaxException e) {} l.restorestate(st); throw new SyntaxException(); }
Pro ja contra + Erittäin helppo koodata käsin kieliopista lähtien. Jos kielioppi on vasenrekursiivinen, algoritmi kaatuu. Algoritmi tekee älyttömästi turhaa työtä kokeillessaan kaikkia vaihtoehtoja.
Vasemman rekursion poisto X α 1 X X Xω 1...... X α m X X Xω n = X ω 1 X X α 1...... X ω m X X α m X ω i ja α i eivät ala X:llä
A x A (E) A x A (E) F AF F A F F F A F AF = F F / A F / AF E F E FE E E + F E E F E E + FE E FE
Sama koodina /* <expression> ::= <term> <expressionprime> <expressionprime ::= + <term> <expressionprime> - <term> <expressionprime> */ public static void parseexpression(lexer l) throws SyntaxException { parseterm(l); parseexpressionprime(l); return; } public static void parseexpressionprime(lexer l) throws SyntaxException { Lexer.State st = l.savestate(); try { l.getplus(); parseterm(l); parseexpressionprime(l); return; } catch (SyntaxException e) {} l.restorestate(st); try { l.getminus(); parseterm(l); parseexpressionprime(l); return; } catch (SyntaxException e) {} l.restorestate(st); return; }
Poistetaan häntäkutsuja public static void parseexpression(lexer l) throws SyntaxException { parseterm(l); while (true) { Lexer.State st = l.savestate(); try { l.getplus(); parseterm(l); continue; } catch (SyntaxException e) {} l.restorestate(st); try { l.getminus(); parseterm(l); continue; } catch (SyntaxException e) {} l.restorestate(st); } }
Ennustava jäsennys Usein on mahdollista päättää heti, onko jokin produktio mahdollinen. Esim. E + TE ei tule kyseeseen, jos seuraava token ei ole +. Tällöin kokeile-ja-peruuta on täysin älytön idea. Ideaalitilanne on, jossa peruutusta ei tarvita lainkaan. Tällöin kyse on ennustavasta jäsennyksestä (engl. predictive parsing.
Expression parseexpression(lexer l) throws ParserException { Expression e1 = parseterm(l); while (true) { switch (l.peek()) { case Lexer.PLUS: l.get(); Expression e2 = parseterm(l); e1 = parseexpressionprime(l, new AdditionExpression(e1,e2)); continue; case Lexer.MINUS: l.get(); Expression e2 = parseterm(l); e2 = parseexpressionprime(l, new AdditionExpression(e1,e2)); continue; default: return e1; } } }
Milloin tietty produktio voidaan ennustaa? NULLABLE(X) on tosi, jos X:stä voidaan johtaa tyhjä merkkijono. NULLABLE(X) = tosi NULLABLE(X) = NULLABLE(Y i ) jos on produktio X jos on produktio X Y 1 Y i Y n ja jos NULLABLE(Y 1 ) NULLABLE(Y i 1 ) FIRST(ω) on kaikkien niiden päätesymbolien (ynnä syötteen loppu) joukko, jotka voivat aloittaa ω:n. FIRST(a) = {a} FIRST(X) = FIRST(X) FIRST(Y i ) jos a on päätesymboli jos on produktio X Y 1 Y i Y n ja jos NULLABLE(Y 1 ) NULLABLE(Y i 1 ) FOLLOW(X) on kaikkien niiden päätesymbolien (ynnä syötteen loppu) joukko, jotka voivat seurata Xää. FOLLOW(Y i ) = FOLLOW(Y i ) FIRST(Y j ) jos on produktio X Y 1 Y i Y j Y n ja NULLABLE(Y i+1 ) NULLABLE(Y j 1 ) FOLLOW(Y i ) = FOLLOW(Y i ) FOLLOW(X) jos on produktio X Y 1 Y i Y n ja jos NULLABLE(Y i+1 ) NULLABLE(Y n)
Esimerkki E TE E T x E + TE T (E) E TE NULLABLE FIRST FOLLOW E ei x, ( ), eof E kyllä +, ), eof T ei x, ( +,, ), eof
Ennustava jäsennystaulukko rivi jokaiselle välikesymbolille sarake jokaiselle päätesymbolille (ynnä syötteen loppu) Merkitse produktio X ω riville X ja sarakkeeseen t jokaiselle t FIRST(ω), ja jos NULLABLE(ω), myös jokaiselle t FOLLOW(X).
Esimerkki E TE E T x E + TE T (E) E TE NULLABLE FIRST FOLLOW E ei x, ( ), eof E kyllä +, ), eof T ei x, ( +,, ), eof x + ( ) eof E E TE E TE E E +TE E TE E E T T x T (E)
Taulukon tulkinta Tee jokaiselle välikesymbolille aliohjelma. Aliohjelman alussa tee switch case kaikille päätesymboleille (ynnä syötteen päättymiselle). Jos välikesymbolin X ja päätesymbolin t risteyskohta sisältää yhden produktion, niin koodaa ko. produktio X:n aliohjelmaan t:n caseen. sisältää useamman kuin yhden produktion 2, koodaa produktiot X:n aliohjelmaan t:n caseen ja käytä peruutusta valinnan tekemiseen produktioiden välissä. on tyhjä, niin koodaa X:n aliohjelmaan t:n caseen kielioppivirheen diagnosointi. Jos taulukossa ei ole yhtään konfliktia, jäsennin on ennustava. 2 Tällöin taulukossa on konflikti.
LL(1) left-to-right parse, leftmost derivation, 1-token lookahead. Jos kieliopista johdettu ennustava taulukko on konfliktiton, ko. kielioppi on LL(1). Jos kielioppi ei ole LL(1), kannattaa kokeilla vasemman rekursion poistoa ja vasenta tekijöintiä (engl. left factoring). Monet hyödylliset kieliopit eivät ole LL(1). Moniselitteinen kielioppi ei ole koskaan LL(1). On mahdollista yleistää LL(n):ään, jolloin FIRST ja FOLLOW sisältävät n:n mittaisia sanasjonoja. Tämä laajentaa jäsennettävien kielten joukkoa.
Top down LL(1)-jäsennys etenee ylhäältä alaspäin, sillä se aloittaa välikesymbolista, ennustaa tarvittavan produktion, ja jäsentää ko. produktion mukaan, ja näin rakentaa jäsennyspuun juuresta lehtiin.
Ennustava jäsennys: pro ja contra + Helppo koodata kieliopin perusteella käsin. + Tehokas ei peruutusta. Vaadittu LL(1)-kielioppi on usein varsin vaikeaselkoinen. Kieliopin muuttaminen voi johtaa vaikeaselkoisiin virheisiin, kun FIRST- ja FOLLOW-joukkojen muuttumista ei muisteta ottaa kaikkialla huomioon. Tämän poistaa LL-generaattorin käyttäminen (esim. ANTLR). Käsin kirjoitetun prediktiivisen jäsentimen muokkaaminen voi johtaa epäselvyyteen siitä, mitä kieltä se oikeasti jäsentää. Tämänkin poistaa LL-generaattorin käyttäminen (esim. ANTLR).
Sisällys
Joitakin muita algoritmeja LL(k) LR(1) ja LALR GLR Cocke-Younger Kasami