proc - Vorlesungen

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