Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
Beide Seiten der vorigen RevisionVorhergehende ÜberarbeitungNächste Überarbeitung | Vorhergehende Überarbeitung | ||
programmieren:schritte:lexer:start [2016/02/17 22:47] – martin | programmieren:schritte:lexer:start [2021/12/29 10:40] (aktuell) – Externe Bearbeitung 127.0.0.1 | ||
---|---|---|---|
Zeile 1: | Zeile 1: | ||
+ | ====== Lexer ====== | ||
+ | Der Lexer liest den Programmtext Zeichen für Zeichen und fasst diese zu kleinsten syntaktischen Einheiten, sogenannten Tokens, zusammen. Unsere Beispielsprache besteht aus folgenden 8 Tokens: | ||
+ | |||
+ | * + | ||
+ | * - | ||
+ | * * | ||
+ | * / | ||
+ | * ( | ||
+ | * ) | ||
+ | * Zahlen (die aus Ziffern und Dezimalpunkten bestehen), z.B. '' | ||
+ | * Variablenbezeichnern, | ||
+ | |||
+ | Die Tokens können im Programmtext unmittelbar hintereinander stehen, sie können aber auch durch Leerzeichen oder Zeilenumbrüche (' | ||
+ | |||
+ | Den Programmtext | ||
+ | < | ||
+ | 1 + 2 * (4 * a1 - 3) | ||
+ | </ | ||
+ | zerlegt der Lexer also in folgende 11 Tokens: | ||
+ | < | ||
+ | zahl[1.0] plus zahl[2.0] mal klammerAuf zahl[4.0] mal text[a1] minus zahl[3.0] klammerZu | ||
+ | </ | ||
+ | |||
+ | ===== Die Klasse Token ===== | ||
+ | |||
+ | Damit der Parser die Tokens nachher weiterverarbeiten kann, muss zu jedem Token sein Typ (Zahl, plus, text, ...) sowie - im Falle von Zahlen und Variablen - auch die konkrete Zahl bzw. die konkrete Variable gespeichert werden. Ein Token wird daher als Objekt der Klasse '' | ||
+ | |||
+ | <code java> | ||
+ | package lexer; | ||
+ | |||
+ | public class Token { | ||
+ | |||
+ | private TokenType tokenType; // Art des Tokens (z.B. zahl, klammerAuf usw.) | ||
+ | private double zahl; // Wird nur belegt, wenn tokenType == zahl | ||
+ | private String text; // Wird nur belegt, falls tokenType == bezeichner | ||
+ | |||
+ | public Token(double zahl){ | ||
+ | this.zahl = zahl; | ||
+ | tokenType = TokenType.zahl; | ||
+ | } | ||
+ | |||
+ | public Token(String text){ | ||
+ | this.text = text; | ||
+ | tokenType = TokenType.variable; | ||
+ | } | ||
+ | |||
+ | public Token(TokenType tokenType){ | ||
+ | this.tokenType = tokenType; | ||
+ | } | ||
+ | |||
+ | public TokenType getTokenType() { | ||
+ | return tokenType; | ||
+ | } | ||
+ | |||
+ | public double getZahl() { | ||
+ | return zahl; | ||
+ | } | ||
+ | |||
+ | public String getText() { | ||
+ | return text; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Die toString()-Methode dient nur Debuggingzwecken | ||
+ | */ | ||
+ | @Override | ||
+ | public String toString() { | ||
+ | |||
+ | String s = "" | ||
+ | |||
+ | switch (tokenType) { | ||
+ | case zahl: | ||
+ | s += " | ||
+ | break; | ||
+ | case variable: | ||
+ | s += " | ||
+ | break; | ||
+ | default: | ||
+ | break; | ||
+ | } | ||
+ | |||
+ | return s; | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Die Klasse TokenType ===== | ||
+ | Die Klasse TokenType ist einfach eine Enum-Klasse mit den möglichen Typen: | ||
+ | |||
+ | <code java> | ||
+ | public enum TokenType { | ||
+ | zahl, text, plus, minus, mal, geteilt, klammerAuf, klammerZu, | ||
+ | |||
+ | /** | ||
+ | * Nur als Knotentyp für Knoten des Syntaxbaums: | ||
+ | */ | ||
+ | negation | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Da wir die TokenTypen der Einfachheit halber auch als Typen der Knoten des vom Parser generierten Syntaxbaums verwenden, gibt es einen zusätzlichen Typ '' | ||
+ | |||
+ | ===== Funktionsweise des Lexers ===== | ||
+ | Die Klasse Lexer bekommt im Konstruktor den Programmtext übergeben und speichert ihn im Attribut '' | ||
+ | |||
+ | <code java> | ||
+ | public class Lexer { | ||
+ | |||
+ | /** | ||
+ | * Die Zeichenkette wird von links nach rechts abgearbeitet. Hier ist | ||
+ | * gespeichert, | ||
+ | */ | ||
+ | private int position; | ||
+ | |||
+ | /** | ||
+ | * Liste zur Speicherung der ermittelten Tokens | ||
+ | */ | ||
+ | private ArrayList< | ||
+ | |||
+ | /** | ||
+ | * Wir speichern den Programmtext in einem Attribut der Klasse, damit er | ||
+ | * nachher nicht von Methode zu Methode weitergereicht werden muss. | ||
+ | */ | ||
+ | private String text; | ||
+ | |||
+ | /** | ||
+ | * | ||
+ | * @param text | ||
+ | * Programmtext, | ||
+ | */ | ||
+ | public Lexer(String text) { | ||
+ | |||
+ | this.text = text; | ||
+ | |||
+ | position = 0; // Wir beginnen mit dem Zeichen ganz links | ||
+ | |||
+ | tokenListe = new ArrayList< | ||
+ | |||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | Die Methode '' | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Zerlegt den Programmtext, | ||
+ | * | ||
+ | * @return Liste mit Tokens | ||
+ | * @throws Exception | ||
+ | * Eine Exception wird ausgelöst, wenn der Programmtext Passagen | ||
+ | * | ||
+ | */ | ||
+ | public ArrayList< | ||
+ | |||
+ | /** | ||
+ | * Wiederholung, | ||
+ | */ | ||
+ | while (position < text.length()) { | ||
+ | |||
+ | /** | ||
+ | * peek() liest das nächste Zeichen im Programmtext, | ||
+ | * Variable position aber nicht. Ruft man peek() mehrmals | ||
+ | * hintereinander auf, liefert es also immer dasselbe Zeichen. | ||
+ | */ | ||
+ | char c = peek(); | ||
+ | |||
+ | if (istZiffer(c)) { | ||
+ | lexZahl(); | ||
+ | } else if (istBuchstabe(c)) { | ||
+ | lexText(); | ||
+ | } else { | ||
+ | switch (c) { | ||
+ | case ' | ||
+ | addToken(TokenType.plus); | ||
+ | break; | ||
+ | case ' | ||
+ | addToken(TokenType.minus); | ||
+ | break; | ||
+ | case ' | ||
+ | addToken(TokenType.mal); | ||
+ | break; | ||
+ | case '/': | ||
+ | addToken(TokenType.geteilt); | ||
+ | break; | ||
+ | case ' | ||
+ | addToken(TokenType.klammerAuf); | ||
+ | break; | ||
+ | case ' | ||
+ | addToken(TokenType.klammerZu); | ||
+ | break; | ||
+ | case ' ': | ||
+ | case ' | ||
+ | case ' | ||
+ | // Leerzeichen werden einfach überlesen | ||
+ | break; | ||
+ | default: | ||
+ | throw new Exception(" | ||
+ | + "' | ||
+ | } | ||
+ | |||
+ | position++; | ||
+ | // verarbeitet, | ||
+ | |||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | return tokenListe; | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Die Hilfsmethoden lexZahl() und lexText() ==== | ||
+ | <code java> | ||
+ | /** | ||
+ | * Die Methode lexText 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 Variablennamen zusammen. | ||
+ | */ | ||
+ | private void lexText() { | ||
+ | |||
+ | StringBuilder variablenBezeichner = new StringBuilder(); | ||
+ | |||
+ | do { | ||
+ | char c = peek(); | ||
+ | variablenBezeichner.append(c); | ||
+ | position++; | ||
+ | } while (Character.isLetter(peek()) || Character.isDigit(peek()) || peek() == ' | ||
+ | |||
+ | tokenListe.add(new Token(variablenBezeichner.toString())); | ||
+ | |||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Die Methode lexZahl liest eine Zahl | ||
+ | */ | ||
+ | private void lexZahl() | ||
+ | |||
+ | String zahlAlsString = ""; | ||
+ | |||
+ | do { | ||
+ | char c = peek(); | ||
+ | zahlAlsString += c; | ||
+ | position++; | ||
+ | } while (Character.isDigit(peek()) || peek() == ' | ||
+ | |||
+ | /** | ||
+ | * Hier machen wir es uns leicht und lassen Java den String in eine Zahl | ||
+ | * konvertieren. Die Methode parseDouble ist für sich genommen natürlich | ||
+ | * auch ein Lexer. | ||
+ | */ | ||
+ | double zahl = Double.parseDouble(zahlAlsString); | ||
+ | |||
+ | tokenListe.add(new Token(zahl)); | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Zum einfachen Hinzufügen eines Tokens, das weder eine Zahl noch Text darstellt, wird eine kleine Hilfsmethode definiert: | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Fügt der tokenListe das übergebene Token hinzu | ||
+ | * | ||
+ | * @param tokenType | ||
+ | */ | ||
+ | private void addToken(TokenType tokenType) { | ||
+ | tokenListe.add(new Token(tokenType)); | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Der Zugriff auf den Programmtext geschieht immer über die Methode '' | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * peek() liest das nächste Zeichen im Programmtext, | ||
+ | * position aber nicht. Ruft man peek() mehrmals hintereinander auf, liefert | ||
+ | * es also immer dasselbe Zeichen. | ||
+ | * | ||
+ | * @return nächstes Zeichen im Programmtext | ||
+ | */ | ||
+ | private char peek() { | ||
+ | if (position < text.length()) { | ||
+ | return text.charAt(position); | ||
+ | } else { | ||
+ | return ' | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Die restlichen Methoden erklären sich von selbst: | ||
+ | |||
+ | <code java> | ||
+ | /** | ||
+ | * Vorsicht: Die übergebene Liste ist nur dann gefüllt, wenn zuvor die | ||
+ | * Methode lex aufgerufen wurde. | ||
+ | * | ||
+ | * @return Liste mit den Tokens, in die der Lexer den Programmtext zerlegt | ||
+ | * hat. | ||
+ | */ | ||
+ | public ArrayList< | ||
+ | return tokenListe; | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * Nur zu Debuggingzwecken | ||
+ | */ | ||
+ | @Override | ||
+ | public String toString() { | ||
+ | |||
+ | String s = ""; | ||
+ | |||
+ | for (Token token : tokenListe) { | ||
+ | s += token.toString() + " "; | ||
+ | } | ||
+ | |||
+ | if (!s.isEmpty()) { | ||
+ | s = s.substring(0, | ||
+ | } | ||
+ | |||
+ | return s; | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Hier geht's weiter zur Beschreibung des [[programmieren: |