Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
Beide Seiten der vorigen RevisionVorhergehende ÜberarbeitungNächste Überarbeitung | Vorhergehende Überarbeitung | ||
programmieren:schritte:parser:start [2016/02/17 22:48] – martin | programmieren:schritte:parser:start [2021/12/29 10:40] (aktuell) – Externe Bearbeitung 127.0.0.1 | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
+ | ====== Parser ====== | ||
+ | Der Parser analysiert die Liste der Tokens anhand einer gegebenen Syntax und erstellt eine Baumstruktur (" | ||
+ | * Eine Summe besteht aus dem linken Summanden gefolgt vom Zeichen + gefolgt vom rechten Summanden | ||
+ | * Entspechend mit Produkt, Differenz und Quotient | ||
+ | * Es gilt "Punkt vor Strich" | ||
+ | * Teilterme in Klammern werden zuerst berechnet. | ||
+ | * Steht ein Minuszeichen vor einem Term, so wird der Termwert mit -1 multipliziert (Negation) | ||
+ | |||
+ | Wie man eine solche Syntax besser aufschreibt (Backus-Naur-Form) beschreibe ich später. Für das Verständnis des Compilers reicht die Beschreibung oben. | ||
+ | |||
+ | Aus dem Term '' | ||
+ | < | ||
+ | zahl[1.0] plus zahl[2.0] mal klammerAuf zahl[4.0] mal minus text[a1] minus zahl[3.0] klammerZu | ||
+ | </ | ||
+ | Man beachte, dass der Lexer noch nicht zwischen einer Differenz (zweites " | ||
+ | |||
+ | {{ : | ||
+ | |||
+ | ===== Die Klasse Knoten ===== | ||
+ | |||
+ | Die rechteckig umrahmten Teile des Baums nennt man **Knoten**, die Verbindungslinien **Kanten**. Der Knoten ganz oben heißt **Wurzel**. Der Baum wird traditionell mit der Wurzel nach oben gezeichnet, da man beim Zeichnen auf Papier anfangs nicht weiß, wie hoch der Baum wird. Für unsere einfache Programmiersprache hat jeder Knoten maximal 2 Kanten, wir können die Knoten also durch folgende klasse abbilden: | ||
+ | |||
+ | <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; | ||
+ | |||
+ | public Knoten(Token token) { | ||
+ | super(); | ||
+ | this.token = token; | ||
+ | } | ||
+ | |||
+ | public Knoten(Token token, Knoten linkerOperand, | ||
+ | this.token = token; | ||
+ | this.links = linkerOperand; | ||
+ | this.rechts = rechterOperand; | ||
+ | } | ||
+ | |||
+ | public Knoten getLinks() { | ||
+ | return links; | ||
+ | } | ||
+ | |||
+ | public void setLinks(Knoten links) { | ||
+ | this.links = links; | ||
+ | } | ||
+ | |||
+ | public Knoten getRechts() { | ||
+ | return rechts; | ||
+ | } | ||
+ | |||
+ | public void setRechts(Knoten rechts) { | ||
+ | this.rechts = rechts; | ||
+ | } | ||
+ | |||
+ | public Token getToken() { | ||
+ | return token; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Will man auf einen Baum im Programm zugreifen, so reicht es, seine Wurzel in einem Attribut zu speichern. Durch sukzessiven Aufruf von '' | ||
+ | |||
+ | ===== Die Klasse Parser ===== | ||
+ | Es gibt verschiedene Arten, einen Parser zu programmieren. Die einfachste, "von Hand" machbare Art ist ein [[http:// | ||
+ | * Eine Summe besteht aus | ||
+ | * einem Produkt/ | ||
+ | * **ausschließlich** aus einem Produkt/ | ||
+ | * Ein Produkt besteht aus | ||
+ | * einem " | ||
+ | * **ausschließlich** aus einem " | ||
+ | * Analog mit Differenz bzw. Quotient | ||
+ | * Ein " | ||
+ | * einer öffnenden Klammer, einer Summe(bzw. Differenz) und einer schließenden Klammer oder | ||
+ | * einem Minuszeichen gefolgt von einem " | ||
+ | * einer Variablen oder | ||
+ | * einer Zahl. | ||
+ | |||
+ | **Bem.:** In dieser Beschreibung steckt implizit schon drin, dass "Punkt vor Strich" | ||
+ | |||
+ | Obiger Beschreibung folgend ruft die Methode '' | ||
+ | Falls eine echte Summe vorliegt, kann diese natürlich wiederum der erste Summand einer weiteren Summe sein ('' | ||
+ | Die Methode '' | ||
+ | |||
+ | Insgesamt erkennt man am Vorgehen die Namensgebung: | ||
+ | |||
+ | Die Einzelheiten der Programmierung kann man am besten am kommentierten Sourcecode ersehen. | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ===== Sourcecode ===== | ||
+ | |||
+ | Hier der Sourcecode der Klasse '' | ||
+ | |||
+ | <code java> | ||
+ | package parser; | ||
+ | |||
+ | import java.util.ArrayList; | ||
+ | |||
+ | import lexer.Token; | ||
+ | import lexer.TokenType; | ||
+ | |||
+ | public class Parser { | ||
+ | |||
+ | /** | ||
+ | * Liste, die vom Lexer kommt | ||
+ | */ | ||
+ | private ArrayList< | ||
+ | |||
+ | /** | ||
+ | * Zur Speicherung des Parse-Baums (Abstract Syntax Tree) reicht es, die | ||
+ | * Wurzel zu speichern. Über sie bekommt man jeden beliebigen Knoten | ||
+ | * darunter. | ||
+ | */ | ||
+ | private Knoten wurzel; | ||
+ | |||
+ | /** | ||
+ | * Nummer des Tokens, das gerade analysiert wird (von 0 beginnend) | ||
+ | */ | ||
+ | private int position; | ||
+ | |||
+ | /** | ||
+ | * | ||
+ | * @param tokenListe | ||
+ | * Liste von Tokens, die geparst werden soll | ||
+ | */ | ||
+ | public Parser(ArrayList< | ||
+ | |||
+ | super(); | ||
+ | |||
+ | this.tokenListe = tokenListe; | ||
+ | |||
+ | position = 0; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * 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 = summeDifferenz(); | ||
+ | |||
+ | return wurzel; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Versucht, eine Summe oder Differenz zu parsen. Parst zunächst den ersten | ||
+ | * Operanden (unter der Annahme, dass es eine Multiplikation oder Division | ||
+ | * ist) und sieht dann nach, ob ein Pluszeichen oder ein Minuszeichen kommt. | ||
+ | * Falls " | ||
+ | * Baum zusammen. | ||
+ | * | ||
+ | * Folgt ein weiteres Plus oder Minus, wird der Baum als erster Operand | ||
+ | * einer weiteren Summe/ | ||
+ | * dazu gesucht usw. | ||
+ | * | ||
+ | * Folgt kein weiteres Plus oder Minus, so endet die Methode und der | ||
+ | * aktuelle Baum wird zurückgegeben. | ||
+ | * | ||
+ | * @return Geparster Teilbaum | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten summeDifferenz() throws Exception { | ||
+ | |||
+ | Knoten linkerOperand = produktQuotient(); | ||
+ | // | ||
+ | |||
+ | while (peek() == TokenType.plus || peek() == TokenType.minus) { | ||
+ | |||
+ | Token operator = nextToken(); | ||
+ | |||
+ | Knoten rechterOperand = produktQuotient(); | ||
+ | |||
+ | Knoten neuerKnoten = new Knoten(operator, | ||
+ | rechterOperand); | ||
+ | |||
+ | linkerOperand = neuerKnoten; | ||
+ | |||
+ | } | ||
+ | |||
+ | return linkerOperand; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Wie PlusMinus, jedoch für Mal und geteilt. Als erster bzw. zweiter | ||
+ | * Operand wird nach einem einfachen Term (Klammerterm, | ||
+ | * Negation) gesucht. | ||
+ | * | ||
+ | * @return | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | private Knoten produktQuotient() throws Exception { | ||
+ | |||
+ | Knoten linkerOperand = einfacherTerm(); | ||
+ | |||
+ | while (peek() == TokenType.mal || peek() == TokenType.geteilt) { | ||
+ | |||
+ | Token operator = nextToken(); | ||
+ | |||
+ | Knoten rechterOperand = einfacherTerm(); | ||
+ | |||
+ | Knoten neuerKnoten = new Knoten(operator, | ||
+ | rechterOperand); | ||
+ | |||
+ | linkerOperand = neuerKnoten; | ||
+ | |||
+ | } | ||
+ | |||
+ | return linkerOperand; | ||
+ | |||
+ | } | ||
+ | |||
+ | private Knoten einfacherTerm() throws Exception { | ||
+ | |||
+ | if (peek() == null) { | ||
+ | throw new Exception(" | ||
+ | } | ||
+ | |||
+ | switch (peek()) { | ||
+ | case klammerAuf: // Klammerterm | ||
+ | nextToken(); | ||
+ | Knoten knoten = summeDifferenz(); | ||
+ | // | ||
+ | erwarte(TokenType.klammerZu); | ||
+ | return knoten; | ||
+ | case text: | ||
+ | return new Knoten(nextToken()); | ||
+ | case zahl: | ||
+ | return new Knoten(nextToken()); | ||
+ | case minus: | ||
+ | Knoten knoten1 = new Knoten(new Token(TokenType.negation)); | ||
+ | nextToken(); | ||
+ | knoten1.setLinks(einfacherTerm()); | ||
+ | // | ||
+ | return knoten1; | ||
+ | default: | ||
+ | throw new Exception(" | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Gibt das nächste Token aus der Tokenliste zurück, erhöht aber nicht die | ||
+ | * Leseposition | ||
+ | * | ||
+ | * @return | ||
+ | */ | ||
+ | public TokenType peek() { | ||
+ | |||
+ | if (position < tokenListe.size()) { | ||
+ | |||
+ | return tokenListe.get(position).getTokenType(); | ||
+ | |||
+ | } | ||
+ | |||
+ | return null; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Gibt das nächste Token aus der Tokenliste zurück und erhöht(!) die | ||
+ | * Leseposition | ||
+ | * | ||
+ | * @return | ||
+ | */ | ||
+ | public Token nextToken() { | ||
+ | |||
+ | if (position < tokenListe.size()) { | ||
+ | |||
+ | Token token = tokenListe.get(position); | ||
+ | |||
+ | position++; | ||
+ | |||
+ | return token; | ||
+ | |||
+ | } | ||
+ | |||
+ | return null; | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Wirft eine Exception, wenn das nächste Token in der Tokenliste nicht dem | ||
+ | * übergebenen Typ entspricht. | ||
+ | * | ||
+ | * @param tokenType | ||
+ | * @throws Exception | ||
+ | */ | ||
+ | public void erwarte(TokenType tokenType) throws Exception { | ||
+ | |||
+ | if (peek() == null) { | ||
+ | throw new Exception( | ||
+ | " | ||
+ | + tokenType); | ||
+ | } | ||
+ | |||
+ | if (peek() != tokenType) { | ||
+ | throw new Exception(" | ||
+ | } | ||
+ | |||
+ | nextToken(); | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Nur zu Debuggingzwecken | ||
+ | */ | ||
+ | @Override | ||
+ | public String toString() { | ||
+ | |||
+ | StringBuilder sb = new StringBuilder(); | ||
+ | |||
+ | toString(wurzel, | ||
+ | |||
+ | return sb.toString(); | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Traversiert vom übergebenen Knoten ausgehend den Teilbaum in pre-order | ||
+ | * Reihenfolge, | ||
+ | * komplette linke Teilbaum, der dranhängt, dann der komplette rechte | ||
+ | * Teilbaum, der dranhängt. | ||
+ | * | ||
+ | * @param knoten | ||
+ | * @param einruecken | ||
+ | * @param sb | ||
+ | */ | ||
+ | private void toString(Knoten knoten, String einruecken, StringBuilder sb) { | ||
+ | |||
+ | sb.append(einruecken); | ||
+ | sb.append(knoten.getToken().toString() + " | ||
+ | |||
+ | if (knoten.getLinks() != null) { | ||
+ | toString(knoten.getLinks(), | ||
+ | } | ||
+ | |||
+ | if (knoten.getRechts() != null) { | ||
+ | toString(knoten.getRechts(), | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | public Knoten getWurzel() { | ||
+ | return wurzel; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | |||
+ | </ | ||
+ | |||
+ | Hier geht's weiter zur Beschreibung des [[programmieren: |