Harjoitus 2 vastaukset

815338A Ohjelmointikielten periaatteet 2014 - 2015. Harjoitus 2
vastaukset
Harjoituksen aiheena on BNF-merkinnän käyttö ja sen yhteys rekursiivisesti
laskeutuvaan jäsentäjään.
Tehtävä 1. Mitkä ilmaukset seuraava kielioppi määrittelee?
<sana> ::= <X><Y>
<X> ::= a | <X>a
<Y> ::= b | b<Y>
Vastaus. Tutkitaan ensin välisymboleita <X> ja <Y>. Luonnollisesti X:stä voidaan
johtaa a. Kun a on johdettu, rekursiivisen määrittelyn (X:n säännön toinen mahdollisuus)
perusteella voidaan johtaa aa, tämän jälkeen aaa jne. Välisymbolin <X> paikalle voidaan
siis johtaa merkkijono aa…a, missä a-kirjaimia on vähintään yksi kappale. Vastaavalla
tavalla huomataan, että välisymbolin <Y> paikalle voidaan johtaa merkkijono bb...b,
missä b-kirjaimia on vähintään yksi kappale. Koska <sana>-välisymboliksi voidaan
johtaa ilmaus, jossa <X> ja <Y> korvataan millä tahansa kyseisistä välisymboleista
johdettavalla lausekkeella, niin kieliopin <sana> -välisymbolin tilalle voidaan johtaa
muotoa aa…abb..b olevat merkkijonot, joissa a ja b-kirjaimia on kumpaakin ainakin yksi
kappale.
Tehtävä 2. Määritellään kielioppi seuraavasti
<lauseke> ::= <A>a<B>b
<A> ::= b | <A>b
<B> ::= a | a<B>
Mitkä seuraavista lausekkeista kuuluvat em. kielioppiin
a) bbaabb,
b) bbaba,
c) bbbaaab,
d) bbaaabb?
Vastaus. Kuten tehtävässä 1, huomataan, että välisymbolin <A> paikalle voidaan johtaa
merkkijono bb...b, missä b-kirjaimia on vähintään yksi kappale. Välisymbolin <B>
paikalle voidaan johtaa merkkijono aa…a, missä a-kirjaimia on vähintään yksi kappale.
Koska lausekkeessa välisymbolien <A> ja <B> välissä on kirjain a ja lausekkeen lopussa
on kirjain b, niin voidaan todeta että lauseke on muotoa bb...ba..ab, missä alussa bkirjaimia on vähintään yksi ja a-kirjaimia vähintään kaksi. Lauseke loppuu siis aina
kirjaimiin ab. Näin ollen kohtien a), b) ja d) merkkijonoja ei voi johtaa
kielioppisäännöillä. Sen sijaan kohdan c) merkkijono bbbaaab voidaan johtaa, joten se
sisältyy määriteltyyn kieleen.
Tehtävä 3. Määrittele BNF (tai EBNF) -notaatiolla seuraavat kielen vakioita ilmaisevat
välisymbolit (nonterminaalit), joiden määrittely on annettu sanallisesti.



<binääriluku> on yhdestä tai useammasta peräkkäisestä numerosta koostuva
merkkijono, missä kukin numero voi olla 0 tai 1.
<kokonaisluku> on joko etumerkitön tai etumerkillä + tai - varustettu yhdestä tai
useammasta peräkkäisestä numerosta koostuva merkkijono, missä kukin numero
voi olla 0, ..., 9.
<desimaaliluku> on merkkijono, joka koostuu joko
1. kokonaisluvusta,
2. desimaaliosasta tai
3. kokonaisluvusta ja sitä seuraavasta desimaaliosasta.
Desimaaliosa alkaa pisteellä (.), jota seuraa etumerkitön kokonaisluku.
Ohje: Käytä tarvittaessa muita välisymboleja, joille määritellään erikseen sääntö.
Vastaus. Tyypit voidaan määritellä seuraavasti (muutkin tavat mahdollisia):
Binääriluvussa on siis ainakin yksi numero, joka voi olla 0 tai 1. Määritellään binääriselle
numerolle välisymboli, jonka nimeksi valitaan bitti. Jos luvussa on useampia bittejä, niin
loppuosa ensimmäisen bitin jälkeen on aina binääriluku. Näin saadaan rekursiivinen
määritelmä:
<binääriluku> ::= <bitti> | <bitti><binääriluku>
<bitti> ::= 0 | 1
Kokonaisluku voidaan rakentaa alhaaltapäin: ensin numerot, sitten etumerkittömät luvut
ja lopuksi luvut joissa voi olla etumerkki. Esitetään välisymbolien määritelmät ylhäältä
lähtien:
<kokonaisluku> ::= [+|-] <etumerkitön>
<etumerkitön> ::= <numero> | <numero><etumerkitön>
<numero> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Desimaaliluku voidaan esittää ylläolevia määritelmiä käyttämällä ja määrittelemällä
lisäksi desimaaliosa:
<desimaaliluku> ::=
<kokonaisluku>
| <desimaaliosa>
| <kokonaisluku><desimaaliosa>
<desimaaliosa> ::= .<etumerkitön>
Tehtävä 4. C -kielen if-lauseen syntaksi on seuraava:
<if_lause> ::= if(<lauseke>) <lause> [else <lause>]
Näytä, että näin tulee määriteltyä monikäsitteinen kielioppi, kun otetaan huomioon, että
if-lause on lauseen erikoistapaus, joten if-lause voi esiintyä säännön oikealla puolella.
Ohje: Näytä, että if-lauseella
if(ehto1)
if(ehto2)
lause2
else
lause3
on kaksi erilaista johtopuuta, ts. syntaksista ei voi päätellä kumpaan if -lauseeseen else
kuuluu. Tätä sanotaan roikkuvan elsen ongelmaksi (dangling else problem). Miten C kielessä tulkitaan ylläoleva koodi?
Vastaus. Lauseelle
if(ehto1)
if(ehto2)
lause2
else
lause3
saadaan kaksi erilaista johtoa, kun otetaan huomioon, että välisymboli <lause> voidaan
korvata välisymbolilla <if-lause>:
<if_lause> -> if(<lauseke>) <lause>
-> if(<lauseke>) <if_lause>
-> if(ehto1) <if_lause>
-> if(ehto1) if(<lauseke>) <lause> else <lause>
-> if(ehto1) if(ehto2) <lause> else <lause>
-> if(ehto1) if(ehto2) lause2 else <lause>
-> if(ehto1) if(ehto2) lause2 else lause3
ja
<if_lause> -> if(<lauseke>) <lause> else <lause>
-> if(ehto1) <lause> else <lause>
-> if(ehto1) <if_lause> else <lause>
-> if(ehto1) if(<lauseke>) <lause> else <lause>
-> if(ehto1) if(ehto2) <lause> else <lause>
-> if(ehto1) if(ehto2) lause2 else <lause>
-> if(ehto1) if(ehto2) lause2 else lause3
Näillä johdoilla on erilaiset johtopuut (piirrä ne!). Javassa lause tulkitaan niin, että else
viittaa lähimpään if-lauseeseen, ts. esimerkiksi
int x = 0;
if(x == 0)
if( x != 0)
x = 1;
else
x = 2;
sijoittaa muuttujaan x arvon 2 (jäisi nollaksi jos else viittaisi ylempään lauseeseen).
Tehtävä 5. Seuraava kielioppi määrittelee joukon symboleja a, b ja c käyttäviä
yksinkertaisia yhteen- vähennys- ja kertolaskua sisältäviä aritmeettisia lausekkeita.
<lauseke> ::= <termi> | <termi> + <termi>| <termi> - <termi>
<termi> ::= <tekijä> | <tekijä> * <termi>
<tekijä> ::= a | b | c
Kirjoita C-kielellä (tai jollain muulla valitsemallasi kielellä) kieliopille rekursiivisesti
laskeutuva jäsentäjä, jolle jäsennettävä lauseke syötetään komentoriviltä ohjelman alussa
ennen jäsennystä. Ohjelma tulostaa virheilmoituksen, jos lauseke ei ollut syntaksiltaan
kieliopin hyväksymää muotoa ja ilmoituksen "Lauseke oikeaa muotoa", mikäli kielioppi
hyväksyy sen. Virheestä ilmoittaminen on helpointa tehdä kutsumalla virhetilanteen
sattuessa tätä varten kirjoitettua funktiota error(), jolle voi antaa virheilmoituksen
parametrina. Ohjelman voi myös lopettaa funktiossa. Huomaa, että nyt kaikki lekseemit
ovat yhden merkin mittaisia, joten funktio getToken() voidaan kirjoittaa niin, että se
hakee syötteenä saadusta merkkijonosta aina seuraavan merkin.
Vastaus. Luennoilla esitettyyn tapaan ohjelma voidaan kirjoittaa niin, että jokaista
välisymbolia kohti kirjoitetaan oma (rekursiivinen) metodi, joka jäsentää kyseisen
välisymbolin. Lekseemi haetaan globaaliin muuttujaan sym metodissa getToken(). Tässä
tapauksessa lekseemi on aina yhden merkin mittainen, joten funktiosta tulee
yksinkertainen. Kun havaitaan virhe, kutsutaan virhefunktiota error, joka päättää
ohjelman. Tällöin ohjelman runko olisi seuraava:
// Globaali muuttuja lekseemille
char sym;
// Globaali muuttuja: missä kohdassa jäsennettävää merkkijonoa ollaan
int curPos = 0;
// Globaali muuttuja jossa on jäsennettävä merkkijono
char orig_expr[MAX_LEN_EXPR];
// Lekseemin tyyppi eli sananen. Voi olla aluksi 0/1 = loppu/kesken
int token;
// Hakee seuraavan lekseemin
int getToken() {
// Lue seuraava merkki jonosta orig_expr muuttujaan sym
// ja kasvata osoitinta curPos
// Palauta 0 jos jonon lopussa
}
// Virhefunktio tulostaa ilmoituksen ja lopettaa ohjelman.
void error(char *msg) {
printf("Virhe: %s",msg);
exit(1);
}
// Jäsentää tekijän
void tekija(){
token = getToken();
// Jos jono loppu niin virhe
switch(sym) {
// a,b tai c: OK
break;
// Muu -> virhe
}
}
// Jäsentää termin
void termi() {
// Termissä aina tekijä ensimmäisenä
tekija();
// Tekijä OK, jatketaan
token = GetToken();
// Jos jono loppu, OK
switch(sym)
{
// * : termi jatkuu
termi();
// + - OK
break;
// Muu -> virhe
}
}
// Jäsentää lausekkeen
void lauseke() {
// Lausekkeessa aina termi ensimmäisenä.
termi();
// Jos ei loppu niin termi hakenut lekseemin
switch(sym)
{
// + tai – tulee uusi termi
termi();
// Muu
virhe
}
// Tarkistetaan että lauseke loppu, muuten virhe
}
int main(int args, char** argv)
{
// Luetaan jäsennettävä lauseke merkkijonoon orig_expr
printf("Anna jasennettava lauseke > ");
scanf("%s",orig_expr);
// Jäsennetään lauseke
lauseke();
// Jos menee läpi tulostetaan että lauseke oikea
printf("Lauseke OK.");
return 0;
}
Harjoituksen yhteyteen on linkitetty kaksi versiota ohjelmasta, joille kummallekin
syötetään jäsennettävä lauseke komentoriviltä. Lausekkeessa ei voi olla välilyöntejä ja
sen maksimipituus on 99 merkkiä.
Ensimmäinen versio ei luokittele lekseemejä, vaan getToken palauttaa arvon 1 jos
lauseketta ei ole luettu loppuun ja arvon 0, jos ollaan lopussa. Jäsentäminen käyttää
lekseemin sisältävää muuttujaa sym.
Toisessa versiossa lekseemien tyypeille on määritelty lueteltu tyyppi. Tällöin
jäsentämisessä voidaan käyttää haetun lekseemin tyyppiä, jolloin virheen raportointi
saadaan tarkemmaksi.