Luku 5 Kontrollivuon ohjaus Useimmat nykykielet ovat luonteeltaan peräkkäistäviä (sequential): ohjelmatekstissä mainitut tehtävät tehdään yksi kerrallaan toinen toisensa jälkeen. Jotkin kielet tukevat yhtäaikaisuutta (engl. concurrency), mutta tähän palataan myöhemmin. Kontrollivuolla (engl. control flow) tarkoitetaan kahta eri asiaa: järjestys, jossa ohjelman suoritus etenee ohjelmatekstissä jollakin tietyllä suorituskerralla (dynaaminen kontrollivuo) kaikki järjestykset, joissa ohjelman suoritus voi ohjelmatekstissä edetä, kun tarkastellaan kaikkia mahdollisia suorituskertoja (staattinen kontrollivuo) Staattinen kontrollivuon analyysi on tärkeä optimoivan kääntäjän tehtävä, ja sitä käytetään myös ohjelman muussa staattisessa analyysissä (esim. sen selvittämisessä, mitkä muuttujat ovat tarpeettomia). Jokaisessa ohjelmointikielessä on konstruktioita, joiden tehtävänä on ohjata dynaamisen kontrollivuon kulkua. Tässä monisteessa niitä kutsutaan kontrollia ohjaaviksi (tälle ei ole tietääkseni englanninkielistä termiä) ja niiden käyttöä kontrollinohjaukseksi. Yksi tärkeimmistä tällaisista konstruktioista on aliohjelma, mutta siihen palataan myöhemmin. Tässä luvussa tarkastellaan aliohjelman sisäistä kontrollivuon ohjausta. 5.1 Ehtolausekkeet Kontrollivuon ohjaus perustuu ohjelman kykyyn havainnoimaan omaa tilaansa ja tekemään sen perusteella päätöksiä. Tämän perustana on ehtolausekkeet: 47
48 LUKU 5. KONTROLLIVUON OHJAUS kokonaislukutyyppisten ja liukulukutyyppisten lausekkeiden arvojen vertailu (pienempi-kuin, suurempi-kuin, yhtäsuuri-kuin ym.) taulukoiden ja muiden koostearvojen vertailu alkioittain koosteiset ehdot, jotka muodostuvat useista loogisin konnektiivein yhdistetyistä ehdoista Tavallisesti loogiset operaattorit (looginen ja, looginen tai) ovat oikosuljettuja (engl. short-circuited), jolloin oikean operandin arvo lasketaan vain, jos koko lausekkeen arvo ei ole selvä jo pelkän vasemman oprandin arvon perusteella. Tämän järjestelyn etuna on, että ehdoissa voidaan tarkastella jotain sellaista asiaa, joiden pelkkä laskeminen on virhe, kunhan vain ensin samassa ehdossa rajataan pois tilanteet, joissa tällaiset virheet syntyvät. Esimerkiksi taulukon indeksointi ehdossa voidaan tehdä, vaikka vasta aiemmin samassa ehtolausekkeessa on tarkistettu, että indeksi on sallittu. 5.1.1 Erillään Ehtolausekkeet voidaan mallittaa formaalisti kahdella eri tavalla. Niistä voidaan tehdä oma syntaktinen kategoriansa (engl. (syntactic) sort). Abstraktina syntaksina tämä menisi jotensakin näin: c N x, y, z Var e, f Expr b BoolExpr b ::= true false x b 1 b 2 b 1 b 2 Tämä toimii yksinkertaisen kielen tapauksessa hyvin mutta muodostuu ongelmaksi kielen monipuolistuessa. Yksi yksinkertainen kysymys, joka herää jo ylläolevasta esimerkistä: voiko muuttujalla olla samanaikaisesti sekä luku- että totuusarvo? Denotationaalisella merkitysopilla tämä ero tulee selväksi. Ensiksi tilanne, jossa muuttujalla voi olla vain jompi kumpi: Env = Var (Q t, f })
5.1. EHTOLAUSEKKEET 49 E : Expr Env (Q E}) (5.1) E jos σ(x) tai σ(x) t, f } E x σ = (5.2) σ(x) muuten E jos E e = E tai E e = E E e + f σ = (5.3) E e + E f muuten E jos E e = E tai E e = E E e f σ = (5.4) E e E f muuten E jos E e = E tai E e = E E e f σ = (5.5) E e E f muuten B : BoolExpr Env t, f, E} B true σ = t (5.6) B false σ = f (5.7) E jos σ(x) tai σ(x) Q B x σ = (5.8) σ( x ) muuten E jos B b 1 σ = E E jos B b B b 1 b 2 σ = 1 σ = t ja B b 2 σ = E (5.9) t jos B b 1 σ = t ja B b 2 σ = t f muuten E jos B b 1 σ = E E jos B b B b 1 b 2 σ = 1 σ = t ja B b 2 σ = E (5.10) t jos B b 1 σ = t tai B b 2 σ = t f muuten Tässä E edustaa virhettä (joko tyyppivirhe tai määrittelemättömän muuttujan virhe). Entä jos muuttujalla on sekä luku- että totuusarvo? Env = (Var Q) (Var t, f }) E jos σn (x) E x (σ n, σ b ) = σ n (x) muuten E σ = (5.11)
50 LUKU 5. KONTROLLIVUON OHJAUS E jos σb (x) B x (σ n, σ b ) = σ b (x) muuten B σ = (5.12) 5.1.2 Yhdessä Toinen vaihtoehto on antaa ehtolausekkeiden olla lausekkeita, jotka erotellaan toisistaan (dynaamisella tai staattisella) tyypillä: c N x, y, z Var e, f Expr e, f ::= true false e f e f Sitä, lasketaanko lauseke lukuarvon vai totuusarvon selville saamiseksi, kutsutaan laskentayhteydeksi (engl. evaluation context). Laskentayhteydestä riippuu, onko ehtolauseke tyyppivirhe vaiko juuri se mitä haluttiin. Denotationaalisessa merkitysopissa tämä esitetään antamalla lausekkeille kaksi semanttista funktiota, yksi kullekin laskentayhteydelle. Tyyppivirhe ilmaistaan tässäkin antamalla semanttisen funktion palauttaa E. Env = Var Q t, f } E : Expr Env Q E} σ(x) jos σ(x) Q E x σ = (5.13) E muuten E true σ = E (5.14) E false σ = E (5.15) E e f σ = E (5.16) E e f σ = E (5.17) E jos E e σ = E tai E f σ = E E e + f σ = (5.18) E e σ + E f σ muuten
5.2. PERÄKKÄISTYS 51 E jos E e σ = E tai E f σ = E E e f σ = E e σ E f σ muuten E jos E e σ = E tai E f σ = E E e f σ = E e σ E f σ muuten (5.19) (5.20) B : Expr Env t, f, E} B true σ = t (5.21) B false σ = f (5.22) E jos σ(x) tai σ(x) Q B x σ = (5.23) σ(x) muuten E jos B e σ = E E jos B e σ = t ja B f σ = E B e f σ = (5.24) t muuten jos B e σ = t ja B f σ = t f muuten E jos B e σ = E E jos B e σ = t ja B f σ = E B e f σ = (5.25) t muuten jos B e σ = t tai B f σ = t f muuten B e + f σ = E (5.26) B e f σ = E (5.27) B e f σ = E (5.28) Jatkossa tässä monisteessa käytetään tätä tulkintaa. 5.2 Peräkkäistys Käskykielten merkittävin kontrollia ohjaava konstruktio unohtuu helposti, sillä se on pääasiassa piilossa. Kyse on peräkkäistämisestä (sequencing): kun kaksi tai useampi lause on ohjelmassa peräkkäin, on tavanomainen tulkinta se, että ohjelman suoritus etenee kyseisten lauseiden kautta yksi kerrallaan. Tätähän tarkastelimme jo aiemmin suoraviivaohjelmien tapauksessa. Koska nyt lausekkeen arvo voi olla tyyppivirhe, pitää myös suoraviivaohjelmien denotationaalista merkitysoppia päivittää. C : Stmt Env Env E}
52 LUKU 5. KONTROLLIVUON OHJAUS σ (x, E e σ)} jos E e σ = E C x e σ = σ (x, B e σ)} jos B e σ = E (5.29) E muuten E jos C s σ = E C s; t σ = (5.30) C t (C s σ) muuten 5.3 Go to Aikoinaan ohjelmointikielten merkittävin kontrollia ohjaava konstruktio oli go to -lause. Se oli pesiytynyt kieliin symbolisten konekielten hyppykäskyistä. Go to ohjasi kontrollin (usein numerolla) nimettyyn paikkaan ohjelmassa. Sen lisäksi käytettiin ehdollista hyppykäskyä, jossa lausekkeen arvon perusteella valittiin, mihin kohtaa ohjelmaa hypätään. Tuloksena on vaikeasti ymmärrettävää ohjelmakoodia, ns. spagettikoodia. Sittemmin, 1970-luvulla, muotiin tuli rakenteinen ohjelmointi (structured programming). Se oli reaktio spagettikoodia vastaan lääkkeinä nähtiin ylhäältä alas (top-down) -suunnittelu, koodin modularisointi, rakenteiset tyypit, muuttujien selkeä nimeäminen ja laaja kommentointi. Rakenteisen ohjelmoinnin liikkeen yhtenä osana kampanjoitiin Dijkstra (1968) etunenässä go to -lauseen hävittämisen puolesta (mutta katso myös Knuth 1974). Nykyisin näyttää siltä, että go to on vähintäänkin uhanalainen laji: Java varaa sen avainsanaksi vain siksi, että kääntäjät voisivat antaa parempia virheilmoituksia. Toisaalta niissäkin kielissä, jotka eivät go to -nimistä lausetta sisällä, on sen tapaisia lauseita kuitenkin eri nimillä (Javan break on varsin voimakas heikennetty versio go to -lauseesta; Schemen call-with-current-continuation on sekin erikinen mutta erittäin voimakas versio go to -lauseesta). Rajoittamattoman hyppykäskyn mallittaminen denotationaalisesti on sen verran hankalaa, että siihen ei tässä monisteessa ryhdytä. Itse asiassa vastaava teoreettinen hankaluus oli yksi Dijkstran argumenteista go to -lausetta vastaan. 5.4 Valintalauseet Ohjelmoinnissa on kaksi pääasiallista valintatilannetta: suoritettava lause pitää valita joko jonkin ehtolausekkeen totuusarvon mukaan tai sitten jonkin toisen lausekkeen arvon mukaan. Toki jälkimmäinen riittäisi yksin,
5.4. VALINTALAUSEET 53 mutta edellinen on niin tärkeä erityistapaus, että sille on oma konstruktionsa. If-lauseen semantiikka on kaikille tuttu: selvitetään annetun testin totuusarvo if-lauseen kohdalla ja sitten suoritetaan jompi kumpi annetusta vaihtoehtoisesta lauseesta. Seuraavan denotationaalisen merkitysopin pohjana on luvussa 4.3 esitetty suoraviivaohjelmien kieli vahvistettuna luvussa 5.1.2 esitetyllä totuusarvojen käsittelytavalla. e, f Expr s, t Stmt s, t ::= if e then s else t E C if e then s else t σ = C s σ C t σ jos B e = E jos B e = t jos B e = f (5.31) If-lauseen konkreetti syntaksi on hieman mielenkiintoisempi kysymys. Aivan vanhimmissa kielissä if-lauseeseen oli kovakoodattu go to, vain hyppääminen oli mahdollista. Tästä ei pidetä korkean tason ohjelmoinnissa, joten moderneissa ohjelmointikielissä if-lauseen then- ja else-osat voivat olla mielivaltaisen monimutkaisia, yleensä lohkoja. Tyypillinen konkreetti syntaksi on Pascalin käyttämän tyyppinen if x 0 then if x < 0 then writeln("negatiivinen") else writeln("nolla") else writeln("positiivinen") tai C:n tyylinen: if (x 0) if (x < 0) puts("negatiivinen\n"); else puts("nolla\n"); else puts("positiivinen\n");
54 LUKU 5. KONTROLLIVUON OHJAUS Jos kumpi tahansa näistä kirjoitetaan ilmiselvällä tavalla konkreetiksi kieliopiksi ja else-osan pois jättäminen sallitaan, kuten on tapana esimerkiksi 1 statement: if ( expression ) statement if ( expression ) statement else statement niin tällainen konkreetti kielioppi on moniselitteinen: kumpaan if-lauseeseen else-osa liittyy ohjelmanpätkässä if (x >= 0) if (x == 0) printf("1"); else printf("0"); eli tulostaako ohjelmanpätkä 0 silloin kun x on negatiivinen vai silloin kun se on positiivinen? Tätä ongelmaa sanotaan orvoksi elseksi (engl. dangling else), ja siihen on useita ratkaisuja: Orvon elsen ongelma juontuu siitä, että if-lauseen loppu ei ole yksikäsitteisesti määrätty. Tämä voidaan korjata niin, että lisätään iflauseen loppuun jokin lopetusavainsana; tyypillisiä ovat endif, end if ja fi (if väärin päin). Näin toimivat esimerkiksi Bourne Shell ja Algolit. Edellistä ratkaisua mukaillen voidaan tehdä myös niin, että thenlauseen tai jopa myös else-lauseen pitää olla lohko. Tällöin ei myöskään jää epäselväksi, mihin if-lause päättyy. Näin toimii esimerkiksi Perl. Vielä yksi edellisten muunnelma on ilmaista ohjelman lohkorakenne sisennyksillä niin, että sisennyksillä on semanttista merkitystä. Tämän seurauksena kieliopin näkökulmasta jokainen then- ja elselause on lohko, joten ratkaisu on sukua edelliselle ratkaisulle. Näin toimii esimerkiksi Python. Mahdollista on myös pakottaa else-haaran käyttäminen kaikissa iflauseissa. Haskell noudattaa tätä ajatusta. Yleisin ratkaisu lienee kuitenkin julistaa, että if-else-parit löydetään etsimällä ensin sisimmälle if-lauseelle sitä tekstuaalisesti lähinnä oleva else ja sitten toiseksi sisimmälle if-lauseelle sitä tekstuaalisesti lähinnä oleva vielä käyttämätön else jne. Tämä vastaa sitä, mitä ohjelmoijat yleensä tarkoittavat. 1 Tässä C-kielen määrittelyssä käytetyssä notaatiossa vaihtoehdot on erotettu toisistaan siten, että ne kirjoitetaan eri riveille. Lisäksi välikesymbolit kirjoitetaan kursiivilla ja päätesymbolit tasalevyisellä kirjasimella.
5.5. TOISTOLAUSEET ELI SILMUKAT 55 If-lause toteuttaa valinnan kahden vaihtoehdon välillä riippuen ehtolauseen tuloksesta. Monesti käyttökelpoisempi on usean vaihtoehdon välillä valitseminen käyttäen valintaperusteena mielivaltaisen (yleensä kokonaislukutyyppisen) lausekkeen arvoa. Tällainen lause on case-lause (Csukuisissa kielissä switch-case). Case-lauseen rakenne on yksinkertainen. Se koostuu erottelulausekkeesta (engl. discriminator) sekä vähintään yhdestä arvo lause-parista. Sen suoritus alkaa laskemalla erottelulausekkeen arvo. Jos lauseessa on arvoa vastaava arvo lause-pari, kyseinen osalause suoritetaan. Yleensä lauseessa on myös mukana oletuslause, joka suoritetaan, jos erottelulausekkeen arvoa ei ollut paritettu minkään lauseen kanssa. switch (op) case OP_ADD: result = lrand + rrand; break; case OP_SUB: result = lrand - rrand; break; case OP_MUL: result = lrand * rrand; break; default: fputs("bad operator!\n", stderr); } 5.5 Toistolauseet eli silmukat Imperatiivisen ohjelmoinnin ytimessä on sijoituslauseiden ja peräkkäisyyden lisäksi silmukat (engl. loops) eli toistolauseet (engl. repetition statements). Varsin harvinainen mutta kaikista yleiskäyttöisin rakenne on loop. s, t ::= loop s exit if e t Tämän silmukan intuitiivinen semantiikka on, että joka iteraatiolla suoritetaan ensin s, sitten katsotaan, onko e tosi ja jos on, silmukka päättyy, muuten suoritetaan t ja aloitetaan uusi iteraatio. Varsin tavallinen tilanne, jossa tästä muodosta on hyötyä, on syötteen lukeminen, sillä monissa kielissä tiedon loppuminen saadaan selville yrittämällä lukea syötteen päättymisen jälkeen: loop c getchar; exit if c = EOF;... end loop
56 LUKU 5. KONTROLLIVUON OHJAUS Silmukkarakenteista kuuluisin on varmastikin while-rakenne: s, t ::= while e do s Tarpeellinen on usein myös perinteinen for-silmukka: s, t ::= for x e 1 to e 2 step e 3 do s joka tavallisesti määritellään kielioppimakeisena while-lauseen perusteella: for x e 1 to e 2 step e 3 do s my x = e 1 ; while (x < e 2 ) do s; x x + e 3 } } Ohjelmointikieltä, jossa on tuki suoraviivaohjelmille sekä if- ja whilelauseille, sanotaan joskus while-kieleksi. Sillä on keskeinen merkitys ohjelmoinnin teoriassa imperatiivisen ohjelmointikielen arkkityyppinä. Tämä johtuu siitä, että while-kielelle on olemassa selkeitä ohjelmien oikeaksi todistamiseen liittyviä formalismeja ja siitä, että while-kieli on yksinkertaisimmasta päästä kieliä, joka on laskennallisesti yhtä ilmaisuvoimainen kuin Turingin kone eli kykenee laskemaan kaikki (yleis)rekursiiviset (engl. general recursive) funktiot Vastaavasti kieltä, joka on muuten samanlainen kuin while-kieli paitsi, että while-silmukan tilalla on for-silmukka ja jossa lisäksi vaaditaan, että silmukkamuuttujaan ei voi sijoittaa silmukan sisällä, sanotaan for-kieleksi. Se ei, yllättäen, ole yhtä ilmaisuvoimainen kuin while-kieli, sillä sillä ei voi tehdä päättymätöntä silmukkaa eikä ylipäätään mitään silmukkaa, jonka iteraatioiden lukumäärä ei ole etukäteen tiedossa. Niinpä for-kieli kykenee laskemaan primitiivirekursiiviset (engl. primitive recursive) funktiot ja on Turingin konetta aidosti heikompi. On houkuttelevaa määritellä while-silmukan denotationaalinen merkitysoppi näin: E jos B e σ = E C while e do s σ = σ jos B e σ = f C while e do s (C s σ) jos B e σ = t mutta se rikkoo aiemmin esitettyä ehtoa, jonka mukaan yhtälön oikealla puolella saa määriteltävä semanttinen funktio (tässä C) esiintyä vain siten, että semanttisten sulkeiden sisällä on jokin vasemmalla puolella esiintyvä (meta)muuttuja. Tässähän C:lle annetaan parametriksi koko vasemmalla puolella esiintyvä while-rakenne!
5.5. TOISTOLAUSEET ELI SILMUKAT 57 Konkreettinen esimerkki siitä, miksi tuo on huono määritelmä, on tapaus while true do x x; tällöinhän yhtälö sievenee muotoon C while true do x x σ = C while true do x x σ (5.32) joka ei aseta minkäänlaisia rajoituksia sille, mikä kyseisen konstruktion sisältö on! Tuo huono määritelmä voidaan kuitenkin kirjoittaa myös näin: missä C while e do s σ = F e s (C while e do s ) σ E σ F e s f σ = E f (C s σ) jos E e = E jos E e = f jos E e = t ja C s σ = E muuten Niinpä C while e do s on (ei-rekursiivisen) funktion F e s (jokin) kiintopiste. Jos olisi olemassa operaattori µ, jolle pätee µ f = f (µ f ) toisin sanoen µ f on f :n kiintopiste voitaisiin while-lauseen semantiikka määritellä seuraavasti: C while e do s σ = (µ(f e s))σ (5.33) missä F on sama kuin edellä. Onko µ-operaattori olemassa? Siitä seuraavassa. 5.5.1 Kiintopisteteoriaa Osittain järjestetty joukko (X, X ) on hila (engl. lattice), jos jokaiselle epätyhjälle äärelliselle S X on olemassa pienin yläraja S (join, supremum) ja suurin alaraja S (meet, infimum). Hila on täydellinen (engl. complete), jos join ja meet on kaikilla, ei pelkästään äärellisillä, epätyhjillä osajoukoilla. Kansi (engl. top) on = P(X) ja pohja (engl. bottom) on = P(X); nämä ovat aina olemassa täydellisessä hilassa. Olkoon (X, ) osittain järjestetty joukko ja olkoon f : X X. Jos kaikilla x, y X pätee x y f (x) f (y), on f järjestyksen säilyttävä eli monotoninen. Olkoon (X, ) osittain järjestetty joukko, olkoon f : X X ja olkoon x X. Tällöin
58 LUKU 5. KONTROLLIVUON OHJAUS 1. x on f -suljettu, jos f (x) x, 2. x on f -konsistentti, jos x f (x) ja 3. x on f :n kiintopiste, jos se on sekä f -suljettu että f -konsistentti eli jos x = f (x). Teoreema 1 (Knaster Tarski). Olkoon (X, ) täydellinen hila ja olkoon f : X X monotoninen. Tällöin f :llä on ainakin yksi kiintopiste, ja sen kiintopisteiden joukko P on itse täydellinen hila. Erityisesti P = x x f (x) } ja P = x f (x) x } pätevät ja ovat itse f :n kiintopisteitä. Todistus. Sivuutetaan. Olkoon (X, ) täydellinen hila, olkoon f : X X monotoninen ja olkoon f :n kiintopisteiden joukko P. Tällöin merkitään µ f = P ( f :n pienin kiintopiste) ja ν f = P ( f :n suurin kiintopiste). Olkoon f : X X funktio ja olkoon S X. Tällöin merkintä f [S] tarkoittaa joukkoa f (x) x S }. Olkoon f : X X funktio. Tällöin f on Scott-jatkuva, jos sille pätee f [S] = f ( S) kaikilla S X. Teoreema 2 (Kiintopisteiteraatio). Olkoon (X, ) täydellinen hila, olkoon f : X X monotoninen ja Scott-jatkuva, olkoon s 0 = ja olkoot s i+1 = f (s i ) kaikilla i N. Jos s n on f :n kiintopiste, niin s n = µ f pätee. Todistus. Todetaan aluksi, että jos µ f =, on lause triviaalisti tosi. Voidaan siksi olettaa, että µ f =. Todistuksen muoto on reductio ad absurdum. Olkoon s n f :n kiintopiste ja olkoon s n = µ f. Koska kiintopisteet muodostavat hilan, täytyy olla µ f s n. Olkoon k N, k < n, siten että s k µ f ja s k+1 µ f. Nyt s k, µ f } = µ f ja f (µ f ) = µ f. Toisaalta f (sk ), f (µ f )} = s k+1, µ f } = µ f! Oletuksesta s n = µ f seuraa siis, että f ei ole Scott-jatkuva. Koska f on Scott-jatkuva, täytyy olla s n = µ f. 5.5.2 Informaatiojärjestys Denotationaalisessa merkitysopissa tarkasteltava osittaisjärjestys on informaatiojärjestys, jossa s t tarkoittaa, että toisaalta s:ssä on vähemmän informaatiota kuin t:ssä.
5.6. VAHTIKOMENNOT 59 Perusdatan, kuten lukujen, kanssa informaatiojärjestys on varsin tylsä. Tyyppivirhettä edustava E on vähiten informaatiota sisältävä alkio eli pohja. Tällöin informaatiojärjestys on seuraavanlainen: E a a b kun a = E Jotta saataisiin täydellinen hila, on lisättävä vastaavanlainen kansialkio. Sen tulkinta on normaalissa ohjelmoinnissa varsin eriskummallinen on liikaa, ristiriitaista tietoa mutta vahtikomentojen kohdassa sille on luonnollinen tulkinta (kaikki vastaukset kelpaavat). Mikään ei pakota siihen, että f ( ) = pätisi. Mikäli se pätee, f :n sanotaan olevan tarkka (engl. strict). Mikäli se ei päde, f :n sanotaan olevan epätarkka (engl. nonstrict). While-lauseen määritelmässä halutaan nimenomaan pienin kiintopiste; tällöin yhtälöin (5.32) tarkoittamassa tilanteessa merkitykseksi tulee pohjafunktio σ E. 5.6 Vahtikomennot Mielenkiintoinen variantti while-kielestä saadaan, kun kaikki if-lauseet ja silmukkarakenteet korvataan Dijkstran vahtikomennoilla Dijkstra (1976): s, t ::= if g do g g GuardedStmt g ::= e s g 1 g 2 Vahtikomento e s ilmaisee, että lause s suoritetaan vain jos e on tosi. Jos e on epätosi, vahtikomennon sanotaan olevan suorituskelvoton (engl. infeasible). Vahtikomento g 1 g 2 valitsee vahtikomennoista g 1 ja g 2 suoritettavaksi jomman kumman, kuitenkin niin, että suorituskelvotonta ei valita. Käytännössä siis vahtikomento e 1 s 1 e n s n toimii niin, että se valitsee jonkin sellaisen i:n, jolla vahtilauseke e i on tosi, ja suorittaa sitten s i :n; ja jos sellaista i:tä ei ole, koko vahtikomento on suorituskelvoton. Vahtikomentopohjainen if-lause suorittaa vahtikomennon, jos se on suorituskelpoinen, ja muuten ei tee mitään, sen sijaan do-lause suorittaa vahtikomennon toistuvasti kunnes se muuttuu suorituskelvottomaksi.
60 LUKU 5. KONTROLLIVUON OHJAUS Esimerkki 1. Seuraava vahtikomentopohjainen if-lause valitsee muuttujien a ja b arvoista pienemmän ja sijoittaa sen muuttujaan min: if a b min a a b min b Vahtikomennot tuovat ohjelmointikieleen epädeterminismin (engl. nondeterminism): täsmälleen samalla syötteellä sama ohjelma voi käyttäytyä eri tavoin. Denotationaalisesti ajatellen tämä tarkoittaa, että ohjelma tuottaa useita vastauksia (osittaisjärjestys on, pohja on ja kansi on Env): C : Stmt Env P(Env) σ (x, E e σ)}} jos E e σ = E C x e σ = σ (x, B e σ)}} jos B e σ = E (5.34) muuten C s; t σ = C t σ (5.35) C if g σ = σ C s σ σ} jos G g σ = G g σ muuten (5.36) C do g σ = µ(f g) σ (5.37) F : GuardedStmt (Env P(Env)) Env P(Env) σ} jos G g σ = F g ϕ σ = ϕσ muuten (5.38) σ G g σ G : GuardedStmt Env P(Env) C s σ jos B e σ = t G e s σ = muuten (5.39) G g 1 g 2 σ = G g 1 σ G g 2 σ (5.40) Epädeterminismiä on kahdenlaista: hyväntahtoista (engl. angelic nondeterminism) ja pahantahtoista (engl. demonic nondeterminism). Näiden kuvaavien nimien takaa löytyy kysymys siitä, miten suoritettava lause valitaan, kun vaihtoehtoja on useita. Hyväntahtoisessa epädeterminismissä valitaan aina ohjelman kannalta suotuisin suoritusvaihtoehto (toisin sanoen, jos valintakohdassa toinen
5.6. VAHTIKOMENNOT 61 vaihtoehto johtaa virhetilanteeseen ja toinen antaa oikean tuloksen, valitaan se oikeaan tulokseen johtava). Tämän toteuttaminen tietokoneohjelmassa tapahtuu yleensä kahdella mahdollisella tavalla: valintaa tehtäessä arvataan ja jos arvaus osoittautuu myöhemmin vääräksi, valinta tehdään uudestaan (peruutus, engl. backtracking) valintaa ei tehdä heti, vaan kaikki vaihtoehdot suoritetaan rinnakkain, ja jokin valmistuneista palautetaan tulokseksi Edellinen toimii vain, jos mikään vaihtoehto ei johda tunnistamattomaan umpisilmukkaan. Kumpikaan tapa ei ole kovin houkutteleva, jos ohjelmalla on valinnasta riippuvia sivuvaikutuksia. Vahtikomentojen epädeterministisyys on käytännössä aina pahantahtoista, sillä se on helpoin toteutettava. Kääntäjälle annetaan täysi valinnan vapaus: se voi esimerkiksi valita aina ensimmäisen niistä, aina viimeisen niistä tai sitten se voi esimerkiksi heittää arpaa niiden välillä (hui!). Fiksu optimoiva kääntäjä saattaa valita aina sen, joka on kevyin suorittaa. Pääsääntö on siis, että valinnanvapaus annetaan kääntäjälle (ja sen toteuttajalle) ja ohjelmoijan pitää hyväksyä se, että mikä tahansa suorituskelpoisista vaihtoehdoista valitaan. Ohjelmoija joutuu koodaamaan Murphyn laki mielessään: jos jokin voi mennä pieleen, se menee pieleen. Esimerkki 2. Seuraavassa ohjelmapätkässä a, b ja c ovat lukutaulukoita ja sekä a että b on järjestetty kasvavaan suuruusjärjestykseen. Ohjelmanpätkä yhdistää a:n ja b:n c:hen niin, että c:ssä on jokainen a:n ja b:n alkio ja c on kasvavassa suuruusjärjestyksessä. Muuttujassa n on taulukon a koko ja muuttujassa m on taulukon b koko. Taulukko c on riittävän iso. var i : unsigned int; j : unsigned int; k : unsigned int; begin i 0; j 0; k 0; do i < n (j m a[i] b[j]) begin c[k] a[i]; i i + 1; k k + 1 end j < m (i n a[i] b[j]) begin c[k] b[j]; j j + 1;
62 LUKU 5. KONTROLLIVUON OHJAUS end end k k + 1 5.7 Väitteet Väitteet (engl. assertions) ovat ehtolausekkeita, joiden odotetaan olevan aina tosia. Niiden tarkoituksena on dokumentoida ohjelmoijien tekemiä oletuksia ja osoittaa, milloin näitä oletuksia rikotaan. Ne ovat pääsääntöisesti testauksen apuvälineitä (testioraakkeleita). Yleiskäyttöisin tapa lisätä väitteet kieleen on lisätä siihen assert-lause: s, t ::= assert e C assert e σ = σ E jos B e σ = t muuten Tässä E edustaa tyyppivirheen lisäksi myös väitevirhettä. Hyödyllinen olisi lohkorakenne pre e 1 ; s; post e 2 }, jossa e 1 on väitelauseke, jonka tulee olla totta lohkoon tultaessa, ja jossa e 2 on väitelauseke, jonka tulee olla totta lohkosta poistuttaessa. Väitettä e 1 sanotaan esiehdoksi (engl. precondition) ja väitettä e 2 sanotaan jälkiehdoksi (engl. postcondition). Ajatus on, että tämä lohkorakenne ilmaisee sopimuksen: lohkon käyttäjän vastuulla on, että esiehto pätee, ja lohkon itsensä vastuulla on, että jälkiehto pätee. Jos lohkon käyttäjä rikkoo sopimuksen ja esiehto ei pädekään, lohkolla ei ole mitään vastuuta eikä sen tarvitse huolehtia jälkiehdosta. Näin esimerkiksi voidaan kirjoittaa seuraavasti: } pre a 0 b > 0; q a / b; r a % b; post a = q b + r 0 r r < b Muita hyödyllisiä väitteitä ovat ns. pysyväisväittämät (engl. invariants). Pysyväisväittämä liittyy tavallisesti johonkin muuttujaan tai tyyppiin rajoittaen ko. muuttujan tai tyypin arvoja. Esimerkiksi kalenterityypin pysyväisväittämä saattaa vaatia, että huhtikuussa on enintään 30 päivää. Toi-
5.7. VÄITTEET 63 saalta while-ohjelmien todistustekniikoissa merkittävä rooli on silmukkainvariantilla, joka ilmaisee jonkin (yleensä hyvinkin epätriviaalin) suhteen silmukkamuuttujan ja muiden muuttujien välillä. Tony Hoare on kirjoittanut mielenkiintoisia artikkeleita Hoare (2003, 2004) väitteiden käytöstä teoriassa ja käytännössä.
64 LUKU 5. KONTROLLIVUON OHJAUS
Luku 6 Lyhytaskelsemantiikka Denotationaalisen semantiikan hankaluutena on, että se vaatii nopeasti tuekseen voimakkaita matemaattisia välineitä pelkät silmukat tarvitsevat kiintopisteteoriaa, ja konstruktioiden kehittyessä myös tarvittava matemaattinen koneisto monimutkaistuu. Gordon Plotkinin Plotkin rakenteinen operationaalinen semantiikka (engl. structural operational semantics) eli siirtymäsemantiikka (engl. transition semantics) eli lyhytaskelsemantiikka (engl. small-step semantics) on vaihtoehto, jolla ei ole tätä ongelmaa. Sen idea on kuvata matemaattisesti ja sopivasti abstrahoituna tietokone, jonka konekieli kohdekieli on. 6.1 Tilat Lyhytaskelsemantiikan käyttämällä abstraktiotasolla tietokone on tilakone: kone on aina jossakin tilassa (engl. configuration), ja kone suorittaa ohjelmaa siirtymällä tilasta toiseen. Kuten oikeassakin tietokoneessa, suoritettava ohjelma on osa koneen tilaa. Samoin tilaan sisältyy ohjelmassa kulloinkin näkyvissä olevien muuttujien arvot (vrt. denotationaalisen semantiikan ympäristö-käsite). Joissakin koneissa tilaan sisältyy myös muuta. Tiloja on kahdenlaisia: välitiloja (engl. nonterminal state) ja päätöstiloja (engl. terminal state). Välitila on sellainen tila, jossa suoritettavaa on vielä, päätöstilassa ohjelman suoritus on päättynyt. Niinpä päätöstilaan ei sisälly ohjelmaa. Seuraavassa on yksinkertainen esimerkki siitä, miten suoraviivaohjelman x x + 1; y y + x suoritus voisi tällaisessa koneessa edetä, kun 65
66 LUKU 6. LYHYTASKELSEMANTIIKKA x:n alkuarvo on 5 ja y:n alkuarvo on 7: ( x x + 1; y y + x, (x, 5), (y, 7)}) ( y y + x, (x, 6), (y, 7)}) (x, 6), (y, 13)} Tässä siis välitilat ovat muotoa Γ N = Stmt Env ja päätöstilat muotoa Γ T = Env. Kaikkien tilojen joukkoa merkitään Γ = Γ N Γ T. Matemaattisesti ajatellen tilojen väliset siirtymät muodostavat relaation ( ) Γ N Γ. 6.2 Päättely Plotkinin keskeinen idea oli määritellä tilakoneen toiminta päättelyjärjestelmänä (engl. inference system). Se koostuu päättelysäännöistä (engl. inference rules), jotka ovat muotoa γ 1 γ 2 γ 3 γ 4 Tällainen päättelysääntö määrittelee, että siirtymä γ 3 γ 4 (eli säännön johtopäätös) on sallittu, jos siirtymä γ 1 γ 2 (eli säännön premissi) on sallittu. Useinkaan säännöllä ei ole yhtään premissiä, mutta useampi kuin yksi on harvinaista. Joskus säännölle annetaan reunaehtoja (engl. side conditions), joiden pitää olla tosi jotta sääntöä saisi käyttää. Päättelyjärjestelmässä on voimassa ns. suljetun maailman sääntö: jos jotain siirtymää ei salli mikään annettu sääntö, kyseinen siirtymä on kielletty. 6.3 While-kieli Suoraviivaohjelman lyhytaskelsemantiikka voidaan määritellä seuraavasti: ( x e, σ) σ (x, E e σ)} ( x e, σ) σ (x, B e σ)} ( s, σ) σ ( s ; t, σ) ( t, σ ) E e σ = E (6.1) B e σ = E (6.2) (6.3)
6.4. EPÄDETERMINISMI 67 ( s, σ) ( s, σ ) ( s ; t, σ) ( s ; t, σ ) (6.4) Tässä on yksinkertaisuuden vuoksi otettu käyttöön lausekkeiden denotationaalisesti määritellyt semanttiset funktiot B ja E. While-lauseen lyhytaskelsemantiikka on yksinkertainen: ( while e do s, σ) ( s ; while e do s, σ) B e σ = t (6.5) ( while e do s, σ) σ B e σ = f (6.6) Lausekkeidenkin laskeminen voitaisiin määritellä esimerkiksi seuraavaan tyyliin: ( y e, σ) σ (y, c)} ( x e + f, σ) ( x c + f, σ) ( y f, σ) σ (y, c)} ( x e + f, σ) ( x e + c, σ) σ(y) e N (6.7) σ(y) e N (6.8) ( x c + c, σ) σ (x, c + c )} (6.9) 6.4 Epädeterminismi Edellä annettu lausekkeiden lyhytaskelsemantiikka on jo epädeterministinen, koska se sallii yhteenlaskun operandien laskemisen kummassa järjestyksessä tahansa. Tämä epädeterminismi on kuitenkin mielenkiinnotonta, koska sillä ei voi olla vaikutusta ohjelman lopputulokseen. Todellinen epädeterminismi ilmenee vahtikomennoissa. Nyt tilat määritellään seuraavasti: Γ N = F} ((Stmt GuardedStmt) Env) Γ T = Env Tässä F merkitsee suorituskelvottomaksi todettua vaihtoehtoa. Lyhytaskelsemantiikka: ( g, σ) ( s, σ ) ( do g, σ) ( s; do g, σ ) ( g, σ) F ( do g, σ) σ (6.10) (6.11)
68 LUKU 6. LYHYTASKELSEMANTIIKKA ( g, σ) ( s, σ ) ( if g, σ) ( s, σ ) (6.12) ( e s, σ) ( s, σ) B e σ = t (6.13) ( e s, σ) F ( g 1, σ) ( s, σ ) ( g 1 g 2, σ) (s, σ ) ( g 2, σ) ( s, σ ) ( g 1 g 2, σ) (s, σ ) ( g 1, σ) F ( g 2, σ) F ( g 1 g 2, σ) F B e σ = f (6.14) (6.15) (6.16) (6.17) Jos kone on välitilassa ja on useampi kuin yksi sääntö, jota voidaan soveltaa, kone valitsee niistä jonkin. Jos kone on välitilassa eikä ole yhtään sovellettavissa olevaa sääntöä, kone jumiutuu (engl. becomes stuck), mikä aina ilmentää ohjelmassa taikka semantiikan määritelmässä olevaa virhettä. 6.5 Siirräntä Siirräntä eli viestintä ulkomaailman kanssa vaatii siirtymärelaation muokkaamista. Ensin kuitenkin laajennetaan kieli sisältämään syöte- ja tulostelauseet: c N x, y, z Var e, f Expr s, t Stmt s, t ::= x e s ; t if e then s else t while e do s read x write e Ideana on, että sellainen siirtymä, johon liittyy tulostusta tai lukemista, laputetaan kyseisellä tapahtumalla. Näin siirtymärelaatio onkin ( ) Γ N Γ T L, missä L = ɛ}?c c Q }!c c Q } on lappujen joukko. Lappu ɛ tarkoittaa, että siirräntää ei ole (ja se jätetäänkin yleensä
6.5. SIIRRÄNTÄ 69 kirjoittamatta näkyviin); huutomerkkilappu tarkoittaa tulostusta ja kysymysmerkkilappu tarkoittaa lukemista. Metamuuttujaa λ käytetään ilmaisemaan mielivaltaista lappua. Lappu kirjoitetaan tavallisesti nuolen päälle. Siirräntälauseiden lyhytaskelsemantiikka on seuraavanlainen: ( read x, σ)?c σ (x, c)} ( x e, σ) σ (x, c)} ( write e, σ)!c σ Samalla pitää peräkkäistys kirjoittaa uudestaan: (6.18) σ(x) (6.19) ( s, σ) λ σ ( s ; t, σ) λ ( t, σ ) ( s, σ) λ ( s, σ ) ( s ; t, σ) λ ( s ; t, σ ) (6.20) (6.21) Esimerkiksi ohjelma read x; x x + 1; write x käyttäytyy seuraavasti (kun syötteenä on 42): ( read x; x x + 1; write x, )?42 ( x x + 1; write x, (x, 42)}) ( write x, (x, 43)})!43 (x, 43)}
70 LUKU 6. LYHYTASKELSEMANTIIKKA