Logiikkaohjelmien kääntäminen Tommi Syrjänen 44089L 4 helmikuuta 2002 Tiivistelmä Logiikkaohjelmointikielten suora tuki epädeterministiselle haulle aiheuttaa erityisvaatimuksia niiden kääntäjille Logiikkaohjelmointijärjestelmän täytyy osata tehdä ja peruuttaa valintoja tehokkaasti Tässä työssä esitetään perusteet Prolog-pohjaisen kielen kääntämisestä virtuaalikoneella ajettavaksi tavukoodiksi 1 Johdanto Logiikkaohjelmointikielen toteutus poikkeaa suuresti perinteisen imperatiivisen kielen toteutuksesta Suurin syy tähän on kielen suora tuki epädeterministisille valinnoille Käyttäjän antamaa kyselyä ratkaistessaan logiikkaohjelmointijärjestelmä voi joutua valitsemaan kahden tai useamman vaihtoehdon välillä, ja väärän valinnan jälkeen korjaamaan tilanteen sekä jatkamaan toisen vaihtoehdon käsittelyä Tässä työssä käsitellään Prolog-kielen pienen osajoukon kääntämistä virtuaalikoneen päällä toimivaksi tavukoodiksi Kappaleessa 2 esitellään kieli tarkemmin, mutta alla on esitetty muutamia kielen tärkeimpiä eroja verrattuna perinteisiin ohjelmointikieliin: Ohjelmoija ei käytä tavallisia algoritmeja, vaan määrittelee ongelman käyttäen loogisia päättelysääntöjä Laskenta on kyselypohjaista, eli käyttäjä antaa haluamansa johtopäätöksen, ja järjestelmä yrittää sitten todistaa sen käyttämällä ohjelmassa esiintyviä sääntöjä Mikäli jossain vaiheessa laskentaa voidaan soveltaa useampaa kuin yhtä sääntöä, järjestelmä käyttää ensin yhtä niistä Mikäli tämä ei johda todistukseen, järjestelmä automaattisesti peruuttaa valinnan ja soveltaa seuraavaa sääntöä Globaaleja muuttujia ei ole, vaan kunkin muuttujan vaikutusalueena on vain se sääntö, jossa se esiintyy Mikäli sama muuttuja esiintyy kahdessa eri säännössä, se nimetään automaattisesti uudelleen toisessa niistä 1
Tässä työssä ei käsitellä kääntäjäoptimointeja, kuten häntärekursion poistoa tai suoritettujen kyselyiden tallettamista hakemistorakenteisiin (indexing) Lisäksi tarkastellaan ainoastaan ohjelmia, joissa ei esiinny negaatioita missään muodossa 2 Logiikkaohjelmointi Logiikkaohjelman syntaksi muodostuu termeistä, predikaateista sekä säännöistä Termi on joko vakio, muuttuja tai funktio Muuttujattomat termit muodostavat ohjelman universumin Predikaatit esittävät relaatioita universumin alkioiden kesken, ja säännöt määrittelevät milloin predikaatit ovat tosia ja milloin epätosia Literaalit ovat muotoa 1 : p(t 1,, t n ), (1) missä p on predikaatti ja t 1,, t n ovat termejä Literaali voi olla joko tosi tai epätosi Esimerkki 1 Olkoon ohjelman universumi luonnollisten lukujen joukko N Tarkastellaan predikaattia plus(x, Y, Z), jonka tulkintana on tavallinen yhteenlasku x + y = z Tällöin literaali plus(1, 2, 3) on tosi, kun taas plus(1, 2, 5) on epätosi Literaali plus(1, X, 3) on tosi, mikäli muuttujan X arvo on 2, muuten se on epätosi Ohjelman säännöillä määritellään predikaattien tulkinta, eli käytännössä kerrotaan, milloin literaalit ovat tosia Sääntö on muotoa: h a 1,, a n, (2) missä literaali h on säännön pää, ja literaalit a 1,, a n muodostavat säännön vartalon Intuitiivisesti sääntö (2) tarkoittaa, että mikäli a 1,, a n ovat kaikki tosia, niin myös h:n täytyy olla tosi Sääntö voidaan tulkita myös proseduurina: literaalin h todistamiseksi täytyy ensin todistaa a 1, sitten a 2, jne aina literaaliin a n asti Esimerkki 2 Seuraavat kaksi sääntöä määrittelevät predikaatin member(x, Y ), joka on tosi, mikäli alkio X kuuluu listaan Y : member(x, [X, R]) member(x, [Z, R]) member(x, R) Perinteiset logiikkaohjelmointijärjestelmät ovat kyselypohjaisia, eli ohjelman käyttäjä antaa syötteeksi atomin h, ja päättelykone yrittää johtaa h:n käyttäen ohjelman sääntöjä Käytännössä kone käy sääntöjä läpi yksi kerrallaan kunnes löytää säännön, jonka päässä oleva atomi h unifioituu atomin h kanssa, eli on olemassa tapa σ korvata h:n ja h :n muuttujat siten, että σ(h) = σ(h ) 1 Tarkkaan ottaen literaali voi olla myös muotoa p(t 1,, t n) tai not p(t 1,, t n), mutta tässä työssä käsitellään ainoastaan positiivisia ohjelmia, joissa ei negaatioita esiinny 2
Kun unifioitava sääntö löytyy, kone yrittää yksi kerrallaan todistaa tämän vartalossa olevia literaaleja Näin jatketaan, kunnes joko todistus valmistuu tai joudutaan tilanteeseen, missä sopivaa sääntöä ei enää löydy Jälkimmäisessä tapauksessa järjestelmä peruuttaa viimeiseksi käytetyn säännön ja yrittää valita uuden, todistukseen ehkä paremmin sopivan säännön Esimerkki 3 Esitetään esimerkin 2 ohjelmalle kysely member(a, [b, a, c])? Ensimmäisen säännön päänä oleva atomi member(x, [X, R]) ei unifioidu tämän kanssa, sillä a b Jälkimmäinen sääntö unifioituu muuttujasidonnalla {X = a, Z = b, R = [a, c]}, joten seuraavaksi kyselyksi otetaan member(a, [a, c]) Tämä puolestaan unifioituu ensimmäisen säännön kanssa ja todistus on valmis 3 Virtuaalikone Logiikkaohjelmien kääntämisessä kohdeympäristönä on yleensä jokin virtuaalikone Tämä siksi, että tavallisissa tietokonearkkitehtuurissa ei ole suoraan tuettu logiikkaohjelmien erityispiirteitä, kuten unifiointia Tässä työssä esitellään WiM-virtuaalikone, joka on yksinkertaistettu versio Prolog-kielen toteutuksissa käytetystä Warren Abstract Machine-koneesta (WAM) Tärkeimpinä rajoitteina WiM tukee vain positiivisia ohjelmia eikä siinä ole kääntäjäoptimointeja WiM-koneen muistiavaruus jakautuu neljään osaan: ohjelmakoodi (program store P S), pino (stack ST ), kasa (heap H) ja polku (T R) Koodialueeseen talletetaan ohjelma tavukoodina, pinoa käytetään laskennan aikana sääntöjen kehysten tallentamiseen, kasaan talletetaan termit ja polkuun tallennetaan laskennan aikana tehdyt muuttujasidonnat Rekistereitä koneessa on seitsemän: P C osoittaa kulloinkin ajettavaan käskyyn (P S[P C]) SP osoittaa pinon seuraavaan vapaaseen alkioon (ST [SP ]) F P osoittaa viimeisimmän pinokehyksen alkuun (ST [F P ]) HP osoittaa kasan seuraavaan vapaaseen alkioon (H[HP ]) T P osoittaa polun seuraavaan vapaaseen alkioon (T R[T P ]) BT P osoittaa viimeisimpään peruutuspisteeseen (ST [BT P ]) mode-rekisteriä käytetään unifioinnin aikana osoittamaan muuttujan tilaa Kone suorittaa ajon aikana jatkuvasti seuraavaa silmukkaa: 1 Lataa käsky I = P S[P C] 2 P C := P C + 1 3 Suorita I 3
vakio a: ATOM a sidottu muuttuja: REF vapaa muuttuja: REF funktio f(t 1,, t n ): STRUCT f/n Kuva 1: Tietorakenteet Logiikkaohjelma käännetään tavukoodiksi siten, että jokaisesta predikaatista ja säännöstä tulee oma proseduurinsa Predikaatin p proseduuri kutsuu järjestyksessä kaikkien sääntöjensä proseduureja Säännön proseduurin aluksi unifioidaan kysely säännön pään kanssa, ja mikäli tämä ei onnistu peruutetaan Muussa tapauksessa käydään järjestyksessä läpi kaikki säännön vartalossa olevat literaalit, ja kutsutaan niiden predikaattien proseduureja 4 Tietotyypit ja pinokehys Ohjelmassa esiintyvien termien (vakiot, muuttujat ja funktiot) käsittelyssä käytetään apuna kaikkia kolmea muistiavaruutta Termit luodaan kasaan tyyppiobjekti-pareina kuvissa 1 ja 2 esitettyyn tapaan, ja uuden termin osoite talletetaan aina suoraan kulloiseenkin pinokehykseen Polkuun merkitään muistiin muuttujien sitomisajankohdat Perusrakenteeltaan kehys on hyvin samanlainen kuin proseduraalisissa kielissä, tärkeimpänä erona on unifioinnin ja peruutuksen vaatimat lisäkentät Kuvassa 3 esitetään kehyksen yleinen rakenne Kenttien merkitys on seuraavanlainen: jatko-osoite osoite, josta ohjelman suorittamista jatketaan, mikäli kysely onnistuu F P old osoitin edellisen pinokehyksen alkuun t 1,, t n osoittimet säännön päässä olevan literaalin argumentteihin lokaali ympäristö osoittimet säännössä esiintyviin muuttujiin lokaali pino ylimääräinen tila, jota käytetään unifioinnin apuna 4
STRUCT: f/3 REF STRUCT: g/2 REF ATOM a REF Kuva 2: Funktiota f(g(x, Y ), a, Z) vastaava rakenne BT P old, T P old, HP old, peruutusosoite peruutuksen toteuttamiseen käytettäviä kenttiä Kutsuttaessa säännön proseduuria kutsuja asettaa argumenttien t i paikoilleen, muuten kehyksen luo kutsuttu proseduuri arvot 5 Termit ja kyselyt Tarkastellaan ensin yksittäisen kyselyn kääntämistä tavukoodiksi Aluksi määritellään joukko funktioita code A( x, ρ), jotka luovat kasaan uuden termin x sekä asettavat tarvittaessa paikalliseen ympäristöön ρ osoittimen uuteen ter- Käsky Merkitys Muuta putatom a SP := SP + 1; vakio ST [SP ] := new(at OM : a); putvar i SP := SP + 1; vapaa muuttuja ST [F P + i] := new(ref : HP ); ST [SP ] := ST [F P + i]; putref i SP := SP + 1; sidottu muuttuja ST [SP ] := ST [F P + i]; putstruct f/n SP := SP n + 1; ST [SP ] := new(st RUCT : f/n, ST [SP ],, ST [ST + n 1]); Taulukko 1: Termien luominen 5
F P jatko-osoite F P old BT P old T P old HP old peruutusosoite t 1 t 2 tilasolut argumentit SP t n l ymäristö l pino paikalliset muuttujat Kuva 3: Pinokehyksen rakenne Käsky Merkitys Muuta enter SP := SP + 6; tilasolut ST [SP 4] := F P kehysosoitin talteen call p/n F P := SP (n + 4); F P uuteen kehykseen ST [F P 1] := P C; paluuosoite P C := Addr(Code(p/n)) Taulukko 2: Proseduurikutsut miin Ainoan poikkeuksen tästä säännöstä muodostavat muuttujat, sillä ainoastaan muuttujan X ensimmäiselle esiintymälle luodaan kasaobjekti, ja sen kaikki muut esiintymät X asetetaan osoittamaan samaan objektiin Taulukossa 1 esitellään funktioissa käytetyt käskyt, ja itse funktiot ovat: code A(a, ρ) = putatom a vakio code A(X, ρ) = putvar ρ(x) ensimmäinen esiintymä code A(X, ρ) = putvar ρ(x) ensimmäinen esiintymä code A( X, ρ) = putref ρ(x) seuraavat esiintymät code A(f(t 1,, t n ), ρ = code A(t 1, ρ) funktio code A(t n, ρ) putstruct f/n 6
Kyselyn kääntäminen tapahtuu hyvin suoraviivaisesti, ja käytännössä luodaan ainoastaan uusi proseduurikutsu Aluksi varataan pinosta tila kutsutun proseduurin argumenteille ja tilasoluille, minkä jälkeen asetetaan argumentit paikoilleen ja kutsutaan proseduuria: code G(p(t 1,, t n ), ρ) = enter code A(t 1, ρ) 6 Unifiointi code A(t n, ρ) call p/n Epäilemättä työläin ja monimutkaisin osa logiikkaohjelmien kääntämisessä on unifioinnin toteuttaminen Selvyyden vuoksi ja tilanpuutteen takia tässä työssä ei esitetä unifioinnin kaikkia yksityiskohtia vaan pidättäydytään yleisellä tasolla Unifioinnin tarkoituksena on selvittää, voidaanko kaksi literaalia muuttaa samoiksi korvaamalla niiden muuttujat sopivasti Muuttuja X voidaan korvata joko toisella muuttujalla Y, vakiolla a tai funktiolla f(t 1,, t n ) olettaen, että X ei esiinny f:n argumenteissa 2 Esimerkki 4 Literaalit p(x, a, f(g, Z)) ja p(b, Z, f(y, a) ovat unifioituvia, ja niiden yleisin unifioija σ = {X = b, Y = g, Z = a} Sitä vastoin q(a, X) ja q(x, b) eivät unifioidu, sillä ei ole mahdollista, että X = a = b Logiikkaohjelmoinnin tapauksessa halutaan selvittää, voidaanko kysely p(t 1 ) unifioida säännön pään p(t 2 ) kanssa Kyselyn käännös nähtiin yllä kohdassa 5 Säännön pää p(t 2 ) käännetään käskyjonoksi, joka yrittää unifioida termit t 1 ja t 2 Mikäli unifiointi ei onnistu, peruutetaan hyppäämällä peruutusproseduuriin backtrack Muussa tapauksessa käsitellään säännön vartalo Kuten esimerkistä 4 huomataan, voi sekä t 1 :ssä että t 2 :ssa olla vapaita muuttujia Näin ollen täytyy unifikaatiokoodin osata hoitaa myös t 1 :n muuttujien sitominen tarvittaessa Tämä toteutetaan käyttämällä erillisiä luku- ja kirjoitustiloja, joiden hallintaan käytetään mode-rekisteriä Lukutilassa luetaan termin t 1 seuraava alitermi, ja sidotaan vastaava t 2 :n alitermi siihen Kirjoitustilassa puolestaan sidotaan t 1 :n vapaa muuttuja X vastaavaan t 2 :n alitermiin Aina, kun muuttuja sidotaan, käydään samalla merkitsemässä polkuun tieto tästä Tämä siksi, että muuten ei peruutettaessa voitaisi poistaa oikeita muuttujasidontoja Unifikaatioalgoritmi on esitetty kuvassa 4 pseudokoodina 7 Säännöt ja proseduurit Sääntöjen kääntäminen on varsin suoraviivaista Ensin täydennetään pinokehys valmiiksi sekä asetetaan peruutuspiste mikäli tarpeellista Tämän jälkeen 2 Useimmat Prolog-toteutukset jättävät tehokkuussyistä viimeisen ehdon tarkistamatta 7
unifyvar(x, t) = if bound(x) then unify(t, ref (X)) else bind(x, t) fi unify(t 1, t 2 ) = case t 1 of atom a: case t 2 of atom a : if a a then goto backtrack fi variable X: unifyvar(x, t 1 ) function f/n: goto backtrack esac variable X: unifyvar(x, t 2 ) function f(x 1,, x n ): case t 2 of function f(y 1,, y n ): for i := 1 to n do unify(x i, y i ) od variable X: unifyvar(x, t 1 ) def ault: goto backtrack esac esac Kuva 4: Unifikaatioalgoritmi suoritetaan unifiointi argumentti kerrallaan, ja mikäli se onnistuu kutsutaan kaikkia vartalossa olevia literaaleja kyselyinä Lopuksi siivotaan pino Sääntö C: p(t 1,, t n ) g 1,, g m käännetään seuraavasti: Käsky Merkitys Muuta pushenv k SP := F P + k; pusharg i SP := F P + 1; ST [SP ] := ST [F P + 4 + i]; Taulukko 3: Ympäristöjen luominen 8
Käsky Merkitys Muuta setbtp l ST [F P + 1] := BT P ; ST [F P + 2] := T P ; ST [F P + 3] := HP ; ST [F P + 4] := l; peruutusosoite BT P := F P ; uusi peruutuspiste nextalt l ST [F P + 4] := l; seuraavan vaihtoehdon paikka delbtb BT P := ST [F P + 1]; poistetaan peruutuspiste Taulukko 4: Peruutuspisteiden luominen code C(C, btparam, btcont) = pushenv n + r + 4; tila argumenteille btinit btparam btcont; peruutuspiste pusharg 1; unifioidaan ensimcode U(t 1, ρ); mäinen argumentti pusharg n; code U(t 1, ρ); code G(g 1, ρ); code G(g m, ρ); fin btparam; where ρ = [X i n + i + 4] r i=1 unifioidaan viimeinen argumentti ensimmäinen kysely viimeine kysely loppusiivous Tässä parametri btparam määrittelee säännön paikan predikaatin määrittelyssä ja btcont on osoite, josta jatketaan mahdollisen peruutuksen jälkeen Peruutuksen tominta on selitetty tarkemmin seuraavassa kappaleessa 8 Peruutus Laskennan aikana voidaan joutua peruuttamaan kahdesta eri syystä: unifioinnin epäonnistuessa tai haluttaessa laskea useampia kuin yksi vastaus Tällöin laskenta pitää palauttaa takaisin viimeisintä valintaa edeltäneeseen tilanteeseen Aina, kun laskennan kestäessä kutsutaan kyselyä, jolla on useita vaihtoehtoisia sääntöjä, luodaan peruutuspiste Peruutuspisteessä talletetaan edellisen pisteen paikka, kasa- ja polkuosoittimien arvot sekä seuraavan vaihtoehtoisen säännön koodin alkukohta Mikäli pisteeseen peruutetaan, muut arvot pysyvät ennallaan, mutta seuraava vaihtoehto päivitetään uuteen Piste tuhotaan heti viimeistä vaihtoehtoa kutsuttaessa Peruutuspiste luodaan käyttämällä käyttämällä funktiota btinit: btinit btparam btcont = case btparam of first: setbtp btcont; 9
middle: nextalt btcont; last: delbtp; single: esac Tässä btparam kertoo säännön sijainnin predikaatin määritelmässä: onko se ensimmäinen, viimeinen, ainoa vai keskellä Mikäli sääntö on predikaatin ainoa, ei peruutuspistettä luoda Itse peruutus tapahtuu hyppäämällä ohjelman globaaliin backtrack-nimiöön, joka on määritelty seuraavasti: backtrack: F P := BT P ; HP := ST [F P + 3]; reset(st [F P + 2], T P ); T P := ST [F P + 2]; P C := ST [F P + 4]; Tässä reset on funtio, joka poistaa sidonnat kaikilta muuttujilta, jotka ovat saaneet arvonsa peruutuspisteen asettamisen jälkeen, ja se näyttää seuraavalta: reset(tpu, tpo) = for i := tpu to tpo do H[T R[i]] := i (???) od 9 Viimeiset silaukset Enää on jäljellä säännön kutsumisen onnistunut päättäminen sekä predikaatin sääntöjen yhdistäminen yhdeksi proseduuriksi Koska käyttäjä voi haluta laskea useamman kuin yhden vastauksen, ei predikaatin kehystä voida poistaa ennen kuin kaikki peruutusvaihtoehdot käyty läpi Niinpä funktio fin määritellään seuraavasti: fin = case btparam of last, single: popenv; first, middle: restore; esac Predikaatin P R säännöt C 1,, C n yhdistetään proseduuriksi: code PR(C 1,, C n ) = code C(C 1, first, l 2 ) l 2 : code C(C 2, middle, l 3 ) l n : code C(C n, last, 0) 10
Käsky Merkitys Muuta popenv iff P > BT P voidaanko kehys poistaa? thensp := F P 2 vapautetaan tila fi; P C := ST [F P 1]; F P := ST [F P ]; restore P C := ST [F P 1]; F P := ST [F P ]; Taulukko 5: Ympäristöjen siivoaminen 10 Yhteenveto Työssä esiteltiin WiM-virtuaalikonearkkitehtuuri, jolla voidaan suorittaa logiikkaohjelmakyselyitä Lisäksi esitetiin perusperiaatteet positiivisten logiikkaohjelmien kääntämiseksi WiM-koneelle Yksittäiset päättelysäännöt käännetään proseduureiksi, joiden aluksi tarkistetaan, unifioituuko säännön pää kyselyn kanssa Jos näin käy, tehdään säännön vartalon literaaleista yksi kerrallaan uusia kyselyitä Mikäli unifiointi epäonnistuu jossain vaiheessa, kutsutaan peruutusproseduuria, joka poistaa viimeisen epäonnistuneen valinnan ja tekee sen tilalle uuden Peruutuksessa käytetään apuna polku-muistialuetta, johon tallennetaan tiedot siitä, milloin mikäkin muuttuja on saanut arvonsa Työn aihepiirin ulkopuolelle jäi optimointien ja negaation toteuttaminen Tästä aiheesta on julkaistu paljon kirjallisuutta, ja hyvä lähtökohta aiheeseen tutustumiseen on vapaasti saatavilla oleva XSB-logiikkaohjelmointijärjestelmä ja sen dokumentaatio [1] Viitteet [1] W Chen and D S Warren Tabled evaluation with delaying for general logic programs Journal of the Association for Computing Machinery, 43:20 74, 1 1996 [2] Leon Sterling and Ehud Shapiro The Art of Prolog, 2nd ed MIT press, 1994 [3] Reinhard Wilhelm and Dieter Maurer Compiler Design Addison-Wesley Publishing Company, 1995 11