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ä.
Tiedäthän, että eräs tapa esittää joukko on luetella sen alkiot aaltosulkeissa. Esimerkiksi neljää pienempien luonnollisten lukujen joukko on {0, 1, 2, 3}.
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} | |
ε | |
{ε} |
Ohjelmointikielten merkkijonovakioissa ei voida esittää kaikkia merkkejä sellaisinaan. Siksi käytetään epäsuoria keinoja. C++-kielessä merkkijono esitetään lainausmerkkien " välissä. Monet merkit voidaan kirjoittaa merkkijonoon sellaisinaan. Sellaiset merkit esittävät itseään. Lainausmerkkiä ei voi käyttää esittämään itseään, koska sille on annettu eri tehtävä: se lopettaa merkkijonon. Siksi on otettu käyttöön toinen keino: merkkijonoon sisältyvä lainausmerkki esitetään \".
Merkki \ ei esitä itseään ilmauksessa \", vaan kertoo, että " ei lopetakaan merkkijonoa vaan tähän kohtaan tulee lainausmerkki. Merkkijonoon saadaan \ kirjoittamalla \\.
Kieli, jota ollaan määrittelemässä, on kohdekieli. Keinot, joilla kohdekieli määritellään, ovat metakieli. C++:n merkkijonojen tapauksessa "he\"i!" on metakielen ilmaus, joka tuottaa kohdekielen merkkijonon he"i!.
Metakielessä ja kohdekielessä on tavallisesti paljon samoja merkkejä. Esimerkiksi pienet kirjaimet kuuluvat usein molempiin. Metakielessä voi olla myös merkkejä, joita kohdekielessä ei ole. Edellä ε oli tällainen. Kohdekielessä voi olla merkkejä, joita metakielessä ei ole. Esimerkiksi näppäimistössäni ei ole merkkiä ε, mutta silti sain sen esiintymään tällä veppisivulla.
Valitse jokaiselle vasemman reunan olennolle, tuottaako se kohdekielen merkin (vasen ruutu), tuottaako se kohdekielen merkkijonon (seuraava ruutu), onko se metakielen merkki (seuraava ruutu) ja onko se metakielen merkkijono (oikea ruutu) olettaen, että kohdekielen aakkostona on kirjaimet.
k | |
{k} | |
koira | |
{koira} | |
ε | |
{ε} |
Ilmaus ”tuottaa kohdekielen merkkijonon” on kankea. Sitä käytetään kun halutaan korostaa kohdekielen ja metakielen eroa, mutta eipä juuri muulloin. Tavallisesti sanotaan ”on merkkijono”. Emmehän myöskään sano ”42 tuottaa parillisen luvun”, vaan ”42 on parillinen”. Siksi äskeinen oikea vastaus ”ε tuottaa kohdekielen merkkijonon” tarkoittaa samaa kuin aikaisemman kohdan oikea vastaus ”ε on merkkijono”.
Jos käytetään sanaa ”merkki” tai ”merkkijono” kertomatta, tarkoitetaanko kohde- vai metakielen merkkiä tai merkkijonoa, niin tavallisesti tarkoitetaan kohdekielen.
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ä. Koska S ei kuulu aakkostoon, on (S) metakielen mutta ei kohdekielen merkkijono. Määritelmästä voidaan tuottaa kohdekielen merkkijono aloittamalla metakielen merkkijonosta S ja korvaamalla nykyisestä metakielen 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ä, mikä 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. Jos et saa ratkaistua jotain riviä, niin lue pitemmälle, ratkaise rivi sieltä löytyvällä työkalulla, ja palaa takaisin tähän.
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ä kohdekielen 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 | 3 | 6
M ::= x | y | z
Tuleeko merkkijonoista 1+2+3, (1+2)+3 ja 1+(2+3) yksi, kaksi vai kolme erilaista jäsennyspuuta kielessä L? Mieti ensin itse ja sitten kokeile yllä olevalla vastausruudulla.
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 ja vain jos se tuottaa ainakin yhdelle merkkijonolle ainakin 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.
Ennen kuin voimme esittää ohjelmointikielten kaltaisia esimerkkejä, on kerrottava eräs asia. Arvaa kumpi oheisista kuvista on tähän mennessä esitetyn perusteella oikea jäsennyspuu merkkijonolle
const int loppu = alku+42;oheisessa kieliopissa?
D ::= "T M = L;"
T ::= K | "T K"
K ::= int | const
M ::= alku | loppu
L ::= V | M | L+V | L+M
V ::= 42
Monet ohjelmointikielet ratkaisevat tämän ongelman jakamalla syntaksin kahteen tasoon: leksikaaliset säännöt ja varsinainen syntaksi.
Leksikaaliset säännöt määräävät miten niin sanotut tekstialkiot kootaan yksittäisistä merkeistä. Tyypillisten ohjelmointikielten tekstialkioita ovat muun muassa vakioarvot kuten 123 ja "merkkijono\n", nimet kuten x, avainsanat kuten while sekä muut itsenäisen merkityksen omaavat merkkiyhdistelmät kuten <, =, <=, + ja ++.
Leksikaaliset säännöt määräävät myös mikä on niin sanottua valkoista tilaa, missä sitä saa olla, ja missä sitä pitää olla. Tyypillisesti sitä ovat ainakin välilyönnit, rivinsiirrot ja kommentit. Tyypillisesti sitä ei saa olla tekstialkioiden sisällä ja saa olla miten paljon tahansa tekstialkioiden välissä (ja koko ohjelman alussa ja lopussa). Tyypillisesti sitä täytyy olla esimerkiksi kirjaimista koostuvien tekstialkioiden välissä.
Varsinainen syntaksi määrää miten isommat rakenteet kootaan tekstialkioista. Siinä käytetään tekstialkioita ikään kuin ne olisivat määritelmän merkkejä. Sen esittämisessä käytetään valkoista tilaa ihmiselle luontevalla tavalla, eikä esitystapa ota kantaa siihen, miten sitä kaiken kaikkiaan saa ja pitää käyttää. Esimerkiksi muuttujan määrittelyn syntaksiksi saatetaan antaa seuraava:
tyyppi nimi = alkuarvo;
Vaikka siinä on välilyönnit =-merkin ympärillä, ne eivät ole C++:ssa välttämättömät. Vaikka siinä ei ole välilyöntiä ;-merkin edessä, sellaisen saa C++:ssa kirjoittaa. Vaikka siinä kaikki on samalla rivillä, sen saa jakaa monelle riville.
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.
S ::= while E do L E ::= MVN L ::= M := N | M := M-N M ::= x | y N ::= 0 | 1 | 2 V ::= = | >
MathCheckin BNF-moodiin ei ole rakennettu erillistä leksikaalista tasoa. Siksi välilyönnit ovat sille merkkejä kuten muutkin merkit, ja niitä on oltava täsmälleen kulloisenkin BNF-määritelmän mukaisesti. Jäsennyspuiden saamiseksi helpommin luettavaksi MathCheckiin on kuitenkin rakennettu kikka, jolla monta peräkkäistä merkkiä voi liimata yhdeksi tekstialkioksi.
Tämä kielioppi syötettiin MathCheckille näin. Merkkiyhdistelmien while, do ja := ympärillä on $ osoittamassa, että kukin niistä tulee tulkita yhdeksi tekstialkioksi.
S ::= "$while$ E $do$ L"
E ::= MVN
L ::= "M $:=$ N" | "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ä.
Kenties 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ä leksikaaliseen tasoon ja varsinaisen syntaksin tasoon, kuten edellä kerrottiin.
Seuraavaksi suunnittelemme kieliopin muuttujan x polynomeille. Matematiikassa esimerkiksi −2x3 + 5 + x − 3x2 on polynomi. Polynomi P koostuu yhdestä tai useammasta termistä T, jotka on yhdistetty toisiinsa operaattorilla + tai -. Lisäksi koko polynomin edessä voi olla -. Potenssilasku esitetään operaattorilla ^. Monimutkaisimmillaan termissä on luku, x, ^ ja luonnollinen luku, mutta alussa oleva luku voi puuttua ja loppuosuus alkaen ^-merkistä voi puuttua. Lisäksi termi voi olla pelkkä luku. Luonnollisia lukuja N ovat (vain) 0, 1 ja 2, ja muita lukuja ovat (vain) e ja pi. Luonnolliset ja muut luvut muodostavat yhdessä kielen L. Jätä välilyönnit kokonaan pois.
Jäljellä olisi vaikka kuinka hienoja BNF-tehtäviä, mutta ehkä toisen kerran!