Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
Nächste Überarbeitung | Vorhergehende Überarbeitung | ||
programmieren:erweiterung:start [2015/02/06 08:52] – Externe Bearbeitung 127.0.0.1 | programmieren:erweiterung:start [2021/12/29 10:40] (aktuell) – Externe Bearbeitung 127.0.0.1 | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
+ | ====== Erweiterung der Sprache ====== | ||
+ | Bisher kennt unsere Programmiersprache nur Terme (expressions), | ||
+ | |||
+ | <code java> | ||
+ | a = 1; | ||
+ | b = 2; | ||
+ | while(a < 10){ | ||
+ | a = a + 1; | ||
+ | b = b * 2; | ||
+ | print(b); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Quelltext ===== | ||
+ | Hier der {{: | ||
+ | ===== Erweiterung der Grammatik ===== | ||
+ | |||
+ | Die [[programmieren: | ||
+ | <code bnf> | ||
+ | Ziffer | ||
+ | Buchstabe = " | ||
+ | Zahl = Ziffer { Ziffer | " | ||
+ | Text = Buchstabe { Buchstabe | Ziffer | " | ||
+ | TrueFalse = " | ||
+ | Term = " | ||
+ | Produkt = Term | Term " | ||
+ | Quotient = Term | Term "/" | ||
+ | ProduktQuotient = Produkt | Quotient ; | ||
+ | Summe = ProduktQuotient | ProduktQuotient " | ||
+ | Differenz = ProduktQuotient | ProduktQuotient " | ||
+ | SummeDifferenz = Summe | Differenz ; | ||
+ | Vergleichsoperator = "<" | ||
+ | Aussage = SummeDifferenz | SummeDifferenz Vergleichsoperator SummeDifferenz ; | ||
+ | |||
+ | Zuweisung = Text " | ||
+ | Wiederholung = " | ||
+ | Ausgabe = " | ||
+ | Anweisung = Zuweisung | Wiederholung | Ausgabe ; | ||
+ | Sequenz = { Anweisung }; | ||
+ | |||
+ | Programm = Sequenz ; | ||
+ | |||
+ | </ | ||
+ | |||
+ | ===== Neue Tokentypen ===== | ||
+ | Für die neuen syntaktischen Elemente brauchen wir zusätzliche Tokentypen: | ||
+ | |||
+ | <code java> | ||
+ | public enum TokenType { | ||
+ | zahl, text, plus, minus, mal, geteilt, klammerAuf, klammerZu, | ||
+ | geschweifteKlammerAuf, | ||
+ | whileKeyword, | ||
+ | kleiner, groesser, identisch, kleinergleich, | ||
+ | zuweisung, | ||
+ | trueKeyword, | ||
+ | |||
+ | /** | ||
+ | * Nur als Knotentyp für Knoten des Syntaxbaums: | ||
+ | */ | ||
+ | negation | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Erweiterung des Lexers ===== | ||
+ | Damit der Lexer die Schlüsselwörter '' | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Die Methode lexVariable geht davon aus, dass das nächste zu verarbeitende | ||
+ | * Zeichen ein Buchstabe ist. Solange weitere Buchstaben oder Ziffern | ||
+ | * kommen, liest sie sie und setzt sie zu einem Text zusammen. | ||
+ | */ | ||
+ | private void lexText() { | ||
+ | |||
+ | String text = ""; | ||
+ | |||
+ | do { | ||
+ | char c = peek(); | ||
+ | text += c; | ||
+ | position++; | ||
+ | } while (istBuchstabe(peek()) || istZiffer(peek()) || peek() == ' | ||
+ | |||
+ | switch (text) { | ||
+ | case " | ||
+ | tokenListe.add(new Token(TokenType.trueKeyword)); | ||
+ | break; | ||
+ | case " | ||
+ | tokenListe.add(new Token(TokenType.falseKeyword)); | ||
+ | break; | ||
+ | case " | ||
+ | tokenListe.add(new Token(TokenType.whileKeyword)); | ||
+ | break; | ||
+ | case " | ||
+ | tokenListe.add(new Token(TokenType.printKeyword)); | ||
+ | break; | ||
+ | default: | ||
+ | tokenListe.add(new Token(text)); | ||
+ | break; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Etwas trickreicher sind die neuen Zeichen. Um etwa ''<'' | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * peek(n) liest das Zeichen im Programmtext an (aktuelle Position + n). Die | ||
+ | * aktuelle Position (Attribut position) wird nicht verändert. | ||
+ | * | ||
+ | * @return Das Zeichen, das n Zeichen weiter steht als die aktuelle Position | ||
+ | */ | ||
+ | private char peek(int n) { | ||
+ | if (position + n < text.length()) { | ||
+ | return text.charAt(position + n); | ||
+ | } else { | ||
+ | return (char) 0; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Damit sieht die Erkennung von ''>'', | ||
+ | |||
+ | <code java> | ||
+ | case '>': | ||
+ | if(peek(1) == ' | ||
+ | addToken(TokenType.groessergleich); | ||
+ | position++; | ||
+ | } else { | ||
+ | addToken(TokenType.groesser); | ||
+ | } | ||
+ | break; | ||
+ | case ' | ||
+ | if(peek(1) == ' | ||
+ | addToken(TokenType.ungleich); | ||
+ | position++; | ||
+ | } else { | ||
+ | throw new Exception(" | ||
+ | } | ||
+ | break; | ||
+ | </ | ||
+ | |||
+ | ===== Test des Lexers ===== | ||
+ | Der Lexer lässt sich wieder mit der Klasse '' | ||
+ | < | ||
+ | text[a] zuweisung zahl[1.0] strichpunkt text[b] zuweisung zahl[2.0] strichpunkt whileKeyword klammerAuf text[a] kleiner zahl[10.0] klammerZu geschweifteKlammerAuf text[a] zuweisung text[a] plus zahl[1.0] strichpunkt text[b] zuweisung text[b] mal zahl[2.0] strichpunkt printKeyword klammerAuf text[b] klammerZu strichpunkt geschweifteKlammerZu | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Erweiterung der Klasse Knoten ===== | ||
+ | Auch Anweisungen (Wiederholung, | ||
+ | |||
+ | <code java> | ||
+ | public class Knoten { | ||
+ | |||
+ | /** | ||
+ | * Im Token steckt der Inhalt des Knotens drin, also ein Operator, eine Zahl oder ein | ||
+ | * Variablenbezeichner. Der Einfachheit halber verwenden wir hier die Klasse Token. | ||
+ | */ | ||
+ | private Token token; | ||
+ | |||
+ | /** | ||
+ | * Kindknoten linkerhand | ||
+ | */ | ||
+ | private Knoten links; | ||
+ | |||
+ | /** | ||
+ | * Kindknoten rechterhand | ||
+ | */ | ||
+ | private Knoten rechts; | ||
+ | |||
+ | /** | ||
+ | * Im Falle einer Anweisung: nächstfolgende Anweisung | ||
+ | */ | ||
+ | private Knoten naechsteAnweisung; | ||
+ | |||
+ | ... | ||
+ | </ | ||
+ | |||
+ | Die Kindknoten der Anweisungen haben für verschiedene Arten von Anweisungen verschiedene Bedeutung: | ||
+ | |||
+ | **wiederholungs-Knoten: | ||
+ | * links: Aussage innerhalb der Klammer | ||
+ | * rechts: erste Anweisung innerhalb des while-Blocks (d.h. innerhalb der geschweiften Klammern) | ||
+ | * naechsteAnweisung: | ||
+ | |||
+ | **print-Knoten: | ||
+ | * links: Aussage, deren wert ausgegeben werden soll | ||
+ | * rechts: '' | ||
+ | * naechsteAnweisung: | ||
+ | |||
+ | **Zuweisungs-Knoten: | ||
+ | * links: text-Knoten, | ||
+ | * rechts: Aussage, deren Wert der Variablen zugewiesen werden soll. | ||
+ | * naechsteAnweisung: | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ===== Erweiterung des Parsers ===== | ||
+ | Die Klasse Parser wird um Methoden zum Parsen von | ||
+ | * einer Aussage | ||
+ | * einer Wiederholung | ||
+ | * einer Zuweisung | ||
+ | * einer Print-Anweisung | ||
+ | * einer Anweisung (d.h. eine beliebige der drei vorhergehenden) | ||
+ | * einer Sequenz (d.h. einer beliebig langen Abfolge von Anweisungen) | ||
+ | erweitert: | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Parst die dem Konstruktor übergebene Liste von Tokens und gibt einen | ||
+ | * Parse-Baum (Abstract Syntax Tree) zurück. | ||
+ | * | ||
+ | * @return Parse-Baum (Abstract Syntax Tree) | ||
+ | * @throws Exception | ||
+ | * Falls die Liste der Tokens nicht der Syntax entspricht | ||
+ | */ | ||
+ | public Knoten parse() throws Exception { | ||
+ | |||
+ | wurzel = sequenz(); | ||
+ | |||
+ | return wurzel; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Parst eine Reihe von Anweisungen. | ||
+ | * | ||
+ | * @return | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten sequenz() throws Exception { | ||
+ | |||
+ | Knoten knoten = null; | ||
+ | Knoten ersteAnweisung = null; | ||
+ | |||
+ | do { | ||
+ | |||
+ | Knoten letzterKnoten = knoten; | ||
+ | |||
+ | knoten = anweisung(); | ||
+ | |||
+ | if(letzterKnoten != null){ | ||
+ | |||
+ | letzterKnoten.setNaechsteAnweisung(knoten); | ||
+ | |||
+ | if(ersteAnweisung == null){ | ||
+ | |||
+ | ersteAnweisung = letzterKnoten; | ||
+ | |||
+ | } | ||
+ | } | ||
+ | |||
+ | } while (knoten != null); | ||
+ | |||
+ | return ersteAnweisung; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Parst eine Anweisung, d.h. eine Wiederholung, | ||
+ | * eine Zuweisung. | ||
+ | * | ||
+ | * @return | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten anweisung() throws Exception { | ||
+ | |||
+ | if(peek() == null){ | ||
+ | return null; | ||
+ | } | ||
+ | /** | ||
+ | * Eine Anweisung beginnt mit whileKeyword, | ||
+ | */ | ||
+ | switch (peek()) { | ||
+ | case whileKeyword: | ||
+ | return wiederholung(); | ||
+ | case printKeyword: | ||
+ | return print(); | ||
+ | case text: | ||
+ | return zuweisung(); | ||
+ | default: | ||
+ | return null; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Die Methode geht davon aus, dass das nächste Token vom Typ printKeyword | ||
+ | * ist. | ||
+ | * | ||
+ | * @return | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten print() throws Exception { | ||
+ | |||
+ | Knoten knoten = new Knoten(erwarte(TokenType.printKeyword)); | ||
+ | |||
+ | erwarte(TokenType.klammerAuf); | ||
+ | |||
+ | Knoten aussage = aussage(); | ||
+ | |||
+ | erwarte(TokenType.klammerZu); | ||
+ | |||
+ | erwarte(TokenType.strichpunkt); | ||
+ | |||
+ | knoten.setLinks(aussage); | ||
+ | |||
+ | return knoten; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Die Methode geht davon aus, dass das nächste Token vom Typ whileKeyword | ||
+ | * ist. | ||
+ | * | ||
+ | * @return | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten wiederholung() throws Exception { | ||
+ | |||
+ | Knoten knoten = new Knoten(erwarte(TokenType.whileKeyword)); | ||
+ | |||
+ | erwarte(TokenType.klammerAuf); | ||
+ | |||
+ | Knoten aussage = aussage(); | ||
+ | |||
+ | erwarte(TokenType.klammerZu); | ||
+ | |||
+ | erwarte(TokenType.geschweifteKlammerAuf); | ||
+ | |||
+ | Knoten anweisungen = sequenz(); | ||
+ | |||
+ | erwarte(TokenType.geschweifteKlammerZu); | ||
+ | |||
+ | knoten.setLinks(aussage); | ||
+ | knoten.setRechts(anweisungen); | ||
+ | |||
+ | return knoten; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Die Methode geht davon aus, dass das nächste Token vom Typ text ist. | ||
+ | * | ||
+ | * @return | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten zuweisung() throws Exception { | ||
+ | |||
+ | Knoten linkeSeite = new Knoten(nextToken()); | ||
+ | |||
+ | Knoten knoten = new Knoten(erwarte(TokenType.zuweisung)); | ||
+ | |||
+ | Knoten rechteSeite = aussage(); | ||
+ | |||
+ | erwarte(TokenType.strichpunkt); | ||
+ | |||
+ | knoten.setLinks(linkeSeite); | ||
+ | knoten.setRechts(rechteSeite); | ||
+ | |||
+ | return knoten; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Versucht, eine Aussage im Programmtext zu parsen. Weiteres: siehe Methode | ||
+ | * SummeDifferenz. | ||
+ | * | ||
+ | * @return Geparster Teilbaum | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten aussage() throws Exception { | ||
+ | |||
+ | Knoten linkerOperand = summeDifferenz(); | ||
+ | // | ||
+ | |||
+ | while (peek() == TokenType.identisch || peek() == TokenType.ungleich | ||
+ | || peek() == TokenType.kleiner || peek() == TokenType.groesser | ||
+ | || peek() == TokenType.kleinergleich | ||
+ | || peek() == TokenType.groessergleich) { | ||
+ | |||
+ | Token operator = nextToken(); | ||
+ | |||
+ | Knoten rechterOperand = summeDifferenz(); | ||
+ | |||
+ | Knoten neuerKnoten = new Knoten(operator, | ||
+ | rechterOperand); | ||
+ | |||
+ | linkerOperand = neuerKnoten; | ||
+ | |||
+ | } | ||
+ | |||
+ | return linkerOperand; | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Ansonsten muss nichts am Parser geändert werden. | ||
+ | |||
+ | ===== Test des Parsers ===== | ||
+ | Der Parser lässt sich wieder mit der Klasse '' | ||
+ | < | ||
+ | Eingabetext: | ||
+ | a = 1; | ||
+ | b = 2; | ||
+ | while(a < 10){ | ||
+ | a = a + 1; | ||
+ | b = b * 2; | ||
+ | print(b); | ||
+ | } | ||
+ | |||
+ | Tokens: | ||
+ | text[a] zuweisung zahl[1.0] strichpunkt text[b] zuweisung zahl[2.0] strichpunkt whileKeyword klammerAuf text[a] kleiner zahl[10.0] klammerZu geschweifteKlammerAuf text[a] zuweisung text[a] plus zahl[1.0] strichpunkt text[b] zuweisung text[b] mal zahl[2.0] strichpunkt printKeyword klammerAuf text[b] klammerZu strichpunkt geschweifteKlammerZu | ||
+ | |||
+ | Syntaxbaum (AST): | ||
+ | zuweisung | ||
+ | | ||
+ | | ||
+ | | ||
+ | l:text[b] | ||
+ | r:zahl[2.0] | ||
+ | n: | ||
+ | | ||
+ | l:text[a] | ||
+ | r: | ||
+ | | ||
+ | l:text[a] | ||
+ | r:plus | ||
+ | | ||
+ | | ||
+ | n:zuweisung | ||
+ | | ||
+ | r:mal | ||
+ | l:text[b] | ||
+ | r:zahl[2.0] | ||
+ | | ||
+ | l:text[b] | ||
+ | </ | ||
+ | |||
+ | Nachfolgend noch in grafischer Form. Die mit " | ||
+ | {{ : | ||
+ | |||
+ | ===== Erweiterung des Interpreters ===== | ||
+ | |||
+ | ===== Mehrere Datentypen ===== | ||
+ | Die größte Herausforderung bei der Erweiterung des Interpreters ist, dass Term- und Variablenwerte jetzt zwei verschiedene Datentypen haben können. Der Term '' | ||
+ | Die einfachste (aber nicht: beste) Lösung besteht darin, den Rückgabewert der Methode '' | ||
+ | |||
+ | Der erste Änderungsschritt besteht darin, die '' | ||
+ | |||
+ | <code java> | ||
+ | public class Interpreter { | ||
+ | |||
+ | /** | ||
+ | * Speichert Zuordnungen von Variablenbezeichnern zu Werten: | ||
+ | */ | ||
+ | private HashMap< | ||
+ | </ | ||
+ | |||
+ | Die Methode '' | ||
+ | |||
+ | < | ||
+ | /** | ||
+ | * Berechnet - ausgehend vom übergebenen Knoten - den Wert des Terms, der | ||
+ | * durch den Knoten und den darunterhängenden Teilbaum gegeben ist. | ||
+ | * | ||
+ | * @param knoten | ||
+ | * Wurzel des Teilbaums, dessen Termwert berechnet werden soll | ||
+ | * @return Wert des Terms | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | public Object interpretiere(Knoten knoten) throws Exception { | ||
+ | |||
+ | if (knoten != null) { | ||
+ | |||
+ | switch (knoten.getToken().getTokenType()) { | ||
+ | </ | ||
+ | |||
+ | Bei der Berechnung der Rückgabewerte müssen die Termwerte des linken und rechten Operanden jeweils passend interpretiert (" | ||
+ | |||
+ | <code java> | ||
+ | case plus: | ||
+ | return (Double) interpretiere(knoten.getLinks()) | ||
+ | + (Double) interpretiere(knoten.getRechts()); | ||
+ | </ | ||
+ | |||
+ | Für die anderen Operatoren sieht das entsprechend aus. Trickreich wird es nur bei den Vergleichsoperatoren '' | ||
+ | |||
+ | < | ||
+ | case ungleich: | ||
+ | |||
+ | Object linkerWert1 = interpretiere(knoten.getLinks()); | ||
+ | |||
+ | if (linkerWert1.getClass() == Double.class) { | ||
+ | return (Double) interpretiere(knoten.getLinks()) != (Double) interpretiere(knoten | ||
+ | .getRechts()); | ||
+ | } else { | ||
+ | return (Boolean) interpretiere(knoten.getLinks()) != (Boolean) interpretiere(knoten | ||
+ | .getRechts()); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Die Knoten, die einer Wiederholung, | ||
+ | |||
+ | < | ||
+ | case zuweisung: | ||
+ | String variablenbezeichner1 = knoten.getLinks().getToken() | ||
+ | .getText(); | ||
+ | |||
+ | Object wert1 = interpretiere(knoten.getRechts()); | ||
+ | |||
+ | /** | ||
+ | * Neuen Wert der Variable speichern: | ||
+ | */ | ||
+ | variablenbelegung.put(variablenbezeichner1, | ||
+ | |||
+ | /** | ||
+ | * Führe die Anweisungen aus, die nach der Zuweisung kommen: | ||
+ | */ | ||
+ | interpretiere(knoten.getNaechsteAnweisung()); | ||
+ | |||
+ | return wert1; | ||
+ | |||
+ | case printKeyword: | ||
+ | |||
+ | /** | ||
+ | * Im linken Knoten steckt der Term, dessen Wert ausgegeben werden soll | ||
+ | */ | ||
+ | System.out.println(interpretiere(knoten.getLinks())); | ||
+ | |||
+ | /** | ||
+ | * Führe die Anweisungen aus, die nach der Print-Anweisung kommen: | ||
+ | */ | ||
+ | interpretiere(knoten.getNaechsteAnweisung()); | ||
+ | |||
+ | return null; | ||
+ | </ | ||
+ | |||
+ | Dadurch, dass am Ende jeder Anweisung mittels '' | ||
+ | |||
+ | ===== Test des Interpreters ===== | ||
+ | Der Lexer lässt sich mit der Klasse '' | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Ausführung des Programms | ||
+ | */ | ||
+ | |||
+ | Interpreter i = new Interpreter(); | ||
+ | |||
+ | System.out.println(" | ||
+ | |||
+ | Object wert = i.interpretiere(p.getWurzel()); | ||
+ | |||
+ | System.out | ||
+ | .println(" | ||
+ | </ | ||
+ | |||
+ | Innerhalb der Ausgabe sieht man auch die Ausgabe des interpretierten Programms: | ||
+ | |||
+ | < | ||
+ | Eingabetext: | ||
+ | a = 1; | ||
+ | b = 2; | ||
+ | while(a < 10){ | ||
+ | a = a + 1; | ||
+ | b = b * 2; | ||
+ | print(b); | ||
+ | } | ||
+ | |||
+ | Bem.: An dieser Stelle gibt der Interpreter auch die Tokenliste und den Syntaxbaum aus. Sie sind weiter oben auf dieser Seite unter "Test des Parsers" | ||
+ | |||
+ | Ausgabe des Programms: | ||
+ | |||
+ | 4.0 | ||
+ | 8.0 | ||
+ | 16.0 | ||
+ | 32.0 | ||
+ | 64.0 | ||
+ | 128.0 | ||
+ | 256.0 | ||
+ | 512.0 | ||
+ | 1024.0 | ||
+ | |||
+ | Termwert: | ||
+ | 1.0 | ||
+ | </ | ||
+ | |||
+ | **Kleine Denkaufgabe: | ||
+ | |||
+ | Bei der Implementierung des Compilers wurden viele Kompromisse eingegangen, | ||
+ | |||
+ | * Die Fehlermeldungen könnten ausführlicher sein | ||
+ | * Die Performance des Lexers kann verbessert werden | ||
+ | * Starke Typisierung wäre wünschenswert | ||
+ | * Die Typen der Knoten des Syntaxbaums sollten von den Typen der Tokens unterschieden werden | ||
+ | * ... | ||
+ | |||
+ | **Aber:** Er läuft! :-) | ||
+ | |||