ALKEIS-kielen semantiikan aksiomaattinen määrittely Antti Jokipii antti.jokipii@republica.fi Antti Vuorenmaa anvuoren@cc.jyu.fi Eero Lempinen eerolem@cc.jyu.fi Pete Räsänen peter@cc.jyu.fi Tiivistelmä Kuvaamme kurssilla esimerkkikielenä kehitetyn ALKEIS-ohjelmointikielen semantiikan aksiomaattisesti sekä käymme läpi kielellä kirjoitettujen ohjelmien oikeellisuustodistuksia. 1 Johdanto Tony Hoaren vuonna 1969 esittelemä tietokoneohjelmien verifiointimenetelmä [1] perustuu kolmikoihin, jotka ovat muotoa {P} S {Q}, missä S edustaa suoritettavaa ohjelmaa tai koodiriviä, {P} joukkoa muuttujien arvoja koskevia väittämiä ennen S:n suoritusta ja {Q} joukkoa väittämiä S:n suorituksen jälkeen. Hoaren logiikalla pyritään todistamaan S:n osittainen oikeellisuus, ts. "Mikäli esiehto (engl. precondition) {P} on totta ennen S:n suoritusta ja S pysähtyy, niin jälkiehto (engl. postcondition) {Q} on totta S:n suorituksen jälkeen." Esi- ja jälkiehtojen sanotaan yhdessä muodostavan ohjelman spesifikaation. Käytännön esimerkkinä kolmikosta voidaan esittää { x = 0 } x := x + 2 { x 1 }, missä esiehto olettaa, että muuttujan x arvo on 0 ennen sijoitusoperaation suorittamista ja jälkiehto olettaa, että x 1 sijoituksen jälkeen. Sittemmin tällaista aksiomaattista lähestymistapaa on sovellettu ohjelmointikielten semantiikan määräämiseen (mm. [2]) osoittamalla kielen jokaiselle syntaktiselle rakenteelle päättelysääntö (engl. inference rule), joka kuvaa ko. rakenteen merkityksen. Tässä artikkelissa määrittelemme kurssilla esimerkkikielenä kehitetyn ja käytetyn ALKEIS-ohjelmointikielen semantiikan aksiomaattisesti ja käymme läpi myös lyhyitä todistuksia, jotka selventänevät päättelysääntöjen ideaa. Seuraavaksi hahmottelemme ALKEISkielen syntaksin karkealla tasolla, jonka jälkeen esittelemme päättelysäännöt erilaisille syntaktisille rakenteille. 2 ALKEIS-kielen tyypit ja syntaksi ALKEIS-kielessä on kaksi tietotyyppiä, kokonaislukujen esittämiseen tarkoitettu int ja liukuluvuille float. Lukualueet ja tarkkuus riippuvat kulloisestakin toteutuksesta. Kielen syntaksin lähtöproduktio on StatementList, joka koostuu yhdestä tai usemmasta Statementista: Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos. 1
StatementList ::= Statement StatementList Statement Statement puolestaan edustaa sijoituslausetta, while-silmukkaa, iflausetta, I/O-operaatiota tai lohkoa: Statement ::= Id := Expr ; while Expr do StatementList od if Expr then StatementList else StatementList fi put Expr ; get Id ; begin Vars StatementList end, missä Id on muuttujan nimi, Expr laskulauseke: Expr ::= Id IntLit FloatLit Expr + Expr Expr - Expr Expr * Expr Expr / Expr Expr mod Expr Expr == Expr Expr <= Expr ( Expr ) ja Vars yhden tai useamman muuttujan esittely: Vars ::= var Id Vars, Id Operaattorit +, -, *, / ja mod vastaavat tavallisia laskutoimituksia toiminnaltaan ja suoritusjärjestykseltään. Mikäli operandit ovat eri tyyppiä, kummatkin muunnetaan floattyyppisiksi ennen operaatiota ja paluuarvo on float-tyyppinen. Vertailuoperaattorit == ja <= palauttavat kokonaisluvun 1, mikäli vertailu on totta, muussa tapauksessa palautetaan 0. Tyyppimuunnokset tehdään samalla tavalla kuin muidenkin operaattoreiden kanssa. 3 Sijoituslause ja aksioomat Sijoitusaksiooma (engl. assignment axiom) määrittää esiehdon annetun sijoitusoperaation ja jälkiehdon perusteella: { P[E/x] } x := E { P }, missä P[E/x] tarkoittaa P:tä, jossa kaikki x:n vapaat esiintymät on korvattu lausekkeen E arvolla. Esimerkiksi seuraava kolmikko on sijoitusaksiooman perusteella tosi: { x + 1 = 2 } x := x + 1 { x = 2 }, sillä esiehto { x + 1 = 2 } saadaan jälkiehdosta { x = 2 } sijoittamalla x:n paikalle lauseke x + 1. Sijoitusaksiooman lisäksi muita aksioomia ovat mm. normaalit lukujen laskusäännöt (kommutatiivisuus, assosiatiivisuus, jne). Niitä ei kuitenkaan tässä käydä. 4 Seuraus Seuraussuhteen (engl. consequence) päättelysääntöjä on kaksi. Ensimmäistä käytetään vahventamaan esiehtoa ja toista heikentämään jälkiehtoa. Seuraus 1: Seuraus 2: P Q, {Q} S {R} {P} S {R} {P} S {Q}, Q R {P} S {R} Seuraus ei siis liity erityisesti mihinkään yksittäiseen ALKEIS-kielen syntaksin produktioon, vaan se on yleispätevä todistamisen apukeino. 2 Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos.
4.1 Esimerkki Olkoon annettuna kolmikko { x = y } x := y + 1 { x > y x = 1 y = 0 }, mikä Seuraus 1:stä soveltamalla saadaan muotoon: { x = 0 y = 0 x = y } x := y + 1 { x > y x = 1 y = 0 }, ja Seuraus 2 :sta soveltamalla muotoon { x = 0 y = 0 x = y } x := y + 1 { x > y }. Huom. Edellisessä esimerkissä ei todistettu annetun ohjelman oikeellisuutta, sillä siinä ei sovellettu sijoitusaksioomaa. Tarkoituksena oli näyttää kuinka esiehtoja voidaan lisätä ja jälkiehtoja karsia. 5 Yhdistäminen Yhdistämissäännöllä (engl. composition rule) liitetään yhteen kaksi peräkkäistä lausetta, joista ensimmäisen jälkiehto on jälkimmäisen esiehto. Tämä on välttämätöntä, jotta todistuksissa voidaan käsitellä useammasta lauseesta koostuvia ohjelmia. Yhdistämissääntö: 5.1 Esimerkki {P} S 1 {Q}, {Q} S 2 {R} {P} S1; S2 {R} { x = 0 } x := x + 1 { x = 1 }, { x = 1 } x := x + 1 { x 2 } { x = 0 } x := x + 1; x := x + 1 { x 2 } Tässä esimerkissä ensimmäisen kolmikon jälkiehto { x = 1 } on sama kuin jälkimmäisen kolmikon esiehto. Yhdistämissäännön perusteella näistä kahdesta voidaan yhdistää kolmikko, jonka esiehto on ensimmäisen kolmikon esiehto ja jälkiehto on jälkimmäisen kolmikon jälkiehto. Yhdistetyn kolmikon lause saadaan liittämällä kolmikoiden lauseet yhteen erotettuna puolipisteellä. Jälleen on huomattava, että edellisessä esimerkissä ei varsinaisesti todistettu mitään (sillä sijoitusaksioomaa ei sovellettu kolmikoiden sijoituslauseisiin). Seuraavassa esimerkissä sovelletaan kaikkia tähän mennessä esitettyjä sääntöjä ja todistetaan kahdesta sijoituslauseesta koostuva ohjelma osittain oikeelliseksi. 5.2 Esimerkki { x = 0 y = x } x := x + 1; y := y + x { y > 0 } Ensimmäiselle sijoituslauseelle saadaan jälkiehto soveltamalla sijoitusaksioomaa: { x = 0 y = x } x := x + 1 { x = 1 y = x 1 }. Käytetään saatua jälkiehtoa toisen sijoituslauseen esiehtona ja etsitään sille jälkiehto soveltamalla toistamiseen sijoitusaksioomaa: { x = 1 y = x 1 } y := y + x { x = 1 y = x 1 + x }. Koska { x = 1 y = x 1 + x } { y > 0 }, niin toisen sijoituslauseen kolmikko voidaan kirjoittaa muodossa (ts. sen jälkiehtoa voidaan heikentää): { x = 1 y = 0 } y := y + x { y > 0 }. Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos. 3
Lopuksi liitetään palat yhteen soveltamalla yhdistämissääntöä (tämä voidaan tehdä, sillä ensimmäisen jälkiehto on jälkimmäisen esiehto): 6 Silmukat { x = 0 y = x } x := x + 1; y := y + x { y > 0 }. ALKEIS-kielen silmukkarakenteen semantiikka määräytyy silmukan päättymisehdon (E) ja muuttumattoman ehdon, invariantin (P), totuusarvojen tarkastelulla: { P E } S { P } { P } while E do S od { P E }, missä S edustaa silmukassa suoritettavia operaatioita. Säännöstä käytetään tässä artikkelissa nimeä toistosääntö (engl. rule of iteration). Koska kielessä käytetään kokonaislukuja 0 ja 1 totuusarvoina, E on tulkittava siten, että lausekkeen E arvo on 1 ja E siten, että E:n arvo on 0. Todistuksia tehdessä käyttökelpoisen invariantin löytäminen ei ole välttämättä helppoa, mutta annettu väittämä P voidaan todistaa invariantiksi osoittamalla, että P on totta ennen silmukan suoritusta, P on totta silmukan suorituksen aikana ja P on totta silmukan suorituksen jälkeen. 6.1 Esimerkki { x 1 } while ( x 1 ) do { x := x - 1} od { x = 0 } Arvataan invariantiksi { x 0 }. Invarianttiehdokas on selvästi totta ennen silmukan suoritusta, sillä esiehdosta { x 1 } seuraa { x 0 }. Aloitetaan silmukan todistaminen mää- rittelemällä sijoituslauseelle x := x 1 esi- ja jälkiehdot: { x 1 } x := x 1 { x 0 }, mikä (sijoitusaksiooman perusteella) pitää paikkansa, sillä jälkiehtoon { x 0 } sijoittamalla muuttujan x paikalle lauseke x 1 saadaan haluttu esiehto { x 1 }. Tätä esiehtoa voidaan vielä vahvistaa ensimmäisen seuraussäännön avulla, jolloin uudeksi esiehdoksi saadaan { x 1 x 0 }. Nyt ollaan siis todistettu tällaisen kolmikon osittainen oikeellisuus: { x 1 x 0 } x := x 1 { x 0 }. Seuraavaksi sovelletaan toistosääntöä käyttäen väittämää x 0 invarianttina P ja silmukan ehtoa x 1 ehtona E: { x 1 x 0 } x := x 1 { x 0 } { x 0 } while x 1 do x := x 1 od { x 0 (x 1) }. Haluttu jälkiehto { x = 0 } saadaan ehdosta { x 0 (x 1) } seuraavalla päättelyllä (oletetaan x kokonaisluvuksi): { x 0 (x 1) } { x 0 x < 1 } { x = 0 }. On paikallaan vielä varmistaa, että invariantti { x 0 } on totta 1. ennen silmukkaa (tämä nähtiin jo alussa), 2. silmukan suorituksen aikana (ok, sillä { x 0 } esiintyy sijoituslauseen esi- ja jälkiehdossa), ja 3. silmukan suorituksen jälkeen (ok, sillä { x 0 } on jälkiehdon { x = 0 } heikennys). 4 Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos.
7 If-lause If-lauseelle on voimassa sääntö if-then-else: { P E } S 1 { Q }, { P E } S 2 { Q } { P } if E then S 1 else S 2 fi { Q } Kuten while-lauseen määrittelyn yhteydessä, tässäkin on muistettava totuusarvojen korvaaminen kokonaisluvuilla. 7.1 Esimerkki { x = 2 y = 5 } if ( x y ) then { x := y + 1 } else ; fi { x > y } Lähdetään jälleen liikkeelle sijoituslauseesta x := y + 1 keksimällä sille esija jälkiehdot (ja todistamalla näin saatava sijoituslause): { x y } x:= y + 1 { x > y }, mikä on totta, sillä jälkiehtoon { x > y } sijoittamalla x:n paikalle y + 1 saadaan tautologia 1 { y + 1 > y }, joten esiehtona voi olla mitä tahansa, erityisesti siis { x y } kelpaa. Seuraavaksi sovelletaan if-then-elsesääntöä, valitaan ehdoksi P { x = 2 y = 5 }, silmukkaehdoksi E { x y }, E:n negaatioksi { x > y } ja jälkiehdoksi Q ehto { x > y }: { x = 2 y = 5 x y } x:= y + 1 { x > y }, { x = 2 y = 5 x > y } { x > y } { x = 2 y = 5 } if x y then x := y + 1 else ; fi { x > y }, mikä on haluttu muoto. 1 tautologia = väite, joka on muuttujiensa arvoista riippumatta aina tosi 8 I/O-operaatiot Syöte- ja tulostusvirrat mallinnetaan merkeistä koostuvina jonoina, joista lukuoperaatiossa (merkitään get) poistetaan alusta ja joihin kirjoitusoperaatiossa (merkitään put) lisätään loppuun alkio. Syötevirtaa merkitään symbolilla IN ja tulostusvirtaa symbolilla OUT. Merkintä IN = al tarkoittaa, että syötevirran ensimmäinen alkio on a, jota seuraa jono L, jossa on 0 tai useampi alkio. Vastaavasti OUT = La tarkoittaa, että tulostusvirran viimeinen alkio on a, jota edeltää jono L, jossa on 0 tai useampi alkio. Seuraavat päättelysäännöt määräävät luku- ja kirjoitusoperaatioiden merkityksen: Luku: { IN = al P[a/x] } get x { IN = L P } Kirjoitus: { OUT = L E = a P } put E { OUT = La E = a P }, missä E tarkoittaa lauseketta, jonka arvo on a. 8.1 Esimerkki { IN = [17, 5]L 1 OUT = L 2 } get x; get y; put x + y { IN = L 1 OUT = L 2 [22] } Lähdetään liikkeelle ensimmäisestä getoperaatiosta. Käytetään esiehtona ohjelman esiehtoa ja poistetaan INjonosta 1. alkio. OUT-jono pysyy muuttumattomana. Kun jälkiehdossa { x = 17 } x:n paikalle sijoitetaan luettu alkio Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos. 5
17, niin saadaan jälleen tautologia, joka ei siis vaadi esiehdolta mitään lisää: { IN = [17, 5]L 1 OUT = L 2 } get x { IN = [5]L 1 OUT = L 2 x = 17 } Siirrytään toiseen get-operaatioon. Käytetään edellisen get-operaation jälkiehtoa tämän get-operaation esiehtona (jotta jatkossa päästään soveltamaan yhdistämissääntöä). INjonosta poistuu jälleen alkio, OUT pysyy ennallaan ja jälkiehdossa { y = 5 } on jälleen tautologia, joten muita vaatimuksia esiehdolle ei tule: { IN = [5]L 1 OUT = L 2 x = 17 } get y { IN = L 1 OUT = L 2 x = 17 y = 5 } Koska ensimmäisen get-operaation jälkiehto on toisen esiehto, yhdistämissäännön perusteella kolmikot voidaan yhdistää: { IN = [17, 5]L 1 OUT = L 2 } get x; get y; { IN = L 1 OUT = L 2 x = 17 y = 5 } Siirrytään put-operaatioon. Käytetään yhdistetyn kolmikon jälkiehtoa putoperaation esiehtona (jotta päästään taas soveltamaan yhdistämissääntöä). Esiehtoa voidaan vahvistaa oletuksella x + y = 22, sillä se suoraa oletuksesta { x = 17 y = 6 }. IN-jono pysyy muuttumattomana, OUT:iin lisätään lausekkeen x + y arvo (joka nyt siis oletuksen mukaan on 22), x, y ja x + y pysyvät muuttumattomina. Saadaan siis kolmikko: { IN = L 1 OUT = L 2 x = 17 y = 5 x + y = 22 } put x + y; { IN = L 1 OUT = L 2 [22] x = 17 y = 5 x + y = 22 } Yhdistetään kolmikot (tämä voidaan tehdä, sillä put-kolmikon esiehto on yhtäpitävä get-kolmikon jälkiehdon kanssa). put-kolmikon jälkiehtoa voidaan heikentää poistamalla siitä ehto { x = 17, y = 5, x + y = 22}. Näin saadaan { IN = [17, 5]L 1 OUT = L 2 } get x; get y; put x + y; { IN = L 1 OUT = L 2 [22] } mikä on sitä, mitä haluttiin. 9 Lohkot ALKEIS-kielen lohko (engl. block) koostuu joukosta lauseita, joissa on voimassa paikallisesti määriteltyjä muuttujia. Lohkon päättelysäännössä { P } S { Q } { P } begin D S end; { Q } D edustaa paikallisten muuttujien määrittelyä (kieliopin Vars-produktio) ja ehdot P ja Q eivät saa sisältää D:ssä esiteltyjä muuttujia. Sisäkkäisten lohkojen samannimiset muuttujat tulee pystyä erottelemaan. Tämä voidaan esimerkiksi tehdä uudelleennimeämällä ohjelman kaikki muuttujat siten, että muuttujien vanhaan nimeen lisätään # -merkki (tai mikä tahansa muu merkki, jota ALKEISkielen konkreetti syntaksi ei salli muuttujien nimissä), jonka jälkeen 6 Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos.
kirjoitetaan kulloisenkin lohkon syvyys ohjelman lohkohierarkiassa. Esimerkiksi siis ohjelman begin end; var x, var y begin end; var y y = y + 1; muuttujat seuraavasti: begin end; var x#1, var y#1 begin end; var y#2 y#2 = y#2 + 1; 10 Terminoituvuus uudelleennimetään Tässä luvussa käsittelemme lyhyesti ALKEIS-kielellä kirjoitettujen ohjelmien pysähtymisongelmaa. Asia ei suoranaisesti liity ohjelmointikielen semantiikan määrittämiseen, mutta on hyvin keskeinen oikeellisuustodistuksissa. Esitystapa mukailee lähteen [3] esitystä. Ohjelma P on terminoituva ts. se pysähtyy, joss se ei jää ikuiseen silmukkaan. Tähänastisilla menetelmillä on kyetty todistamaan osittainen oikeellisuus, ts. jos esiehdot täyttyvät ja ohjelma terminoituu, niin myös jälkiehdot täyttyvät. Ohjelman täydellinen oikeellisuus edellyttää, että aina esiehtojen ollessa voimassa ohjelma terminoituu ja antaa oikeat tulokset. Ohjelmissa on kaksi erityyppistä syntaktista rakennetta, jotka mahdollistavat ikuiseen silmukkaan jäämisen: 1. while-silmukat 2. rekursiiviset aliohjelmakutsut. While-silmukka todistetaan terminoituvaksi löytämällä sille ns. terminoiva lauseke (engl. termination expression). Rekursio todistetaan terminoituvaksi induktiotodistuksella. Koska käsittelemässämme ALKEISkielen versiossa ei aliohjelmia ole, keskitymme tarkastelemaan vain silmukoita. Seuraavaksi käydään läpi terminoivan lausekkeen määritelmä ja todistetaan yksinkertainen silmukka terminoituvaksi. 10.1 Aito ja koherentti järjestysrelaatio Joukon W osittainen järjestys on koherentti (engl. well-founded), joss kaikille W:n väheneville sekvensseille x 1 x 2 x 3... on olemassa luonnollinen luku k siten, että x i = x j i, j k. Järjestysrelaatio on aito (engl. strict), joss se ei ole refleksiivinen ja missään vähenevässä sekvenssissä ei esiinny sama alkio useampaa kertaa (ja näin kaikki vähenevät sekvenssit ovat äärellisiä). 10.2 Terminoiva lauseke Olkoon joukon W aito ja koherentti järjestysrelaatio. Lauseke E on silmukan S terminoiva lauseke, joss 1. E:n arvo W jokaisella S:n iteraatiolla ja 2. jokaisella toistolla E:n arvo on :n mielessä pienempi kuin edellisellä kerralla. Osoitettaessa annettua silmukkaa terminoituvaksi on siis löydettävä Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos. 7
määritelmissä esiintyvät joukko W, järjestys ja lauseke E, jolla on W:n ja :n suhteen vaaditut ominaisuudet. Seuraavaksi katsotaan, kuinka tämä käytännössä tehdään. 10.3 Esimerkki Seuraava ohjelma laskee luonnollisen luvun 2 (muuttuja n) kertoman: { n N k = 1 i = n } while ( i 1 ) do od { k = n! } k := k * i; i := i 1; Nyt joukoksi W kelpaavat luonnolliset luvut, relaatioksi normaali luonnollisten lukujen järjestys ja terminoivaksi lausekkeeksi E muuttujan i arvo. Käytetään 3 ehtoa i Ν silmukkainvarianttina. Tarkistetaan terminoivan lausekkeen määritelmässä annetut ehdot: 1. E:n arvo W jokaisella S:n iteraatiolla ; Tämä on totta, sillä se sisältyy invarianttiehtoon i Ν. 2. Jokaisella toistolla E:n arvo on :n mielessä pienempi kuin edellisellä kerralla ; { i = A } k:= k * i; i := i 1 { A i = A 1 }. Näin siis voidaan päätellä esimerkin while-silmukan terminoituvan. Mikäli lisäksi osoittaisiin jälkiehtojen seuraavan esiehdoista, voitaisiin todeta ohjelma täydellisesti oikeelliseksi. 11 Yhteenveto Semantiikan aksiomaattinen määritteleminen ALKEIS-kielelle ja yksinkertaisten ohjelmien todistukset näyttävät siis olevan mahdollisia. Asiat mutkistuvat kun kieleen tuodaan aliohjelmat ja oliot. Artikkelissa ei todistettu esitetyn päättelysäännöstön oikeellisuutta (engl. soundness) tai täydellisyyttä (engl completeness). Seuraussääntö, yhdistämissääntö ja terminoituvuuden tarkastelu eivät varsinaisesti määritä kielen semantiikkaa, mutta ovat keskeisiä käsitteitä todistuksia tehdessä. Viitteet [1] C.A.R. Hoare, An Axiomatic Basis for Computer Programming, Communications of the ACM 12, 1969 [2] C. A. R. Hoare and N. Wirth, An axiomatic definition of the programming language Pascal, Acta Informatica, 2(4), 1973 [3] K. Slonneger, B. L. Kurtz "Formal Syntax and Semantics of Programming Languages: A Laboratory-Based Approach", luku 11, ISBN 0-201-65697-3 Addison- Wesley, 1995 2 Tässä luonnolliset luvut alkavat ykkösestä. 3 Oikeasti pitäisi vielä osoittaa, että i Ν kelpaa invariantiksi. 8 Ohjelmointikielten periaatteet, päätösseminaari 2.12.2002, Jyväskylän yliopisto, tietotekniikan laitos.