Aufgabe 1 und 2

Werbung
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
Herunterladen