Metateorija programskih jezikov#

Vsak programski jezik ima svojo teorijo, torej pravila, ki formalno opisujejo, kako se programi izvajajo ali določajo, katere programe smatramo za veljavne. Od teorije so odvisne lastnosti programskega jezika, na primer to, da se veljavni programi lahko vedno uspešno izvedejo, ali pa to, da je rezultat enolično določen. Lastnostim programskega jezika, ki sledijo iz dane teorije, pravimo metateorija. Za primer si oglejmo že znani programski jezik IMP.

Teorija#

Sintaksa#

Del teorije IMPa smo že srečali, in sicer njegovo abstraktno sintakso, ki jo tu le ponovimo:

aritmetični izraz e::=ne1+e2e1e2e1e2logični izraz b::=truefalsee1=e2e1<e2e1>e2ukaz c::=ifbthenc1elsec2whilebdocc1;c2:=eskip

Operacijska semantika#

V sintaksi uporabljamo besede in simbole, ki nakazujejo na pomen posameznih izrazov in ukazov, ampak to pomena formalno ne podaja. Najprej podajmo dinamično oziroma operacijsko semantiko, ki določa, kako se programi izvajajo. To smo enkrat že storili prek programa v OCamlu, zdaj pa to storimo še v jeziku matematike.

Začnimo z aritmetičnimi izrazi, ki naj bi predstavljali cela števila. Ker v aritmetičnih izrazih lahko dostopamo do lokacij , moramo pomen podati relativno na neko stanje, ki ga predstavimo z množico parov lokacij in prirejenih vrednosti {1n1,,knk}. Tromestno relacijo s,en, ki pove, da izraz e v stanju s predstavlja število n, podamo kot najmanjšo relacijo, zaprto za sledeča pravila:

(n)ss,ns,nns,e1n1s,e2n2s,(e1+e2)(n1+n2)s,e1n1s,e2n2s,(e1e2)(n1n2)s,e1n1s,e2n2s,(e1e2)(n1n2)

Na primer, za stanje s={6,m5} velja s,(3+4)42, saj imamo sledeče drevo izpeljave:

(6)ss,6s,33s,44s,(3+4)7s,(3+4)42

Če aritmetični izraz e vsebuje dostop do lokacije , ki v stanju s ni nastavljena, bi to v implementaciji sprožilo napako ob izvajanju, v matematični formulaciji pa preprosto pomeni, da za nobeno število n ne velja s,en.

Podobno podamo relacijo s,br, ki pove, kdaj logični izraz b v danem stanju s predstavlja resničnostno vrednost rB={tt,ff}:

s,truetts,falseffs,e1n1s,e2n2n1=n2s,(e1=e2)tts,e1n1s,e2n2n1n2s,(e1=e2)ffs,e1n1s,e2n2n1<n2s,(e1<e2)tts,e1n1s,e2n2n1n2s,(e1<e2)ffs,e1n1s,e2n2n1>n2s,(e1>e2)tts,e1n1s,e2n2n1n2s,(e1>e2)ff

Pri ukazih so pravila malo drugačna, saj za razliko od izrazov ne predstavljajo nobenih vrednosti, temveč je njihov končni rezultat spremenjeno stanje. Poleg tega se zaradi morebitnih neskončnih zank izvajanje ukazov ne konča vedno. Zaradi slednjega bomo pomen ukazov podali prek semantike malih korakov s,cs,c, ki ukaza c v stanju s ne bo povezala s končnim rezultatom, temveč z novim ukazom c in novim stanjem s, ki predstavljata spremembe, ki so se zgodile v enem računskem koraku.

s,btts,ifbthenc1elsec2s,c1s,bffs,ifbthenc1elsec2s,c2s,btts,whilebdocs,(c;whilebdoc)s,bffs,whilebdocs,skips,c1s,c1s,(c1;c2)s,(c1;c2)s,(skip;c2)s,c2s,ens,:=es[n],skip

Vidimo, da takrat, kadar pogoj v logičnem ukazu predstavlja resnico, v naslednjem računskem koraku začnemo izvajati prvo, sicer pa drugo vejo, obe v nespremenjenem stanju. Podobno ločimo dva primera pri zankah. Če je pogoj resničen, izvedemo telo, ki mu sledi še ena ponovitev zanke, sicer pa zaključimo izvajanje. Pri zaporednem izvajanju korak naredimo v prvem ukazu in ustrezno spremenimo stanje. Če je prvi ukaz končal z izvajanjem, začnemo izvajati drugi ukaz. Pri prirejanju spremenimo stanje in zaključimo izvajanje. Ukaz skip predstavlja zaključeno izvajanje, zato pravila zanj nimamo.

S tako podanimi pravili lahko pokažemo, da je semantika ukazov deterministična. Če velja tako s,cs,c kot s,cs,c, tedaj je s=s in c=c. Dokaz poteka z indukcijo, pri tem pa si pomagamo s pomožnima trditvama, da sta tudi semantiki aritmetičnih in logičnih izrazov deterministični.

Določanje veljavnih programov#

Želeli bi pokazati, da vsak program lahko vedno naredi korak, vendar to ni res, saj lahko nekateri programi dostopajo do lokacij, ki v stanju niso nastavljene. Da bi se tem primerom izognili, smo v implementaciji napisali funkcijo check, ki je program preverila, preden ga je začela izvajati. Matematično pa bomo to zopet podali z induktivnimi relacijami. Relacijo Le, ki podaja, kdaj aritmetični izraz e dostopa le do lokacij iz množice L, podamo s sledečimi pravili:

LLLnLe1Le2L(e1+e2)Le1Le2L(e1e2)Le1Le2L(e1e2)

Podobno to storimo za logične izraze:

LtrueLfalseLe1Le2L(e1=e2)Le1Le2L(e1<e2)Le1Le2L(e1>e2)

Pri ukazih je situacija malenkostno drugačna, saj spreminjajo stanje in s tem tudi množico veljavnih lokacij. Tako podamo relacijo Lc,L, ki pove, da ukaz c dostopa le do lokacij iz množice L, po izvajanju pa bodo definirane vse lokacije iz množice L.

LbLc1,L1Lc2,L2Lifbthenc1elsec2,L1L2LbLc,LLwhilebdoc,LLc1,LLc2,LL(c1;c2),LLeL:=e,L{}Lskip,L

Izrek o varnosti#

Eden glavnih izrekov, ki jih običajno dokažemo, ko obravnavamo metateorijo programskega jezika, je izrek o varnosti. Ta govori o tem, da se veljavni programi lahko uspešno izvedejo. Razdeljen je na dva dela:

  1. napredek, ki pove, da veljaven program bodisi lahko naredi še en računski korak bodisi je uspešno zaključil izvajanje,

  2. ohranitev, ki pove, da veljaven program po enem računskem koraku ostane veljaven.

Oba skupaj povesta, da vsak veljaven program izvaja računske korake do točke, ko zaključi izvajanje, ali pa teče v neskončnost. Oglejmo si vsakega posebej.

Napredek#

Naj bo s stanje, definirano na vseh lokacijah iz množice L. Tedaj:

  • Za vsak aritmetični izraz e, za katerega velja Le, obstaja nZ, da velja s,en.

  • Za vsak logični izraz b, za katerega velja Lb, obstaja rB, da velja s,br.

  • Za vsak ukaz c in množico lokacij L, za katera velja Lc,L:

    1. bodisi obstajata ukaz c in stanje s, da velja s,cs,c,

    2. bodisi velja c=skip.

Dokaz#

Vsak del dokažemo z indukcijo.

  • Obravnavajmo vsa pravila, po katerih smo lahko prišli do zaključka Le.

    • Če smo uporabili pravilo LL, potem je L, torej po predpostavki obstaja n, da velja (n)s, zato velja tudi s,n.

    • Če smo uporabili pravilo Ln, potem velja s,nn.

    • Če smo uporabili pravilo Le1Le2L(e1+e2), potem po indukcijski predpostavki obstajata n1 in n2, da velja s,e1n1 in s,e2n2. Tedaj velja tudi s,(e1+e2)(n1+n2). Podoben dokaz uporabimo še v primerih L(e1e2) in L(e1e2).

  • Obravnavajmo vsa pravila, po katerih smo lahko prišli do zaključka Lb.

    • Če smo uporabili pravilo Ltrue, potem velja s,truett. Podoben dokaz uporabimo še v primeru Lfalse.

    • Če smo uporabili pravilo Le1Le2L(e1=e2), potem po poprej dokazani trditvi obstajata n1 in n2, da velja s,e1n1 in s,e2n2. Ker velja bodisi n1=n2 bodisi n1n2, vedno obstaja rB, da velja s,(e1=e2)r. Podoben dokaz uporabimo še v primerih L(e1<e2) in L(e1>e2).

  • Obravnavajmo vsa pravila, po katerih smo lahko prišli do zaključka Lc,L.

    • Če smo uporabili pravilo LbLc1,L1Lc2,L2Lifbthenc1elsec2,L1L2, potem po prejšnji trditvi obstaja rB, da velja s,br. Če je r=tt, potem velja c,sc1,s, če pa je r=ff, pa velja c,sc2,s. V obeh primerih torej lahko naredimo korak.

    • Podoben dokaz lahko naredimo v primeru Lwhilebdoc,L.

    • Če smo uporabili pravilo Lc1,LLc2,LL(c1;c2),L, po indukcijski predpostavki za Lc1 velja:

      1. bodisi obstajata ukaz c1 in stanje s, da velja s,c1s,c1, zaradi česar velja tudi s,(c1;c2)s,(c1;c2),

      2. bodisi je c1=skip, zaradi česar velja s,(skip;c2)s,c2.

      V obeh primerih torej lahko naredimo korak.

    • Če smo uporabili pravilo LeL:=e,L{}, potem po prejšnji trditvi obstaja n, da velja s,en, zato velja tudi s,(:=e)s[n],skip.

    • Če smo uporabili pravilo Lskip,L, potem velja c=skip.

Opazimo lahko, da v dokazu napredka za ukaze ne uporabimo druge množice lokacij L. Ta pride v poštev, če želimo narediti več kot en korak.

Varnost#

Vzemimo ukaza c,c, množici lokacij L,L ter stanji s,s, tako da:

  1. je stanje s definirano na vseh lokacijah iz množice L,

  2. velja Lc,L, ter

  3. velja s,cs,c.

Tedaj obstaja množica lokacij L, da velja Lc,L in je s definirano na vseh lokacijah iz množice L.

Pozorni bodimo na dejstvo, da množica lokacij L ne ustreza stanju s, saj gre za množico lokacij po koncu izvajanja ukaza c, med tem ko se je v s lahko v enem samem koraku spremenila le ena. Na primer, velja (:=3;m:=4),{,m} in ,(:=3;m:=4){3},(skip;m:=4). V tem primeru je L=, L={,m} ter L={}.