Normale Parser, wie sie im Allgemeinen gelehrt werden, haben eine Lexerstufe, bevor der Parser die Eingabe berührt. Der Lexer (auch "Scanner" oder "Tokenizer") zerlegt die Eingabe in kleine Token, die mit einem Typ versehen sind. Auf diese Weise kann der Hauptparser Token als Terminalelemente verwenden, anstatt jedes Zeichen als Terminal behandeln zu müssen, was zu spürbaren Effizienzgewinnen führt. Insbesondere kann der Lexer auch alle Kommentare und Leerzeichen entfernen. Eine separate Tokenizer-Phase bedeutet jedoch, dass Schlüsselwörter nicht auch als Bezeichner verwendet werden können (es sei denn, die Sprache unterstützt das Abstreifen, das etwas in Ungnade gefallen ist, oder stellt allen Bezeichnern ein Siegel wie vor $foo
).
Warum? Nehmen wir an, wir haben einen einfachen Tokenizer, der die folgenden Token versteht:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
Der Tokenizer stimmt immer mit dem längsten Token überein und bevorzugt Schlüsselwörter gegenüber Bezeichnern. Also interesting
wird lexed als IDENT:interesting
, aber in
wird lexed als IN
, niemals als IDENT:interesting
. Ein Code-Snippet wie
for(var in expression)
wird in den Token-Stream übersetzt
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Bisher funktioniert das. Aber jede Variable in
würde als Schlüsselwort lexiert und IN
nicht als Variable, die den Code beschädigen würde. Der Lexer behält keinen Status zwischen den Token bei und kann nicht wissen, dass dies in
normalerweise eine Variable sein sollte, außer wenn wir uns in einer for-Schleife befinden. Außerdem sollte der folgende Code legal sein:
for(in in expression)
Der erste in
wäre eine Kennung, der zweite ein Schlüsselwort.
Es gibt zwei Reaktionen auf dieses Problem:
Kontextbezogene Schlüsselwörter sind verwirrend. Verwenden wir stattdessen Schlüsselwörter.
Java hat viele reservierte Wörter, von denen einige nur dazu dienen, Programmierern, die von C ++ zu Java wechseln, hilfreichere Fehlermeldungen zukommen zu lassen. Durch das Hinzufügen neuer Schlüsselwörter wird der Code unterbrochen. Das Hinzufügen von kontextbezogenen Schlüsselwörtern ist für einen Leser des Codes verwirrend, es sei denn, sie verfügen über eine gute Syntaxhervorhebung, und die Implementierung von Tools ist schwierig, da fortgeschrittenere Analysetechniken verwendet werden müssen (siehe unten).
Wenn wir die Sprache erweitern möchten, besteht der einzig vernünftige Ansatz darin, Symbole zu verwenden, die zuvor in der Sprache nicht legal waren. Insbesondere können dies keine Bezeichner sein. Mit der foreach-Schleifensyntax hat Java das vorhandene :
Schlüsselwort mit einer neuen Bedeutung wiederverwendet . Mit Lambdas fügte Java ein ->
Schlüsselwort hinzu, das zuvor in keinem legalen Programm vorkommen konnte ( -->
würde immer noch als legal lexiert '--' '>'
und ->
möglicherweise zuvor als lexiert '-', '>'
, aber diese Sequenz würde vom Parser abgelehnt).
Kontextbezogene Schlüsselwörter vereinfachen Sprachen, lassen Sie uns sie implementieren
Lexer sind unbestreitbar nützlich. Aber anstatt einen Lexer vor dem Parser auszuführen, können wir sie zusammen mit dem Parser ausführen. Bottom-up-Parser kennen immer die Token-Typen, die an einem bestimmten Ort akzeptabel sind. Der Parser kann dann den Lexer auffordern, einen dieser Typen an der aktuellen Position abzugleichen. In einer for-each-Schleife befindet sich der Parser an der Position, die ·
in der (vereinfachten) Grammatik angegeben ist, nachdem die Variable gefunden wurde:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
An dieser Stelle sind die legalen Token SEMICOLON
oder IN
, aber nicht IDENT
. Ein Schlüsselwort in
wäre völlig eindeutig.
In diesem speziellen Beispiel hätten Top-Down-Parser auch kein Problem, da wir die obige Grammatik umschreiben können
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
und alle für die Entscheidung notwendigen Token können ohne Rückverfolgung angezeigt werden.
Betrachten Sie die Benutzerfreundlichkeit
Java tendierte immer zur semantischen und syntaktischen Einfachheit. Zum Beispiel unterstützt die Sprache das Überladen von Operatoren nicht, da dies den Code weitaus komplizierter machen würde. Wenn wir uns also zwischen in
und :
für eine für jede Schleife bestimmte Syntax entscheiden, müssen wir berücksichtigen, welche weniger verwirrend und für Benutzer offensichtlicher ist. Der Extremfall wäre wahrscheinlich
for (in in in in())
for (in in : in())
(Hinweis: Java verfügt über separate Namespaces für Typnamen, Variablen und Methoden. Ich denke, dies war meistens ein Fehler. Dies bedeutet nicht, dass das spätere Sprachdesign weitere Fehler hinzufügen muss .)
Welche Alternative bietet klarere visuelle Trennungen zwischen der Iterationsvariablen und der iterierten Sammlung? Welche Alternative erkennt man schneller, wenn man sich den Code ansieht? Ich habe festgestellt, dass das Trennen von Symbolen bei diesen Kriterien besser ist als eine Wortfolge. Andere Sprachen haben andere Werte. Zum Beispiel formuliert Python viele Operatoren auf Englisch, damit sie natürlich gelesen werden können und leicht zu verstehen sind. Dieselben Eigenschaften können es jedoch ziemlich schwierig machen, ein Stück Python auf einen Blick zu verstehen.