MiniJavaCompiler/doc/typecheck.md

7.8 KiB

Typecheck (Fabian Noll)

Überblick und Struktur

Die Typprüfung beginnt mit der Funktion typeCheckCompilationUnit, die eine Kompilationseinheit als Eingabe erhält. Diese Kompilationseinheit besteht aus einer Liste von Klassen. Jede Klasse wird einzeln durch die Funktion typeCheckClass überprüft. Innerhalb dieser Funktion wird eine Symboltabelle erstellt, die den Namen der Klasse als Typ und this als Identifier enthält. Diese Symboltabelle wird verwendet, um Typinformationen nach dem Lokalitätsprinzip während der Typprüfung zugänglich zu machen und zu verwalten.

Die Typprüfung einer Klasse umfasst die Überprüfung aller Konstruktoren, Methoden und Felder. Die Methode typeCheckConstructorDeclaration ist für die Typprüfung einzelner Konstruktordeklarationen verantwortlich, während typeCheckMethodDeclaration für die Typprüfung einzelner Methodendeklarationen zuständig ist. Beide Funktionen überprüfen die Parameter und den Rumpf der jeweiligen Konstruktoren bzw. Methoden. Der Rumpf wird durch rekursive Aufrufe von typeCheckStatement überprüft, die verschiedene Arten von Anweisungen wie If-Anweisungen, While-Schleifen, Rückgabeanweisungen und Blockanweisungen behandelt.

Ablauf und Symboltabellen

Eine zentrale Komponente des Typecheckers ist die Symboltabelle (symtab), die Informationen über die Bezeichner und ihre zugehörigen Datentypen speichert. Die Symboltabelle wird kontinuierlich angepasst, während der Typechecker die verschiedenen Teile des Programms durchläuft.

Anpassung der Symboltabelle

  • Klassenkontext: Beim Typcheck einer Klasse wird eine initiale Symboltabelle erstellt, die die this-Referenz enthält. Dies geschieht in der Funktion typeCheckClass.

  • Konstruktorkontext: Innerhalb eines Konstruktors wird die Symboltabelle um die Parameter des Konstruktors erweitert. Dies geschieht in typeCheckConstructorDeclaration. Der Rückgabetyp eines Konstruktors ist implizit void, was überprüft wird, um sicherzustellen, dass kein Wert zurückgegeben wird.

  • Methodenkontext: Innerhalb einer Methode wird die Symboltabelle um die Parameter der Methode erweitert sowie den Rückgabetyp der Methode, um die einzelnen Returns dagegen zu prüfen. Dies geschieht in typeCheckMethodDeclaration.

  • Blockkontext: Bei der Überprüfung eines Blocks (typeCheckStatement für Block) wird die Symboltabelle für jede Anweisung innerhalb des Blocks aktualisiert. Lokale Variablen, die innerhalb des Blocks deklariert werden, werden zur Symboltabelle hinzugefügt. Das bedeutet, dass automatisch, sobald der Block zu Ende ist, alle dort deklarierten Variablen danach nicht mehr zugänglich sind.

Unterscheidung zwischen lokalen und Feldvariablen

Bei der Typprüfung von Referenzen (typeCheckExpression für Reference) wird zuerst in der Symboltabelle nach dem Bezeichner gesucht. Sollte dieser gefunden werden, handelt es sich um eine lokale Variable. Wenn der Bezeichner nicht gefunden wird, wird angenommen, dass es sich um eine Feldvariable handelt. In diesem Fall wird die Klasse, zu der die this-Referenz gehört, durchsucht, um die Feldvariable zu finden. Dies ermöglicht die Unterscheidung zwischen lokalen Variablen und Feldvariablen. Dies ist auch nur möglich, da wir die Feldvariablen und Methoden nicht in die Symboltabelle gelegt haben und stattdessen nur die this-Referenz.

Fehlerbehandlung

Ein zentraler Aspekt des Typecheckers ist die Fehlerbehandlung. Bei Typinkonsistenzen oder ungültigen Operationen werden aussagekräftige Fehlermeldungen generiert. Beispiele für solche Fehlermeldungen sind:

  • Typinkonsistenzen: Wenn der Rückgabetyp einer Methode nicht mit dem deklarierten Rückgabetyp übereinstimmt oder die Anzahl der Parameter nicht übereinstimmt.

  • Ungültige Operationen: Wenn eine arithmetische Operation auf inkompatiblen Typen durchgeführt wird.

  • Nicht gefundene Bezeichner: Wenn eine Referenz auf eine nicht definierte Variable verweist.

Diese Fehlermeldungen helfen Entwicklern, die Ursachen von Typfehlern schnell zu identifizieren und zu beheben. Generell sind diese oftmals sehr spezifisch, was das Problem recht schnell identifizieren sollte. Z.B. falsche Reihenfolge / falsche Typen der Parameter beim Methodenaufruf sind direkt erkennbar.

Typprüfung von Kontrollstrukturen und Blöcken

If-Anweisungen

Bei der Typprüfung einer If-Anweisung (typeCheckStatement für If) wird zuerst der Typ der Bedingung überprüft, um sicherzustellen, dass es sich um einen booleschen Ausdruck handelt. Anschließend werden die Then- und Else-Zweige geprüft. Der Typ der If-Anweisung selbst wird durch die Vereinheitlichung der Typen der Then- und Else-Zweige bestimmt. Falls einer der Zweige keinen Rückgabewert hat, wird angenommen, dass der Rückgabewert void ist. Dies wurde so gelöst, um im Typchecker feststellen zu können, ob beide Zweige einen Return haben. Wenn nur einer der Zweige ein Return hat, wird im umliegenden Block ein weiteres benötigt, was durch den Typ void erzwungen wird. Dadurch weiß der Typchecker Bescheid.

Block-Anweisungen

Die Typprüfung eines Blocks erfolgt in typeCheckStatement für Block. Jede Anweisung im Block wird nacheinander überprüft und die Symboltabelle wird entsprechend aktualisiert. Der Typ des Blocks wird durch die Vereinheitlichung der Typen aller Anweisungen im Block bestimmt. Wenn der Block keine Anweisungen hat, wird der Typ void angenommen.

Rückgabeanweisungen

Die Typprüfung einer Rückgabeanweisung (typeCheckStatement für Return) überprüft, ob der Rückgabewert der Anweisung mit dem deklarierten Rückgabetyp der Methode übereinstimmt. Dafür wurde zu Beginn der Methodentypprüfung der Rückgabetyp der Methode in die Symboltabelle eingetragen. Wenn der Rückgabewert null ist, wird überprüft, ob der deklarierte Rückgabetyp ein Objekttyp ist. Dies stellt sicher, dass Methoden immer den korrekten Typ zurückgeben. Generell wird bei der Prüfung nach dem UpperBound geschaut und ebenfalls wird nachgeschaut, ob, wenn der Rückgabetyp Object ist, der Return-Wert auch eine tatsächlich existierende Klasse ist, indem in die Klassentabelle geschaut wird.

Konstruktorüberladung und -prüfung

Die Typprüfung unterstützt Konstruktorüberladung. Bei der Typprüfung von Konstruktoraufrufen (typeCheckStatementExpression für ConstructorCall) wird überprüft, ob es mehrere Konstruktoren mit derselben Anzahl von Parametern gibt. Falls mehrere passende Konstruktoren gefunden werden, wird ein Fehler gemeldet.

  • Parameterabgleich: Die Parameter eines Konstruktors werden gegen die Argumente des Aufrufs abgeglichen. Dies umfasst die Prüfung der Typen und, falls es sich um null handelt, die Überprüfung, ob der Parameter ein Objekttyp ist.

  • Fehlerbehandlung: Wenn kein passender Konstruktor gefunden wird, wird eine detaillierte Fehlermeldung generiert, die die erwarteten Signaturen und die tatsächlichen Argumenttypen anzeigt. Wenn mehrere passende Konstruktoren gefunden werden, wird ebenfalls ein Fehler gemeldet.

Methodenüberladung und -prüfung

Die Typprüfung unterstützt auch Methodenüberladung. Bei der Typprüfung von Methodenaufrufen (typeCheckStatementExpression für MethodCall) wird überprüft, ob es mehrere Methoden mit demselben Namen, aber unterschiedlichen Parametertypen gibt.

  • Parameterabgleich: Die Parameter einer Methode werden gegen die Argumente des Aufrufs abgeglichen. Dies umfasst die Prüfung der Typen und, falls es sich um null handelt, die Überprüfung, ob der Parameter ein Objekttyp ist.

  • Fehlerbehandlung: Wenn keine passende Methode gefunden wird, wird eine detaillierte Fehlermeldung generiert, die die erwarteten Signaturen und die tatsächlichen Argumenttypen anzeigt. Wenn mehrere passende Methoden gefunden werden, wird ebenfalls ein Fehler gemeldet.