1 Aufgabe 1 und 2 jay-Änderungen Ich habe zuerst Aufgabe 1 gelöst und diese Lösung für Aufgabe 2 weiterentwickelt. Ich zeige hier nur das Endergebnis. Die Quellen der beiden Case-Klassen und deren Unterklassen wurden nach aufg2/sem und aufg2/run kopiert. Weiterhin habe ich die jay-Datei um die break- und continue-Anweisungen erweitert. Hinter den beiden neuen Anweisungen kann optional (Aufgabe 2) ein Ausdruck folgen. import aufg2.sem.Case; import aufg2.sem.Jump; %% stmt : | | | | ... BREAK BREAK expr CONTINUE CONTINUE expr { { { { $$ = new parserAt $$ = new parserAt Jump.Break(); } = $1; $$ = new Jump.Break($2); } Jump.Continue(); } = $1; $$ = new Jump.Continue($2); } %% protected Hashtable symbolTable = new Hashtable(); { ... symbolTable.put("break", new Integer(BREAK)); symbolTable.put("continue",new Integer(CONTINUE)); } 2 Die semantische Analyse Bei der semantischen Analyse der neuen Anweisungen muß geklärt werden, ob diese sich in einer gültigen Umgebung befinden. Es darf z.B. kein break außerhalb einer Schleife oder außerhalb einer case-Anweisung stehen. In der Klasse ck.sem.Stmt werden dazu zwei Klassenvariablen nBreaks und nContinues eingeführt. Ein break ist z.B. erlaubt, wenn nBreaks größer Null ist. protected static int nBreaks = 0; public int getNBreaks() { return nBreaks; } protected static int nContinues = 0; public int getNContinues() { return nContinues; } Die semantische Analyse von Schleifen und die der Case-Anweisung zählen die beiden neuen Variablen nun hoch und runter: Case: public Type sem () { if (! didSem) { didSem = true; nBreaks++; ... // alter Code nBreaks--; } return type; } Stmt.While: public Type sem () { if (! didSem) { nBreaks++; nContinues++; ... // alter Code nBreaks--; nContinues--; } return type; } 3 Die Klassen Break und Continue des Parse-Baums stammen als innere Klasse von aufg2.sem.Jump auch von der Klasse Jump ab. Jump-Objekte habe einen Unterbaum. Der Unterbaum ist bei einem break oder continue ohne den optionalen Ausdruck null, sonst der Parse-Baum des Ausdrucks. In der sem-Methode von Jump teilen sich Break- und Continue-Objekte Teile ihrer semantischen Analyse. Generell sollen beide Anweisungen nur mit geraden Ebenenzahlen arbeiten. Daher wird der optionale Unterbaum auf den richtigen Typ hin untersucht. Ist der Typ nicht Int, wird Int nach einer möglichen Umwandlung gefragt. Kann Int nicht wandeln, wird der Typ des Unterbaums gefragt. Haben beide Anfragen keinen Erfolg, kommt es zu einer Fehlermeldung. Bei Erfolg einer der Anfragen, wird der neue Parse-Baum in sub[0] hinterlegt. public class Jump extends Stmt { public Jump () { this(null); } public Jump (Sem expr) { sub = new Sem [] { expr}; } public Type sem () { if (! didSem) { didSem = true; type = Int.self; if(sub[0]!=null) { Type st = sub[0].sem(); // check sub[0] if(st == null) // error in sub[0].sem occured type = null; else // sub[0].sem was ok if(st != Int.self) {// is type of sub[0] Int? Sem t; // ask Int to convert expr t = Int.self.cast(Int.self, this, 0, st); if(t == null) // ask type of sub[0] to convert t = st.cast(Int.self, this, 0, st); if(t == null) { // cann’t convert type type = null; error("cann’t convert "+st+"to "+Int.self); return type; } // convert was ok, store new tree sub[0]=t; } } } return type; // null indicates error } public static class Break extends Jump { ... } public static class Continue extends Jump { ... } } 4 Die Klasse Break ruft in ihrer sem-Methode die der Oberklasse Jump auf. Danach ist der optionale Unterbaum vom Typ Int, oder es ist ein Fehler aufgetreten. Als nächstes wird geprüft, ob es in der aktuellen Umgebung überhaupt break-Anweisungen geben darf. Dazu wird die Klassenvariable nBreaks aus ck.sem.Stmt abgefragt. Wurde das break ohne den optional folgenden Ausdruck benutzt, ist die Semantikanalyse hier zu Ende. Der Knoten im Interpreterbaum wird gebaut und die Methode geht zu Ende. Hat das Objekt aber einen Ausdruck als Unterbaum, wird dieser weiter untersucht. Ist der Unterbaum ein Value, kann der Ausdruck mit den maximal erlaubten break- bzw. continue-Ebenen verglichen werden. Weiterhin wird in diesem Fall ein anderer Konstruktor des Interpreter-Knotens verwendet. Analog funktioniert die semantische Analyse der Klasse Continue und ist daher hier nicht extra dargestellt. public static class Break extends Jump { public Break () { this(null); } public Break (Sem expr) { super(expr); } public Type sem () { if (! didSem) { super.sem(); // check expr and type of expr if(type == null) // a error occured, return return type; if(nBreaks<=0) { // is the break in a correct environment? type = null; error("no break allowed"); return type; } if(sub[0] == null) { // no expr tree, build run tree and return value = new aufg2.run.Jump.Break(); return type; } // is the expr a number ? if(sub[0].getValue() instanceof Value) { // get number of expr int n = ((ck.run.Int)sub[0].getValue()).value; if(nBreaks<= n) { // check if the number of levels is ok type = null; // level isn’t ok, return error("no break "+n+"allowed"); return type; } // level is ok, build run tree with level value = new aufg2.run.Jump.Break(n); return type; } // build run tree with expr tree as level value = new aufg2.run.Jump.Break(sub[0].getValue()); } return type; } } 5 Die Laufzeit-Klassen Die Klassen JumpException und deren inneren Unterklassen BreakException und ContinueException im Paket ck.run dienen zur Implemtierung der break- und continue-Anweisung. Eine JumpException kapselt eine Integer-Zahl. Die Zahl repräsentiert die break- oder continue-Ebenen. Werden Objekte mit einer Ebenenzahl unter eins erzeugt, kommt es zu einem Fehler. Über die Methode getLevel kann die aktuelle, interne Information einer JumpException erfragt werden. Dabei wird diese auch gleich um eines erniedrigt. Sinkt die Ebene dabei unter eins, darf getLevel nicht weiter aufgerufen werden. package ck.run; public abstract class JumpException extends RuntimeException { protected int n; public JumpException () { this(1); } public JumpException (int n) { this.n=n; if(n<1) throw new RuntimeException("illegale JumpException with"+ " level "+n+" created"); } public String toString() { return getClass().getName()+" level "+n; } public int getLevel () { if(n==0) throw new RuntimeException("illegale JumpException,"+ " level is 0"); return n--; } public static class BreakException extends JumpException { public BreakException () { this(1); } public BreakException (int n) { super(n); } } public static class ContinueException extends JumpException { public ContinueException () { this(1); } public ContinueException (int n) { super(n); } } } 6 Die Klasse aufg2.run.Jump bzw. deren inneren Unterklassen Break und Continue kapseln die Knoten im Interpreterbaum der beiden neuen Anweisungen. Jump-Objekte kennen entweder ihre break- bzw. continue-Level als Zahl oder berechnen sich diese als Ergebnis eines Ausdrucks als Unterbaum. Der Unterbaum ist durch die semantische Analyse immer vom Typ Int. In eval von Break oder Continue werden dann einfach die zugehörigen JumpExceptions erzeugt. public abstract class Jump extends Run { int n; public Jump () { this(1); } public Jump (int n) { this.n = n; } public Jump (Run expr) { sub = new Run [] { expr }; } public abstract Run eval () throws Exception; public static class Break extends Jump { public Break () { super(); } public Break (int n) { super(n); } public Break (Run expr) { super(expr); } public Run eval () throws Exception { if(sub == null) throw new JumpException.BreakException(n); throw new JumpException.BreakException( ((Int)sub[0].eval()).value); } } public static class Continue extends Jump { ... } // analog } Die von Break- und Continue-Objekten erzeugten JumpExceptions werden nun in eval der Klassen ck.run.While und aufg2.run.Case abgefangen und bearbeitet. In While werden beide Arten der Exceptions abgefangen. Ist die JumpException für eine weiter außen gelegene Kontrollstruktur, wird die Exception einfach erneut ausgelöst. Der Level in der JumpException wurde bereits bei Aufruf der Methode getLevel runtergezählt. Ist die JumpException für das while selber, wird je nach Art der JumpException ein break oder ein continue ausgeführt. public class While extends Run { ... public Run eval () throws Exception { Run result = null; while (((Bool)sub[0].eval()).value) try { if (sub[1] != null) result = sub[1].eval(); } catch (JumpException e) { if(e.getLevel()>1) throw e; if(e instanceof JumpException.BreakException) break; continue; } return result; } } 7 Die Methode eval der Klasse aufg2.run.Case hat sich gegenüber dem letzten Aufgabenblatt leicht geändert. In einer ersten for-Schleife wird der erste wahre case-Zweig oder der erste default-Zweig gesucht. default-Zweige bestehen Dank der semantischen Analyse im Gegensatz zu case-Zweigen nicht aus einem if. Ist der Einstiegs-Punkt gefunden, werden nun alle weiteren case-Zweige ohne testen der Bedingung und alle weiteren default-Zweige ausgeführt. Damit kommt es zu einem fall through bei den Zweigen. Beim Ausführen werden BreakException abgefangen und bearbeitet, man kann damit die Zweige durch break-Anweisungen abbrechen. ContinueExceptions werden nicht abgefangen. public class Case extends Run { ... public Run eval () throws Exception { int n; Run result = null; for (n = 0; n < sub.length; ++ n) if ( sub[n] instanceof If) { if(((Bool) ((Run)sub[n].sub(0)) .eval()) .getValue() ) break; } else // default break; for (; n < sub.length; ++ n) // eval with fall through try { if (sub[n] instanceof If) result = ((Run)sub[n].sub(1)) .eval(); else // default result = sub[n].eval(); } catch (JumpException.BreakException e) { if(e.getLevel()>1) throw e; break; } return result; } } 8 Bemerkung - In dieser Lösung führen negative Ausdrücke oder ein Nullwert hinter break oder continue zu einem Laufzeitfehler beim Erzeugen der JumpException bzw. zu einem Fehler in der Semantik-Analyse. Eine andere Idee ist, daß bei einem negativen Wert die Zählrichtung einfach umgekehrt wird. Dann muß man auch zur Laufzeit Variablen der Art nBreaks und nContinues mitführen. Bsp.: float f; program while 2=2 do while 3=3 do while 4=4 do read f; if f > 3 then continue f-3 fi; if f>=-3 then break f fi; continue f+3 od; od; od; Innerhalb der innersten while-Schleife wäre ein break 1 analog zu break -3 und continue 3 analog zu continue -1. - In dieser Lösung wurde das CompilerKit selber geändert. Rein theoretisch geht das auch ohne, erfordert dann aber mehr Arbeit. Man müßte Code aus dem Kit duplizieren, wie z.B. bei den Klassen ck.sem.Stmt.While und ck.run.While. - Die Körper einer Funktion bzw. einer Prozedur und das Hauptprogramm werden unabhängig voneinander semantisch geprüft. Damit kann es auch keine Probleme beim Hoch- und Runterzählen von nBreaks und nContinues geben. Damit kann ein break bzw. continue nie aus einer Funktion bzw. Prozedur herauszeigen. Vielleicht möchte man dieses bei späteren lokalen Funktionen und Prozeduren aber wieder erlauben. Diese dürften dann nicht separat getestet werden. Bsp.: while cond { func x { break } ... } 9 Tests Im Katalog q finden Sie einige Tests. In mixed/cmp/makefile können Sie beim Ziel test diese durch ein- und auskommentieren testen. Hier das Beispiel break_continue_expr.q: float f; program while 1=1 do write "at start of loop 1\n"; while 2=2 do write "at start of loop 2\n"; while 3=3 do write "at start of loop 3\n"; read f; if f > 3 then continue f-3 fi; if f>=-3 then break f fi; continue f+3 od; write "after loop 3\n" od; write "after loop 2\n" od; write "after loop 1\n" Eine Ausführung: suleika $ 5 CLASSPATH=../..:$HOME/cb/Uebungen java mixed.cmp.Cmp2 \ > ../../q/break_continue_expr.q at start of loop 1 at start of loop 2 at start of loop 3 1 after loop 3 at start of loop 2 at start of loop 3 2 after loop 2 at start of loop 1 at start of loop 2 at start of loop 3 3 after loop 1 10 suleika $ 5 CLASSPATH=../..:$HOME/cb/Uebungen java mixed.cmp.Cmp2 \ > ../../q/break_continue_expr.q at start of loop 1 at start of loop 2 at start of loop 3 4.1 at start of loop 3 5.2 at start of loop 2 at start of loop 3 6 at start of loop 1 at start of loop 2 at start of loop 3 Zahlen größer gleich sieben und Zahlen kleiner eins führen verständlicherweise zu Fehlern. 11 Aufgabe 3 In Aufgabe 3 sollte ein cast-Operator analog zu C eingebaut werden. Ich habe mich auch in Bezug auf Vorrang und Bewertungsreihenfolge an C gehalten, d.h. der cast-Operator hat den gleichen Vorrang wie die unären Operatoren ~, + und - und wird von rechts her bewertet. Auch die Syntax ist analog zu C. In dem Parse-Baum wird ein Objekt der Klasse Cast für einen cast-Operator eingebaut. Die Objekte bekommen als ersten Paramter den Typ, zu dem hin gewandelt werden soll, und als zweites Argument den zu wandelnden Ausdruck. Für das Eingabesymbol ) werden Strings auf den Werte-Stack gelegt. Damit kann man die Zeilennummer durch Zuweisen an parseAt zurücksetzen. import aufg3.sem.Cast; %token %type <String> <Type> ’,’, ’!’, ’(’, ’)’ cast_types %right <String> ’~’ %% expr : ... | ’~’ expr { parserAt $$ = new | ’(’ cast_types ’)’ expr %prec ’~’ { parserAt $$ = new | ’+’ expr %prec ’~’ { $$ = $2; | ’-’ expr %prec ’~’ { parserAt $$ = new | ... cast_types | FLOAT %% : INT = $1; Unary.Complement($2); } = $3; Cast($2, $4); } } = $1; Unary.Minus($2); } { $$ = Int.self; } { $$ = Flt.self; } 12 Die Klasse Cast stammt aus dem Paket aufg3.sem und stammt von ck.sem.Unary ab, da sie einen Unterbaum besitzt. In dem Konstruktor wird als Typ des Objekts der im ersten Argument angegebene Typ hinterlegt und der als zweites Argument angegebene Unterbaum über einen super-Aufruf im sub-Array abgelegt. In der sem-Methode wird als erstes der Unterbaum geprüft. Hattet diese Erfolg, werden die Typen des Unterbaums und des Cast-Objekts verglichen. Sind die Typen gleich, muß nichts weiter getan werden. Der Interpreterbaum des Unterbaums kann einfach übernommen werden. Sind die Typen verschieden, muß ein Umwandlungsknoten eingebaut werden. Zuerst wird der Typ des Cast-Objekts gefragt, ob es die Umwandlung kann. Wenn nicht, so wird der Typ des Unterbaums gefragt. Schlagen beide Anfragen fehl, kann der Unterbaum nicht gecastet werden, und es wird eine Fehlermeldung ausgegeben. Hatte eine der Anfragen Erfolg, so wird der bei der Anfrage gelieferte Umwandlungsknoten in sub[0] hinterlegt und value übernommen. Es gibt natürlich kein aufg3.run-Paket. package aufg3.sem; import ck.sem.Type; import ck.sem.Unary; import ck.sem.Sem; public class Cast extends Unary { public Cast(Type t, Sem node) { super(node); this.type = t; } public Type sem () { if (! didSem) { didSem=true; if(sub[0].sem() == null ) type = null; else if (type != sub[0].sem()) { // convert ? Sem t; Type st = sub[0].sem(); if((t = type.cast(type, sub[0], st)) == null) if((t = st.cast(type, sub[0], st)) == null) { type = null; error(this+": can’t cast"+st+" to "+type); return type; } sub[0] = t; value = t.getValue(); } else value = sub[0].getValue(); } return type; } } 13 Das waren schon alle Änderungen. Wir wollen uns wieder an einem kleinen Beispiel (case.q) die Bäume ansehen. float f = program write write write (float) (int) 4.3; 3 / 4, "\n"; 3 / (float) 4, "\n"; 3 / (int) f, "\n" Hier der Baum vor der semantischen Analyse. Die Cast-Knoten sind fett hervorgehoben. Seq Binary$Assign f: Flt$Variable Flt$Self aufg3.sem.Cast aufg3.sem.Cast Flt Flt$Self: Flt 4.3 Unary$Write Binary$Div Int Int$Self: Int 3 Int Int$Self: Int 4 Unary$Write Str Str$Self: Str "\n" Unary$Write Binary$Div Int Int$Self: Int 3 aufg3.sem.Cast Int Int$Self: Int 4 Unary$Write Str Str$Self: Str "\n" Unary$Write Binary$Div Int Int$Self: Int 3 aufg3.sem.Cast Unary$Ref f: Flt$Variable Flt$Self Unary$Write Str Str$Self: Str "\n" 14 Hier der Baum nach der semantischen Analyse. Cast- und Umwandlungs-Knoten sind hervorgehoben. Seq Str$Self Binary$Assign Flt$Self f: Flt$Variable Flt$Self aufg3.sem.Cast Flt$Self: Flt 4.0 Flt$FromInt Flt$Self: Flt 4.0 aufg3.sem.Cast Int$Self: Int 4 Flt$ToInt Int$Self: Int 4 Flt Flt$Self: Flt 4.3 Unary$Write Int$Self Binary$Div Int$Self: Int 0 Int Int$Self: Int 3 Int Int$Self: Int 4 Unary$Write Str$Self Str Str$Self: Str "\n" Unary$Write Flt$Self Binary$Div Flt$Self: Flt 0.75 Flt$FromInt Flt$Self: Flt 3.0 Int Int$Self: Int 3 aufg3.sem.Cast Flt$Self: Flt 4.0 Flt$FromInt Flt$Self: Flt 4.0 Int Int$Self: Int 4 Unary$Write Str$Self Str Str$Self: Str "\n" Unary$Write Int$Self Binary$Div Int$Self Int Int$Self: Int 3 aufg3.sem.Cast Int$Self Flt$ToInt Int$Self Unary$Ref Flt$Self f: Flt$Variable Flt$Self Unary$Write Str$Self Str Str$Self: Str "\n" 15 Hier der Interpreterbaum. Umwandlungsknoten sind hervorgehoben. Cast-Knoten kann es nicht mehr geben. Seq Variable$Assign: Flt$Variable @135058536 Flt 4.0 Int$Write Int 0 Str$Write Str "\n" Flt$Write Flt 0.75 Str$Write Str "\n" Int$Write Int$Div Int 3 Flt$ToInt Variable$Ref: Flt$Variable @135058536 Str$Write Str "\n" Die Ausgabe: suleika cmp $ make test CLASSPATH=../..:$HOME/cb/Uebungen java mixed.cmp.Cmp3 ../../q/cast.q 0 0.75 0