På adskillige opfordringer fra studerende, som har ønsket et større opgave i Prolog/sprogstoffet, har jeg gjort nedenstående afleveringsopgave fra sidste år tilgængelig. Bemærk, at en del af løsningen på denne opgave er indarbejdet i kapitel 8 i »Sprog og abstrakte maskiner‹, 3. rev. udgave.

NBNBNB: Dette dokument er produceret vha. »Save as HTML« fra Word, så der kræves en del kvalificeret gætværk fra læserens side for at finde ud af, hvad der skulle have stået. (((Projektforslag: Skriv et velfungerende program, der omsætter Worddokumenter til HTML.)))

© 2000, Henning Christiansen

Roskilde Universitetscenter, Datalogi Forår 1999

Modul 2 kursus Notat nr. 8

 

 

Afleveringsopgave, del 1:

Typecheck og fortolker for sproget Pax

 

Pax er et lille Pascal-inspireret programmeringssprog, som er opfundet med henblik på denne opgave. Opgaven går ud på vha. Prolog at konstruere en type-checker og en fortolker (evt. også debugger) for sproget. Fortolkeren opbygges efter principperne beskrevet i ªSprog og abstrakte maskiner´, og omkring typecheck indeholder dette notat tilstrækkelig introduktion.

Det kan måske se besværligt ud at programmere en separat typechecker, men det gør det en hel del nemmere at skrive fortolkeren, da man i denne ikke behøver tage højde for alle mulige fejlsituationer.

I det følgende beskriver vi først sproget på overordnet vis, hvorefter (afsnit 2) vi giver en introduktion til typecheck og antyder, hvordan det kan implementeres i Prolog. Afsnit 3 giver hints til, hvordan en fortolker kan struktureres, og der introduceres nogle hjælpeprædikater, som er tilgængelige til brug ved opgavens løsning. Disse og andre hjælpeprædikater er tilgængelige på modul-2-kursets server sammen med nogle eksempelprogrammer, som vises nedenfor. Opgaveformuleringen er at finde i afsnit 4, hvor der også er nogel gode råd til arbejdet med opgaven.

1. Pax: Et imperativt sprog med procedurer

Her gives en kort introduktion til sproget. Semantikken skitseres, og i øvrigt forventes denne at være ªden sædvanlige´.

Pax er et programmeringssprog som arbejder med heltal, boolske værdier og heltalsarrays, og det har et udvalg af sædvanlige kontrolstrukturer og udtryk. Der kan erklæres procedurer, som kan være rekursive: En given procedure kan kalde sig selv og de procedurer, som er erklæret tidligere i programteksten. Der findes ikke lokale procedurer (dvs. procedurer i procedurer).

Vi ser bort fra konkret syntaks og syntaksanalyse og repræsenterer Pax-programmer som Prolog-termer efter principperne beskrevet i ªSprog og abstrakte maskiner´. Vi genbruger eksisterende operatorer i Sicstus Prolog samt en enkelt, der defineres således:

:- op(800,xfx, :=).

Et program repræsenteres ved en struktur af formen

program(variabel-erklæringer, procedure-erklæringer,sætning )

Her følger et eksempel på et program med tomme procedureerklæringer. Programmet erklærer to heltalsvariable, indsætter værdier i dem, og udregner største fælles divisor vha. Euklids algoritme.

program(

(var(a,int) ; var(b,int)),

tom,

( a:= 221 ; b:= 493 ;

while( a =\= b,

if( a > b,

a:= a-b,

b:= b-a)) ;

write_text('Resultat: ') ;

write(a) ;

nl))

Variabelerklæringer sættes sammen vha. semikolon og hvis der ikke er nogen, skrives blot tom. Tilsvarende benyttes tom også, når der ikke er procedurer at erklære, og tilsvarende for den tomme sætning. Det korteste Pax-program, der eksisterer, ser altså således ud:

program(tom, tom, tom )

Der kan erklæres variable af tre forskellige typer: heltal, bool'ske værdier og arrays af heltal. Eksempler:

 

 

var(a,int)

var(b,bool)

var(c,int_array(7))

Heltalsvariable kan rumme vilkårlige heltal (indenfor det omfang, Prolog kan præstere): ..., -3, -2, -1, 0, 1, 2, 3, .....

Boolske variable kan antage værdierne true og false.

Et array har celler nummereret fra 1 til den øvre grænse som angives ved heltalskonstanten i erklæringen, og den øvre grænse skal være mindst 2 (ud fra princippet om, at arrays med længde 0 eller 1 ikke er megen nytte til). Hver celle i et array kan rumme et heltal. Variable kan indgå i udtryk på sædvanlig måde, og vha. ª:=´ operatoren kan variablen gives en ny værdi. Hele arrays kan noteres ved lister, som det vil fremgå nedenfor. Såvel et helt array som dets enkeltceller kan benyttes som variable, og enkeltcellerne (arrayopslag) noteres vha. en ^-operator. Eksempler:

a:= 1

b:= true

c:= [11,22,33,44,55,66,77]

c^3 := 3333 (og vi må forvente

c= [11,22,3333,44,55,66,77])

c^(a+2) := c^3 * 5 (svarende til (c^3) * 5)

Variable initialiseres automatisk, når de erklæres, (afhængig af deres type) til 0, false eller [0, ..., 0].

Som variabelnavne kan benyttes alt hvad Prologs atom(-) accepterer, bortset fra true og false.

I Pax har vi udtryk med typer svarende til de, som kunne benyttes i variabelerklæringerne. Logiske betingelser som benyttes i f.eks. if og while skal ikke opfattes som værende af kategori betingelse men som udtryk af type bool; flere detaljer om typer senere.

For heltal kan vi benytte +, -, *, // (for heltalsdivision) og prefiks-minus. Vi har sammenligningerne mellem tal (hvis resultat er af type bool): =, >, >=, <, =<, =\=. For boolske værdier kan benyttes /\, \/, \+ med betydningerne ªog´, ªeller´ og ªikke´. Bemærk af hensyn til opgavens løsning, at alle operatorer pånær de tre sidste, er valgt så de svarer til lignende faciliteter i Prolog: operatorer som forstås af ªis´, eller prædikater, som kan fortolkes direkte af Prolog (=,> osv.).

Her følger et eksempel på på et Pax-program med en procedureklæring. Proceduren indeholder en rekursiv implementation af quicksort. Vi benytter procenttegn til at angive kommentarer som i Prolog; disse kommentarer er ikke en del af programteksten.

program(

(var(n,int); var(a, int_array(4))),

proc( qsort, left, right,

(var(i,int); var(j,int);

var(x,int); var(w,int)),

(i:= left; j:= right;

x:= a^( (left+right)//2) ;

repeat( (while(a^i<x, i:= i+1) ;

while(x<a^j, j:=j-1) ;

if(i=<j,

(w:=a^i; a^i:= a^j; a^j:= w;

i:= i+1; j:= j-1))

),

% until

i > j); % end repeat

if( left<j, proc(sort,left, j)) ;

if( i < right, proc(sort,i,right)) )

), % end proc qsort

% hovedprogram:

(n:= 4 ;

a:= [30,10,40,20] ;

proc(qsort,1,n);

write(a)) )

Vi bemærker, at proceduren har to parametre (her kaldet left og right), et sæt lokale variable (her i, j, x, w) samt en krop.

Det generelle mønster for er procedureerklæringer således idet, vi kan have procedurer med netop én eller netop to parametre.

proc( navn, parameter, lokale-variable, sætning)

proc( navn, parameter1, parameter2, lokale-variable, sætning)

Når en procedure kaldes sker det (som man kan se ovenfor) vha. en konstruktion som således ud:

proc( navn, aktuel-parameter)

proc( navn, aktuel-parameter1, aktuel-parameter2)

De aktuelle parametre kan være vilkårlige heltalsudtryk, og parameteroverførsel sker ved call-by-value, dvs. de aktuelle parameter udregnes først, og inde i proceduren bindes de til parametrene, der så fungerer som lokale variable inde i proceduren.

I fortolkeren kan der hensigtmæssigt benyttes en rekursionsstak til opbevaring af de forskellige variable; dette beskrives i afsnit 3 nedenfor, og der stilles i forvejen programmerede faciliteter til rådighed til håndtering af rekursionsstakken.

Udover lokale variable (der som nævnt også inkluderer parametrene der optræder i procedureerklæringen) kan man referere globale variable. Proceduren qsort ovenfor refererer eksempelvis til det globale array a.

Pax indeholder følgende sætningsformer, hvoraf de fleste er beskrevet i et mindre eksempel (while-programmer) i ªSprog og abstrakte maskiner´, og disse kan sammensættes vha. semikolon som vist i de to eksempelprogrammer ovenfor.

tom foretager intet

variabel := udtryk sædvanlig assignment

if( udtryk, sætning1, sætning2) sædvanlig if-then-else

if( udtryk, sætning) sædvanlig if-then

while( udtryk, sætning) sædvanlig while-løkke

repeat( sætning, udtryk) beslægtet med while.

Sætningen udføres indtil

udtrykket evaluerer til

true. Modsat while, udføres kroppen mindst én gang ved repeat.

write( udtryk ) Evaluerer udtryk og udskriver værdi

write_text( Prologterm ) Udskriver vilkårlig

Prolog-term, f.eks. ªquoted atoms´

nl udskriver et linjeskift

Vi vil nu diskutere synlighed af erklæringer (også kaldet virkefelt) i Pax, der er valgt bl.a. med henblik på, at det skal være nemt at implementere. Som nævnt kan en procedure kun kalde procedurer erklæret tidligere samt sig selv. Betragt følgende skitse på et program:

program( tom,

( proc( p, ...) ;

proc( q, ...) ),

...)

Hovedprogrammet kan kalde såvel p som q, kroppen for q kan kalde p og q, og p kan kun kalde p.

Der indføres ikke et forbud mod at erklære flere procedurer eller variable med sammenfaldende navne; det vil blot være det senest erklærede emne, som er det aktuelt gældende.

 

 

2. Om typecheck og typebetingelser for Pax

Pax er et sprog med typer og erklæringer, og det sætter noget begrænsninger på, hvad der kan accepteres som et legalt program. Her skitseres først disse begrænsninger, og dernæst antydes, hvordan en abstrakt syntaksbeskrivelse i Prolog kan udvides, så den medtager disse begrænsninger.

Den nemmeste måde at illustrere begrænsningerne på, er måske ved et program, som ikke overholder dem.

program( ( var(n, int); var(b, bool);

var(c,int_array(7) ),

( proc(p1,x,var(i,int), i:= b) ;

proc(p2,x,y,var(a,int_array(4)),

if(a, a^3:= false, x:= [1,2,3])),

(p1(c) ; p2(4) ;

c:= [1,2,n] ; xxx:= yyy) )

Dette program benytter sprogets ªabstrakte´ operatorer, men har mange ting, som vi ud fra et typesynspunkt vil kalde forkerte.

I proceduren p1 forsøges en boolsk værdi (angivet ved den globalevariabel b) lagt over i en heltalsvariablen i. I p2 erklæres en lokal variabel a, som er et heltalsarray, og der forsøges lagt værdien false ind på arrayets 3. plads. Parametren x optræder på venstre side i en assignment, hvilket sådan set er OK, men højresiden skulle i såfald have en type svarende til heltal og ikke som her en type svarende til int_array(3). I hovedprogrammet er det også helt galt, alle procedurer havde pr. definition heltalsparametre, men her kaldes p1 med arrayet c som parameter, p2 kaldes med et forkert antal parametre, c som er af type svarende til int_array(7) forsøges assignet en værdi af type int_array(3). Endelig er sætningen xxx:= yyy ikke tilladt, da den benytter to variable, som ikke er erklæret.

Ethvert korrekt udtryk i Pax har en veldefineret type , og følgende typer er mulige: int, bool, int_array(2), int_array(3), int_array(4), ... .

Vi kan beskrive principperne for korrekt ªtypning´ på følgende måde:

  • Typen af en variabel er den, som er angivet i erklæringen.
  • Typen af en parameter inde i en procedure er altid int, og parameteren kan bruges som en vilkårlig lokal variabel.
  • Inde i en procedure kan der kun refereres til følgende variable:
  • globale variable,
  • lokale variable til proceduren (herunder dens parametre).
  • Hvis v er en variabel at type int_array(n) og u et udtryk af type int, da er v^u af type int (Vi betragter ikke indeksfejl som typefejl, men som køretidsfejl).
  • For en sætning v:= udtryk, skal v og udtryk have samme type.
  • De udtryk, som optræder som betingelse i if, while og repeat konstruktionerne skal have type bool.
  • Et udtryk u1 + u2 har type int, såfremt u1 og u2 har type int; tilsvarende for -, *, //.
  • Et udtryk u1 = u2 har type bool, såfremt u1 og u2 har type int; tilsvarende for >, >=, <, =<, =\=.
  • Et udtryk u1 /\ u2 har type bool, såfremt u1 og u2 har type bool; tilsvarende for /\.
  • Et udtryk \+ u har type bool, såfremt u har type bool.
  • Et procedurekald p(u) er korrekt typet, såfremt p er erklæret med én parameter og u har type int.
  • Et procedurekald p(u1, u2) er korrekt typet, såfremt p er erklæret med to parametre og u1 og u2 har type int.
  • Et udtryk af formen [u1, ..., un] har type int_array(n) hvis u1,...,un alle har type int (for n³2).
  • Ingen andre udtryk, end de, som er omtalt overfor, er korrekt typede.
  • Ethvert andet syntakstræ er korrekt typet, såfremt de indgående udtryk er korrekt typede.

Som vi har beskrevet i ªSprog og abstrakte maskiner´ afs. 4.3.1, kan man beskrive den abstrakte syntak af et sprog ved prædikater, som accepterer netop sprogets abstrakte syntakstræer. Eksempelvis kan vi beskrive reglen for plus ved følgende regel:

udtryk(U1+U2):- udtryk(U1), udtryk(U2).

Hvis vi for en stund ser bort fra variable i udtrykkene, kan vi beskrive typebetingelsen ved at tilføje et ekstra argument til de syntaktiske prædikater, som holder typen. Vi ændrer prædikaternes navne ved at sætte ªt_´ foran, så man kan se, at det er et nyt prædikat, som foretager typecheck. I eksemplet med plus-udtrykket kommer reglen til at se således ud:

t_udtryk(U1+U2,int):-

t_udtryk(U1,int), t_udtryk(U2,int).

Men nu var vi brug for at sammenholde forekomster af variable i udtryk med de typer, variablene fik ved deres erklæringer. Til det formål indføres en symboltabel, som skabes ved erklæringerne og transporteres ud til udtrykkene.

Hvis vi har en erklæring var(n, int), er det relevant, at der produceres en symboltabel, som indeholder et par (x, int). For at transportere symboltabellerne ud i alle udtryk og deludtryk kan type-checkprædikaterne udstyres med nok et argument, som indeholder den aktuelle symboltabel. Plus-reglen kommer nu til at se således ud:

t_udtryk(U1+U2,SymbolTabel,int):-

t_udtryk(U1, SymbolTabel, int),

t_udtryk(U2, SymbolTabel, int).

Når vi når ned til en variabel et eller andet sted i et udtryk kan vi teste sammenholde erklæring og anvendelse ved følgende regel, idet vi antager, at symboltabellen er repræsenteret som en liste af de omtalte slags par.

t_udtryk(Variabel, SymbolTabel, Type):-

atom(Variabel), !,

member((Variabel, Type), SymbolTabel).

På denne måde vil vi opnå en typechecker, som accepterer korrekt typede programmer, og som fejler (dvs. Prolog siger ªno´, hvis programmet ikke er korrekt.

Af hensyn til at lokalisere typefejl – og de programmeringsfejl, man utvivlsomt vil lave under løsning af opgaven – foreslås det, at man erstatter kaldet af member med er prædikat fremstillet til formålet kaldet symbol_tabel_find, så reglen kommer til at se sådan ud:

t_udtryk(Variabel, SymbolTabel, Type):-

atom(Variabel), !,

symbol_tabel_find(Variabel, SymbolTabel, Type).

Dette hjælpeprædikat vil udsende en forklarende fejlmeddelelse i tilfælde af, at et symbol ikke findes i tabellen eller er registreret med en anden type end den, man i kaldet angiver, at man ønsker at finde. Prædikatet er tilgængeligt på kursets server sammen med andre hjælpeværktøjer, som beskrives i det følgende.

Her er eksempler på fejlmeddelelser produceret af symbol_tabel_find ved typecheck at fejlagtige programmer.

Typefejl for b: forventede int, fandt bool

Fejl: bb ikke erklæret: forventede int

Illegal term anvendt som variabel: [1,2,3]

Sammenhængen mellem procedurers parameterantal i erklæring og anvendelse kan beskrives ved knytte nogle typer vi vil kalde proc1 og proc2 (for procedurer med hhv. én og med to heltalsparametre), der opbevares i symboltabellen helt ligesom variables værdier. Så erklæringen af en procedure ved navn p med to argumenter vil resultere i en indgang (p,proc2) i symboltabellen.

Sætninger og udtryk fungerer som ªforbrugere´ af symboltabeller, hvor erklæringer vil være ªproducenter´. For procedureerklæringer må der lidt omhyggelighed til, for at sørge for, at den kendte tabel (m. globale variable og forudgående samt aktuelle procedurer optræder) bliver tilgængelig inde i kroppen – og at parametre og lokale variable gøres tilgængelige lokalt (og kun lokalt).

Det er nemmest, hvis de prædikater, som producerer tabeller udstyrese med to argumenter, en tabel-før og en tabel-efter. Så hvis vi eksempelvis har en symboltabel [(x,int)] før vi processerer erklæringen var(b, bool), vil vi efterfølgende få en tabel

[(b, bool), (x,int)].

Hvis denne tabel senere når hen til en procedureerklæring

proc(p,x,y,var(a,int_array(3)), krop),

vil der efter denne være opbygget en tabel af formen

[(p,proc2),(b, bool), (x,int)].

Inde i kroppen af prædikatet er det relevant med denne her tabel:

[(a, int_array(3)), (y,int), (x, int),

(p,proc2),(b, bool), (x,int)]

Dette kunne give anledning til en regel for typecheck af hele programmer, der ser således ud:

t_program( program(VarErk, ProcErk, Saetning)):-

t_var_erk(VarErk, [], SymbolTabel1),

t_proc_erk(ProcErk, SymbolTabel1, SymbolTabel2),

t_saetning(Saetning, SymbolTabel2).

 

3. Skitse af en fortolker for Pax; hjælpeværktøj.

Idéen med typecheckeren er, at den kører som en selvstændig proces, som går forud for fortolkningen, således, at når vi skriver fortolkeren kan vi koncentrere os om korrekt typede programmer, hvor der ikke skal skænkes en tanke til symboltabeller. Når et Pax-program er blevet godkendt af typecheckeren (hvis man altså har programmeret sin typechecker korrekt!), ved man, at de argumenter, som kommer frem til f.eks. regneoperationer har de rette typer.

Den eneste egenskab, som fortolkeren må kontrollere løbende er index-check for arrays, dvs. hvis eksempelvis a er bundet til array-værdien [1,2,3,4], og i er en heltalsvariabel, så is værdi udregnes, og det skal kontrolleres, at denne værdi ligger mellem 1 og 4 for at a^i kan udregnes Alt vdr. indekscheck er indbygget i prædikater, der er givet på forhånd til håndtering af rekursionsstakken – men nu er fænomenet nævnt for en ordens skyld.

Fortolkeren kan grundlæggende opbygges, som beskrevet i afsnit 4.3.2 i ªSprog og abstrakte maskiner´ og benyttet for while-programmer i 6.2. Blot er der brug for en mere detaljeret model af et lager i form af en rekursionsstak, da vi her har procedurekald med rekursion og arrays. Prædikater til behandling af rekursionsstakken er tilgængelige til opgavens løsning, og nu forklares den valgte repræsentation for rekursionsstakken.

Stakken repræsenteres som en liste af stakafsnit, hvor vi tænker på bunden af stakken som det sidste (= ªhøjreste´) element, og det indeholder de globale variable. Der hægtes så nye afsnit på, når der kaldes en procedure, og stakkes af, når en procedure forlades. Bindinger til variable repræsenteres ved par af formen (Variabel-navn, Værdi). Det er vigtigt at bemærke forskellen mellem bindingerne i rekursionsstakken og informationen gemt i en symboltabel: Symboltabellen vedrører statiske egenskaber ved symbolerne, i vores tilfælde deres type, hvorimod stakafsnittene i rekursionsstakken rummer bindinger af variable til deres dynamisk genererede værdier.

Betragt som eksempel quicksortprogrammet vist i afsnit 1. Lige inden procedurekaldet proc(qsort,1,n) ser rekursionsstakken således ud:

[ [(a,[30,10,40,20]),(n,4)] ]

Nu kaldes proceduren, parametrenes værdier udregnes og overføres til lokale variable (kaldet left og right) til dette procedurekalds stakafsnit, og der afsættes plads til de lokale variable i, j, x og w. Stakken ser nu således ud:

[ [(w,0),(x,0),(j,0),(i,0),(right,4),(left,1)],

[(a,[30,10,40,20]),(n,4)] ]

Nu går procedurens sorteringsalgoritme igang, og når repeat-sætningen er udført og lige inden de to if-sætninger ser stakken således ud; der er de samme stakafsnit, men nogle variables værdier er ændret:

[ [(w,30),(x,10),(j,1),(i,2),(right,4),(left,1)],

[(a,[10,30,40,20]),(n,4)] ]

Man kan se videre, at betingelse i første if-sætning vil evaluere til false, mens betingelsen for den anden bliver til true, hvilket giver anledning til et rekursivt procedurekald. Når parametrene er udregnet og overført, og der er afsat plads til et nyt sæt lokale variable, har vi følgende stak:

[ [(w,0),(x,0),(j,0),(i,0),(right,4),(left,2)],

[(w,30),(x,10),(j,1),(i,2),(right,4),(left,1)],

[(a,[10,30,40,20]),(n,4)] ]

Vi har nu stakafsnit svarende til to inkarnationer af proceduren qsort, dvs. der ligger to indgange i stakken for (eksempelvis) parametren right, den øverste for det seneste og nu aktive kald, og en ªlavere-liggende´ for det første procedurekald. Der vil blive aktiveret endnu flere rekursive kald, hvilket resulterer i endnu flere stakafsnit – op på et tidspunkt, når de har afsluttet og deres stakafsnit er afstakket, når vi (hvis ellers fortolkeren er programmeret korrekt) tilbage til en stak:

[ [(w,30),(x,10),(j,1),(i,2),(right,4),(left,1)],

[(a,[10,20,30,40]),(n,4)] ]

Endelig returnerer det sidste kald til hovedprogrammet, det smides nok en stakafsnit, og vi ender med en stak med ét afsnit (det globale):

[ [(a,[10,20,30,40]),(n,4)] ]

Og som det kan ses, har sætningerne i de rekursive kald sørget for, at det globalt erklærede array er blevet sorteret.

Vi kan se, at sætningerne har brug for at konsultere stakken når

  • de skal aflæse en variabels værdi (evt. en celle i et array), og
  • en ny værdi skal skrives i en variabel (evt. en celle i et array).

En given aktivering af en procedure har kun brug for at konsultere to stakafsnit:

  • det øverste i hvilket dens lokale variable (herunder parametre) findes, og
  • den nederste i hvilken de globale variable findes.

På grund af sprogets struktur (ingen lokale procedurer inde i procedurer)giver det ikke mening at konsultere de mellemliggende stakafsnit.

Ydermere er virkefeltsreglerne indrettet så visseligt at hvis der i en given aktivering af en procedure refereres til en variabel x, så

  • hvis x findes i det øverste afsnit, så er x lokal, og der er værdien i den øverste afsnit, som er den relevante,
  • eller, hvis x ikke findes her, så er x global og findes i det nederste afsnit.

Bemærk at et forudgående typecheck garanterer, at den må findes et af de to steder.

Som eksempel på en global variabel har vi arrayet a i quicksortprogrammet, hvor alle aflæsninger og tilskrivninger i de mange rekursive kald netop (og heldigvis) nåede ned til det globale afsnit.

Der er givet to prædikater til at operere på stakke i det beskrevne format:

hent_var(Var,Stak) og

gem_var(Var, Værdi, StakFør, StakEfter)

Variable kan angives som atomer (og referere til simple variable eller hele arrays) og som enkelt-celler i et array ved VariabelNavn^Index, hvor Index er et heltal.

Den regel i fortolkeren, som skal behandle en arrayindicering (som et udtryk) som a^(i+j), skal først udregne i+j, og hvis det nu f.eks. giver resultatet 7, er det relevant at kalde

hent_var(a^7, Stak)

Tilsvarende, for en sætning af formen a^(i+j):= (x+y)//z. Her skal i+j igen udregnes (og vi antager, den stadig er 7), og (x+y)//z skal også udregnes, skal vi sige med værdi 117, så kan vi derefter kalde

gem_var(a^7, 117, Stak1, Stak2)

hvor Stak1 refererer til den aktuelle stak og Stak2 til den opdaterede, som gives videre til næste sætning.

Disse to prædikater er indrettet, så de selv finder ud af, om den refererede variabel er lokal eller global. Af hensyn til de fejl, som med garanti vil opstå under udarbejdelse af en løsning på opgaven, er disse prædikater udstyret med et omfattende sæt af tests, som rapporterer en lang række fejlsituationer, som kan forekommer. Eksempelvis skal en given variabel findes i stakken allerede for at man kan få lov til at gemme en værdi i den, i modsat fald får man en fejlmeddelelse.

Udover at sende rekursionsstakken med rundt til alle sætninger og udtryk, er der også behov for information om de erklærede procedurer: Der skal være en måde, hvorpå en sætning som proc(qsort, 1, n) kan finde ud af, hvad den skal foretage sig. Vi kan her vælge at bruge en proceduretabel, som genereres ud fra procedureerklæringerne, og som ganske enkelt er en liste med alle procedurefinitioner (dvs. deres abstrakte syntakstræ, som det nu engang er givet). I quicksortprogrammet, hvor der kun er én procedure, vil proceduretabellen se således ud:

[proc( qsort, left, right, (var(i,int) ; ...),

(i:= left ; j:= right ; ...))]

Reglen for sætningsformen proc(...) kan så i eksemplet med quicksort slå op i tabellen ved et kald som følger, idet Pt forventes at være instantieret til den nævnte tabel:

member( proc(qsort,ParId1, ParId2, VarErkl, Saetning), Pt)

Vi får da

ParId1 = left

ParId2 = right

VarErkl = (var(i,int) ; ...)

Saetning = (i:= left ; j:= right ; ...)

Denne generelle for procedurekald kan dernæst evaluere de udtryk, der stå for de aktuelle parametre (her ª1´ og ªn´), og sætte et nyt stakafsnit op, som binder parametrenes værdier til de formelle parametre, samt rummer pladser til de (øvrige) lokale variable – sådan som vi så det i rekursionsstakkene vist for et par sider siden.

Når det rette stakafsnit er opbygget – og sat i forrest=øverst på stakken – kan vi kalde den normale procedure for fortolkning af sætninger med procedurens krop som argument. Når dette fortolkerkald har afsluttet, skal vi så sørge for, at den stak som proc(qsort, 1, n) totalt har produceret er den rette, dvs. med det øverste stakafsnit pillet af igen, men hvor resten af stakken (med de indtrufne sideeffekter på global variable) er bevaret.

Til at fortolke sætninger generelt kunne man foreslå et prædikat med følgende form, idet vi generelt skelner fortolkerprædikaterne fra andre ved at sætte ªf_´ foran navnet på den syntaktiske kategori:

f_sætning( Sætning, StakFør, ProcedureTabel, StakEfter)

Proceduretabellen skal så blot kopieres uændret rundt til alle delsætninger, så den kan nås i anvendelser af denne regel:

f_sætning( proc(ParId1, ParId2, ...), St1, PT, St2):- ...

Vi kan spørge om, hvad den ªoperationelle mening´ af variabel- og procedureerklæringer i et program er. For variabelerklæringerne handler det om at generere et stakafsnit med ªpladser´ for de erklærede variable, samt at initialisere disse til den startværdi, som variablenes type udsiger.

Tag som eksempel de globale variabelerklæringer i quicksortprogrammet (var(n,int) ; var(a, int_array(4))). Det, at ªudføre´ eller fortolke dem bør give anledning til følgende endnu uberørte stakafsnit:

[(a,[0,0,0,0]),(n,0)]

Til fortolkning af variabelerklæringer kunne man foreslå et prædikat af følgende form:

f_var_erk( VariabelErklæringer, StakAfsnitFør, StakAfsnitEfter)

Tilsvarende for procedureerklæringer, her skal blot omsamles en proceduretabel.

Dette kunne alt i alt give anledning til en regel for fortolkning af hele programmer, der ser således ud:

 

f_program( program(Var, Proc, Saetning)):-

f_var_erk(Var, [], Stakafsnit),

f_proc_erk(Proc, [], ProcTabel),

f_saetning(Saetning, [Stakafsnit], ProcTabel, _).

Bemærk, at den afsluttende rekursionsstak ignoreres, et Pax-program forventes selv at kunne præsentere sit resultat til omverdenen ved hjælp passende brug ad write og write_text.

4. Præcisering af opgaven

Opgaven lyder som følger:

Skriv en typechecker og en fortolker for sproget Pax, efter de principper, som er beskrevet i dette skrift og i ªSprog og abstrakte maskiner´

Det anbefales, at man arbejder sammen i grupper på to eller tre personer; énmandsgrupper frarådes, med mindre man har en stor erfaring i Prologprogrammering, og grupper på over tre frarådes, da det så bliver svært for alle at få ªfingrene i koden´, hvilke er en af de vigtigste pointer med opgaven.

Aflevering: Opgaven afleveres senest mandag 15. marts om morgenen, som det seneste, men det anbefales, at man sigter efter eftermiddagen fredag 12. marts. Opgavebesvarelsen afleveres i papirform i Hennings postkasse i hus 20.1. Opgaverne rettes af hjælpelærerne, evt. i samarbejde med kursuslæreren.

Gode råd omkring fremgangsmåde:

  • Brug en computer og et Prologsystem løbende, aftest hele tiden jeres løsningsforslag for Pax's syntaksregler mens I udtænker dem, og evt. før i helt har forstået, hvad I har tænkt.
  • Aftest hele tiden ved hjælp af bittesmå eksempelprogrammer, sætninger, udtryk, .... Brug evt. fragmenter af de to programmer vist i dette notat, da de er aftestet med en løsning på opgaven.
  • Vdr. typecheckeren sørg også for at afteste programmer, I ved indeholder typefejl!
  • Brug Prologs tracer/debugger for at undersøge kørslen af jeres bittesmå eksempler; sæt evt. kontroludskrifter på jeres egen fortolker, så den kommer til at fremstå som en tracer.
  • Man kan vælge enten at implementere først hele typecheckeren og dernæst fortolkeren, eller at arbejde parallelt med de to, så de hele tiden dækker den samme delmængde af Pax. Det sidste kan anbefales, hvis man begynder at køre træt på typecheckeren.
  • ªPlease, do not hessitate´ som man siger, at kontakte kursets lærer eller hjælpelærere for at få et råd til at komme videre, hvis der skulle være brug for det.
  • Hvis I begynder at finde ud af, I ikke kan nå at implementere hele Pax, så sørg for, at det I har implementeret, kommer til at fremstå klart. Læg evt. jeres egne begrænsninger ind på Pax's konstruktioner.

Altså i stedet for at forsøge at løse hele opgaven i ét huk, anbefales det, at man arbejder langsomt og støt bottom-up og gradvist udvider sin løsning til at omfatte større og større dele af Pax.

Krav til en besvarelse på opgaven:

  • Der forventes ikke noget, som minder om en projektrapport.
  • Vedlæg udskrift af programtekst samt testudskrifter fra kørsler, helst inklusive (og om muligt) de to med Euklids algoritme og quicksort.
  • Der må gerne være en side eller to som indledning, der forklarer, hvad I har lavet, og hvad der måske har voldt problemer, og sådan man ved, hvordan programtudskrifetr skal forstås.
  • Det er ikke et ultimativt krav, at man har løst hele opgaven korrekt, men man får mest ud af opgaven hvis man er nået rundt om de fleste del af sproget og konstateret, at man har fået det (stort set) til at fungere.
  • Skriv venligst tydeligt navn på alle projektdeltagere på forsiden.

Omkring opgavens omfang kan der være individuelle udsving i, hvor lang tid hver enkelt eller grupperne har behov for, men det er hensigten, at den skulle kunne løses indenfor den tid, man ellers skulle have brugt på kurset (hvis man ellers griber det rigtigt an). Hvis der opstår problemer, så tag kontakt med lærer eller hjælpelærere hellere tidligt end sent.

Det kan oplyses, at for testløsninger udviklet i forbindelse med design af opgaven fylder typechecker og fortolker for hele Pax hver især ca. 3 siders Prologkode, 181 resp. 225 linjer med et gennemsnit af 22 tegn pr. linje.

Hvis opgaven forekommer for nem for nogen, og som måtte finde interesse i at udvide den kan der foreslås følgende:

  • Udvid fortolkeren til at være en debugger, som for hver simpel sætning i programmet og start på en større struktur (f.eks. en løkke eller et procedurekald) afventer et svar fra brugeren:
  • ªc´ (for ªcreep) eller linjeskift betyder: Fortsæt detaljeret trace med udskrift af variables værdier og hvad man ellers kan finde på.
  • ªs´ (for ªskip´): Udfør uden at generere kontroludskrifter.
  • ªb´ (for ªbackwards´): der spoles ét skridt tilbage i udførelsen.

Dette er faktisk slet ikke så kompliceret, som det lyder, hvis man vel og mærket har forstået (= for alvor internaliseret) hvad kontrol er i Prolog. Find evt. på andre interessante kommandoer, f.eks. så man kan spørge på og ændre variables værdier i forhold til den aktuelle rekursionsstak.

  • Udvid Pax med flere konstruktioner, f.eks:
  • Gensidigt rekursion mellem procedurer
  • Procedurer inde i procedurer
  • Arrays ikke blot af heltal men med vilkårlige typer (inc. arrays af array).
  • En for-sætning
  • Brugerdefinerede datatyper a la Pascals records (Å klasser uden metoder)