A Theta programozási nyelv

Utasítások

A Theta egy utasítás-orientált nyelv. Két féle utasítás található a nyelvben: egyszerű utasítás és vezérlő utasítás. A vezérlő utasítások közül néhánynak egy van több kód-törzse van komponensenként. Az egyenlőségből álló törzset utasítások követnek:

        <body> -> [<equate>]* [<statement>]*

Ezt követi egy egymásba-ágyazott utasítás struktúra.

Egyszerű utasítás

A egyszerű utasítások egy adott számítást végeznek el. Állhatnak deklarációkból  , kifejezésekből   és hívásokból  .

Egy hívó utasítás egy eljárást hív meg. Ennek a formája:

<primary> "(" [<args>] ")"

(A "primary" egy korlátozott formája a kifejezéseknek.) Egy hívó utasítás szemantikája megegyezik egy hívó kifejezés   szemantikájával, kivéve hogy az eljárások hívásával tetszőleges számú visszatérési értéket kaphatunk, és tetszőleges ilyen értéket el is dobhatunk.

 

Store utasítás

Ez a speciális utasítás a tömb típusú változók összetevőinek változtatására való. Ez az utasítás hasonlít szintaktikailag a kifejezésekre , de igazából egy hívás. A következő formával rendelkezik:

<primary> "[" <expr1> "]" ":=" <expr2>

(A "primary" egy korlátozott, meghatározott formája a kifejezéseknek.) Ez az alak teljesen egy gyorsírása a store metódus hívásának, és ekvivalens a következő hívási kifejezéssel:

<primary> "." "store" "(" <expr1> "," <expr2> ")"

A "primary" és a többi kifejezés kiszámítása nem meghatározott sorrendben történik.

Akkor megengedett egy alak, ha a neki megfelelő hívási utasítás megengedett, és következésképpen nem szorítja szűk keretek közé a tömbök használatát, de a felhasználó által definiált típusoknál jól használható lesz. A "primary"-tól visszatérő objektumnak rendelkeznie kell a "store" metódussal, aminek két argumentuma van, melynek típusa őstípusai az expr1 és expr2 típusoknak.

A return utasítás

A return utasítás formája:

return ["(" <expr> ["," <expr>]* ")"]

A return utasítás befejezi az őt tartalmazó eljárás vagy iterátor végrehajtását. A return utasítást követő kifejezések számának meg kell egyeznie a rutin fejlécében megadottal, és a típusuk a megfelelő típus, vagy annak altípusa lehet. A kifejezések kiértékelésének sorrendje nem definiált.

Yield utasítás

A yield utasítás csak egy iterátoron   belül használható. A formája a következő:

yield "(" <expr> ["," <expr>]* ")"

A yield utasítás átmenetileg felfüggeszti az iterátor végrehajtását, és átadja a vezérlést a hívó for utasításnak. A kifejezések számának meg kell egyeznie az iterátor fejlécében szereplő yields típus listával található elemek számával, és a típusának a fejléc lista megfelelő eleme altípusának kell lennie. Az értékeket kifejezések (nem meghatározott sorrendben történő) kiszámításával kapjuk meg, és átadódnak a for utasításnak úgy, hogy hozzárendelődnek a megfelelő ciklusváltozókhoz. Miután a for ciklus törzse lefutott, az iterátor végrehajtása folytatódik a yield utasítást követő utasítással.

A signal utasítás

Kivétel kiváltása a signal utasítással történik, ennek formája:

signal <name> ["(" <expr> ["," <expr>]* ")"]

ahol

        <name> -> <idn>

A signal utasítás végrehajtása a kifejezések kiértékelésével kezdődik (ha vannak) nem definiált sorrendben. A rutin végrehajtása megszakad, és a végrehajtás a hívóban folytatódik.

A kivétel neve a rutin fejlécében felsoroltak egyike lehet, vagy a "failure". Ez utóbbi esetben pontosan egy sztring típusú kifejezést kell megadni, egyébként pedig az adott kivételnek megfelelő számú, és típusút. (A megadott típus lehet a várt típus altípusa is.)

Az if utasítás

Az if utasítás formája:

if <expr> then <body> [elseif <expr> then <body>]* [else <body>] end

A kifejezésnek bool típusúnak kell lennie. A kiértékelés befejeződik, ha true értéket kapunk. Az elseif alkalmazásával kényelmesen írhatunk többágú elágazásokat.

A while utasítás

A while utasítás alakja:

while <expr> do <body> end

Az utasítás törzsét addig hajtja végre, míg a kifejezés igaz értéket (true) ad. A kifejezésnek "bool" típusúnak kell lennie.

A for utasítás

A for utasítással iterátorokat használhatunk, és ez az egyetlen módja az iterátorok használatának. Az iterátor elemek egy sorozatát készíti el (ahol az elem egy vagy több objektumból álló csoport lehet), egyszerre egy elemet; a for utasítás törzse végrehajtódik minden elemre a sorozatban.

A for utasítás formája:

for <for_idns> in <invoc> do <body> end

ahol

<for_idns> -> <idn_list> | <decl> ["," <decl>]*

A ciklusváltozók lehetnek már korábban deklaráltak, vagy új változók, amelyek lokálisak a for utasításra. Fontos, hogy nem használhatunk egyszerre mindkét típusú változóból.

Az első ciklusváltozó felveszi az első objektumot az átadott elemből, a második (ha van) a másodikat, és így tovább. (Említettük, hogy az iterátor elemeket ad vissza, amik objektumok csoportjai lehetnek, esetleg ezek a csoportok csak egy elemet tartalmaznak.)

A for utasítás végrehajtása a következő. Először az iterátor meghívódik, és visszaad egy elemet, vagy terminál. Ha visszaadott egy elemet, a végrehajtása átmenetileg felfüggesztődik, az elem objektumai értékül adódnak a ciklusváltozóknak, és a ciklus törzse végrehajtódik. A ciklus következő végrehajtásánál folytatódik az iterátor végrehajtása azon a ponton, ahol felfüggesztődött. Ha az iterátor terminált, a for utasítás végrehajtása is befejeződik. Ha a for utasítás befejeződése szintén terminálja az iterátort is.

A következő példa készít egy "array[int]" tömböt, és feltölti számokkal 1-től 10-ig:

a: array[int] := array_new[int]()
for i: int in 1.to(10) do
a.append(i)
end

A példában az int objektum "to" iterátorát használtuk, amely egymást követő egész számokat ad vissza, az első az adott objektum, az utolsó pedig a megadott argumentum.

A break utasítás

A break utasítás formája:

break

Hatására az őt tartalmazó legbelső for vagy while utasítás végrehajtása befejeződik. Fordítási hibát okoz, ha a break utasítást nem for vagy while utasítás törzsében használjuk.

A continue utasítás

A continue utasítás formája:

continue

Hatására az őt tartalmazó legbelső for vagy while utasítás törzsének végrehajtása befejeződik, és elkezdődik egy újabb iteráció végrehajtása (ha lehetséges). Fordítási hibát okoz, ha a continue utasítást nem for vagy while utasítás törzsében használjuk.

A tagcase utasítás

A tagcase utasítás a "oneof" vagy "maybe" objektumok szétbontására használható. Ennek formája:

tagcase <expr> <tag_arm> <tag_arm>* [others ":" <body>] end

ahol

        <tag_arm> -> when <name> ["," <name>]* ["(" <idn> ":" <type_designator> ")"] ":" <body>

A kifejezés értéke egy "oneof" vagy "maybe" objektum lehet. Az objektum címkéjét (tag) összehasonlítjuk a megadottakkal. Mikor egyezést találunk, ha meg van adva egy deklaráció az adott ágban, akkor az felveszi a megfelelő komponensét az objektumnak. (Ez a változó lokális a törzsre nézve.) Ezután a megfelelő törzs kerül végrehajtásra. Ha nem találtunk egyezést, akkor az others ág kerül végrehajtásra.

Egy tagcase utasítás szintaktikailag helyes, ha megfelel a következő követelményeknek:

  1. A kifejezés típusa "oneof" vagy "maybe" kell legyen. (jelöljük T-vel)
  2. A tag ágakban szereplő tag-ek nevei a T típus tag-jei közül kerülhetnek ki, és egyik tag sem fordulhat elő egynél többször.
  3. Ha T minden tag-je jelen van, nem kell others ág; egyébként egy others ágnak jelen kell lennie.
  4. Minden tag ág tartalmaz egy deklarációt, a típus leírónak (type_designator) a tag ág minden tag-jéhez tartozó típus szupertípusának kell lennie.

Egy példa:

x: oneof[none: null, some: int]
...
tagcase x
when none: ...
when some(y: int): ... y+7 ...
end

A typecase utasítás

Egy változónak vagy kifejezésnek a Theta-ban mindig van a fordító által ismert típusa, és a fordító ellenőrzi, hogy a változó által mutatott objektum aktuális típusa, vagy a kiszámolt kifejezés típusa mindig altípusa legyen ennek a típusnak. Sokszor hasznos lehet meghatározni egy objektum aktuális típusát. Ezt könnyen megtehetjük a typecase utasítás használatával. Ennek formája a következő:

typecase <expr> <type_arm> [<type_arm>]* [others ":" <body>] end

ahol

        <type_arm> -> when <type_designator> [ ( <idn> ) ] ":" <body>

A kifejezés kiértékelődik, és az eredményül kapott objektum aktuális típusát használjuk, hogy kiválasszuk a megfelelő ágat (type_arm). A kiválasztás az ágak sorrendjében halad, és az első olyan ág választódik ki, melyben megadott típus altípusa az aktuális típusnak. Az objektum értékül adódik az adott ágban megadott változónak (ha ilyen létezik), és a megfelelő törzs végrehajtódik. Ez a változó lokális a törzsre nézve. Az others ág mindig illeszkedik, de nem ad további információt az objektum típusáról.

Egy helyes typecase utasítás megfelel a következő követelményeknek:

  1. Nincs olyan típus, ami több mint egy ágban szerepel.
  2. Minden ág típusa altípusa kell legyen a kifejezés (expr) típusának. A típus altípusa magába foglalja az összes altípusát, kivéve, ha maga a típus, így ez mindig szűkebb, mint a típus maga.
  3. Ha "S" altípusa "T"-nek, és mindkettőt használjuk egy-egy típus ágban, akkor az "S" ága meg kell előzze a "T" típust tartalmazó ágat. (A speciálisabb típus meg kell előzze az általánosabb típust, különben a speciálisabb típus ága haszontalan.)

A következő példában a "set" és a "bag" típusok altípusai a "collection" típusnak, és a "stack" altípusa a "bag"-nek.

x: collection
...
typecase x
when stack(y): ...% y "stack" ebben az ágban
when bag(z): ...% z "zsák" ebben az ágban
others: ...% x-ez csak "collection"-ként tudjuk használni ebben az ágban
end

Az others ág választódik ki, ha az "x" aktuális típusa "set" (vagy más "collection" típus, amelyik nem altípusa "stack"-nek vagy "bag"-nek).

A begin utasítás

A begin utasítással hozhatunk létre blokkutasításokat. Segítségével több utasítást egyetlen egyszerű utasítássá csoportosíthatunk. Ennek formája:

begin <body> end

Mivel a Theta-ban a vezérlő utasításoknak van body része, ezért a begin utasítást leggyakrabban az except utasítással használjuk.

Az except utasítás

Ahhoz, hogy kivétel-kezelőket adjunk meg utasításokhoz, a hívóban specifikálni kell, hogy mi történjen, mikor egy kivétel váltódik ki az adott utasítás végrehajtása közben (Az except utasítással kezelhetők le az exit utasítás által dobott szignálok is.). Ennek formája:

<statement> except [<handler>]* [others ["(" <idn> ":" string ")"] ":" <body>] end

ahol:

        <handler> -> when <name> ["," <name>]* ["(" <decl> ["," <decl>]* ")"] ":" <body>

Legyes S az az utasítás, amelyikhez a handler kapcsolva van, és legyen X az egész exept utasítás. Minden handler ág tartalmaz egy vagy több kivétel nevet, és egy törzset. A törzs végrehajtódik, ha valamelyik nevű kivétel kiváltódik az S végrehajtása közben. A felsorolt neveknek egyedinek kell lenniük. Az opcionális others ág használható azon kivételek kezelésére, melyek nincsenek explicit felsorolva egyetlen kivétel ágban sem. S bármilyen utasítás lehet, még másik except utasítás is.

Ha az S végrehajtása közben az E kivétel váltódik ki, a vezérlés azonnal a legbelső alkalmazható kezelőre kerül: a legbelső kezelő az E-hez az, amelyik tartalmazza a végrehajtás alatt álló utasítást. Ha a kivétel-kezelő végrehajtása befejeződött, a vezérlés azzal az utasítással folytatódik, mely az után az utasítás után áll, amelyhez a kezelő kapcsolva volt. Így ha a legbelső kezelő az S-hez volt kapcsolva, akkor a vezérlés az S utáni utasítással folytatódik. Ha az S végrehajtása anélkül fejeződik be, hogy szignál váltódna ki, akkor a csatlakoztatott kezelők nem hajtódnak végre.

Deklarációk nélküli kezelők (handler-ek)

Ha egy E kivétel kezelőjének nincs deklarációja, akkor ha ez választódik ki az E kezelésére, minden E-hez kapcsolt paraméter egyszerűen figyelmen kívül lesz hagyva. Így egy kezelőt használhatunk arra, hogy olyan kivételeket kezeljünk le, amelyek paraméter nélkül váltódtak ki, vagy a csatolt paraméter nem érdekes.

Deklarációval rendelkező kezelők (handler-ek)

A handler with declarations is used to handle exceptions with the given names when the exception results are of interest. The declared variables, which are local to the handler, are assigned the exception results before the body is executed.

Deklarációval rendelkező kezelőt használhatunk, ha éredekelnek minket az adott nevű kivételhez csatolt paraméterek is. A deklarált változók lokálisak a kezelőre nézve, és értéküket a törzs lefutása előtt megkapják.

Mikor egy kivétel illesztődik egy kezelőhöz, csak a kivétel neve lesz figyelembe véve. Ha az E kivételhez tartozó kezelő tartalmaz deklarációkat, ennek le kell kezelnie minden lehetséges paramétert, mely az E-vel ki tud váltódni. Például, legyen egy utasításban két rutin hívás, az egyik szignatúrája "`signals`(foo(int))", a másiké "`signals` (foo(string,real))". Ebben az esetben lehetetlen deklarációval rendelkező kezelővel lekezelni a "foo" kivételt; nincs egyetlen olyan kezelő, mely mindkét esethez illeszthető. (A megoldás a deklaráció nélküli kezelő használata lehet, vagy az utasítást fel kell bontani két különböző except utasításra, így különböző kezelőket használhatunk.)

Az others kezelő

Az others ág opcionális, és a kezelő lista utolsó helyén állhat. Ez lekezel minden kivételt, mely nem lett lekezelve egyetlen más ágban sem. Ha egy változót deklarálunk, ennek "string" típusúnak kell lennie. A változó, mely lokális a kezelőre nézve, a kivétel kisbetűssé alakított nevét fogja tartalmazni; a kivétel minden visszatérési értéke figyelmen kívül marad.

Példa

A következő példában a p eljárás kiválthat e1, e2 és e3 kivételeket, ahol e1 és e2 paraméter nélküli, míg az e3-nak egy int paramétere van. A g eljárás kiválthat e1 (paraméter nélküli) és e4 (szintén paraméter nélküli) kivételeket.

begin
p(q( )) except when e1: % e1 kezelése
when e3(x:int): % e3 kezelése
end
end except when others: % e2 és e4 kezelése
end

Az első ág lekezeli az e1 kivételt, amelyik kiváltódhatott a q vagy a p hívásában is. Az e2 és e4 kivételek nem kezelődnek le a belső except utasításban, de lekezelődnek eggyel kintebb.

A failure kivétel (exception)

A "failure(string)" kivétel implicit hozzáadódik minden rutin interfészéhez, ezért explicit nem szabad felsorolni.

Ha egy rutin végrehajtása közben egy olyan kivétel keletkezik, amelyet egyetlen except utasítással sem kezelünk le a rutin törzsében, akkor a rutin terminál, és automatikusan kivált egy "failure" kivételt. Ha a nem kezelt kivétel nem a "failure", akkor a kiváltott "failure" kivétel sztring paraméterében a nem kezelt kivétel nevét fogja tartalmazni; egyébként pedig a paraméter a kezeletlen "failure" paramétere lesz. Ez szemantikailag azonos azzal, mint ha minden rutin törzse egy except utasítással lenne kiegészítve a következő formában:

begin 
% routine_body
end
except when failure (s: string): signal failure(s)
others (s: string): signal failure ("unhandled exception: " || s)
end

Resignal utasítás

A resignal utasítás egy rövidített formája a kivételkezelésnek:

<statement> resignal <name> ["," <name>]*

Minden felsorolt névnek egyedinek kell lennie, és mindegyik a rutin fejlécében felsoroltak egyike lehet, vagy a "failure". A resignal utasítás úgy viselkedik, mint egy except utasítás ugyanezekkel a nevekkel, ahol mindegyik kezelő kiváltja az adott kivételt ugyanazzal a paraméterekkel. Ezért ha egy resignal kifejezés specifikál egy kivételt a rutin fejlécével összhangban a következő formában: name(T1, ..., Tn), ehhez a kezelő a következő:

when name (x1: T1, ..., xn: Tn): signal name(x1, ..., xn)

 A fordító ellenőriz minden kivételt, ami kiváltható azon utasítás által, amelyhez a resignal kapcsolódik. Ahogy feljebb említettük, hibás, ha egy kivétel kiváltódhat rossz argumentum számmal, vagy a paraméter típusa nem altípusa a kezelőben deklaráltnak.

Exit utasítás

Az exit utasítás a szignálokhoz hasonló vezérlés-átadást tesz lehetővé a helyi kezelőknek, anélkül, hogy a jelenlegi végrehajtás terminálna.

Az exit utasítás formája:

exit <name> ["(" <expr> ["," <expr>]* ")"]
 

Egy exit utasítás kivált egy lokális kivételt, amelynek le kell kezelődnie a befoglaló except utasítás valamelyik when ágában; a fordító hibát jelez, ha egy exit nincs kezelve, vagy egy resignal vagy others handler-rel lenne kezelve. Továbbá a kezelőnek minden paramétert: ha több paraméter lehetséges, az illeszkedő when ágnak deklarálnia kell minden olyan változót, mely szupertípusa minden, az adott exit utasításban szereplő kifejezés típusának. 

Make utasítás

A make utasítás segítségével adunk egy új létrehozott változónak kezdeti változónak. Csak egy "maker"-en belül használható; Egy osztály "make" rutinjának feladata hogy az objektumot inicializálja, kezdeti értéket adjon neki. A "maker" "make"-et használ, és nincs visszatérési értéke termináláskor. Ha a make utasítás normális módon terminál, akkor a "maker" normális módon terminál.

A make utasítás a következő formával rendelkezik:

make "{" [<ivar_inits>] "}" [then <body> end]

Minden "make" utasítás rendelkezik egy (lehetőleg üres) ivar_inits résszel körülvéve "kacskaringós összekapcsolásokkal". Ez a rész (amit az osztály konstruktor kifejezésével   azonosítunk) két tagból áll:

        <ivar_inits> -> <field_inits> ";" <maker_invoc> | <field_inits> | <maker_invoc>
<field_inits> -> <field_init> ["," <field_init>]*
<field_init> -> <idn> ":=" <expr>
<maker_invoc> -> <idn> [<actual_parms>] "(" [<args>] ")"

A field_inits rész inicializálja a maker osztály példányváltozóit; Minden példányváltozóhoz lennie kell egy field_init-nek. (Ha az osztálynak nincsenek példányváltozói, a field_inits-t nem használjuk). A maker_invoc rész az öröklött példányváltozókat inicializálja. Ez a hívás akkor van jelen, ha a marker osztálynak van egy ősosztálya.  ; Az idn-nek egy (ősosztály   által rendelkező) maker   nevének kell hogy legyen.

A make utasítás rendelkezhet egy opcionális "then törzs end" záradékkal, ahol utólagos munkákra használhatunk, miután a példányváltozókhoz értékeket rendeltünk. Ezen záradékon belül a most létrehozott objektumokra a változó saját nevével hivatkozhatunk, és a most létrehozott objektumok példányváltozóit használhatjuk mind közvetlenül változóként, mind a metódus törzsében. 

A make utasítás kiértékelése három lépésben történik. A későbbi lépések csak akkor hajthatók végre, ha a korábbi lépések normális módon befejeződtek (vagyis nem váltottak ki kivételt). A lépések a következők:

  1. Az összes field_inits-ben található exprs nem meghatározott sorrendben hajtódik végre, és az most definiált objektum példányváltozóihoz a nekik megfelelő visszatérési objektumok rendelődnek hozzá.
  2. Ha létezik, akkor a maker_invoc hívása végrehajtódik.
  3. Ha létezik, akkor a törzs kiértékelődik.

Ha mind a három lépés normálisan fejeződik be, akkor a make utasítás terminál, és a benne levő makerek normálisan terminálnak.

A make utasítás nem szerepelhet egy másik make utasítás törzsében.