Бедный "лексер" для C # - PullRequest
31 голосов
/ 23 марта 2009

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

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

Кажется, я должен быть в состоянии использовать Regex для выполнения тяжелой работы, но я не вижу простого способа сделать это. Во-первых, кажется, что 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);
}

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

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

Ответы [ 7 ]

24 голосов
/ 23 марта 2009

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

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

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

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

Выход:

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: )
6 голосов
/ 23 марта 2009

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

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

5 голосов
/ 23 марта 2009

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

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

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

2 голосов
/ 23 марта 2009

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

Прямая загрузка

2 голосов
/ 23 марта 2009

Изменение моего исходного ответа.

Взгляните на 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

Использует регулярные выражения для каждого типа токена:

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

    ....
}

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

0 голосов
/ 23 марта 2009

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

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

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

0 голосов
/ 23 марта 2009

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

...