Бедняга "лексер" для C#

Я пытаюсь написать очень простой парсер на C#.

Мне нужен лексер -- что-то, что позволяет мне связывать регулярные выражения с токенами, поэтому он читает в регулярных выражениях и возвращает мне символы.

Кажется, что я должен иметь возможность использовать Regex для реальной тяжелой работы, но я не вижу простого способа сделать это. Во-первых, регулярное выражение работает только на строках, а не на потоках (почему это!?!?).

в основном, я хочу реализовать следующее интерфейс:

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);
}

Итак, pluz отправляет codz...
Нет, серьезно, я собираюсь начать писать реализацию вышеуказанного интерфейса, но мне трудно поверить, что в .NET (2.0) уже нет простого способа сделать это.

Итак, какие-либо предложения по простому способу сделать выше? (Кроме того, я не хочу никаких "генераторов кода". Производительность не важна для этой вещи и я не хочу вводить какие-либо сложности в процессе сборки.)

7 ответов


оригинальная версия, которую я разместил здесь в качестве ответа, имела проблему в том, что она работала только тогда, когда было более одного "регулярного выражения", которое соответствовало текущему выражению. То есть, как только совпадет только одно регулярное выражение, оно вернет токен, тогда как большинство людей хотят, чтобы регулярное выражение было "жадным". Это особенно относится к таким вещам, как "цитируемые строки".

единственное решение, которое находится поверх регулярного выражения, - это читать входные строки (Что означает, что у вас не может быть маркеров, которые охватывают несколько строк.) Я могу жить с этим - в конце концов, это лексер бедняка! Кроме того, обычно полезно получить информацию о номере строки из лексера в любом случае.

Итак, вот новая версия, которая решает эти вопросы. Кредит также идет на этой

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() => 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();
}

пример:

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(@"([""'])(?:\|.)*?", "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);

выход:

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: )

Если у вас нет очень нетрадиционной грамматики, я бы сильно рекомендуем не сворачивать собственный лексер / парсер.

обычно я нахожу, что лексеры / Парсеры для C# действительно отсутствуют. Однако F# поставляется с fslex и fsyacc, которые вы можете узнать, как использовать в этом уроке. Я написал несколько лексеров / синтаксических анализаторов в F# и использовал их в C#, и это очень легко сделать.

Я полагаю, что на самом деле это не лексер/парсер бедняка, видя, что вам нужно изучить совершенно новый язык для начала, но это начало.


Это может быть излишним, но посмотрите на Ирония на CodePlex.

ирония-это набор разработки для реализации языков на платформе .NET. Он использует гибкость и мощь языка c# и .NET Framework 3.5 для реализации совершенно новой и оптимизированной технологии построения компилятора. В отличие от большинства существующих решений в стиле yacc / lex ирония не использует генерацию кода сканера или парсера из грамматических спецификаций, написанных в специализированном метаязык. По иронии судьбы грамматика целевого языка кодируется непосредственно в c#, используя перегрузку оператора для выражения грамматических конструкций. Модули сканера и парсера иронии используют грамматику, закодированную как класс c#, для управления процессом синтаксического анализа. Пример определения грамматики в классе c# и его использование в рабочем синтаксическом анализаторе см. В примере грамматики выражения.


изменение моего первоначального ответа.

посмотри SharpTemplate который имеет синтаксические анализаторы для разных типов синтаксиса, например

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

он использует regexes для каждого типа токена:

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_\.@]*?)(?![a-zA-Z0-9_\.@])", false);
    }
}

который используется следующим образом

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;
        ....
    }

    ....
}

он толкает и выскакивает из стека, чтобы сохранить состояние.


Малкольм Кроу имеет отличную реализацию LEX/YACC для C# здесь. Работает путем создания регулярных выражений для LEX...

скачать


можно использовать Flex и Bison для C#.

исследователь из университета Ирландии разработал частичную реализацию, которую можно найти по следующей ссылке:Flex / Bison для C#

его определенно можно считать "бедным лексером", поскольку у него, похоже, все еще есть некоторые проблемы с его реализацией, такие как отсутствие препроцессора, проблемы с "висячим другим" случаем и т. д.


Если вы посмотрите на ExpressionConverter в моем библиотека преобразователей WPF, Он имеет базовую лексику и синтаксический анализ выражений C#. Никаких регулярных выражений, по памяти.