Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 164 additions & 93 deletions homework/sheet08.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,123 +13,183 @@ REPL.
Der Fokus liegt auf Variablen und C++-Referenzen, auf Funktionen, auf Klassen,
Einfach‑Vererbung und dem Unterschied zwischen statischem und dynamischem Dispatch
(nicht‑virtual vs. virtual) - ohne Pointer/Heap und ohne Casts. Die Sprache ist eine
Teilmenge von C++ und soll (abgesehen von den REPL‑Erweiterungen) mit einem
echte Teilmenge von C++ und soll (abgesehen von den REPL‑Erweiterungen) mit einem
C++‑Compiler kompilierbar sein. Präprozessor‑Zeilen (`#include ...`) dürfen in
Dateien vorkommen; Ihr Interpreter soll sie ignorieren bzw. als Kommentare
Dateien vorkommen; Ihr Interpreter soll sie ignorieren bzw. wie Kommentare
behandeln.

Sie *können* ANTLR zur Erstellung Ihres Lexers und Parsers einsetzen, Sie können
aber auch gern einen selbst implementierten LL-Parser einsetzen.

Definieren Sie zunächst eine passende Grammatik und den AST, bevor Sie Lexer und
Parser umsetzen. Achten Sie auf eine angemessene semantische Analyse, achten Sie
dabei auf die in der Vorlesung besprochene C++-Semantik. Der Interpreter selbst soll
ein einfacher Tree-walking Interpreter wie in der Vorlesung besprochen sein.
dabei auf die in der Vorlesung besprochene sowie auf diesem Blatt skizzierte und
über die Tests gegebene C++-Semantik. Der Interpreter selbst soll ein einfacher
Tree-walking Interpreter wie in der Vorlesung besprochen sein.

## Betrachtete Sprache: Sub-Dialekt von C++

### Sprachkern

Unterstützen Sie mindestens folgende C++-Konzepte:

- Basisdatentypen: `bool`, `int`, `char`; zusätzlich Klassentypen; `void` für
Funktionen ohne Rückgabewert
- Typen:
- `bool` (Werte `true`, `false`), `int` (Ziffernfolgen ohne Dezimalpunkt, etwa
`42`), `char` (Character in einfachen Anführungsstrichen, etwa `'a'`),
`string` (Zeichenkette in doppelten Anführungsstrichen, etwa `"foo"`)
- `void` für Funktionen ohne Rückgabewert
- Zusätzlich Klassentypen (s.u.)
- Escapes in Literalen mit `\`, beispielsweise `'\0'` als einzelnes `char`
- Variablen und Zuweisungen:
- Deklaration `T x;` und Initialisierung `T x = expr;`
- Einfache Zuweisung `=`
- Arrays (eindimensional, minimal):
- Nur `T x[INT]` für primitive Typen und Klassen
- Operationen: Deklaration, Index‑Zugriff/‑Zuweisung
- Nur lokale Variablen (keine globalen Variablen)
- Ausdrücke:
- Arithmetik: `+`, `-`, `*`, `/`, `%` (nur für `int`)
- Vergleich: `==`, `!=`, `<`, `<=`, `>`, `>=` (`int`, `char`; `bool` nur `==`
und `!=`)
- Logik: `&&` und `||` (mit Short‑Circuit), `!`
- Klammern `(...)`
- Arithmetik (nur `int`): `+`, `-`, `*`, `/`, `%` (binäre Ausdrücke); `+`, `-`
(unäre Ausdrücke)
- Zuweisung: `=`
- Vergleich: `==`, `!=`, `<`, `<=`, `>`, `>=` (`int` und `char`; `bool` und
`string` nur `==` und `!=`)
- Logik: `&&` und `||` (beide mit *Short‑Circuit*) und `!` (nur `bool`)
- Klammern zur Gruppierung von Ausdrücken `(...)`
- Funktionsaufrufe `f(args)`
- Feld-/Methodenzugriff: `obj.f`, `obj.m(args)`
- Kontrollfluss:
- `if`-`then`-`else`, `while`, Block `{ ... }`
- `if`-`then`-`else` mit optionalem `else`-Teil, `while`, Block `{ ... }`
- `return`
- Funktionen:
- Definition, Deklaration und Aufruf
- Funktionen/Methoden:
- Definition und Aufruf
- Überladung (Overloading) nur per exakt passender Signatur (Name + Anzahl +
exakte Typen inkl. `ref`‑Markierung)
exakte Typen inkl. `&`‑Markierung)
- C++‑Referenzen "light":
- Deklaration Variable: `T& x;` bzw. Parameter `T& p`
- Initialisierung ist obligatorisch und nur mit LValues (Variablen,
Feldzugriffen, Array‑Index) erlaubt
- Zuweisung an eine `ref`‑Variable schreibt in das referenzierte Ziel
- Klassen, Einfach-Vererbung, Polymorphie:
- `class A { public: /* Felder + Methoden */ }` mit Attributen und Methoden
- Konstruktoren; aber keine Destruktoren und keine Initialisierungslisten
- Deklaration Variable: `T& x = expr;` bzw. Deklaration Parameter: `T& p`
- Initialisierung für Referenz-Variablen obligatorisch
- Klassen, Einfach-Vererbung (genau eine optionale Basisklasse), Polymorphie:
- `class A { public: /* Felder + Methoden */ }` mit Feldern und Methoden
(alles "public" sichtbar)
- Felder können vom Typ her Basistypen oder Klassen sein
- Parameterloser Konstruktor und weitere Konstruktoren (jeweils ohne
Initialisierungslisten), Verwendung nur als `T x;` (ruft `T()` auf) oder
`T x = T(args);` (kein direkter Aufruf `T x(args);`!)
- Methoden können als `virtual` deklariert werden
- Variablen vom Klassentyp sind Werte (feldweise Kopie bei Rückgabe/Zuweisung)
- `class D : public B { public: /* Felder + Methoden */ }`: Vererbung mit nur
einer Basisklasse
- Zuweisung `Base b; b = d;` (mit `class D : public B { ... }` und `D d;`)
führt zum Slicing
- Polymorphe Nutzung erfolgt ausschließlich über Referenzen:
`B& b = d; b.m();` ruft die überschriebene Methode in `D` auf (wenn `D::m()`
als `virtual` deklariert ist)
- `*this` in Methoden hat Typ `C&`; `*this` ist nur als RValue lesbar (z.B.
`return *this;`); Zuweisung an `*this` gibt es nicht
- Eingebaute Funktionen: `print_bool`, `print_int`, `print_char` (Ausgabe eines
Werts des jeweiligen Typs)
- Programmorganisation: ein einzelnes Source‑File, keine Includes/Präprozessor
- `class D : public B { public: /* Felder + Methoden */ }`: Vererbung mit
genau einer Basisklasse, keine Zyklen
- Eingebaute Funktionen (Runtime/Standardbibliothek): `print_bool`, `print_int`,
`print_char`, `print_string` (Ausgabe eines Werts des jeweiligen Typs)
- Kommentare:
- Zeilenweise Kommentare: `//` bis zum Zeilenende
- Block-Kommentare: `/*` bis zum `*/` (kann über mehrere Zeilen gehen)
- Präprozessor-Direktiven: `#` bis zum Zeilenende (soll als Kommentar
behandelt werden)
- Programmorganisation:
- Ein einzelnes Source‑File mit einer optionalen `main()`-Funktion
- Präprozessor-Anweisungen sollen Sie wie Kommentare ignorieren (behandeln Sie
`#include` wie einen zeilenweisen Kommentar, d.h. ab einem `#` wird der
Input bis zum nächsten Zeilenumbruch ignoriert)
- Main-Funktion: zulässig sind die Formen `int main()` und `void main()`

*Hinweis*: Polymorphie in dieser Sprache folgt C++‑Prinzipien (Slicing bei
Wertkopie, dynamischer Dispatch über Referenzen), nicht Java‑Semantik.

### Reservierte Schlüsselwörter

`int`, `bool`, `char`, `string`, `true`, `false`, `if`, `else`, `while`, `return`,
`class`, `void`, `public`, `virtual`

### Semantik‑/Typregeln

- Keine Mehrfachdefinitionen in demselben Scope (Variablen/Funktionen/Klassen)
- Sichtbarkeit: Verwendungen müssen im aktuellen Scope sichtbar (auflösbar) sein
- Sichtbarkeit:
- Verwendungen müssen im aktuellen Scope sichtbar (auflösbar) sein
- Variablen: "define-before-use", d.h. Variablen müssen bereits im ersten Pass
sichtbar sein
- Funktionen, Methoden, Klassen: "define-after-use" - mehrere Passes notwendig
(nur beim Start des Interpreters)
- LValues: benannte Variablen `a` bzw. Feldzugriffe `obj.f`; alles andere sind
RValues
- Variablen sind nicht aufrufbar; Funktionen sind nicht zuweisbar (keine Funktion
als LValue)
- Rückgaberegel: In non‑`void`‑Funktionen existiert auf allen Pfaden mindestens
ein `return`
- Keine impliziten Typkonversionen außer im booleschen Kontext (in
`if`/`while`‑Bedingungen werden `int`/`char` wie in C++ implizit in `bool`
konvertiert, d.h. `0` wird als `false` und alles ungleich `0` als `true`
behandelt); keine Casts
- Overload‑Auflösung: exakter Match Name und Arität und identische Typen inkl.
`ref`‑Markierung; bei Mehrdeutigkeit Fehler
- Funktionen/Methoden: Argumentanzahl muss zur Parameterliste passen
- LValues: benannte Variablen `a`, `obj.f` (Feldzugriff), `a[i]`; alles andere
sind RValues
`if`/`while`‑Bedingungen werden `int`/`char`/`string` wie in C++ implizit in
`bool` konvertiert, d.h. `0` bzw. der leere String werden als `false` und alles
andere als `true` behandelt)
- Ausdrücke:
- Bei binären Operatoren müssen beide Seiten den selben Typ haben
- Zuweisung ist ein Ausdruck; die linke Seite des Zuweisungsoperators muss ein
LValue sein
- Das Ergebnis der Zuweisung ist Wert/Typ der rechten Seite
- Operatorpräzedenz und Assoziativität: wie in C++ üblich (Reihenfolge: unär,
multiplicative, additive, relational, equality, &&, \|\|, assignment;
assignment rechtsassoziativ)
- Funktionen/Methoden:
- Argumentanzahl muss zur Parameterliste passen
- Overload‑Auflösung: exakter Match Name und Arität und identische Typen inkl.
`&`‑Markierung; bei Mehrdeutigkeit Fehler
- Referenzen
- Parameter und lokale Referenzvariablen müssen mit LValues initialisiert
- Referenz‑Parameter und ‑Variablen können nur mit LValues initialisiert
werden
- Keine Neubindung
- Keine Referenzen als Felder/Globals oder Arrays
- Keine `ref`‑Rückgaben (Rückgabe nur als Kopie)
- Keine Neubindung: Zuweisung an eine Referenz‑Variable schreibt in das
referenzierte Ziel
- Keine Referenzen als Felder/globale Variablen
- Keine `&`‑Rückgaben (Rückgabe nur als Kopie)
- Klassen/Methoden:
- Nicht‑virtuelle Methoden binden statisch; virtuelle dynamisch bei Verwendung
von Referenzen
- Parameterloser Default‑Konstruktor wird synthetisiert, wenn keiner definiert
wurde
- Initialisierung von Feldern über impliziten (generierten) parameterlosen
Default-Konstruktor: `bool`: `false`, `int`: `0`, `char`: `'\0'`, `string`:
`""`, Felder von Klassentyp werden per Default‑Konstruktor initialisiert
- Bei Vererbung wird von Konstruktoren bei abgeleiteten Klassen jeweils der
parameterlose Basiskonstruktor implizit vor dem Body aufgerufen
- Kein `this`. Unqualifizierte Namen in Methoden binden an lokale
Variablen/Parameter vor eigene Members vor geerbte Members vor globale
Scope.
- Variablen vom Klassentyp sind Werte (feldweise Kopie bei Rückgabe/Zuweisung)
- Bei abgeleiteten Klassen wird beim Konstruktoraufruf implizit zunächst der
parameterlose Default-Konstruktor der Basisklasse aufgerufen
- Abgeleitete Klassen erben alle Felder und Methoden der Basisklasse
- Zuweisung `Base b; b = d;` (mit `class D : public B { ... }` und `D d;`)
führt zum Slicing (einzige Ausnahme der Regel für selbe Typen auf beiden
Seiten der Zuweisung)
- Polymorphe Nutzung erfolgt ausschließlich über Referenzen:
`B& b = d; b.m();` ruft die überschriebene Methode in `D` auf (wenn `B::m()`
zusätzlich als `virtual` deklariert ist)
- Virtuelle Bindung wie in C++: Ist die Methode im statischen Typ `virtual`,
erfolgt dynamischer Dispatch bei Aufruf über Referenzen; Overrides in
abgeleiteten Klassen sind auch ohne erneutes `virtual` virtuell
- Fehlerbehandlung:
- Lexer-, Parser-, Typ-Fehler beenden die Analyse mit klarer Meldung
- Laufzeitfehler (z.B. Division durch 0, Zugriff auf nicht initialisierte
Variable) sind sauber zu melden
- REPL (**abweichend** von C++): Neue Funktionen/Klassen dürfen in der REPL
definiert werden; sie werden in den globalen Scope aufgenommen und stehen danach
zur Verfügung.
- Laufzeitfehler (z.B. Division durch 0) sind sauber zu melden
- REPL (**abweichend** von C++): Neue Variablen/Funktionen/Klassen dürfen in der
REPL definiert werden; sie werden in den Sitzungs-Scope aufgenommen und stehen
danach zur Verfügung. Für Funktionen und Klassen ist in der REPL kein
"define-after-use" mehr möglich.

### Nicht Teil des Umfangs

Weitere mit C++ verbundene Konzepte wie beispielsweise Präprozessor, Header-Files,
Pointer, Templates, Sichtbarkeiten in Klassen, Trennung Deklaration/Implementierung
bei Klassen (Trennung .h und .cpp) brauchen Sie nicht umsetzen.
Weitere, bisher nicht benannte mit C++ verbundene Konzepte wie beispielsweise
Präprozessor, Header-Files, Pointer, Arrays, Templates, Sichtbarkeiten in Klassen,
Trennung Deklaration/Implementierung bei Klassen (Trennung .h und .cpp) brauchen Sie
nicht umsetzen.

Darunter fallen auch (nicht vollständig):
Darunter fallen auch (Aufzählung nicht vollständig):

- Pointer/Adressen: `&` (Adressoperator), `*` (Dereferenzierung, außer für `this`
bzw. `*this`), `->`/`new`/`delete`, Speicherverwaltung
- Pointer/Adressen: `&` (Adressoperator), `*` (Dereferenzierung),
`->`/`new`/`delete`, Speicherverwaltung
- Casts, Inkrement/Decrement, Compound‑Assignments, Mehrfachvererbung, Templates,
`static`, `const`, `friend`, Namespaces
- `sizeof`, `typedef`, `using`
- Unterscheidung signed und unsigned Datentypen
- Sichtbarkeiten (außer `public`)
- Mehrdimensionale Arrays
- `this`, dadurch auch kein Shadowing von Member-Namen durch lokale Variablen bzw.
Parameter
- Arrays (eindimensional, mehrdimensional)
- Kein `break/continue` in Schleifen
- Globale Variablen
- Referenzen als Rückgabetyp, Referenzen als Feld in Klassen
- Reine Funktionsdeklarationen (auch mit namenlosen Parametern)
- Initialisierungslisten, delegierende Konstruktoren, Destruktoren
- Shadowing: In Methoden sollen Parameter und lokale Variablen nicht den selben
Namen haben wie die Felder der Klasse

### Hinweis "*most vexing parse*"

Expand All @@ -138,44 +198,55 @@ parse*"-Problem](https://en.wikipedia.org/wiki/Most_vexing_parse), wo nach dem
Muster `T ID ( ... ) ;` sowohl ein Funktionsprototyp als auch eine
Variablendeklaration mit Konstruktor‑Syntax möglich wäre.

Versuchen Sie, dieses Problem durch geschickte Definitionen in der Grammatik
einzuschränken. Beispielsweise wurde oben bereits einschränkend definiert, dass eine
Variablendeklaration entweder die Form `T x;` haben soll oder mit Initialisierung
`T x = expr;`. Die in C++ ebenfalls übliche Form `T x(expr);` braucht nicht
unterstützt werden. Für Konstruktoren erlauben Sie am besten nur `T x;` und
`T x = T(arg);`. Dann kann `T ID ( ... ) ;` nur noch ein Funktionsprototyp sein ...
Da mit den oben definierten Regeln Funktionsprototypen in diesem Projekt nicht
erlaubt sind und für Funktionsdefinitionen immer ein anschließender Block auftaucht
(`T ID ( ... ) { ... }`), Variablen für Basistypen entweder über die Form `T x;`
oder mit Initialisierung als `T x = expr;` deklariert werden, und Variablen für
Klassentypen in der Form `T x;` (Aufruf parameterloser Konstruktor) bzw. der Form
`T x = T(arg);` (Erzeugung eines temporären Objekts mit dem passenden Konstruktor
und elementweises Kopieren) deklariert werden, tritt das klassische *most vexing
parse* in diesem Projekt nicht auf.

## REPL-Modell

### Initialisierung

Der Interpreter soll beim Start eine optional angegebene Datei mit C++-Code einlesen
und verarbeiten können. Dabei soll die `main()` im eingelesenen Code bis vor die
schließende Klammer bzw. das beendende `return` ausgeführt werden und der
Aktivierungsrahmen von `main()` "offen" gehalten werden.
und verarbeiten können. Für die semantische Prüfung des eingelesenen Codes soll eine
Mehrpass-Prüfung realisiert werden (Funktionen und Klassen: "define-after-use"
erlaubt). Eine optional enthaltene `main()`-Funktion soll in einem "Sitzungs-Scope"
ausgeführt werden, welcher vom globalen Scope "abzweigt". Der Sitzungs-Scope wird
offen gehalten.

### REPL

Danach soll dem User eine REPL angeboten werden, die einen Prompt in der Konsole
ausgibt und in der weitere C++-Statements eingegeben werden können, die dann im
Kontext des bisher verarbeiteten Codes interpretiert werden.

Abweichend von C++ können in der REPL neue Klassen und Funktionen definiert und
genutzt werden. Diese landen im aktuellen Scope.

Nach der Interpretation soll es jeweils eine Ausgabe des letzten Ergebnisses (oder
Fehlers) auf der Konsole geben, und dann soll ein neuer Prompt ausgegeben und auf
die nächste User-Eingabe gewartet werden.

Implementieren Sie eine spezielle Eingabe, mit der die laufende `main()`-Funktion
und damit die REPL beendet werden kann.
ausgibt und in der weitere C++-Statements (inklusive Blöcke und
Expression-Statements) eingegeben werden können, die dann im Sitzungs-Scope
interpretiert werden. Dadurch wird in den neu eingegebenen Statements der Zugriff
auf Variablen in der `main()`-Funktion und auf frühere Funktions- und
Klassendefinitionen möglich.

In der REPL können zusätzlich auch neue Klassen und Funktionen und Variablen
definiert und genutzt werden. Klassen und Funktionen erweitern dabei den globalen
Scope und haben keinen Zugriff auf Sitzungsvariablen; neue Variablen landen dagegen
im Sitzungs-Scope. Im Unterschied zur Initialisierungsphase gilt in der REPL stets
"define-before-use", d.h. alle benutzten Namen müssen vorher bereits definiert
worden sein.

In der REPL wird nur das Ergebnis von Expression‑Statements automatisch ausgegeben;
alle anderen Statements erzeugen keine Ausgabe. Für explizite Ausgaben nutzen Sie
`print_*()`. Fehler (Lexer, Parser, semantische Analyse, Interpreter/Laufzeit)
sollen ebenfalls auf der Konsole ausgegeben werden.

Implementieren Sie eine spezielle Eingabe, mit der die REPL beendet werden kann.

*Hinweis*: Achten Sie darauf, dass Ihr Interpreter die Eingabe nicht zu früh beendet
und zu früh mit der Interpretation beginnt! Oft ist ein Zeilenumbruch das korrekte
Signal zum Start der Verarbeitung. Es gibt aber auch Situationen (z.B.
Funktionsdefinition o.ä.), wo ein Zeilenumbruch noch nicht eine vollständige Eingabe
anzeigt. Hier soll nach einem Zeilenumbruch ein Hilfsprompt ausgegeben werden, damit
der User weiss, dass die aktuelle Eingabe noch läuft.
Signal zum Start der Verarbeitung. Es gibt aber auch Situationen (etwa Funktions-
oder Klassendefinitionen o.ä.), wo ein Zeilenumbruch noch nicht eine vollständige
Eingabe anzeigt. Hier soll nach einem Zeilenumbruch ein Hilfsprompt ausgegeben
werden, damit der User weiss, dass die aktuelle Eingabe noch läuft.

## Tests

Expand All @@ -184,7 +255,7 @@ Im
finden Sie einige Positiv- und Negativ-Tests. Für die Positivtests ist die erwartete
Antwort des Interpreters angegeben, bei der Interpretation der Negativtests sollte
es eine entsprechende Fehlermeldung (Lexer, Parser, semantische Analyse,
Interpreter) geben.
Interpreter/Laufzeit) geben.

Betrachten Sie die Testfälle als ausführbare Ergänzung der obigen Spezifikation.

Expand Down
Loading