Kääntäjätekniikka
Luento 20 (30.5.2017)
Funktionaalinen lähdekieli
ML/Haskell/Lisp/...
- tietorakenteet ovat rekursiivisia puita
- atomidata (kokonaisluvut ym)
- puukonstruktorit
- Lispissä NIL ja CONS, ML:ssä ja Haskellissa ohjelmoijan määriteltävissä
- kuin funktioita mutta vain säilyttävät argumenttinsa tekemättä niille mitään
- rekursiivinen hahmonsovitus ja paloittainen määrittely (ei Lispissä)
- jos hahmo ei sovitu, testataan seuraavaa
- funktiot ovat käsiteltävissä datana
- funktio voidaan tallentaa muuttujaan, antaa parametrina funktiolle tai palauttaa funktiosta paluuarvona
- funktioita voidaan luoda toisten funktioiden sisällä ja ne voivat viitata ulompien funktioiden paikallisiin muuttujiin
- muuttujiin ei voi sijoittaa (Haskell ja ML)
- sen sijaan voidaan luoda muuttuvia tieto-olioita
Funktio-ohjelman kääntämisen vaiheet
- sokerinpoisto
- hahmonsovituksen yksinkertaistus (pattern match compilation)
- konstruktorikutsujen normalisointi
- funktiomäärittelyiden sisäkkäistyksen poisto (lambda lifting tai closure conversion)
- funktionaaliselle välikielelle (esim. STG) kääntäminen
- konventionaaliselle välikielelle (LLVM IR, C, kolmiosoitekoodi) kääntäminen
- ... loppuosa kuten tavallisissa kääntäjissä ...
Hahmonsovituksen kääntäminen
Idea:
case E of
C p1 p2 p3 -> E1
C p4 p5 p6 -> E2
D p7 -> E3
D p8 -> E4
_ -> E5
(olettaen että p1 ja p4 ovat erillisiä, samoin p7 ja p8) käännetään muotoon
case E of
C x1 x2 x3 -> case x1 of
p1 -> case x2 of
p2 -> case x3 of
p3 -> E1
_ -> E5
_ -> E5
p4 -> case x2 of
p5 -> case x3 of
p6 -> E1
_ -> E5
_ -> E5
_ -> E5
D x4 -> case x4 of
p7 -> E3
p8 -> E4
_ -> E5
_ -> E5
Hahmonsovituksen vaikeus
- Algoritmi on suoraviivainen jos hahmot ovat erillisiä ja kattavia
- hahmot ovat erillisiä jos mikään arvo ei sovi useampaan kuin yhteen hahmoon
- hahmojoukko on kattava jos jokainen arvo sopii johonkin hahmoon
- Erillisyydestä luopuminen tekee kääntämisestä haastavaa
- Ks. tarkemmin Philip Wadler: Efficient Compilation of Pattern-Matching. Luku 5 (s. 78–103) kirjassa The Implementation of Functional Programming Languages (pääkirjoittaja Simon L. Peyton Jones); Prentice-Hall 1987. PDF saatavilla
Konstruktorikutsujen normalisointi
Jos on määritelty
data T = C U V W
niin jokainen C
lähdekielisessä lausekkeessa pitää muuttaa muotoon, jossa sillä on kaikki tarvittavat argumentit (tehdään \(\eta\)-lavennus):
(\ x y z -> C x y z)
Näin konstruktorien osittaisapplikaatio muuttuu funktioiden osittaisapplikaatioksi.
Lambda lifting
- Sisäkkäiset funktiomäärittelyt voidaan poistaa tekemällä ns. lambda lifting
- toisen funktion sisällä määritelty funktio siirretään toplevel-määrittelyksi
- funktion käyttämät ulompien funktioiden paikalliset muuttujat muutetaan sen parametreiksi
f x = let g y = x + y in g
==>
f_g x y = x + y
f x = f_g x
Sulkeumamuunnos
- Vaihtoehtoisesti funktioarvo voidaan esittää sulkeumana (closure)
- sulkeuma sisältää
- kutsuosoitteen (entry point address) ja
- kopiot funktion vapaiden muuttujien arvoista
- funktiolle pitää kutsuttaessa välittää osoite sen sulkeumaan (ns. static link)
- sulkeuma sisältää
Jos paikallisiin muuttujiin voi sijoittaa
- Jos paikallisen muuttujan arvo voi muuttuja alustuksen jälkeen, välitetään arvon sijasta osoite muuttujaan
- tällaiset muuttujat tulee kääntäjän sijoittaa muistisiivouksen alaiseen kekoon
- ns. escape analysis
- koskee sekä lambda liftingiä että sulkeumamuunnosta
- tällaiset muuttujat tulee kääntäjän sijoittaa muistisiivouksen alaiseen kekoon
Lambda lifting ja sulkeumamuunnos ovat melko sama asia
Tarkastellaan esimerkkiä
f x = let g y = x + y in g
Lambda lift tuottaa muodon
f_g x y = x + y
f x = g x
Sulkeumamuunnos tuottaa muodon
f x = let g = \[x] y -> x + y in g
Lausekkeen f 3
tuloksena on kummassakin tapauksessa muistiolio, jossa on tallennettuna 3 ja g:n kutsuosoite
- lambda liftin tapauksessa ns PAP, eli partial application
- sulkeumamuunnoksen tapauksessa sulkeuma
Spineless Tagless G-machine (STG)
- Simon L. Peyton Jones and Jon Salkild: The spineless tagless G-machine. In FPCA '89 Proceedings of the fourth international conference on Functional programming languages and computer architecture, 184-201, 1989. doi:10.1145/99370.99385
- Simon L. Peyton Jones: Implementing lazy functional languages on stock hardware: the Spineless Tagless G-machine. Journal of Functional Programming 2 (2), 127–202, 1992. doi:10.1017/S0956796800000319
- Simon Marlow and Simon Peyton Jones: Making a fast curry: push/enter vs. eval/apply for higher-order languages. Journal of Functional Programming 16 (4–5), 415–449, 2006. doi:10.1017/S0956796806005995 JYU access
- Simon Marlow, Alexey Rodriguez Yakushev, and Simon Peyton Jones: Faster Laziness Using Dynamic Pointer Tagging. In ICFP '07 Proceedings of the 12th ACM SIGPLAN international conference on Functional programming, 277–288, 2007. doi:10.1145/1291151.1291194
- Alberto de la Encina and Ricardo Peña: From natural semantics to C: A formal derivation of two STG machines. Journal of Fuctional Programmming 19 (1), 47–94, 2009. doi:10.1017/S0956796808006746 JYU access
STG:n peruskieli
- Lausekkeita ovat
- muuttuja
- vakio
- funktiokutsu, jossa funktio ja argumentit ovat muuttujia tai vakioita
- primitiivioperaatiot (esim. yhteenlasku)
- rekursiivinen let, joka luo paikallisia muuttujia
- case, jolla tehdään (ei-rekursiivinen) hahmonsovitus
- Let-lausekkeessa ja toplevelissä muuttuja voidaan alustaa seuraavilla konstruktioilla:
- funktiomäärittely
- konstruktorikutsu, jossa on annettu oikea määrä parametreja
- lauseke
- Olennaisesti siis kuten kolmiosoitekoodi, mutta sisäkkäisellä lohkorakenteella
- muistivaraus tapahtuu aina let-lausekkeessa (tai toplevelissä)
Laiska laskenta vs innokas laskenta
Laiskassa laskennassa (lazy evaluation) on kaksi olennaista komponenttia:
- kun lauseke sidotaan muuttujaan, sitä ei vielä tässä vaiheessa lasketa
- muuttujaan sidotaan ns. thunk, joka on olennaisesti parametriton funktiosulkeuma
- kun muuttujan arvo halutaan selvittää, pitää tarkistaa, onko muuttujassa valmiiksi laskettu arvo vai thunk
- kun thunk on laskettu, se korvataan laskennan tuloksella
- hyödyllistä korvata thunk laskennan ajaksi erillisellä väliaikaisarvolla (blackhole)
- jos muuttujasta löydetään blackhole, tarkoittaa se ikuista silmukkaa
- hyödyllistä korvata thunk laskennan ajaksi erillisellä väliaikaisarvolla (blackhole)
Innokkaassa laskennassa (eager evaluation) lausekkeen arvo lasketaan loppuun ennen muuttujaan sijoittamista
Miksi STG oli tagless mutta ei enää
- Jos ohjelmassa voi esiintyä laiskaa laskentaa, pitää joka kerta muuttujan arvoa selvittäessä varmistaa, onko muuttujassa thunk ja jos näin on, pitää sen arvo laskea
- perusidea tallettaa joka arvo-olioon tagi, joka kertoo onko se thunk vai ei
- jos on, kutsutaan thunkin laskentakoodia
- Alkuperäisessä STG:ssä tagi ja sen testaus korvattiin seuraavasti:
- joka arvolla on entry point
- jos arvo on laskettu, sen koodi vain palauttaa arvon
- joka muuttujan haku aiheuttaa epäsuoran kutsun
- Nykyprosessoreilla tagin tarkistus on nopeampaa kuin epäsuora kutsu, joten STG ei ole enää tagless
Push/enter vs eval/apply
Tarkastellaan seuraavaa kutsua:
f a b c
Nyt f
voi olla määritelty esimerkiksi
f x y = g
g z = z
jolloin kutsu tulee kääntää t1 := CALL f (a,b); CALL t1 (c)
. Tai f
voi olla määritelty
f x y z = z
jolloin oikea kutsu on CALL f (a,b,c)
. Jos kääntäjä ei tiedä, miten f
on määritelty, sen tulee varautua molempiin (ja lisäksi myös tapauksiin, joissa f ottaa yhden tai enemmän kuin kolme parametria).
Push/enter
f a b c
käännetään
PUSH c
PUSH b
PUSH a
CALL f
Määrittely
f x y = g
g z = z
puolestaan käännetään
f: IF not enough args GOTO f0
POP # x
IF not enough args GOTO f1
POP # y
CALL g
RET
f0: Create closure for f, put to RAX
RET
f1: Create a PAP for f(x), put to RAX
RET
g: If not enough args GOTO g0
POP rax # z
RET
g0: create a closure for g, put to RAX
RET
Eval/apply
f a b c
käännetään
CALL apply_3 (f,a,b,c)
missä apply_3
on kirjastofunktio, joka tekee suunnilleen näin:
VALUE *apply_3(VALUE *f, VALUE *a1, VALUE *a2, VALUE *a3)
{
VALUE * tmp;
switch (f->args) {
case 0:
return apply_3(f->entrypoint(), a1, a2, a3);
case 1:
tmp = f->entrypoint(a1);
return apply_2(tmp, a2, a3);
case 2:
tmp = f->entrypoint(a1, a2);
return apply_1(tmp, a3);
case 3:
return f->entrypoint(a1, a2, a3);
default:
return make_PAP(f, a1, a2, a3);
}
}
Kumpi parempi?
Push/enter tuntuu yksinkertaisemmalta, mutta eval/apply on empiirisesti parempi.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.