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)); } ¶ 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; } ¶ 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 { ... } } ¶ 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; } } ¶ 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); } } } ¶ 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; } } ¶ 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; } } ¶ 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 } ... } ¶ 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 ¶ 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. ¶ 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 = $1; $$ = new Unary.Complement($2); } | '(' cast_types ')' expr %prec '~' { parserAt = $3; $$ = new Cast($2, $4); } | '+' expr %prec '~' { $$ = $2; } | '-' expr %prec '~' { parserAt = $1; $$ = new Unary.Minus($2); } | ... cast_types | FLOAT : INT { $$ = Int.self; } { $$ = Flt.self; } %% ¶ 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; } } ¶ 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 3 / (float) (int) 4.3; 3 / 4, "\n"; 3 / (float) 4, "\n"; (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" ¶ 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" ¶ 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