Tästä ensimmäisestä LL(1)-ehdosta (14) seuraa erityisesti, että korkeintaan yksi välikkeen A säännöistä voi tuottaa tyhjän merkkijonon ε eli tehdä välikkeestä A tyhjentyvän (eli nollautuvan). Toinen osa LL(1)-ehtoa koskee vain tyhjentyviä välikkeitä: Olkoon välike A N tyhjentyvä ja sen säännöistä viimeinen eli A α k se ainoa, joka voi tuottaa tyhjän merkkijonon ε. Silloin ehto vaatii, että follow(a) first(α j ) = (15) kaikilla muilla sen säännöillä 1 j < k. Nimittäin jos olisi jokin follow(a) first(α j ) x niin kumpaa säännöistä j vaiko k pitäisi käyttää tällä x? Kielioppi G on yleisessä LL(1)-muodossa, jos sen kaikki välikkeet ja säännöt täyttävät molemmat ehdot (14) ja (15). LL(1)-kielioppi ei voi olla moniselitteinen. LL(1)-kielioppi ei voi sisältää vasenta rekursiota. Kun kielioppi G on tätä yleistä LL(1)-muotoa, niin sille voidaan laatia rekursiivisesti etenevä jäsentäjä seuraavin periaattein: Pidetään yllä muuttujassa next seuraavaa syötemerkkiä. error(...) tarkoittaa lopeta koko rekursiivinen jäsennys virheilmoitukseen... Käytännön ohjelmoinnissa se voisi vaikkapa nostaa poikkeuksen (exception). Tehdään tässä esimerkissä sellainen jäsentäjä, joka palauttaa arvonaan vastaavan jäsennyspuun. Tätä kusutaan ennustavaksi (englanniksi predictive ) jäsentämiseksi, koska jäsentäjä osaa ennustaa oikein, mitä produktiota seuraavaksi pitää soveltaa, lukematta syötettä enempää kuin nextin verran eteenpäin. Jokaiselle päätesymbolille a Σ kirjoitetaan oma aliohjelma: a: 1 if next = a 2 then next lue seuraava syötemerkki 3 return uusi lapseton solmu nimeltään a 4 else error(tässä kohdasta olisi pitänyt olla a) Jokaiselle välikkeelle A N kirjoitetaan oma aliohjelma. Jos A ei ole tyhjentyvä, niin tämä aliohjelma on: 144
A: 1 if next first(α 1 ) then haara(α 1 ) 2 elseif next first(α 2 ) then haara(α 2 ) 3 elseif next first(α 3 ) then haara(α 3 ). elseif next first(α k ) then haara(α k ) else error(tästä kohdasta olisi pitänyt alkaa A) Huomaa, että nämä first-joukot ovat vakioita, jäsentäjä ei siis laske niitä. Niiden arvothan on jo laskettu LL(1)-ehtoa (14) testattaessa. Jokainen haara(x 1 X 2 X 3...X m ) on oma ohjelmanpätkänsä 1 y 1 X 1 2 y 2 X 2 3 y 3 X 3. y m X m return uusi solmu nimeltään A lapsinaan y 1, y 2, y 3,...,y m joka siis 1. ensin kutsuu rekursiivisesti muita jäsentäjän aliohjelmia X 1, X 2, X 3,...,X m oikeassa järjestyksessä 2. sitten palauttaa tuloksenaan jäsennyspuun, jonka juurena on nykyinen välike A ja sen lapsina näiden kutsujen palauttamat puut. (Tai jos jäsentimen halutaan tekevän jotakin muuta kuin jäsennyspuun, niin sitten tekee mitä halutaan pohjautuen siihen, mitä rekursiokutsut ovat ensin tehneet ja palauttaneet.) Jos välike A on tyhjentyvä niin vain sen viimeinen sääntö A α k tuottaa tyhjän merkkijonon ε. Silloin sen aliohjelma päättyykin. elseif next first(α k 1 ) then haara(α k 1 ) else haara(α k ) eli tämä tyhjentyvä viimeinen haara siirtyykin elseen errorin tilalle. Toisin sanoen, jos nextin mukaan kyseessä ei ollut mikään tyhjentymättömistä haaroista A α 1 α 2 α 3... α k 1 niin sitten ainoa mahdollisuus on tyhjentyvä haara A α k. Koko jäsentäjän pääohjelmaksi tulee 145
1 next lue syötteen ensimmäinen merkki 2 τ S eli kutsutaan lähtösymbolia vastaavaa aliohjelmaa 3 if next = EOF 4 then return näin rakennettu koko syötteen jäsennyspuu τ 5 else error(syötteen olisi pitänyt loppua tähän kohtaan) Usein halutaan sellainen jäsennysohjelma, joka ei pysähdy heti ensimmäiseen erroriin, vaan jatkaa eteenpäin, ja raportoi muitakin syötteessä olevia virheitä. Silloin kirjoitetaan kunkin tyhjentymättömän välikkeen A aliohjelman päättävän errorin tilalle tulosta(tästä kohdasta olisi pitänyt alkaa A); while next follow(a) do next lue seuraava syötemerkki; return uusi lapseton virhesolmu nimeltään A joka siis selaa ohi tämän virheellisen A, ja jatkaa jäsennystä sitä seuraavasta merkistä. Jokaiselle välikkeelle A N määritellään first(a) = first(α 1 ) first(α 2 ) first(α 3 )... first(α k ) (16) eli sen first-joukko koostuu kaikista sen sääntöjen oikeiden puolten α i firstjoukoista. Tällaisen oikean puolen α V first-joukko lasketaan puolestaan seuraavasti: Jos α = ε, niin first(α) = {ε. Jos α on muotoa b... jollakin päätemerkillä b Σ, niin first(α) = {b. Jos α on muotoa Bβ, jossa välike B ei ole tyhjentyvä, niin first(α) = first(b) joka taas lasketaan kuten yhtälössä (16). Jos α on muotoa Bβ, jossa välike B on tyhjentyvä, niin eli edetään eteenpäin jonossa α. first(α) = first(b) \ {ε first(β) Kaiken vasemman rekursion poisto takaa, ettei tämä ole kehämääritelmä. Välikkeiden follow-joukot voidaan puolestaan laskea toistamalla seuraavia sääntöjä, kunnes mikään joukko ei enää kasva: Lisää EOF lähtösymbolin S joukkoon follow(s). 146
Jos kieliopissa on jokin sääntö muotoa A αbβ, niin lisää joukkoon follow(b) kaikki joukon first(β) päätesymbolit. (Eli kaikki muut sen alkiot, mutta ei mahdollista tyhjää merkkijonoa ε). Jos kieliopissa on jokin sääntö muotoa A αbβ jossa ε first(β) niin lisää joukkoon follow(b) kaikki joukon follow(a) alkiot. Esimerkki 62. Esimerkin 59 tekijöidyssä kieliopissa tarvitaan LL(1)-jäsentäjää varten seuraavat joukot: first(t) = {a, ( first(e ) = {+,, ε first(e) = first(t) follow(e ) = follow(e) = {EOF, ). Näiden perusteella voidaan kirjoittaa jäsentäjä edellä kuvattuun tapaan. Lyhennetään koodia kirjoittamalla yksi yhteinen aliohjelma kaikille päätemerkeille b { +,, (, ),a: Terminaali(b): 1 if next = b 2 then next lue seuraava syötemerkki 3 return uusi lapseton solmu nimeltään b 4 else error(tässä kohdassa olisi pitänyt olla b) Pääohjelmaksi tulee: 1 next lue ensimmäinen syötemerkki 2 τ E 3 if next = EOF 4 then return τ 5 else error(syötteen olisi pitänyt loppua tähän kohtaan) Välikkeen E aliohjelmaksi tulee: E: 1 if next { (, a then y 1 T y 2 E return uusi solmu nimeltään E ja lapsinaan y 1, y 2 2 else error(tästä kohdasta olisi pitänyt alkaa E) Välikkeen E aliohjelmaksi tulee: 147
E : 1 if next { + then y 1 Terminaali( + ) y 2 E return uusi solmu nimeltään E ja lapsinaan y 1, y 2 2 elseif next { then y 1 Terminaali( ) y 2 E return uusi solmu nimeltään E ja lapsinaan y 1, y 2 3 else return uusi lapseton solmu nimeltään E (Tässä siis on haara säännölle E ε.) Välikkeen T aliohjelmaksi tulee: T: 1 if next {a then y 1 Terminaali(a) return uusi solmu nimeltään T ja lapsenaan y 1 2 elseif next { ( then y 1 Terminaali( ( ) y 2 E y 3 Terminaali( ) ) return uusi solmu nimeltään T ja lapsinaan y 1, y 2, y 3 3 else error(tästä kohdasta olisi pitänyt alkaa T) Tätä systemaattisesti kirjoitettua jäsennintä voi selvästi vielä parannella paikallisin muutoksin: esimerkiksi aliohjelman T rivillä 2 tarkastetaan kahdesti, että next on (. Tehdään siis parempi C-pseudokoodilla. void E() { tulosta(e TE ) T(); E (); void E () { if (next == + ) { tulosta(e +E) E(); else if (next == - ) { tulosta(e -E) E(); else tulosta(e ε) 148
void T() { if (next == a ) { tulosta(t a) else if (next == ( ) { tulosta(t (E)) E(); if (next ) ) error(sulkeva sulku puuttuu); else error(t ei voi alkaa merkillä next); Pääohjelma käynnistää ja päättää jäsennyksen: E(); if (next EOF) error(tässä piti olla EOF). Katsotaan esimerkki 63 sen toiminnasta. Sitten korvataan sen tulosteet yksinkertaisella koodingeneroinnilla. Esimerkki 63. Syötejonon a-(a+a) jäsennys tulostaa: E TE T a E -E E TE T (E) E TE T a E +E E TE T a E ε E ε Tulostus vastaa vasenta johtoa: E TE ae a E a TE a (E)E a (TE )E a (ae )E a (a + E)E a (a + TE )E a (a + ae )E a (a + a)e a (a + a). Oikeassa ohjelmassa tulosta-komennot voivat tehdä jotain hyödyllisempää (kuten laskea lausekkeen arvoa, generoida koodia,...). 149
// Lelukääntäjä: tuottaa konekoodia edellisen kieliopin // mukaisten lausekkeiden arvon laskemiseksi; tulos rekisteriin // r1... EI ole testattu, vastuu lukijalla: void Ep() { if(next == + ) { T(); printf("pop r1\npop r2\nadd r1, r2\npush r1\n"); Ep(); else if(next == - ) { T(); printf("pop r2\npop r1\nsub r1, r2\npush r1\n"); Ep(); void T() { if(numero_tai_muuttuja(next)) { printf("push % else if(next == ( ) { T(); Ep(); if(next!= ) ) printf("virhe: piti olla loppusulku\n"); else printf("virhe: T ei voi alkaa merkillä % int main() { T(); Ep(); printf("pop r1\n"); return 0; Edellisessä koodissa välike E on oleellisesti poistettu, ja se on korvattu sääntöjen oikealla puolella suoraan johdolla TE. Kielioppi generoi edelleen saman kielen: S TE E +TE TE ε T a (TE ) Lähtömuuttujasymboli S vastaa siis pääohjelmaa (main). 150
Konekielikäskymme: push x laita x pinoon pop x poista pinon päällimmäinen, Tulos ja laita x:ään add r1,r2r1 r1 + r2 sub r1,r2r1 r1 r2 on siis lopuksi rekisterissä r1. Generoitu konekieli ei tosin ole kovin tehokasta... Tätä ei kysytä tentissä! Se on esimerkkinä oikeasta jäsentämisestä ja kääntämisestä tosin ilman sellaisia käytännön kysymyksiä kuin jäsennysvirheiden käsittely, jne. Lelu-ohjelmamme tulostus syötteellä (x + y) (a + b) : push x push y pop r1 pop r2 add r1, r2 push r1 push a push b pop r1 pop r2 add r1, r2 push r1 pop r2 pop r1 sub r1, r2 push r1 pop r1 Peruuttavasta jäsentämisestä Voimme ryhtyä ohjelmoimaan tämän kaltaista rekursiivisesti etenevää jäsentäjää myös sellaiselle kieliopille G joka ei olekaan LL(1). Silloin tehdäänkin peruuttava (englanniksi backtracking ) jäsentäjä ennustavan sijaan. 1. Jäsentäjä arvaa (ennustamisen sijaan) mikä voisi olla seuraava produktio. 2. Jos jäsentäjä joutuu myöhemmin umpikujaan, eli huomaa arvanneensa väärin, niin se peruuttaa rekursiossaan viimeisimmän arvauksensa,... 3....ja arvaakin sen sijaan jonkin muun produktion. Intuitiivisesti, otamme aiemman kuvan 21 generoi-ja-testaa -algoritmin, ja toteutamme sen epädeterminismin tällä peruuttavalla etsinnällä. Tämän menetelmän hankaluuksia ovat edestakaisin vaeltelu syötemerkkijonossa: Jäsennin kulkee eteenpäin arvattuaan produktion jota se kokeilee seuraavaksi, ja taaksepäin peruuttaessaan vääräksi osoittautuneen arvauksensa. tehottomuus jos kieliopissa on paljon kokeiltavia vaihtoehtoja: Jäsennin joutuu kokeilemaan ne kaikki rekursiivisesti. pysähtyminen jos kielioppiin on jäänyt vasenta rekursiota: Jäsennin voi juuttua arvailemaan loputtomiin liikkumatta syötemerkkijonossa. Tällaisten peruuttavien etsintämenetelmien ohjelmointi yksinkertaistuu huomattavasti, jos otetaan käyttöön laiskat listat. 151
Laiskaa listaa ylläpidetään keskeneräisenä : Kun siltä kysytään Mikä on seuraava alkiosi? niin se laskee seuraavan alkionsa vasta silloin ja vain sen seuraavan alkionsa, eikä vielä muita. Peruuttavassa jäsennyksessä välikettä A vastaava jäsennysfunktio ottaa parametrinaan syötemerkkijonosta sen loppuosan u, joka on yhä jäsentämättä antaa tuloksenaan laiskan listan päätemerkkijonoja w, jossa w on se loppuosa merkkijonosta u, joka jää jäljelle kun sen alkuosasta jäsennetään tämä välike A. Siis u = vw jossa A v. Tuloslista koostuu kaikista tällaisista w, eli...... kaikista eri vaihtoehdoista jatkaa jäsennystä, kun ensin on jäsennetty tämä A. Silloin välikkeen A säännöistä A α 1... α k muodostetaan aliohjelma A(u) koodinaan 1 return Laiska(α 1, u)... Laiska(α k, u) jossa operaatio X Y yhdistää kaksi laiskaa listaa Y ja Y yhdeksi laiskaksi listaksi: 1 if lista X osoittautuu tyhjäksi 2 then return Y 3 else Z listan X ensimmäinen alkio; 4 L listan X loput alkiot; 5 return lista jonka ensimmäinen alkio on Z ja loput Y L. Laiskuuden ideana on laskea listaa X vain sen verran, että if-lausessa tiedetään kumpi haaroista then vaiko else pitää valita. Rivillä 5 kuljetaan listoja X ja Y vuorotahtiin; silloin jäsennys antaa reilun tilaisuuden jokaiselle eri kokeiltavalle vaihtoehdolle. Nämä Laiskat haarat voidaan puolestaan määritellä rekursiolla sääntöjen oikeiden puolten α i rakenteen suhteen: Laiska(ε, u) = return se laiska lista, jonka ainoa alkio on u itse koska tyhjän merkkijonon jäsentäminen ei kuluta yhtään syötemerkkiä. Päätemerkillä b Σ on Laiska(bβ, u) ehto 1 if merkkijono u on muotoa bw 2 then return Laiska(β, w) 3 else return tyhjä laiska lista koska tyhjä tuloslista tarkoittaa että ei jäsenny mitenkään. Välikkeellä B saa Laiska(Bβ,u) muodon 1 return Laiska(β, w 1 ) Laiska(β, w 2 ) Laiska(β, w 3 )... jossa w 1, w 2, w 3,... on laiskan listan B(u) sisältö 152
koska se tarkoittaa että jatketaan jäsentämällä β jokaisesta sellaisesta merkkijonosta w j joka jää jäljelle kun merkkijonon u alusta on jäsennetty välike B. Pääohjelmaksi alkusymbolille S tulee 1 return löytyykö ε laiskasta listasta S(koko syöte)? koska se tarkoittaa että voiko koko syötteen alusta jäsentää välikkeen S niin, ettei mitään jää jäljelle? Jos halutaan tämän kyllä/ei-vastauksen sijasta tuottaa jäsennyspuut, niin laajennetaan jokaisessa jäsennysfunktiossa A(u) jokainen tuloslistan alkio pelkästä merkkijonosta w pariksi (τ, w) jossa τ on sellainen jäsennyspuu, jonka juuri on nykyinen välike A ja tuotos on se merkkijono v jolla u = vw ja A v. Näin saa laiskan listan koko syötteen kaikista jäsennyspuista. Tällaista laiskoilla listoilla toteutettua peruttavaa jäsennintä voi onneksi tehostaa, jos kielioppisäännöt ovat sopivia. Esimerkiksi jos välikkeen säännöistä A α 1... α k tiedetään, että ne ovatkin muotoa joko α 1 tai α 2 tai α 3 tai...tai α k jos ne esimerkiksi täyttävät LL(1)- ehdot, vaikka koko kielioppi ei täytäkään niin silloin vastaavaksi aliohjelmaksi A(u) voidaankin ottaa 1 return Laiska(α 1, u)... Laiska(α k, u) jossa X Y sanookin että käytä listaa Y vain jos lista X osoittautuukin tyhjäksi : 1 if lista X osoittautuu tyhjäksi 2 then return Y 3 else return X. 5.6.2 LR-kieliopeista Simuloidaankin merkkijonon oikeaa johtoa rekursiivisesti. Saadaan LR(1)-kieliopit ja -kielet: Left to right scan, producing Right parse with 1 symbol lookahead. Yleisemmin, LR(k)-kielissä seuraavat k merkkiä määrittävät seuraavan johtoaskeleen. LR(0) = ns. yksinkertainen LR (Simple LR, SLR). LR(1) = deterministiset kielet, joten tasot k > 1 ovat enää teoreettisesti kiinnostavia. LR-jäsennys sisältää LL-jäsennyksen sillä lim LL(k) = LR(1). k Intutiivisesti, odotamme jäsentäessämme mahdollisimman pitkään emmekä heti kokeile sääntöä, eli teemmekin oikean emmekä vasenta johtoa. 153