SymbTab3: Strukturen und Klassen
Strukturen und Klassen bilden jeweils einen eigenen verschachtelten Scope, worin die Attribute und Methoden definiert werden.
Bei der Namensauflösung muss man dies beachten und darf beim Zugriff auf Attribute und Methoden nicht einfach in den übergeordneten Scope schauen. Zusätzlich müssen hier Vererbungshierarchien in der Struktur der Symboltabelle berücksichtigt werden.
- (K3) Aufbau von Symboltabellen für Nested Scopes inkl. Strukturen/Klassen mit einem Listener
- (K3) Attribute von Klassen und Strukturen auflösen
Strukturen
Strukturen: Erweiterung der Symbole und Scopes
Quelle: Eigene Modellierung nach einer Idee in [Parr2010, p. 162]
Strukturen stellen wie Funktionen sowohl einen Scope als auch ein Symbol dar.
Zusätzlich stellt eine Struktur (-definition) aber auch einen neuen Typ
dar, weshalb Struct
auch noch das Interface Type
"implementiert".
Strukturen: Auflösen von Namen
class Struct(Scope, Symbol, Type):
def resolveMember(name):
return symbols[name]
=> Auflösen von "a.b
" (im Listener in exitMember()
):
a
im "normalen" Modus mitresolve()
über den aktuellen Scope- Typ von
a
istStruct
mit Verweis auf den eigenen Scope b
nur innerhalb desStruct
-Scopes mitresolveMember()
In der Grammatik würde es eine Regel member
geben, die auf eine Struktur
der Art ID.ID
anspricht (d.h. eigentlich den Teil .ID
), und entsprechend
zu Methoden enterMember()
und exitMember()
im Listener führt.
Das Symbol für a
hat als type
-Attribut eine Referenz auf die Struct
,
die ja einen eigenen Scope hat (symbols
-Map). Darin muss dann b
aufgelöst
werden.
Klassen
class A {
public:
int x;
void foo() { ; }
};
class B : public A {
public
int y;
void foo() {
int z = x+y;
}
};
Klassen: Erweiterung der Symbole und Scopes
Quelle: Eigene Modellierung nach einer Idee in [Parr2010, p. 167]
Bei Klassen kommt in den Tabellen ein weiterer Pointer parentClazz
auf die
Elternklasse hinzu (in der Superklasse ist der Wert None
).
Klassen: Auflösen von Namen
class Clazz(Struct):
Clazz parentClazz # None if base class
def resolve(name):
# do we know "name" here?
if symbols[name]: return symbols[name]
# NEW: if not here, check any parent class ...
if parentClazz and parentClazz.resolve(name): return parentClazz.resolve(name)
else:
# ... or enclosing scope if base class
if enclosingScope: return enclosingScope.resolve(name)
else: return None # not found
def resolveMember(name):
if symbols[name]: return symbols[name]
# NEW: check parent class
if parentClazz: return parentClazz.resolveMember(name)
else: return None
Quelle: Eigene Implementierung nach einer Idee in [Parr2010, p. 172]
Hinweis: Die obige Implementierungsskizze soll vor allem das Prinzip demonstrieren - sie ist aus
Gründen der Lesbarkeit nicht besonders effizient: beispielsweise wird parentClazz.resolve(name)
mehrfach evaluiert ...
Beim Auflösen von Attributen oder Methoden muss zunächst in der Klasse selbst gesucht werden, anschließend in der Elternklasse.
Beispiel (mit den obigen Klassen A
und B
):
B foo;
foo.x = 42;
Hier wird analog zu den Structs zuerst foo
mit resolve()
im lokalen Scope aufgelöst. Der Typ
des Symbols foo
ist ein Clazz
, was zugleich ein Scope ist. In diesem Scope wird nun mit
resolveMember()
nach dem Symbol x
gesucht. Falls es hier nicht gefunden werden kann, wird in
der Elternklasse (sofern vorhanden) weiter mitresolveMember()
gesucht.
Die normale Namensauflösung wird ebenfalls erweitert um die Auflösung in der Elternklasse.
Beispiel:
int wuppie;
class A {
public:
int x;
void foo() { ; }
};
class B : public A {
public
int y;
void foo() {
int z = x+y+wuppie;
}
};
Hier würde wuppie
als Symbol im globalen Scope definiert werden. Beim Verarbeiten von
int z = x+y+wuppie;
würde mit resolve()
nach wuppie
gesucht: Zuerst im lokalen Scope
unterhalb der Funktion, dann im Funktions-Scope, dann im Klassen-Scope von B
. Hier sucht
resolve()
auch zunächst lokal, geht dann aber die Vererbungshierarchie entlang (sofern
wie hier vorhanden). Erst in der Superklasse (wenn der parentClazz
-Zeiger None
ist),
löst resolve()
wieder normal auf und sucht um umgebenden Scope. Auf diese Weise kann man
wie gezeigt in Klassen (Methoden) auf globale Variablen verweisen ...
Anmerkung: Durch dieses Vorgehen wird im Prinzip in Methoden aus dem Zugriff auf ein Feld
x
implizit ein this.x
aufgelöst, wobei this
die Klasse auflöst und x
als Attribut darin.
Wrap-Up
-
Symboltabellen: Verwaltung von Symbolen und Typen (Informationen über Bezeichner)
-
Strukturen und Klassen bilden eigenen Scope
-
Strukturen/Klassen lösen etwas anders auf: Zugriff auf Attribute und Methoden
Symboltabellen praktisch
Betrachten Sie folgenden Java-Code:
- Umkreisen Sie alle Symbole.
- Zeichen Sie Pfeile von Symbol-Referenzen zur jeweiligen Definition (falls vorhanden).
- Identifizieren Sie alle benannten Scopes.
- Identifizieren Sie alle anonymen Scopes.
- Geben Sie die resultierende Symboltabelle an (Strukturen wie in VL besprochen).
package a.b;
import u.Y;
class X extends Y {
int f(int x) {
int x,y;
{ int x; x - y + 1; }
x = y + 1;
}
}
class Z {
class W extends X {
int x;
void foo() { f(34); }
}
int x,z;
int f(int x) {
int y;
y = x;
z = x;
}
}
- [Mogensen2017] Introduction to Compiler Design
Mogensen, T., Springer, 2017. ISBN 978-3-319-66966-3. DOI 10.1007/978-3-319-66966-3.
Kapitel 3 - [Parr2010] Language Implementation Patterns
Parr, T., Pragmatic Bookshelf, 2010. ISBN 978-1-9343-5645-6.
Kapitel 6, 7 und 8 - [Parr2014] The Definitive ANTLR 4 Reference
Parr, T., Pragmatic Bookshelf, 2014. ISBN 978-1-9343-5699-9.
Kapitel 6.4 und 8.4