Tässä tehtävässä tutustutaan BNF:ään eli Backus-Naur-formiin. Se on erittäin laajasti käytetty keino määritellä ohjelmointikielten kieliopillisia rakenteita. Matematiikassa sama asia tunnetaan nimellä yhteysriippumaton kielioppi (englanniksi context-free grammar) eli CFG.
John Backus ja Peter Naur toivat BNF:n ohjelmointikielten suunnitteluun vuoden 1960 molemmin puolin. He eivät kuitenkaan olleet sen keksijöitä. Varhaisin tunnettu olennaisesti saman idean käyttäjä oli Pāṇini, joka käytti sitä sanskriitin kielen rakenteiden esittämiseen yli 2400 vuotta sitten. Siitä, että hän ei saanut nimeään maailmanluokan huippukeksintöönsä, hän saa syyttää vain tulospistetyperyyttään: mitäs jätti tuloksensa julkaisematta virallisesti tulospistesertifioiduissa tiedelehdissä.
Tässä vaiheessa on tärkeää tunnistaa ja erottaa toisistaan neljä käsitettä.
Valitse jokaiselle vasemman reunan olennolle, onko se merkki (vasen ruutu), onko se merkkijono (keskimmäinen ruutu) ja onko se kieli (oikea ruutu) olettaen, että aakkostona on kirjaimet.
k | |
{k} | |
koira | |
{koira} | |
koira, kissa, heppa | |
{koira, kissa, heppa} | |
ε | |
{ε} |
Alla on ensimmäinen esimerkkimme BNF-määritelmästä eli BNF:llä ilmaistusta kieliopista. Se määrittelee täsmäävien sulkujonojen kielen.
S ::= A | SA A ::= ε | (S)
Määritelmä antaa määrittelemänsä kielen nimeksi S. Sen aakkostossa on kaksi merkkiä: ”(” ja ”)”. Määritelmä käyttää apukäsitteenä toista kieltä, jota se kutsuu nimellä A. Kieleen A kuuluvat tyhjä merkkijono sekä kaikki ne merkkijonot, joissa on alussa ”(”, sitten täsmäävä sulkujono ja lopuksi ”)”. Muuta A:han ei kuulu. Kieleen S kuuluvat täsmälleen ne merkkijonot, jotka saadaan liittämällä yhden tai useampia A:han kuuluvia merkkijonoja peräkkäin.
Määritelmä sanoo, että S saadaan korvata A:lla, S saadaan korvata SA:lla, A saadaan korvata tyhjällä merkkijonolla ja A saadaan korvata (S):llä. Määritelmästä voidaan tuottaa merkkijono aloittamalla S:stä ja korvaamalla nykyisestä merkkijonosta mikä tahansa iso kirjain kunnes jäljellä ei enää ole isoja kirjaimia. Esimerkiksi ()(()) voidaan tuottaa seuraavasti:
Yllä oleva voidaan esittää tiiviimmin näin: S → SA → S(S) → A(S) → (S)(S) → (A)(S) → ()(S) → ()(A) → ()((S)) → ()((A)) → ()(()). Jos jäljellä on ainakin kaksi isoa kirjainta, ei ole väliä, kumpi niistä korvataan ensiksi. Esimerkiksi osuudessa SA → S(S) → A(S) korvattiin ensin lopusta A ja sitten alusta S, mutta samaan oltaisiin päädytty korvaamalla ensin alusta S ja sitten lopusta A, eli näin: SA → AA → A(S). Merkkijonon tuottaminen voidaan esittää myös kuvana, kuten yllä tehtiin. Tällaisen kuvan esittämä puumainen rakenne tunnetaan nimellä jäsennyspuu. Jäsennyspuu ei ota kantaa siihen, mikä iso kirjain korvataan ensin.
Seuraava kielioppi muistuttaa paljon sitä, miten monien ohjelmointikielten lausekkeet on määritelty. Siinä on kuitenkin jippo tai pari, jotta kaikki kohdat eivät olisi ratkaistavissa yleistiedolla vaan olisi tarpeen hieman miettiä, mitä se todella määrittelee.
Lauseke ::= Tulo | Lauseke + Tulo | Lauseke − Tulo Tulo ::= Tekijä | Tulo · Tekijä | Tulo / Tekijä Tekijä ::= Atomi | + Atomi | − Tekijä Atomi ::= Luku | Muuttuja | ( Lauseke )
”Luku” on epätyhjä jono numeroita ja ”Muuttuja” on kirjain. Klikkaa ruudut sen mukaan mihin kieliin rivin alun merkkijono kuuluu. Rivin ensimmäinen ruutu vastaa atomia, toinen tekijää, kolmas tuloa ja neljäs lauseketta.
6 | |
( | |
−x | |
+x | |
−−x | |
+−x | |
2·x·y | |
−2·+x·y | |
3x | |
x−x | |
+1++1 | |
(3·x+1) |
MathCheckin BNF-tehtävissä kielen nimenä on aina iso kirjain ja isot kirjaimet tulkitaan aina kielten nimiksi. Siksi isot kirjaimet eivät kuulu aakkostoon, eli niitä ei voi esiintyä lopullisissa merkkijonoissa. Jos valitaan L = Lauseke, T = Tulo, E = Tekijä, A = Atomi, U = Luku ja M = Muuttuja, korvataan · ja − helpommin kirjoitettavilla * ja - sekä laiskotellaan Luvun ja Muuttujan kohdalla, niin äskeinen kielioppi voidaan esittää MathCheckille näin:
L ::= T | L+T | L-T
T ::= E | T*E | T/E
E ::= A | +A | -E
A ::= U | M | (L)
U ::= 0 | 1 | 2
M ::= x | y | z
Tämän esimerkin kieliopeista ylempi on sikäli parempi, että se vastaa matematiikassa ja ohjelmointikielissä tavallisesti noudatettavaa laskujärjestystä, mutta alempi ei vastaa. Siksi yleensä käytetään ylempää. Tästä huolimatta alempikin kielioppi tuottaa saman kielen, sillä se tuottaa samat merkkijonot, eikä matematiikassa kielen käsitteessä millään muulla ole väliä kuin mitkä merkkijonot ovat mukana.
Sama kieli saadaan myös määrittelemällä L ja T seuraavasti.
L ::= T | L+L | L-L
T ::= E | T*T | T/T
Tässä kieliopissa merkkijonolle 1+x-y (ja hyvin monelle muullekin) saadaan kaksi eri jäsennyspuuta, kuten kuvista näkyy. Kielioppia sanotaan moniselitteiseksi (ambiguous), jos se tuottaa ainakin yhdelle merkkijonolle kaksi eri jäsennyspuuta. Jos halutaan, että jäsennyspuut vastaavat merkkijonoille tarkoitettua merkitystä, niin moniselitteisyyttä pitää välttää. Moniselitteisyyden välttäminen on kuitenkin toisinaan liian vaikeaa, joten ohjelmointikielelle tms. voidaan joutua käyttämään moniselitteistä kielioppia.
Alla on määritelty leikkikielen while-silmukka. Siihen sisältyy ehto ja lause. Ehto muodostuu muuttujasta, vertailuoperaattorista ja numerosta. Esimerkin pitämiseksi lyhyenä erilaisia muuttujia jne. on tarjolla vain muutama, ja lause voi olla vain kahta erilaista muotoa oleva sijoituslause. Välilyöntejä on pakko olla täsmälleen kuten niitä on BNF-määritelmässä. Kirjoita kelvollinen silmukka. Jotta vastauksessa voisi olla välilyöntejä, on se laitettava lainausmerkkeihin, mutta vaivasi säästämiseksi ne on annettu valmiina.
S ::= while E do L E ::= MVN L ::= M := M | M := M-N M ::= x | y N ::= 0 | 1 | 2 V ::= = | >
Tämä kielioppi syötettiin MathCheckille näin:
S ::= "while E do L"
E ::= MVN
L ::= "M := M" | "M := M-N"
M ::= x | y
N ::= 0 | 1 | 2
V ::= = | >
Tässä vaiheessa on tarpeen kertoa, miten symbolin ::= oikealla puolella oleva vaihtoehto kirjoitetaan MathCheckille. Jos siinä ei ole välilyöntejä eikä se ala merkillä " eikä ', niin sen voi kirjoittaa sellaisenaan. Jos siinä ei ole merkkejä ", niin sen voi kirjoittaa merkkien " välissä. Jos siinä ei ole merkkejä ', niin sen voi kirjoittaa merkkien ' välissä. Merkeillä " ja/tai ' rajattuja osuuksia voi olla peräkkäin, jolloin MathCheck yhdistää ne ennen käyttöä. Tämä pätee vaikka niiden välissä olisi välilyöntejä tai rivinsiirtoja.
Rivi X ::= ""|NX tulkitaan siten, että …"" esittää tyhjän merkkijonon, | ilmoittaa että yksi vaihtoehto loppuu ja seuraava alkaa, ja NX tarkoittaa kieleen N kuuluvaa merkkijonoa jatkettuna kieleen X kuuluvalla merkkijonolla. Pystyviiva ei ole osa ensimmäistä vaihtoehtoa, koska lainausmerkki lopettaa ensimmäisen vaihtoehdon juuri ennen sitä.
Toivottavasti sait MathCheckiltä esimerkin merkkijonosta, jolla on kaksi jäsennyspuuta. Tämä ilmiö tunnetaan nimellä ”dangling else”. Se ratkaistaan yleensä ilmaisemalla sanallisesti, että kukin else-haara kuuluu siihen lähinnä edeltävään if-lauseeseen, jolla ei jo ole else-haaraa.
Saisimme välilyöntien käytön joustavammaksi määrittelemällä apukielen B ::= " " | " B". Ohjelmointikielten määrittelyssä ei kuitenkaan yleensä tehdä niin, vaan jaetaan määritelmä kahteen tasoon. Niin sanotulla leksikaalisella tasolla määritellään kielen ne rakenneosat, joiden sisällä ei voi olla välilyöntejä lainkaan, kuten avainsanat (esim. while) ja lukuvakiot (esim. 40100). Samalla tasolla määritellään niin sanottu valkoinen tila, joka tyypillisesti voi sisältää ainakin välilyöntejä, rivinsiirtoja ja kommentteja. Niin sanotulla varsinaisen syntaksin tasolla avainsanoja, lukuvakioita jne. käytetään ikään kuin ne olisivat määritelmän merkkejä. Määritelmässä niitä laitetaan peräkkäin ottamatta kantaa valkoisen tilan käyttöön.
Jäljellä olisi vaikka kuinka hienoja BNF-tehtäviä, mutta ehkä toisen kerran!