Egy egyszerű aritmetikai kifejezésfában szerepelhetnek bináris összeadás (Plus
) és szorzás (Mult
) csomópontok valamint konstans egészek (Int
). Írjuk le egy ilyen fa formáját és kiértékelési szabályait Stratego nyelven! Használjuk a könnyű tesztelhetőség érdekében az io-wrap
adta felületet!
Használtuk a libstrategolib
könyvtárt, így a fordítási parancssor:
strc -i eval-example.str -la stratego-lib
. Az így kapott program a standard bemenetről olvas egy ATermet és a standard kimenetre, ATermként írja ki eredményét (ha minden mintaillesztés sikeres; ha nem, úgy hibakeresési információkat ír ki).
A stratego-shell
által is kipróbálható a szabályrendszer. Először adjuk meg a szabályokat és a stratégiát, majd próbáljuk ki azt közvetlenül megadott termen!
Készítsük el egy egyszerű funkcionális nyelv interpreterét. Legyen moduláris a programunk konstrukciója: alakítsuk azt ki úgy, hogy új nyelvi elemekkel könnyen bővíthessük.
A nyelvtant SDF-ben adjuk meg. Ez a nyelv a Stratego/XT részeként lehetőséget ad mind a környezetfüggetlen nyelvtan mind a lexikai elemek megadására. Egy SDF fájl elején megadjuk a modul nevét (kötelezően), az esetlegesen importált modulok listáját, majd következik az exports
szakasz. Itt a sorts
szakaszban adhatjuk meg az esetlegesen bevezetett nemterminálisokat. Ezután külön-külön szakaszokban (melyek használata mind opcionális) írhatjuk le a lexikai és a környezetfüggetlen nyelvtant. A szakaszokban a produkciós szabályokat fordított formában, "redukciós szabályként" adjuk meg. Általános formájuk: jobboldal -> baloldal {attribútumok}
Először a lexikai elemző szabályait adjuk meg. Legyen a modul neve a0lex
; ennek megfelelően a0lex.sdf
lesz e forrásfájl neve. A nyelv nem fehér szóköz-érzékeny; ezért az újsor illetve szóköz jelekhez LAYOUT
baloldalú produkciós szabályt adunk meg. Az elemzőgenerátor úgy fogja módosítani a környezetfüggetlen nyelvtan jobboldalait, hogy ott bármely két jel között megengedett nulla vagy több LAYOUT
.
Nyelvünkben megengedünk egész számokat és logikai értékeket. Lehetőséget adunk függvények névhez kötésére, rekurzív függvényhívásra (ezt külön nem jelöljük). Bevezetjük az if b t f
konstrukciót, ami b
logikai értéktől függően t
-t vagy f
-et ad vissza.
Kössük ki, hogy a kulcsszavak védett nevek; azokat azonosítóként nem használhatjuk fel. Ehhez használhatunk "tiltó produkciós szabályt".
Így még nem egyértelmű a nyelvtan. Például 12
elemezhető [IntConst("1"),IntConst("2")]
-ként és [IntConst("12")]
-ként is. Vezessünk be egyértelműsítő szabályokat. Az SDF fájl lexical restrictions
szakaszában megadhatunk többféle megkötést is. Most a -/-
operátort használjuk: a -/- b
azt jelenti, hogy a
szimbólumot b
nem követheti.
Az a0syn
modulban adjuk meg a környezetfüggetlen nyelvtan szabályrendszerét.
Használja a lexikai elemző jeleit, ezért az szerepel az importált modulok listájában.
Egy funkcionális nyelvben "minden kifejezés". Ennek megfelelően a legáltalánosabb nemterminális az Expr
.
Először bevezetjük a legegyszerűbb önálló kifejezéseket: a literálokat és a változókat.
Az egyes produkciós szabályoknak kapcsos zárójelben attribútumokat adhatunk. A cons
attribútummal jelöljük az absztrakt szintaxisfában a vonatkozó csomópont nevét. A szintaxisfa ATermként kerül feldolgozásra: minden egyes cons
-szal jelölt részfa egy konstruktoros résztermnek felel meg. A left, right, assoc, non-assoc
attribútumokkal jelölhetjük A [...] A -> A
formájú szabályok esetén az asszociativitást.
Be kell vezetni a bináris műveleteket. Ezek között prioritási relációt kell felállítani. Erre szolgál a context-free priorities
szakasz, ahol szabályokat rendezve adhatunk meg.
Speciális bináris művelet a függvényalkalmazás: itt az operátor epszilon.
Adjuk meg továbbá az if
szintaxisát. Folytassuk a szakaszt ennek megfelelően.
A let
-kifejezés megadása nem triviális. Lehetőséget kívánunk adni kölcsönösen rekurzív függvények megadására. Az egyes függvényeket Név Paraméterek = Függvénytörzs
formában szeretnénk megadni. Ezt emeljük ki egy LetDef
baloldalú szabállyá. Ha egy let
kifejezésben több függvénydefiníció van, akkor azokat az and
kulccszó választja el. Folytassuk a szabályok felsorolását.
Ezután fogjuk össze nyelvtanunk moduljait egy Main
modulba. Adjuk meg, hogy nyelvtanunk összesen mely modulokat használja. Adjunk meg kezdőszimbólumot. A program maga egyetlen kifejezés lesz.
A nyelvtant először össze kell szerkeszteni egy egységes forrásfájlba. Erre a pack-sdf
programot használjuk. E fájlnak a kiterjesztése konvenció szerint ".def".
Az összeszerkesztett fájl alapján generáljuk elemző táblázatokat az sdf2table
programmal.
Ezek után az sglri
programmal tetszőleges forrásnyelvi szöveget elemezhetünk. A pp-aterm
program az ATermet szebben formázva jeleníti meg.
Ahhoz, hogy a nyelvtan elemzése során generált ATerm-eket Stratego programmal feldolgozhassuk, ismernünk kell azok szignatúráját. Ezt nem kell kézzel végeznünk; rendelkezésünkre állnak megfelelő progamok a Stratego/XT keretrendszerben. Először az sdf2rtg
programmal a szintaxisfákat leíró reguláris fanyelvtant generálunk, majd ebből a rtg2sig
programmal Stratego nyelvű forrásfájlt generálunk.
Először alakítsuk ki az elemző általános infrastruktúráját. A program fő modulját a0i
néven nevezzük; konvenció szerint a fájl neve a0i.str
lesz. Az interpreter az előzőekben már használt io-wrap
stratégia segítségével beolvas egy ATermet a standard bemenetről, elvégzi a megfelelő transzformációkat (a program startkifejezését kiértékeli), majd az eredménnyel vagy hibát jelző válasszal leáll.
Először kibontjuk magát a startkifejezést, majd az eval
szabályt - ami a tényleges redukciót végzi - alulról felfele haladva (tehát mohó kiértékelési stratégia szerint) addig alkalmazzuk, míg csak lehet. Az innermost
stratégia a standard könyvtár része és bottomup(try(s; innermost(s)))
-ként definiáljuk. bottomup(s)
a termet alulról felfele bejárva alkalmazza s
-t; definíciója: bottomup(s)=all(bottomup(s)); s
Ha a megálláskor kapott kifejezésben vannak hibák, úgy a kiértékelés sikertelen - ezeket összegyűjtjük egy listába. Erre az összegyűjtésre a standard könyvtár bu-collect
stratégiáját használjuk. Ha ez a lista nem üres, hibajelzéssel állunk meg.
Ha sikerül konstans értékké redukálni a kifejezést, úgy magát az értéket adjuk eredményül.
A minden modul által használt szortokat, konstruktorokat, stratégiákat, importokat egy közös, a0common
nevű modulban foglaljuk össze. Importáljuk az előbb legenerált a0sorts
modult; erre mindenhol szükség lesz.
A tovább nem redukálható értékeket Const(v,t)
formájú termmel írjuk le, ahol v
a tényleges érték, t
pedig típusannotáció.
Ha egy részterm hibás, Error(s)
formájú termre cseréljük, ahol s
egy hibaüzenet.
Először valósítsuk meg az egészeken végzett műveleteket. Hozzunk létre egy a0int
modult és adjuk hozzá a0i
importjaihoz.
Az egész-literálokat konstanssá alakítjuk. A típus jelöléséül az Int()
termet használjuk. Az értéket az elemzés során kiolvasott sztring számmá alakításával kapjuk. Erre a dec-string-to-int
stratégiát használjuk.
Egyszerűen megvalósítható az összeadás, kivonás, szorzás művelete. Rendelkezésre állnak megfelelő stratégiák, amik számok rendezett párján elvégzik a megfelelő műveletet.
Az osztás és maradékszámítás művelete esetén fenn áll a zéróosztás veszélye, ezért bevezetjük a zsafe(s)
stratégiát, ami egy (a,b)
párt b=0
nulla esetén hibaüzenetre cseréle, s csak ellenkező esetben hajtja végre a paraméterül megadott s
stratégiát. Ezzel biztonságossá tehető a beépített div
és mod
stratégia.
Használtuk az if
stratégiát. Ha az első paraméterként megadott stratégia alkalmazható az aktív termre, akkor a második, különben a harmadik paraméterként megadott stratégiát alkalmazza az eredetileg aktív termre. Ezen a ponton különbözik az őrzött választástól. Elemi stratégiák kombinációjaként felírva: if(s,st,sf) = ?x; ((s; !x) < st + sf)
Most már le tudjuk fordítani értelmező programunkat. Erre az strc
programot használjuk. Használjuk a standard könyvtárt; ennek összeszerkesztését a -la stratego-lib
kapcsolóval kérjük. A fő forrásfájl megadása után a fordító összegyűjti a szükséges modulokat, C nyelvű programot generál, majd gcc
-vel lefordítja.
Az így kapott futtatható a0i
program szöveges reprezentációjú ATermeken dolgozik. Elemezzünk egy forrásnyelvi szövegrészletet és futtassuk le rajta elemzőnket eddigi állapotában!
Egészítsük ki nyelvünket logikai kifejezéseken végezhető műveletekkel! Definiálunk egy BoolVal
szortot, felveszünk megfelelő konstruktorokat. A Bool()
termmel jelöljük egy konstansról, hogy logikai érték. Adjuk meg a logikai műveleteket BoolVal
ok felett; ezek alapján adjuk meg a logikai műveleteket megadó termek redukciós szabályait.
Készítsük el a relációs jelekhez tartozó operátorok szabályait. A beépített összehasonlító stratégiák egy (a,b)
számpáron végeznek transzformációt. Ha a reláció teljesül, az aktív termet nem változtatja; ha nem teljesül, a transzformáció sikertelen. E sikeres/sikeretlen logikát át kell írni True()
ill. False()
termekre; ehhez bevezetjük a bool-b
stratégiát.
A tényleges összehasonlítást a beépített gt, lt, geq, leq
stratégiákkal végezzük. Speciális eset az egyenlőség; ez teljesen általános, nem csak számokra értelmezzük. Termek közötti strukturális ekvivalenciát a eq = ?(x,x); !(x,x)
stratégia állapít meg.
Logikai művelet még az esetszétválasztás. Az If
termek redukciójának definíciója triviális.
Ezek után elegendő kiegészítenünk a0i
importjainak listáját. A különböző modulokban található eval
szabályokat a fordító nemdeterminisztikus választással kapcsolja össze. Ha lefordítjuk a programunkat, egyszerű állításokat már el tudunk dönteni.
Egy let-kifejezés általános formája: let definíció1 and definíció2 ... and definíción in törzs
. Az egyes definíciók nevekhez értéket rendelnek. A let-kifejezést úgy redukáljuk, hogy a törzsben a neveket behelyettesítjük, majd a törzset redukáljuk. Tesszük ezt mindaddig, míg nem konstans a törzs.
Az értelmező bővítése let-kifejezésekkel nemtriviális. Feladatunk immár előfeldolgozási lépéseket is igényel. A szintaxis megengedi, hogy konstans értéket és függvényt is köthessünk névhez. Előbbiek Let(név,törzs)
, utóbbiak Let(név,[paraméterek],törzs)
formájú termek. Előfeldolgozási lépésként a függvényabsztrakciókat névtelen függvénnyé alakítjuk.
A névtelen függvény elsőrendű érték a nyelvünkben. Function([szabad-változók,törzs,[kötött-változók])
formájú termmel reprezentáljuk. Így a a0let
modul első néhány sora:
A kiértékelés megkezdése előtt meg kell vizsgálnunk, hogy minden változónevet deklaráltunk-e. Az ellenőrzés során az egy adott pontig megismert neveket listába gyűjtjük. Ha a listában nem szerepel egy adott változónév - Var(x)
term x
résztermje - akkor a változó csomópontját egy Error("x undef")
formájú termmel cseréljük le.
Egészítsük ki a0i
modulban a kiértékelési stratégiát úgy, hogy e két előfeldolgozási lépést is végrehajtsuk! Az új vezérlő stratégia:
Ha egy let-kifejezés törzsében konstans szerepel, úgy a let-kifejezés értéke ez a konstans. Ellenkező esetben megkíséreljük behelyettesíteni a definíciós részben kötött változókat. Ha ilyet nem tudunk, akkor a let-kifejezés nem redukálható. Ha a változónév ismeretlen, akkor egymásba ágyazott let-kifejezésekről lehet szó. Ebben az esetben a redukció egy szinttel feljebb folytatódik a kiértékelési stratégia miatt.
Már csak a függvények kiértékelése van hátra. A függvényapplikációt úgy értékeljük ki, hogy a következő szabad változóhoz hozzárendeljük a kötött értéket. Ha elfogytak a szabad változók, úgy a függvény egy egyszerű let-kifejezéssé alakítható, ahol a definíciós részben az egyes kötött változókhoz a kötött értékeket rendeljük, a törzs pedig maga a függvénytörzs.
Próbáljuk ki kész értelmezőnket! Valósítsuk meg nyelvünkben a Fibonacci-függvényt!
Ezzel láthatjuk, hogy Strategoban könnyen, kevés kóddal, modulárisan tudunk fordítóprogramokat és egyébb szoftvertranszformációs rendszereket készíteni.
A forrásfájlokat Stratego/XT 0.17-tel le lehet fordítani; ezzel már kipróbálásra kerültek. A fordítást egyszerűsítendő található benne egy Makefile. A kipróbálást segíti a parse.sh
és a interpret.sh
melyek a standard bemeneten kapott forrásfájlt elemzik illetve értelmezik. A peldak
alkönyvtárban olyan rövid programok találhatók, melyeket az itt elkészített értelmezővel le tudunk futtatni.
A Stratego részét képezi néhány jól kidolgozott példanyelv.
A Spoofax/IMP rendszer új projekt létrehozásakor a projektet feltölti egy entitások és kapcsolataik leírására alkalmas nyelv és annak Java nyelvű generátora leírásával.