Blockpraktikum: FAQ

Im folgendem wird eine Reihe von Fragen von allgemeingültigem Interresse beantwortet. Sollten Sie ein Problem haben und hier keine Antwort dazu finden, gehen Sie bitte wie hier beschrieben vor.

  1. Muß ich mich an die Vorgaben (Scanner, Environment) halten?
  2. Die Methode "toHtml()" ist optional. Muß ich "format(int)" implementieren?
  3. Wo fange ich am besten an?
  4. Wie komme ich von der Grammatik zu den Knotentypen?
  5. Warum gibt es pro Klasse eine statische "parse" Methode?
  6. Warum nimmt die "parse" Methode ein Environment als Argument?
  7. Warum nehmen die "run" & "evaluate" Methoden ein Environment als Argument?
  8. Warum gibt es die Methode "typeCheck" obwohl sie nicht gefordert wird?
  9. Warum ist  der Empfänger bei MessageSend optional?
  10. Warum gibt es für "Robot", "North", "move", "turnLeft", usw. keine eigenen Tokentypen?
  11. Wie kann man sich die Arbeit in der Gruppe sinnvoll aufteilen?
  12. Gibt es Aspekte an denen man sich Anfangs besser nicht die Zähne ausbeissen sollte?
  13. Wie kann ich das "return"-Statement realisieren?
  14. Warum ergibt der Ausdruck 4 * 5 / 2 den Wert 8 und nicht 10?
  15. Warum erwartet das Testwerkzeug einen erfolgreichen Typecheck, wenn das Programm fehlerhaft ist?
  16. Ist der Testfall "parseErr1.task" fehlerhaft?
  17. Ein nicht kompilierbares Programm erzeugt dennoch ein (natürlich falsches) Laufzeitsollergebnis. Warum?
  18. Beim Einsatz des Testers unter UNIX tritt ein Fehler der Art cannot open file... auf.
  19. Das Testat für das Praktikum wurde mir/uns verweigert. Was kann man da tun?


Muß ich mich an die Vorgaben (Scanner, Environment) halten?
Nein. Ihre Aufgabe besteht darin alle öffentlichen und später privaten verbindlichen Tests zu bestehen. Wie Sie das erreichen ist Ihnen überlassen. Sie können also insbesondere den Scanner verändern (obwohl dies nicht notwendig ist), eine andere Environmentklasse benutzen, unsere Vorschläge zu einer "Value-Hierachie" (Klasse EnvValue) ignorieren, usw.
Damit aber eine Testierung erfolgen kann müssen Sie die Schnittstellen zum automatischen Testwerkzeug einhalten (siehe Klassen Interpreter, AbstractSyntaxNode und ExecutableNode).
Hierbei können Sie selbstverständlich die Implementierung, z.B., von Interpreter Methoden ändern, nur deren Signatur muß erhalten bleiben.
Unter Umständen wird ein Tutor Sie besser beraten können, wenn Sie den von uns vorgeschlagene Weg folgen, da er diesen im Gegensatz zu anderen Ansätzen kennt und dazu Auskunft erteilen kann.

Die Methode "toHtml()" ist optional. Muß ich "format(int)" implementieren?
Ja. Der PrettyPrint eines SJarel-Programms ist Teil der Aufgabe.
Dadurch, daß "toString()" (in Klasse AbstractSyntaxNode) die Methode "format(int)" aufruft, erhalten Sie so gleichzeitig eine Möglichkeit Ihre Knotenobjekte zu Testzwecken auszugeben.

Wo fange ich am besten an?
Viele Wege führen nach Rom, aber in diesem Fall bietet sich eine "bottom-up" Vorgehensweise an. In der Beispielklasse ProgramNode können Sie nachvollziehen wie wir die Grammatik vereinfacht haben, so daß nicht alle Sprachkonstrukte implementiert sein müssen, bevor man eine Teillösung testen kann.
So ist z.B., die Teilmenge der Grammatik, die sich mit Ausdrücken beschäftigt ist relativ komplex. Sio können also zunächst einmal Expression :: = Literal annehmen, später dann Expression :: = MultiplicativeExpression (aber z.B., ohne bereits alle Alternativen für SimpleExpression zu verwirklichen), und so weiter.
Obwohl Ihr Interpreter während dieser Konstruktionsphase dann möglicherweise kein einziges normal geformtes SJarel-Programm erkennen kann (z.B., kann unsere ProgramNode Beispielklasse nur immer eine "task"-Methode und nur ein Statement erkennen), können Sie so schrittweise Ihren Fortschritt beobachten und erst zum Schluß echte SJarel-Programme angehen.

Wie komme ich von der Grammatik zu den Knotentypen?
Eine einfache Regel heißt: Jedes Nonterminal in der Grammatik wird zu einem Knotentyp (siehe auch die Frage zur Aufteilung der Arbeit). Die rechten Seiten der Nonterminal-Definitionen geben Ihnen dabei Hinweis dazu welche Attribute die Knotentypen jeweils haben müssen. Kommt auf der rechten Seite die beliebige Wiederholung ("{...}") vor, so gibt es zwei Möglichkeiten:
  1. Sie merken sich im entsprechendem Knoten eine Sequenz (=> Array, java.util.Vector, List).
  2. Sie definieren den Knoten so, daß er ein Element aus der Sequenz enthalten kann und einen weiteren Knoten der gleichen Art (analog der List Implementierung aus der Vorlesung => Klasse Node).
Kommen auf der rechten Seite nur Alternativen vor, dann werden Sie für dieses Nonterminal nie Objekte erzeugen müssen, können aber eine entsprechende Klasse als Fallunterscheider für das Parsen benutzen (siehe Klasse Statement in ProgramNode.java). Es ist natürlich nicht notwendig sich sklavisch an die "Nonterminal => Knotentyp" Regel zu halten. Sie müssen z.B., für MethodDeclarator keinen eigenen Knoten vorsehen sondern können die dort enthaltene Information auch gleich in MethodDefinition speichern. Entscheiden Sie sich für die Lösung, die Ihnen weniger aufwendig erscheint, aber achten Sie dabei auch auf Übersichtlichkeit.

Warum gibt es pro Klasse eine statische "parse" Methode?
Zunächst erscheint es logisch "parse(Environment)" genau wie z.B., "typeCheck(Environment)" als "abstract" in der Klasse AbstractSyntaxNode zu deklarieren, so daß Unterklassen automatisch dazu aufgefordert werden "parse" zu implementieren. Dagegen sprechen aber folgenden Gründe: Idealerweise würde man einfach im Knotenkonstruktor den Tokenstrom lesen und den Knoten entsprechend erzeugen (z.B., "WhileStatementNode(Scanner)"). Leider ist der Rückgabetyp von Konstruktoren in Java automatisch der Klassentyp in dem der Konstruktor vorkommt. Es gibt aber Fälle, wo man z.B,. ein "Statement" parsen möchte, dann aber ein "WhileStatement" als Rückgabeknoten erhält. Der Konstruktor von "Statement" kann aber leider kein Objekt vom Typ "WhileStatement" zurückliefern.

Warum nimmt die "parse" Methode ein Environment als Argument?
Es ist auch möglich das Parsen völlig ohne Environment durchzuführen. Für zwei Aspekte ist es jedoch hilfreich:
  1. Man kann vorher im Environment nützliche Informationen ablegen, z.B., alle SJarel Konstanten und alle primitiven Typen, so daß man beim Parsen das Environment als Auskunft für die Rolle von gefundenen Bezeichnern benutzen kann.
  2. Sie können das Environment beim Parsen erweitern, z.B., deklarierte Variablen hinzunehmen, so daß Sie im folgenden überprüfen können ob eine Variablenbenutzung auch eine entsprechende Deklaration besitzt. Sie können also auf diese Art viele Programmfehler bereits vor der Ausführung finden. Ein vollständiges Aufspüren aller vor der Laufzeit entdeckbarer Fehler ist jedoch während des Parsens nicht möglich. Da z.B., eine Methode aufgerufen werden kann bevor sie definiert wurde, sind bestimmte Wohlgeformtheitsfehler erst durch "typeCheck()" auffindbar.

Warum nehmen die "run" & "evaluate" Methoden ein Environment als Argument?
Neben dem wie schon beim Parsen erwähnten optionalen Aspekt der "Auskunftfunktion" des Environments ist es für diese Methoden unerlälßlich während der Ausführung auf das Environment zurückzugreifen. Das Environment fungiert hier als Speicher, in dem Variablen eingetragen werden (Deklarationen), verändert werden (Zuweisungen, u.a.,) und ausgelesen werden (Variablenzugriffe).

Warum gibt es die Methode "typeCheck" obwohl sie nicht gefordert wird?
Es ist richtig, daß Sie das Minimalziel auch ohne Implementierung von "typeCheck(Environment" (in Klasse AbstractSyntaxNode) erreichen können. Auf der einen Seite bietet sich die Überprüfung des SJarel-Programs auf Wohlgeformtheitsfehler schon vor der Ausführung als natürliche Erweiterung einer Minimallösung an und auf der anderen Seite können Sie hiermit den Ausführungsteil einfacher gestalten. Wenn die "run(Environment)" Methode bereits ein wohlgeformtes Programm annehmen kann, muß auf viele mögliche Fehlerfälle nicht mehr geachtet werden. Beispiele für Fehlerfälle, die duch "typeCheck(Environment)" abgefangen sind:

Warum ist  der Empfänger bei MessageSend optional?
Wie in Java auch gibt es sowohl die Möglichkeit Nachrichten explizit an einen Empfänger zu schicken, z.B. "empfänger.nachricht()" oder den Empfänger implizit zu lassen, z.B., "nachricht()". Letzterer Fall ist eigentlich einen Abkürzung für "this.nachricht()", d.h., die Nachricht soll an das Objekt selbst geschickt werden. Im SJarel-Interpreter ist das immer ein Objekt vom Typ "Robot", das auch die benutzerdefinierten Methoden versteht. Beachten Sie deshalb für alle benutzerdefinierten Methoden eine impliziten Empfänger vorraussetzen dürfen, jedoch nicht für die "task"-Methode.

Warum gibt es für "Robot", "North", "move", "turnLeft", usw. keine eigenen Tokentypen?
Diese Einheiten werden vom Scanner als "IdentifierToken" erkannt und zurückgeliefert. Für diese Entscheidung gibt es folgende Gründe:

Wie kann man sich die Arbeit in der Gruppe sinnvoll aufteilen?
Es bietet sich an die Struktur des Interpreters zur Arbeitsteilung auszunutzen. Eine Implementierung besteht aus
  1. den Statementknoten (Erben von ExecutableNode)
  2. den Ausdrucksknoten (Erben von EvaluatableNode)
  3. den deklarativen Knotentypen (Erben von AbstractSyntaxNode)
  4. der Environment-Funktionalität inkl. der Werte-Klassen (Erben von EnvValue)
Es bleibt Ihnen überlassen wie Sie diese Arbeitspakete angehen wollen, z.B., jeweils in 2-er Teams oder als Einzelspezialisten.

Gibt es Aspekte an denen man sich Anfangs besser nicht die Zähne ausbeissen sollte?
Manche Aspekte des Interpreters sind recht einfach umzusetzen, andere wiederum sind interessanter. Wir schlagen vor, daß Sie sich Anfangs mit einer "task"-methode in SJarel-Programmen begnügen sollten und erst nach einigen Erfolgen die Herausforderung benutzerdefinierte Methoden zu unterstützen angehen sollten.
Eine weitere interessante Aufgabe besteht auch darin das "return"-Statement richtig zu implementieren (siehe auch diese spezielle Frage). Falls Sie nicht gleich einen Lösungsweg sehen, fangen Sie zunächst einfach mal ohne "return" an. Es gibt Lösungen, die sich nachträglich ohne zusätzliche Arbeit in Interpreter ohne "return"-Unterstützung einbauen lassen.

Wie kann ich das "return"-Statement realisieren?
Das "return"-Statement bietet gleich zwei Herausforderungen:
  1. i.A., gibt es einen Wert zurück, aber es scheint keinen offensichtlichen Weg zu geben diesen Wert zurückzugeben. Die Methode "run" gibt bereits ein Environment zurück und globale Variablen würden bei rekursiven Aufrufen nicht funktionieren.
  2. es ist das einzige Statement, daß den normalen Kontrollfluß unterbrechen kann. Durch ein "return" können Methoden, Schleifen und Anweisungsfolgen frühzeitig abgebrochen werden.
Wir kennen zwei ganz unterschiedliche Lösungsansätze, die jeweils beide Herausforderungen "Hand-in-Hand" lösen:
Eine Lösung benutzt Java-Exceptions, nicht um Fehler zu behandeln, sondern um den Kontrollfluß gemäß der return-Semantik zu realisieren. Benutzt man eine Ausnahme, so hat man damit auch gleichzeitig einen Weg gefunden den Rückgabewert zu transportieren.
Eine andere Lösung benutzt das Environment, um den Rückgabewert "abzulegen". Somit gibt es, z.B. für Schleifen, ein Ort nachzusehen, ob sich der Kontrollfluß ändern sollte.

Warum ergibt der Ausdruck 4 * 5 / 2 den Wert 8 und nicht 10?
In Java werden die Operatoren links-assoziativ behandelt. In SJarel leider rechtsassoziativ. Das hat zur Folge, dass in SJarel zunächst 5/2 berechnet wird (abgerundet auf 2) und dann mit 4 multipliziert wird.
Der Grund hierfür ist der Aufbau der SJarel Grammatik. Eine Grammatik, die zu linksassoziativen Operatoren führen würde, wäre nicht mehr mit den hier angstrebten Mitteln (rekursiver Abstiegsparser) handhabbar gewesen. Das naheliegende Prinzip der Erkennung von Ausdrücken durch rekursive Aufrufe würde hier zu einer Endlosrekursion führen. Es gibt andere Parsetechniken, die dieses Manko nicht haben, deren Behandlung aber in diesem Rahmen zu weit führen würde.
Bei der Erzeugung von Testfällen müssen Sie auf diesen Unterschied zwischen Java und SJarel achten, bzw. sich nicht über die Diskrepanzen wundern.

Warum erwartet das Testwerkzeug einen erfolgreichen Typecheck, wenn das Programm fehlerhaft ist?
Das Testwerkzeug war leider an dieser Stelle fehlerhaft. Bitte laden Sie die neuen Version der Datei PureJavaTester.java herunter. Das Archiv vorgaben.zip ist auch entsprechend aufgefrischt worden.
Für das Bestehen des Praktikums sind Abweichungen von erwarteten Typchecks nicht relevant.

Ist der Testfall "parseErr1.task" fehlerhaft?
Dieser Testfall ist fehlerhaft. Er wurde von einer nicht ganz fehlerfreien Version von PureJavaTester.java generiert. Eine neue Version von PureJavaTester.java und der Datei "testsuite.ini" ist verfügbar und wurde auch dem Vorgabenarchiv hinzugefügt. Bitte benutzen Sie die neue Testsuite Datei testsuite.ini für Ihre Tests.

Ein nicht kompilierbares Programm erzeugt dennoch ein (natürlich falsches) Laufzeitsollergebnis. Warum?
Der PureJavaTester generiert für jede Taskdatei eine entsprechende RoboXXX.java Datei (fortlaufende Nummerierung in XXX). Die entsprechende RoboXXX.class Datei wird zur Ausführung benutzt. Sollte durch frühere Experimente bereits, z.B., eine Robo02.class erzeugt worden sein, dann wird diese ausgeführt; auch dann wenn die aktuelle Version von Robo02.java einen Fehler enthält und gar keine Robo02.class erzeugt hätte. In diesem Fall bitte einfach vorher die entsprechende (oder einfach alle) RoboXXX.class Datei(en) löschen.

Beim Einsatz des Testers unter UNIX tritt ein Fehler der Art cannot open file... auf.
Das liegt an den Pfadtrennzeichen, die unter Windows so: \ und unter Unix so: / aussehen. Ersetzen Sie in Ihren Testsuite-Dateien einfach jedes Auftreten von \ durch ein /.

Das Testat für das Praktikum wurde mir/uns verweigert. Was kann man da tun?
Wenn Ihr Tutor Ihnen beim Praktikum das Testat verweigert hat, und Sie nochmals von dem jeweiligen Tutor in Anwesenheit eines Dozenten testiert werden möchten, wenden Sie sich bitte bis zum 10. April per Email gleichzeitig an den jeweiligen Tutor und an <inf1p@st.informatik.tu-darmstadt.de>. Bitten Sie Ihren Tutor darum, sich wegen eines Termins mit uns in Verbindung zu setzen.