colleges recursieve datastructuren college 9 interpreteren: waarde van bomen bepalen transformeren: vorm van bomen veranderen parseren herkennen van expressie in de tekst herkennen van functies onderwerp van vorig college onderwerpen van dit college interpreteren waarde van expressie berekenen voor gegeven x en y transformeren de expressies zelf veranderen 1 2 prioriteit in recursive descent parser prioriteit van operatoren versleutel de prioriteit in de syntax b.v.: expr = term + expr term term = fact * term fact fact = var int ( expr ) var ( args ) args = ε arg2 arg2 = expr expr, arg2 kun je hier met look ahead 1 steeds de juiste keuze maken? gaat dit ook goed als je operator toevoegt? los dit op in de regels van de grammatica i.p.v. E -> E * E E + E ( E ) num schrijven we E -> E + T T T -> T * F F F -> ( E ) num helaas is dit weer linksrecursief, maar we weten hoe we dit moeten aanpakken let ook op bindingsrichting 3 3 2 1 = ( 3 2 ) 1 = 0 maar 3 ( 2 1 ) = 3 1 = 2 voor sommige operatoren is dit geen probleem 4 1
parser voor factor Knoop* parsef () E -> E + T T T -> T * F F Knoop* n = scanner. token (); F -> num switch ( n -> id ) case Int: scanner. volgendetoken (); return n; case Einde: return n; default: Error ( "parse error: integer, of einde verwacht" ); return n; 5 parser voor term Knoop* parset ( ) Knoop* f = parsef ( ); Knoop* o = scanner.token ( ); switch ( o -> id ) case Op: if ( o -> soort[0] == '*') o -> child ( 0 ) = f; scanner. volgendetoken (); o -> child ( 1 ) = parset ( ); return o; else return f; case Einde: return f; default: Error ( "parse error: * of einde verwacht" ); return f; E -> E + T T T -> T * F F F -> num E -> T (+ E)? T -> F (* T)? F -> num hoewel het een operator is, kunnen we o->op niet gebruiken 6 parser voor expressie Knoop* parsee ( ) Knoop* t = parset ( ); Knoop* o = scanner.token ( ); switch ( o -> id ) case Op: if ( o -> soort[0] == '+' ) o -> child ( 0 ) = t; scanner. volgendetoken ( ); o -> child ( 1 ) = parsee ( ); return o; else return t; case Einde: return t; default: Error ( "parse error: + of einde verwacht" ); return t; E -> E + T T T -> T * F F F -> num E -> T (+ E)? T -> F (* T)? F -> num 7 statische tests je kunt niet alles met een parser voor een CFG controleren wat kun je nog meer controleren? hele input gelezen operatoren zijn bekend variabelen zijn bekend functies zijn bekend operatoren hebben juiste aantal argumenten functies hebben juiste aantal argumenten types? 8 2
syntax sequents ϕ 1 ϕ n o ψ 1 ψ m met ϕ i en ψ j logische formules bedenk een handig symbool voor o, dat niet botst met namen van variabelen en operatoren ϕ, ψ = p, p, p q, p q, p q, p q, (p) ook hier hoort volgens de standaard interpretatie een prioriteit bij operatoren bindt sterker dan en : p q = ( p) q sterker dan : p q r = (p q) r (volg de standaard logica regels) het is handig als variabelen namen langer dan 1 letter mogen zijn twee aanpakken 1: interpretatie expressies evalueren we beperken ons tot interpretatie pak direct de syntax boom en bepaal waarde expressies relatief eenvoudig, maar niet erg efficient geen aparte compilatie nodig 2: vertalen (compileren) genereer code die waarde expressie bepaalt ingewikkelder, maar circa 10 keer sneller dan interpretatie er bestaan ook mengvormen b.v. Java byte-code of.net genereer hoog niveau code (platform onafhankelijk) interpreteer die code 9 10 interpretatie basis: denotationele semantiek geeft betekenis aan expressies ingrediënten: syntax operatie pattern-match op syntax Scott brackets: geeft match op syntax aan E [[ a 1 + a 2 ]] = E [[ a 1 ]] + E [[ a 2 ]] E [[ a 1 * a 2 ]] = E [[ a 1 ]] * E [[ a 2 ]] E [[ n ]] = n voor getallen functie die binding van variabelen kent state :: Var -> Int we nemen altijd aan dat het een juiste boom is: bindingen kloppen 11 binding van variabelen wiskundig: state is functie van variabelen naar waarde state :: Var -> Int b.v. s = x 1, y 2 dan s x = 1 en s y = 2 mooi voor semantische regeltjes lastig te maken in C++ maak een datastructuur voeg wat functies toe 12 3
syntax n numeral x variable a expression a = n x a 1 + a 2 a 1 * a 2 semantiek basis semantiek A [[ a 1 + a 2 ]] s = A [[ a 1 ]] s + A [[ a 2 ]] s A [[ a 1 * a 2 ]] s = A [[ a 1 ]] s * A [[ a 2 ]] s A [[ n ]] s = n A [[ x ]] s = s x voorbeeld met s = x 1, y 2 A [[ x + 3 ]] s = A [[ x ]] s + A [[ 3 ]] s = s x + 3 = 1 + 3 getal = 4 syntax context geeft verschil aan 13 syntax booleans b = True False a 1 == a 2 a 1 < a 2 b 1 /\ b 2 semantiek B [[ True ]] s B [[ False ]] s B [[ a 1 == a 2 ]] s B [[ a 1 < a 2 ]] s B [[ b 1 /\ b 2 ]] s syntax = true echte vergelijking = false syntax = A [[ a 1 ]] s == A [[ a 2 ]] s = A [[ a 1 ]] s < A [[ a 2 ]] s = if ( B [[ b 1 ]] s ) B [[ b 2 ]] s else false voorbeeld met s = x 1, y 2 logische waarde luiheid zit hier ingebakken B [[ x + 3 < y ]] s = A [[ x + 3 ]] s < A [[ y ]] s = A [[ x ]] s + A [[ 3 ]] s < s y = s x + 3 < 2 = 1 + 3 < 2 = 4 < 2 = false 14 expressies we hebben dus 2 semantische functies A [[ a ]] s levert een getal B [[ b ]] s levert een logische waarde hoe implementeren we dat? twee aparte evaluatie functies één functie met getypeerd resultaat gebruik de C-truc: 0 false, 1 true alle methoden hebben voor- en nadelen kies wat in gegeven situatie het beste past implementatie context in moderne talen kan dat direct in C++ is het handiger A [[ x ]] s = s x te vervangen door A [[ x ]] s = lookup x s ook het binden van een variabele doen we met een functie 15 16 4
context in C++ maak een lijst van bindingen class ENV public: char * naam; int val; ENV * next; met deze expressies geen update nodig ENV ( char s [], int n, ENV* p = NULL ) : val ( n ), next ( p ) naam = new char [ strlen ( s )+1 ]; strcpy ( naam, s ); ; context in C++ 2 int lookup ( ENV* env, char s [] ) had de checker if ( env == NULL ) moeten zien Error ( "Naam niet gevonden" ); else if ( strcmp ( s, env->naam ) == 0 ) return env -> val; else return lookup ( env -> next, s ); ENV*& bind ( char s [], int n, ENV* e ) return e = new ENV ( s, n, e ); 17 18 implementatie A [[ a ]] s minstens 3 mogelijkheden: overloaded functies functie die gevalsonderscheid doet methode eval bij knopen we zullen de mogelijkheden verder bekijken 19 A [[ a ]] s met overloaded functie int eval ( Getal * g ) return g->waarde ( ); int eval ( Operator *o ) int x = eval ( o -> child ( 0 )); int y = eval ( o -> child ( 1 )); switch ( o->op ) case '+': return x+y; case '*': return x*y; compiler wil hier statisch de juiste kiezen, maar dat kan niet dynamic binding bij methoden had de parser moeten zien default: Error ( "Onbekende operator in eval" ); 20 5
A [[ a ]] s met gevalsonderscheid int eval ( Knoop *k ) switch ( k -> id ) case Num: return k -> waarde ( ); case Op: switch ( k -> op ) case '+': int x = eval ( k -> child ( 0 )); int y = eval ( k -> child ( 1 )); return x+y; case '*': compiler ziet niet dat dit goed gaat, mag dus niet default: Error ( "Onbekende operator in eval" ); 21 A [[ a ]] s met gevalsonderscheid 2 int eval ( Knoop *k ) switch ( k -> id ) case Op: Operator * o = static_cast <Operator*> ( k ); switch ( o -> op ) case '+': int x = eval ( o -> child ( 0 )); int y = eval ( o -> child ( 1 )); return x+y; case '*': expliciete type conversie nu kan dit wel je kunt alles naar alles casten, foutgevoelig default: Error ( "Onbekende operator in eval" ); 22 class Knoop protected: Knoop** children; int stap; char * soort; public: KnoopType id; int arity; A [[ a ]] s met methode eval alleen als er variabelen zijn Knoop (.) : arity ( a ), stap ( 3 ), id ( kt ) virtual int eval ( ENV* e ) Error ( "Kan de basisklasse Knoop niet evalueren" ); ; nieuwe methode eval getal class Getal : public Knoop int w; public: Getal ( int n ) : w ( n ), Knoop ( "Getal", Int, 0 ) int waarde ( ) return w; int eval ( ENV* e ) return w; ; nieuwe methode 23 24 6
int Var :: eval ( ENV* e ) if ( arity==0 ) return lookup ( e, name ); else Error ( ) ; pas op: eval variabele met argumenten is het een functie had de checker moeten zien eigenlijk moet je verschil maken tussen x + 1 x () + 1 functies en variabelen kunnen eigenlijk dus niet zo maar in zelfde klasse eval operator int Operator :: eval ( ENV* e ) int x = child ( 0 ) -> eval ( e ); int y = child ( 1 ) -> eval ( e ); switch ( op ) case '+': return x+y; case '*': return x*y; default: Error ( "Onbekende operator in eval" ); had de checker moeten zien 25 26 syntax keuze n numeral x variable a expression a = n x a 1 + a 2 a 1 * a 2 if b then a 1 else a 2 semantiek A [[ a 1 + a 2 ]] s = A [[ a 1 ]] s + A [[ a 2 ]] s A [[ a 1 * a 2 ]] s = A [[ a 1 ]] s * A [[ a 2 ]] s A [[ n ]] s = n A [[ x ]] s = s x A [[ if b then a 1 else a 2 ]] s = if ( B [[ b ]] s ) A [[ a 1 ]] s A [[ a 2 ]] s zorg dat A 1 of A 2 wordt geëvalueerd moet het zo? door het aannemen van een bovengrens voor aantal variabelen wordt de implementatie iets eenvoudiger rij i.p.v. lijst lijsten mogen echter geen probleem meer zijn 27 28 7
moet het echt zo? transformaties kunnen we niet zonder environment? idee: vervang alle variabelen door waarde in boom uitrekenen kan zonder environment nadelen je moet ook kopie van expressie maken voor substitutie heb je toch een environment nodig werkt zo iets ook bij functies? ook hier bedenken we eerst wat we willen bijvoorbeeld optimalisatie: T [[ 0 + x ]] = T [[ x ]] T [[ x + 0 ]] = T [[ x ]] T [[ 0 * x ]] = T [[ 0 ]] T [[ x * 0 ]] = T [[ 0 ]] T [[ 1 * x ]] = T [[ x ]] T [[ x * 1 ]] = T [[ x ]] T [[ n * m ]] = n * m T [[ n + m ]] = n + m simpele wiskunde regeltjes wat als die recursief zijn? 29 geen environment nodig 30 implementatie mogelijkheden nieuwe methoden Knoop overloaded functie kan niet, want je kunt niet statisch de juiste kiezen gevalsonderscheid daarvoor heeft Knoop een enum KnoopType Int, Op,.. ; er zijn dan type_casts nodig, niet leuk nieuwe methode toevoegen dynamic binding gebruiken vaak de mooiste oplossing we hoeven alleen te weten of een Knoop een getal is, indien dat zo is willen we ook de waarde 31 class Knoop.. public: virtual bool waarde ( int& v) return false; virtual void opti ( Knoop *& root ); ; waarom die? T [[ 0 + x ]] = T [[ x ]] T [[ x + 0 ]] = T [[ x ]] T [[ 0 * x ]] = T [[ 0 ]] T [[ x * 0 ]] = T [[ 0 ]] T [[ 1 * x ]] = T [[ x ]] T [[ x * 1 ]] = T [[ x ]] T [[ n * m ]] = n * m T [[ n + m ]] = n + m 32 8
waarde voor getallen class Getal : public Knoop int w; public: bool waarde ( int & v ) v = w; return true; ; voor alle andere knopen voldoet de default dit moet bottum-up: optimalisatie knopen void Knoop :: opti (Knoop *& root) for ( int i=0; i<arity; i+=1 ) child ( i ) -> opti ( child ( i )); maf, maar nodig als er voor deze knoop niets speciaals te doen is, optimaliseer dan in ieder geval de kinderen 33 34 optimalisatie operatoren void Operator :: opti ( Knoop *& root ) for ( int i=0; i<arity; i+=1 ) child ( i ) -> opti ( child ( i )); int x, y; if ( child ( 0 ) -> waarde ( x )) if ( child ( 1 ) -> waarde ( y )) switch (op).. case '+': root = new Getal ( x + y ); return; case '*': root = new Getal ( x * y ); return; default: return; root nodig eigenlijk delete nodig bottum up 2 getallen, reken uit T [[ 0 + x ]] = T [[ x ]] T [[ x + 0 ]] = T [[ x ]] T [[ 0 * x ]] = T [[ 0 ]] T [[ x * 0 ]] = T [[ 0 ]] T [[ 1 * x ]] = T [[ x ]] T [[ x * 1 ]] = T [[ x ]] T [[ n * m ]] = n * m T [[ n + m ]] = n + m 35 optimalisatie operatoren 2 else // x is getal, y niet switch ( op ) case '+': if ( x == 0 ) root = child ( 1 ); return; case '*': switch ( x ) case 0: root = new Getal ( 0 ); return; case 1: root = child ( 1 ); return; default: return; default: return;.. 1 * x 0 + x 0 * x T [[ 0 + x ]] = T [[ x ]] T [[ x + 0 ]] = T [[ x ]] T [[ 0 * x ]] = T [[ 0 ]] T [[ x * 0 ]] = T [[ 0 ]] T [[ 1 * x ]] = T [[ x ]] T [[ x * 1 ]] = T [[ x ]] T [[ n * m ]] = n * m T [[ n + m ]] = n + m 36 9
voorbeeld optimalisatie resultaat optimalisatie int main ( ) invoer = new stringinvoer ( "1*x*1 + 3* ( 0*tmp + 4 )" ); scanner. volgendetoken ( ); Knoop* k = parsee ( ); k -> drukaf ( ); k -> opti ( k ); cout << "---- optimized ----\n"; k -> drukaf ( ); system("pause"); return EXIT_SUCCESS; 37 1*x*1 + 3* ( 0*tmp + 4 ) ---------------------- 4 + tmp * 0 * 3 + 1 * x * 1 ---- optimized ---- 12 + x vindt dit alles? T [[ 0 + x ]] = T [[ x ]] T [[ x + 0 ]] = T [[ x ]] T [[ 0 * x ]] = T [[ 0 ]] T [[ x * 0 ]] = T [[ 0 ]] T [[ 1 * x ]] = T [[ x ]] T [[ x * 1 ]] = T [[ x ]] T [[ n * m ]] = n * m T [[ n + m ]] = n + m 38 gevalsonderscheid subtypen het helpt om een attribuut in de klasse te stoppen dat het (sub)type aangeeft om hier echt gebruik van te maken heb je vaak een type_cast nodig, niet zo mooi dus overloaded functies werken meestal niet goed te gebruiken regel wordt statisch bepaald een extra methode is meestal de beste oplossing maak de methode virtual geef nieuwe implementatie als er iets speciaals moet gebeuren in die subklasse zou ook het probleem met operator namen beter oplossen dan soort[0] semantiek algemeen assignments: pas waarde van variabele in environment aan evaluatie levert dus waarde + nieuwe environment C++ is lastig omdat environment kan veranderen tijdens evaluatie globale objecten: zitten ook in de environment moeten er weer uitgehaald worden als de scope stopt andere vormen van semantiek: behalve deze denotationele semantiek zijn er nog vele anderen: operational, natural, axiomatic, maken redeneren over programma's mogelijk 39 40 10
herschrijven voor sequents gebruik de gegeven regeltjes regeltjes veranderen expressie i.t.t. de interpretatie van numerieke expressies besluit of je de afleidingsboom wil bouwen of niet je kunt strategie kiezen eerst een kant alles herschrijven probeer regels in de gegeven volgorde bedenk iets slims onthouden waar je bent pointer bewerkte deel bevat alleen variabelen, die kun je ook speciaal bewaren weet waar je aan begint kopieren van expressies zelfs als je geen boom bouwt moet je sequents kopieren b.v. Φ ο α β, Ψ R : Φ ο α, Ψ Φ ο β, Ψ maak een goede copy operator vergeet niet te veranderen wat nodig is bedenk hoe je de te evalueren sequents beheert bedenk hoe je resultaten combineert 41 42 denotationele semantiek sequents ook hier kun je semantiekregeltjes opstellen geven transformaties + strategie gebruik verzameling variabelen als environment strategie: b.v. maak eerst linkerkant leeg door volgorde van semantische regels S [[ ϕ, Φ ο Ψ ]] e = S [[ Φ ο ϕ, Ψ ]] e S [[ α β, Φ ο Ψ ]] e = S [[ α, Φ ο Ψ ]] e S [[ β, Φ ο Ψ ]] e S [[ v, Φ ο Ψ ]] e = S [[ Φ ο Ψ ]] ( e v ) S [[ ο ϕ, Ψ ]] e = S [[ ϕ ο Ψ ]] e S [[ ο α β, Ψ ]] e = S [[ ο α, Ψ ]] e S [[ ο β, Ψ ]] e S [[ ο v, Ψ ]] e = Closed, if v e 43 terminatie van het herschrijven we hoeven niet altijd het hele tableau uit te werken bij zelfde variabele links en rechts is sequent gesloten bij één tegenvoorbeeld is de bewering al ongeldig bewering is gesloten als alle sequents gesloten zijn 44 11
correctheid wanneer is je implementatie correct? als hij voor alle mogelijke sequents het goede antwoord geeft hoe kun je dat nagaan? kijk naar de code voor alle regeltjes test de juiste implementatie met minstens 1 voorbeeld test open en gesloten tableaus gebruik b.v. unittest voor het uitvoeren van deze tests volgende week toets over lijsten en bomen telt als 2 practicumopgaven gesloten boek pen en papier individueel opgave mag een week later ingeleverd worden Sjaak Smetsers geeft college 45 46 wat hebben we gedaan interpreteren basis is semantiek + goede parse tree nauwelijks of niet in boek en dictaat veel literatuur over imperatief: www.daimi.au.dk/~hrn heel T3 gaat over semantiek in vertalerbouw maak je een programma dat code genereert die expressies evalueert 47 12