2009-03-23 11 views
28

Ich versuche, einen sehr einfachen Parser in C# zu schreiben.Poor Man "lexer" für C#

Ich brauche einen Lexer - etwas, mit dem ich reguläre Ausdrücke mit Tokens verknüpfen kann, so dass es in Regexs liest und mir Symbole zurückgibt.

Es scheint, als ob ich in der Lage sein sollte, Regex zu verwenden, um das eigentliche schwere Heben zu tun, aber ich sehe keinen einfachen Weg, es zu tun. Zum einen scheint Regex nur an Strings zu arbeiten, nicht an Streams (warum ist das so?!?).

Grundsätzlich möchte ich eine Implementierung der folgenden Schnittstelle:

interface ILexer : IDisposable 
{ 
    /// <summary> 
    /// Return true if there are more tokens to read 
    /// </summary> 
    bool HasMoreTokens { get; } 
    /// <summary> 
    /// The actual contents that matched the token 
    /// </summary> 
    string TokenContents { get; } 
    /// <summary> 
    /// The particular token in "tokenDefinitions" that was matched (e.g. "STRING", "NUMBER", "OPEN PARENS", "CLOSE PARENS" 
    /// </summary> 
    object Token { get; } 
    /// <summary> 
    /// Move to the next token 
    /// </summary> 
    void Next(); 
} 

interface ILexerFactory 
{ 
    /// <summary> 
    /// Create a Lexer for converting a stream of characters into tokens 
    /// </summary> 
    /// <param name="reader">TextReader that supplies the underlying stream</param> 
    /// <param name="tokenDefinitions">A dictionary from regular expressions to their "token identifers"</param> 
    /// <returns>The lexer</returns> 
    ILexer CreateLexer(TextReader reader, IDictionary<string, object> tokenDefinitions); 
} 

So Pluz die Codz senden ...
Nein, im Ernst, ich bin eine Implementierung der obigen Schnittstelle Schreiben noch beginnen zu Ich kann es kaum glauben, dass es in .NET (2.0) noch keine einfache Möglichkeit dafür gibt.

Also, irgendwelche Vorschläge für eine einfache Möglichkeit, das Obige zu tun? (Ich möchte auch keine "Code Generatoren". Leistung ist nicht wichtig für diese Sache, und ich möchte keine Komplexität in den Build-Prozess einführen.)

+0

Meiner Meinung nach gibt es eine Menge guter Werkzeuge zum Generieren von Lexer/Parser-Code (wie ANTLR oder Irony), aber wenn Sie nie einen grundlegenden Parsing-Code von Grund auf schreiben, könnte es schwierig sein, diese Generatoren zu nutzen. Ich habe vor kurzem einen einfachen Open-Source-Mathe-Ausdruck-Parser geschrieben (https://github.com/gsscoder/exprengine). Es evaluiert das Durchlaufen des abstrakten Syntaxbaums (AST) unter Verwendung des Besuchermusters. Der Lexer/Parser wird von Grund auf neu geschrieben, keine Generatoren! Ich hoffe es kann hilfreich sein. – gsscoder

Antwort

23

Die ursprüngliche Version, die ich hier gepostet als Antwort ein Problem in hatte, dass es funktioniert nur, wenn es mehr als einen „Regex“ war, der die aktuelle abgestimmt Ausdruck. Das heißt, sobald nur ein Regex übereinstimmte, würde er ein Token zurückgeben - während die meisten Leute wollen, dass der Regex "gierig" ist. Dies war insbesondere bei Dingen wie "Anführungszeichen" der Fall.

Die einzige Lösung, die auf Regex sitzt, ist das Lesen der Eingabe Zeile für Zeile (was bedeutet, dass Sie keine Token haben können, die sich über mehrere Zeilen erstrecken). Ich kann damit leben - es ist schließlich ein lexer eines armen Mannes! Außerdem ist es normalerweise nützlich, in jedem Fall Informationen zur Zeilennummer aus dem Lexer zu erhalten.

Also, hier ist eine neue Version, die diese Probleme anspricht. Kredit geht auch this

public interface IMatcher 
{ 
    /// <summary> 
    /// Return the number of characters that this "regex" or equivalent 
    /// matches. 
    /// </summary> 
    /// <param name="text">The text to be matched</param> 
    /// <returns>The number of characters that matched</returns> 
    int Match(string text); 
} 

sealed class RegexMatcher : IMatcher 
{ 
    private readonly Regex regex; 
    public RegexMatcher(string regex) 
    { 
     this.regex = new Regex(string.Format("^{0}", regex)); 
    } 

    public int Match(string text) 
    { 
     var m = regex.Match(text); 
     return m.Success ? m.Length : 0; 
    } 

    public override string ToString() 
    { 
     return regex.ToString(); 
    } 
} 

public sealed class TokenDefinition 
{ 
    public readonly IMatcher Matcher; 
    public readonly object Token; 

    public TokenDefinition(string regex, object token) 
    { 
     this.Matcher = new RegexMatcher(regex); 
     this.Token = token; 
    } 
} 

public sealed class Lexer : IDisposable 
{ 
    private readonly TextReader reader; 
    private readonly TokenDefinition[] tokenDefinitions; 

    private string lineRemaining; 

    public Lexer(TextReader reader, TokenDefinition[] tokenDefinitions) 
    { 
     this.reader = reader; 
     this.tokenDefinitions = tokenDefinitions; 
     nextLine(); 
    } 

    private void nextLine() 
    { 
     do 
     { 
      lineRemaining = reader.ReadLine(); 
      ++LineNumber; 
      Position = 0; 
     } while (lineRemaining != null && lineRemaining.Length == 0); 
    } 

    public bool Next() 
    { 
     if (lineRemaining == null) 
      return false; 
     foreach (var def in tokenDefinitions) 
     { 
      var matched = def.Matcher.Match(lineRemaining); 
      if (matched > 0) 
      { 
       Position += matched; 
       Token = def.Token; 
       TokenContents = lineRemaining.Substring(0, matched); 
       lineRemaining = lineRemaining.Substring(matched); 
       if (lineRemaining.Length == 0) 
        nextLine(); 

       return true; 
      } 
     } 
     throw new Exception(string.Format("Unable to match against any tokens at line {0} position {1} \"{2}\"", 
              LineNumber, Position, lineRemaining)); 
    } 

    public string TokenContents { get; private set; } 

    public object Token { get; private set; } 

    public int LineNumber { get; private set; } 

    public int Position { get; private set; } 

    public void Dispose() 
    { 
     reader.Dispose(); 
    } 
} 

Programm Beispiel:

string sample = @"(one (two 456 -43.2 "" \"" quoted""))"; 

var defs = new TokenDefinition[] 
{ 
    // Thanks to [steven levithan][2] for this great quoted string 
      // regex 
    new TokenDefinition(@"([""'])(?:\\\1|.)*?\1", "QUOTED-STRING"), 
    // Thanks to http://www.regular-expressions.info/floatingpoint.html 
    new TokenDefinition(@"[-+]?\d*\.\d+([eE][-+]?\d+)?", "FLOAT"), 
    new TokenDefinition(@"[-+]?\d+", "INT"), 
    new TokenDefinition(@"#t", "TRUE"), 
    new TokenDefinition(@"#f", "FALSE"), 
    new TokenDefinition(@"[*<>\?\-+/A-Za-z->!]+", "SYMBOL"), 
    new TokenDefinition(@"\.", "DOT"), 
    new TokenDefinition(@"\(", "LEFT"), 
    new TokenDefinition(@"\)", "RIGHT"), 
    new TokenDefinition(@"\s", "SPACE") 
}; 

TextReader r = new StringReader(sample); 
Lexer l = new Lexer(r, defs); 
while (l.Next()) 
{ 
    Console.WriteLine("Token: {0} Contents: {1}", l.Token, l.TokenContents); 
} 

Ausgang:

Token: LEFT Contents: (
Token: SPACE Contents: 
Token: SYMBOL Contents: one 
Token: SPACE Contents: 
Token: LEFT Contents: (
Token: SYMBOL Contents: two 
Token: SPACE Contents: 
Token: INT Contents: 456 
Token: SPACE Contents: 
Token: FLOAT Contents: -43.2 
Token: SPACE Contents: 
Token: QUOTED-STRING Contents: " \" quoted" 
Token: SPACE Contents: 
Token: RIGHT Contents:) 
Token: RIGHT Contents:) 
+1

Vier Jahre später, aber vielen Dank, mein Herr! Gestern habe ich es geschafft, eine Regex-Lexer-Ausgabe genau so zu bekommen, wie ich es möchte, aber selbst nach der Optimierung war es langsam (ungefähr 15 Sekunden für 36k-Tokens). Während das, was der ältere Lexer getan hat (der alle Regex auf einmal lief und dann Übereinstimmungsindizes mit dem aktuellen Index in eine Datei verglich, unter der Annahme von, schätze ich: "Regex ist langsam, also mach es einmal, dann nie wieder")), Ihre, jedoch schlagen Sie um genau 14,5 Sekunden! Anscheinend ist Regex nicht so teuer, wie die meisten Leute denken würden. Danke noch einmal! –

+0

Eine kleine Anpassung in dieser Zeile: 'this.regex = new Regex (string.Format ("^{0} ", regex));', geh sicher '^ {0} 'in Klammern wie diese'^({0}) ', sonst stimmen Übereinstimmungen wie' \ r | \ n' nicht richtig überein! –

0

Wenn Sie sich den ExpressionConverter in Meine WPF Converters library, es hat grundlegende Lexing und Parsing von C# -Ausdrücken. Keine Regex, aus dem Gedächtnis.

6

Sofern Sie eine sehr unkonventionelle Grammatik haben, würde ich stark empfehlen, nicht Ihren eigenen Lexer/Parser zu rollen.

Ich finde normalerweise Lexer/Parser für C# wirklich fehlen. F # kommt jedoch mit fslex und fsyacc, die Sie lernen können, wie man in this tutorial verwendet. Ich habe mehrere Lexer/Parser in F # geschrieben und sie in C# verwendet, und es ist sehr einfach zu machen.

Ich nehme an, es ist nicht wirklich ein armer Mann lexer/Parser, zu sehen, dass Sie eine völlig neue Sprache lernen müssen, um loszulegen, aber es ist ein Anfang.

2

Ändern meiner ursprünglichen Antwort.

Werfen Sie einen Blick auf SharpTemplate, die Parser für verschiedene Syntaxtypen, z.

#foreach ($product in $Products) 
    <tr><td>$product.Name</td> 
    #if ($product.Stock > 0) 
     <td>In stock</td> 
    #else 
    <td>Backordered</td> 
    #end 
    </tr> 
#end 

Es verwendet reguläre Ausdrücke für jede Art von Token:

public class Velocity : SharpTemplateConfig 
{ 
    public Velocity() 
    { 
     AddToken(TemplateTokenType.ForEach, @"#(foreach|{foreach})\s+\(\s*(?<iterator>[a-z_][a-z0-9_]*)\s+in\s+(?<expr>.*?)\s*\)", true); 
     AddToken(TemplateTokenType.EndBlock, @"#(end|{end})", true); 
     AddToken(TemplateTokenType.If, @"#(if|{if})\s+\((?<expr>.*?)\s*\)", true); 
     AddToken(TemplateTokenType.ElseIf, @"#(elseif|{elseif})\s+\((?<expr>.*?)\s*\)", true); 
     AddToken(TemplateTokenType.Else, @"#(else|{else})", true); 
     AddToken(TemplateTokenType.Expression, @"\${(?<expr>.*?)}", false); 
     AddToken(TemplateTokenType.Expression, @"\$(?<expr>[a-zA-Z_][a-zA-Z0-9_\[email protected]]*?)(?![a-zA-Z0-9_\[email protected]])", false); 
    } 
} 

die wie folgt

foreach (Match match in regex.Matches(inputString)) 
{ 
    ... 

    switch (tokenMatch.TokenType) 
    { 
     case TemplateTokenType.Expression: 
      { 
       currentNode.Add(new ExpressionNode(tokenMatch)); 
      } 
      break; 

     case TemplateTokenType.ForEach: 
      { 
       nodeStack.Push(currentNode); 

       currentNode = currentNode.Add(new ForEachNode(tokenMatch)); 
      } 
      break; 
     .... 
    } 

    .... 
} 

von einem Stapel Zustand zu halten, es drückt und Pops verwendet wird.

1

Es ist möglich, Flex und Bison für C# zu verwenden.

Ein Forscher an der University of Ireland hat eine teilweise Umsetzung entwickelt, die unter folgendem Link zu finden ist: Flex/Bison for C#

es auf jeden Fall ein "armen mans Lexer angesehen werden können, als er mit ein paar Fragen noch zu haben scheint seine Implementierung, wie zB kein Präprozessor, gibt es mit einem "dangling else" -Fall usw.

+0

Die Seite wurde 2004 nicht aktualisiert, und der Lexer selbst ist von der Spezifikation C# 0.28 abgeleitet. Ich glaube nicht, dass dieser "arme Mann Lexer" in der realen Welt verwendet werden sollte. – Juliet

+1

Das ist ein guter Punkt, aber ich dachte, dass, da er versuchte, etwas Einfaches zu tun, dieser schnelle und schmutzige (und offensichtlich unvollendete) Lexer ein guter Ausgangspunkt wäre. – espais

2

Malcolm Crowe hat eine großartige LEX/YACC-Implementierung für C# here. Werke von regulären Ausdrücken für die LEX ... und

Direct download

+0

FWIW: Link ist jetzt tot. –

+0

Ich habe den Link mit dem aus dem Artikel aktualisiert. – Kieron

5

Es kann zu viel des Guten, aber einen Blick auf Irony auf CodePlex.

Irony ist ein Entwicklungskit zur Implementierung von Sprachen auf .NET-Plattform. Es nutzt die Flexibilität und Leistungsfähigkeit von C# language und .NET Framework 3.5, um eine komplett neue und optimierte Compiler-Technologie zu implementieren. Im Gegensatz zu den meisten existierenden Yacc/Lex-style-Lösungen verwendet Irony keine Scanner- oder Parser-Code-Generierung aus Grammatikspezifikationen, die in einer spezialisierten Metasprache geschrieben sind. In Irony wird die Grammatik der Zielsprache direkt in C# codiert, wobei ein Operatorüberladen verwendet wird, um Grammatikkonstrukte auszudrücken. Die Scanner- und Parser-Module von Irony verwenden die als C# -Klasse codierte Grammatik, um den Analyseprozess zu steuern. Ein Beispiel für die Grammatikdefinition in der C# -Klasse finden Sie in der Grammatik-Beispielformulierung und in einem funktionierenden Parser.

+1

Ah ich sehe - klingt wie C# Version von Boost Spirit für C++. Danke ... obwohl, wie Sie aus meiner Antwort sehen können, auf jeden Fall übertrieben für das, was ich suche. –

+1

Interessantes Projekt in der Tat, zumindest haben Sie IDE-Unterstützung auf die gleiche Weise wie normale C# (weil Grammatik nur ein gewöhnlicher C# -Code wird :)). Ich denke, es ist ein bisschen so, als würde LINQ Ihnen dabei helfen, mit dem Schreiben von echtem SQL aufzuhören. – IgorK