239 Copyright 1999 by Axel T. Schreiner. All Rights Reserved. 5 Einfache Unterprogramme — proc Ziel proc soll mixed um parameterlose Prozeduren und Funktionen mit float- und erweitern. Scanner und Parser muß man mehr oder weniger kopieren, aber die Semantikanalyse ck.sem und der Interpreter ck.run können wiederverwendet und erweitert werden: int-Resultaten • • • • Im Scanner kommen wenigstens einige reservierte Worte hinzu. Im Parser kommen Regeln zur Vereinbarung und zum Aufruf von Unterprogammen hinzu. In ck.sem und ck.run gibt es neue Klassen zur Repräsentierung von Unterprogrammen. Type, TypeAdapter und Datentyp-Klassen im Laufzeitsystem wie Int werden um Funktionen erweitert. Der Prototyp für proc kann durch Ableitung der vorhandenen Klassen realisiert werden; aus Effizienzgründen wird das Resultat dann in ck integriert. Die Architektur wird zunächst für eine minimale Untermenge von simple entwickelt: 5-1 5-2 5-3 Laufzeitsystem — proc.run Semantikanalyse — proc.sem? Parse-Baum — proc 243 246 255 240 Beispiel proc hatmixed und damit auch simple als Teilsprachen. newton.q und qgl.q sowie first.q , euclid.q oder errors.q sind daher auch proc-Programme. Das folgende Beispiel zeigt einige neue Möglichkeiten: {q/gcd.q} // proc -- Euklid’s Algorithmus -- ats 10/94, 1/99 int x, y; // [need parameters] proc gcd begin if x > y then x = x - y; call gcd else if x < y then y = y - x; gcd fi fi end; // loops by tail recursion int func euclid begin gcd; return x end; // returns gcd(x,y) // procedure call // call is optional // return is not required // return value is required program write "x? "; read x; write "y? "; read y; write "gcd: ", euclid, "\n" // function call {} Ausführung $ java proc.cmp.Cmp gcd.q x? 36 x? 54 gcd: 18 241 Sprachbeschreibung grobe Struktur Vereinbarungen program Aktionen Vereinbarungen Konstanten Variablen Prozeduren Funktionen const Name = Ausdruck, ...; int Name = Ausdruck, ...; float Name = Ausdruck, ...; proc Name [begin Aktionen end]; int func Name [begin Aktionen end]; float func Name [begin Aktionen end]; Aktionen leere Aktion Zuweisung Eingabe Ausgabe Entscheidung Schleife Prozeduraufruf Prozedurabbruch Funktionswert ; Name = Ausdruck; read Name, ...; write String, Ausdruck, ...; if Bedingung then Aktionen else Aktionen fi while Bedingung do Aktionen od [call] Name; return; return expr; Ausdruck mit binären Operatoren, Vorzeichenoperatoren und Funktionsnamen. Dabei haben Vorzeichen höchsten Vorrang, Multiplikationen haben Vorrang vor Additionen. Bei gleichem Vorrang werden Vorzeichen von rechts, binäre Operatoren von links bewertet. Es gibt Klammern (mit höchstem Vorrang). Name besteht aus Buchstaben und Ziffern, ein int-Literal besteht aus den Ziffern 0 bis 9, ein float-Literal enthält zusätzlich einen Dezimalpunkt oder einen Exponenten, String besteht aus beliebigen Zeichen und Ersatzdarstellungen mit \, eingeschlossen in Doppelanführungszeichen. Jeder Name muß vereinbart werden. Kein Name darf zweimal vereinbart werden. Konstanten müssen konstant initialisiert werden. Variablen können (und nur konstant) initialisiert werden. Funktionen und Prozeduren können deklariert und dann definiert werden. Kommentare reichen von // bis zum Zeilenende. Zwischenraum trennt die Eingabesymbole, wird aber sonst ignoriert. 242 Bedeutung proc Programme manipulieren ganze und Gleitkomma-Zahlen in einem bestimmten Bereich. Aktionen werden der Reihe nach ausgeführt. Programm endet normal nach der letzten Aktion und mit Fehler, falls Bedeutung undefiniert. Zuweisung: die rechte Seite wird mit aktuellen Variablenwerten bewertet, dann links zugewiesen. Eingabe: Zahlen (Konstanten) werden aus der Eingabe gelesen und sequentiell zugewiesen; Zwischenraum wird ignoriert. Fehler: end of file, conversion, size. Ausgabe: Strings werden als Text, Werte werden als Zahlen (Konstanten) in die Ausgabe geschrieben. Zeilentrenner und Zwischenraum müssen explizit ausgegeben werden. Fehler: end of medium. Entscheidung: Die Bedingung wird ausgewertet, dann findet eine der abhängigen Aktionenfolgen statt. Schleife: Die Bedingung wird immer zuerst ausgewertet; solange sie zutrifft, findet die abhängige Aktionsfolge statt. Prozedur und Funktion: An Stelle des Aufrufs wird der Körper ausgeführt; rekursive Aufrufe sind erlaubt. Vorzeitiges Ende durch explizites return bei Prozedur, return expr bei Funktion. Ausdruck: Addition, Subtraktion, Multiplikation, Division, Rest nach Division, positives Vorzeichen, negatives Vorzeichen, Bit-Komplement und Klammern haben die übliche mathematische Bedeutung. Ist wenigstens ein Operand ein float-Wert, dann ist das Resultat ein float-Wert. int-Werte können an float-Variablen zugewiesen werden, aber nicht umgekehrt. Für float-Werte gibt es kein Bit-Komplement. Fehler: overflow, zero divide. Bedingung: Alle sechs Vergleiche mit Ausdrücken als Operanden. Name: Ziel einer Zuweisung, aktueller Wert einer Variablen, Funktionswert: der mit return gelieferte Wert. Fehler: undefined, kein return. int-Zahl: Ziffernfolge, dezimal; in der Ein- und Ausgabe auch mit Vorzeichen. float-Zahl: Ziffernfolge, dezimal, mit Dezimalpunkt oder Exponent, der ein Vorzeichen haben kann; in der Ein- und Ausgabe auch mit Vorzeichen oder als int-Zahl. 243 Laufzeitsystem — proc.run Ein vereinfachter Prototyp — proc.run.Run Das folgende, sinnlose Programm enthält die meisten Operationen: int func f; proc p begin write f, "\n" end; int func f begin return 2+3 end; program call p // forward declaration // use, implicit return at end // definition Erlaubt man Rekursion, muß man Funktionen und Prozeduren deklarieren und dann benutzen können, bevor sie definiert sind. Alternativ könnte man bei der Semantikanalyse (wie in Java) zwei Durchgänge verwenden. Benötigt man nur einen einzigen Test, kann man die relevanten Konstruktoren wieder von Hand aufrufen und viel Aufwand sparen: $ java proc.run.Run Seq Proc @419028 Seq Int$Write Proc$Call Int$Function @419030 Str$Write Str "\n" Proc$Return Int$Function @419030 Int$Return Int$Add Int 2 Int 3 Proc$Call Proc @419028 5 und Int.Function werden sequentiell nicht ausgeführt — daher kann man die Prozedurkörper in der Programmfolge einhängen und darstellen. Proc 244 Architektur body-tree Proc Call program eval() Call Mit return kann ein Unterprogramm abgebrochen werden (REn-Struktur, analog zu break n bei Schleifen) — der Interpreter muß dazu eine Exception verwenden und in Proc.eval() abfangen. Interpreter-Klassenhierarchie — proc.run Der Prototyp ist klein genug, daß man mit einem Texteditor die Methoden und Klassen extrahieren kann, die im Paket proc.run für den Interpreter zusätzlich gebraucht werden (Fettgedrucktes ist neu): a Run: jag.Node, Serializable cm Proc.Call c Proc.Return a c Unary Proc c a c c Int.Function Proc.ReturnValue Int.Return Proc.ReturnJump: Exception Basis für Interpreter-Baum Unterprogramm-Aufruf eval() ruft Unterprogramm auf Prozedurende eval() beendet Prozedur mit ReturnJump Unary(Run) Basis für unäre Operatoren Proc() Prozedurkopf setBody(Run) Angabe des Prozedurkörpers eval() [leer] eval(Object[]) Aufruf, liefert Funktionswert Function() int-Funktionskörper ReturnValue(Run) Funktionsende eval() beendet Funktion mit ReturnJump Return(Run) int-Funktionswert ReturnJump() übergibt null ReturnJump(Run) übergibt Resultatwert returnValue() liefert Resultatwert 245 Walkthrough — Prozeduraufruf Seq Proc$Call Proc @419028 sendet eval() an... Call ist innere Klasse des Prozedurkopfs Proc und sendet eval(null) (Argumente?) an den Prozedurkopf. public Run eval () throws Exception { return Proc.this.eval(null); } Proc @419028 Der Körper-Unterbaum muß mit einer Variante von return beendet werden, die im Prozedurkopf abgefangen wird. public Run eval (Object[] args) throws Exception { try { sub[0].eval(); } catch (ReturnJump r) { return r.returnValue(); } throw new RuntimeException("missing return"); } Seq Int$Write Proc$Call Int$Function @419030 Str$Write Str "\n" Proc$Return werden wie früher bewertet. erbt Methoden wie oben. muß in einer Prozedur unbedingt erreicht werden. public Run eval () throws Exception { throw new ReturnJump(); } Eine Funktion wird mit einem Knoten einer Unterklasse von Proc.ReturnValue beendet, die folgendes erbt: public Run eval () throws Exception { throw new ReturnJump(sub[0].eval()); } Der ReturnJump transportiert das Resultat zurück zu Proc. 246 Semantikanalyse — proc.sem? Ein vereinfachter Prototyp — proc.sem0.Sem0 Der Parse-Baum wird aus dem Interpreter-Baum nach den früher besprochenen Prinzipien entwickelt. Für den Prototyp wird wieder das folgende Programm herangezogen: int func f; proc p begin write f, "\n" end; int func f begin return 2+3 end; program call p // forward declaration // use, implicit return at end // definition Die relevanten Konstruktoren werden wieder von Hand aufgerufen: $ java proc.sem0.Sem0 Seq p: Proc Void$Self Seq Unary$Write Unary$Ref f: Proc Int$Self Unary$Write Str Str$Self: Str "\n" Proc$Return f: Proc Int$Self Proc$ReturnValue Binary$Add Int Int$Self: Int 2 Int Int$Self: Int 3 Proc$Call p: Proc Void$Self 247 Architektur Zur Laufzeit markieren Proc und Int.Function den Unterschied zwischen Prozedur und Funktion mit Int-Resultat. Int.Function und Int.Return sollen eine differenzierte Code-Generierung ermöglichen. Im Parse-Baum modelliert Proc Unterprogramme und in type wird der Resultattyp notiert. Für Prozeduren benötigt man dann einen Typ Void, der keine besonderen Fähigkeiten besitzt. Um rekursive Aufrufe zu ermöglichen, muß ein Unterprogramm in der Symboltabelle so beschrieben werden, daß ein Aufruf generiert werden kann, auch wenn die Semantikanalyse des Körpers noch nicht abgeschlossen ist; dies erklärt, warum setBody() gebraucht wird. Verweis Nachricht symbolTable Proc body-tree sem() program Unary.Ref Call Proc program body-tree Call eval() Call Der Parser muß auf einen Namen mit Unary.Ref verweisen — erst in der Semantikanalyse entscheidet sich, was zur Interpretation generiert wird. In Dump muß man Rekursion verhindern. 248 Klassenhierarchie für den Parse-Baum — proc.sem0 Mit einem Texteditor kann man wieder die Methoden und Klassen extrahieren, die im Paket proc.sem0 zur Generierung des Parse-Baums zusätzlich gebraucht werden (Fettgedrucktes ist neu): a Sem: jag.Node c Name c Proc c Sem() Name(String) Proc(String,Type) setBody(Sem) Proc.Return Basis für Parse-Baum Basis für Objekte mit Namen Unterprogramm hinterlegt Körper Prozedurende sem() a Unary a Unary.Arithmetic c Proc.ReturnValue c Unary.Ref c Proc.Call a Void i Type c TypeAdapter c Void.Self ReturnValue(Sem) Ref(Sem) Call(Sem) TypeAdapter() TypeAdapter(Class) Self() Basis für unäre Operatoren Basis für unäre Arithmetik Funktionsende Verweis auf Objekt Prozeduraufruf Resultat von Prozeduren Generierung von Operatoren beschreibt Typ ohne Fähigkeiten beschreibt Typ aus ck.run beschreibt wertlosen Typ Im Gegensatz zu Unary.Ref kann Proc.Call erzwingen, daß wirklich eine Prozedur aufgerufen wird. Eine Konstruktion als member class ist nicht sinnvoll, den bei der Konstruktion des Parse-Baums ist noch nicht klar, ob ein Name durch Proc repräsentiert wird. 249 Design der Semantikanalyse — proc.sem1.Sem1 Zum Design zieht man das Testprogramm int func f; proc p begin write f, "\n" end; int func f begin return 2+3 end; program call p // forward declaration // use, implicit return at end // definition und einen Vergleich von Parse- und Interpreter-Baum heran (gleiche Zeile bezeichnet einen value-Verweis): Parse-Baum nach sem() Interpreter-Baum Seq Void$Self p: Proc Void$Self Seq Void$Self Unary$Write Int$Self Unary$Ref Int$Self f: Proc Int$Self Unary$Write Str$Self Str Str$Self: Str "\n" Proc$Return Void$Self f: Proc Int$Self Proc$ReturnValue Int$Self Binary$Add Int$Self: Int 5 Int Int$Self: Int 2 Int Int$Self: Int 3 Proc$Call Void$Self p: Proc Void$Self Seq Proc @419172 Seq Int$Write Proc$Call Int$Function @419092 Str$Write Str "\n" Proc$Return Int$Function @419092 Int$Return Int 5 Proc$Call Proc @419172 Der Vorwärts-Verweis auf f deutet insbesondere die Probleme bei Rekursion an: Schon wenn ein Unterprogramm deklariert wird, muß sein Proc-Knoten für den Interpreter-Baum erzeugt werden, damit für Aufrufe Knoten der member class Proc.Call generiert werden können. Dies hat nichts mit der Architektur als member class zu tun: Existiert Proc noch nicht, könnte auch ein Unary-Knoten nicht darauf verweisen; alle Call-Knoten müßten dann nachgebessert werden, wenn Proc schließlich erzeugt wird. Code-Generierung ist einfacher: Dort erzeugt man eine Marke, die ein Assembler später korrigieren muß. 250 Methoden für die Semantikanalyse — proc.sem1 Fast alle Klassen existieren. Man muß lediglich sem() implementieren (Fettgedrucktes ist neu): a Sem: jag.Node sem() c c Name Proc c Proc.Return Proc(String,Type) setBody(Sem) sem() a a c c c Unary Unary.Arithmetic Proc.ReturnValue Unary.Ref ReturnValue(Sem) sem() Ref(Sem) sem() Proc.Call sem() a Void i Type c TypeAdapter c c Int.Self Void.Self function() returnValue(Sem) TypeAdapter() TypeAdapter(Class) function() returnValue(Sem) function() Basis für Parse-Baum liefert type, soll prüfen, generieren Basis für Objekte mit Namen Unterprogramm, generiert Proc hinterlegt und prüft Körper Prozedurende in Prozedur? generiert Return Basis für unäre Operatoren Basis für unäre Arithmetik Funktionsende paßt Resultattyp? generiert Return Verweis auf Objekt modifiziert, generiert auch Call Prozeduraufruf kontrolliert, daß Ziel Prozedur Resultat von Prozeduren Generierung von Operatoren Function Return beschreibt Typ ohne Fähigkeiten beschreibt Typ aus ck.run generiert Function, falls möglich generiert Return, falls möglich beschreibt ck.run.Int, erbt beschreibt wertlosen Typ generiert Proc 251 Walkthrough — Prozeduraufrufe f: Proc Int$Self wird durch Aufruf des Konstruktors deklariert; der angegebene Typ muß Proc für den Interpreter generieren können. sem() liefert danach den Typ, getValue() den Proc-Knoten, falls vorhanden. public Proc (String name, Type type) { super(name); didSem = true; this.type = type; if ((value = type.function()) == null) { error(type+": no functions"); type = null; } } p: Proc Void$Self wird wie oben deklariert. Nur im Körper kann return auftreten; mit einem Stack wird zugleich der nötige Resultattyp untersucht. protected static Stack activation = new Stack(); setBody() hinterlegt, prüft und generiert abschließend den Körper. public void setBody (Sem body) { if (type != null) { if (sub != null) { error(this+": duplicate definition"); sub = null; } activation.push(type); if (body.sem() == null) type = null; // functions permitted // activate Ein Unterprogramm muß eine Variante von return enthalten. else if (body instanceof Seq && body.degree() == 0) { error(this+": missing return"); type = null; } else { ((ck.run.Proc)value).setBody(body.getValue()); sub = new Sem[] { body }; } activation.pop(); // inactivate } } 252 Seq Void$Self f: Proc Int$Self Unary$Ref Int$Self verteilt sem() wie früher besprochen . ist so angelegt wie oben beschrieben . kann auch auf ein Unterprogramm stoßen . sem() wird ersetzt und erzeugt bei Bedarf einen Call-Knoten zum Proc-Knoten, der in diesem Fall noch keinen Unterbaum besitzt. public Type sem () { if (! didSem) { didSem = true; if ((type = sub[0].sem()) != null) { if (sub[0] instanceof Lvalue) value = type.ref(sub[0]); else if (sub[0] instanceof Proc) value = ((ck.run.Proc)sub[0].getValue()).new Call(); else value = sub[0].value; if (value == null) { type = null; error(sub[0]+": cannot reference"); } } } return type; } Unary$Write Int$Self Proc$Return Void$Self funktioniert wie früher . fold() würde nicht statfinden, da Call kein Value ist . erfährt durch den Stack, ob return zulässig ist und Void erwartet wird, und generiert Proc.Return. public Type sem () { if (! didSem) { didSem = true; if (activation.size() == 0) error(this+": outside procedure body"); else if ((type = (Type)activation.peek()) != null) if (type != Void.self) { error("result value required"); type = null; } else value = new ck.run.Proc.Return(); } return type; } 253 f: Proc Int$Self Int Int$Self: Int 2 Int Int$Self: Int 3 Binary$Add Int$Self: Int 5 wurde oben deklariert ; setBody() prüft und generiert jetzt mit sem() den Körper . wurden bei Konstruktion des Baums fixiert. wurde früher beschrieben läßt den Proc$ReturnValue Int$Self public Type sem () { if (! didSem && super.sem() != null) { . Unterbaum bearbeiten... // type is sub[0].type durch den Stack, ob return ...erfährt zulässig ist und welcher Typ erwartet wird... if (activation.size() == 0) error(this+": outside function body"); else { Type rt = (Type)activation.peek(); if (rt == Void.self) { // void: no result type = null; error("no result value permitted"); } else { ...versucht gegebenenfalls umzuwandeln... Sem s = rt.cast(rt, this, 0, type); if (s == null) s = type.cast(rt, this, 0, type); if (s == null || s.sem() == null) { error(this+", "+rt+": cannot convert function result"); type = null; } else { sub[0] = s; type = rt; } } ...und verlangt, daß der Resultattyp einen Return-Knoten generiert. if (type != null && (value = type.returnValue(sub[0])) == null) { error(this+": cannot return "+type); type = null; } } } return type; } 254 Seq Void$Self p: Proc Void$Self f: Proc Int$Self verteilt sem() wie früher besprochen . erben sem() von Sem . Da der Konstruktor alle nötigen Informationen hinterlegt hat, findet in sem() keine weitere Aktion statt. public Type sem () { if (!didSem) error(this+": type not available"); return type; } p: Proc Void$Self Proc$Call Void$Self ist so angelegt wie oben beschrieben läßt den Aufruf von der Oberklasse konstruieren , prüft aber zusätzlich, daß es sich um eine echte Prozedur handelt. public Type sem () { if (! didSem && super.sem() != null) if (! (sub[0] instanceof Proc) || sub[0].type != Void.self) { type = null; value = null; error(sub[0]+": not a procedure"); } return type; } extends Unary.Ref . wurde oben besprochen . 255 Parse-Baum — Cmp.jay Das Frontend für proc wird aus mixed %token %type %type <String> <Sem> <Proc> weiterentwickelt: PROC, FUNC, BEG, END, CALL, RETURN proc, func p.head, f.head %% defs : | | | ... defs proc ’;’ defs func ’;’ ... { $1.add($2); } { $1.add($2); } proc : p.head { $$ = null; } | p.head BEG _stmts END{ $3.add(new Proc.Return()); $1.setBody($3.freeze()); } p.head : PROC Name func : f.head { $$ = null; } | f.head BEG stmts END { $1.setBody($3); } f.head : INT FUNC Name | FLOAT FUNC Name { $$ = ((Scanner)yyLex).proc($3, Int.self); } { $$ = ((Scanner)yyLex).proc($3, Flt.self); } stmt : | | | | { { { { ... Name CALL Name RETURN RETURN expr { $$ = ((Scanner)yyLex).proc($2, Void.self); } $$ = new $$ = new $$ = new parserAt $$ = new Unary.Ref($1); } // hmmm... Proc.Call($2); } Proc.Return(); } = $1; Proc.ReturnValue($2); } In dieser Grammatik kann eine Variable oder ein Funktionsname als Anweisung stehen. und f.head sind nötig: Setzt man ihre rechten Seiten in proc und func ein und arbeitet mit eingebetteten Aktionen vor den Körpern, ergeben sich Konflikte. p.head ist zur Unterscheidung von Deklarationen und Definitionen nötig, um Konflikte durch leere Anweisungen zu vermeiden. BEGIN 256 Im Scanner kommen die reservierten Worte hinzu. Außerdem muß man Unterprogramm-Deklarationen in der Symboltabelle notieren und bei mehrfachen Deklarationen die Typen kontrollieren. setBody() moniert mehrfache Definitionen. public Sem proc (Name symbol, Type type) { if (symbol instanceof NewName) { Proc proc = new Proc(symbol.name, type); proc.pos = symbol.pos; symbolTable.put(symbol.name, proc); // inefficient... symbol = proc; } else if (symbol instanceof Proc) { Type st = symbol.sem(); if (st != null && st != type) lexerror(symbol+": previously declared with "+st); } else lexerror(symbol+": duplicate"); return symbol; }