web-dev-qa-db-ja.com

C#用の貧乏人の「レクサー」

非常に単純なパーサーをC#で作成しようとしています。

レクサーが必要です。正規表現をトークンに関連付けることができるので、正規表現を読み込んでシンボルを返します。

正規表現を使用して実際の重い物を持ち上げることができるはずのようですが、簡単な方法がわかりません。一つには、正規表現は文字列でのみ機能し、ストリームでは機能しないようです(なぜですか!?!?)。

基本的に、私は次のインターフェースの実装が必要です。

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)でこれを行う簡単な方法がまだないことを信じるのは難しいと思います。

それで、上記を行う簡単な方法の提案はありますか? (また、「コードジェネレーター」は必要ありません。パフォーマンスはこの点では重要ではなく、ビルドプロセスに複雑さを導入したくありません。)

32

回答としてここに投稿した元のバージョンには、現在の表現に一致する「正規表現」が複数ある場合にのみ機能するという問題がありました。つまり、1つの正規表現だけが一致するとすぐにトークンが返されますが、ほとんどの人は正規表現を「貪欲」にしたいと考えています。これは特に「引用符で囲まれた文字列」などに当てはまりました。

正規表現の上にある唯一の解決策は、入力を1行ずつ読み取ることです(つまり、複数行にまたがるトークンを持つことはできません)。私はこれと一緒に暮らすことができます-結局のところ、それは貧乏人のレクサーです!さらに、通常は、どのような場合でもレクサーから行番号情報を取得すると便利です。

そこで、これらの問題に対処する新しいバージョンを次に示します。クレジットも 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() => 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: )
25

やり過ぎかもしれませんが、CodePlexの Irony を見てください。

Ironyは、.NETプラットフォームに言語を実装するための開発キットです。これは、c#言語と.NET Framework 3.5の柔軟性と能力を使用して、コンパイラー構築のまったく新しい合理化されたテクノロジーを実装します。ほとんどの既存のyacc/Lexスタイルのソリューションとは異なり、Ironyは、特殊なメタ言語で記述された文法仕様からのスキャナーまたはパーサーコード生成を採用していません。 Ironyでは、ターゲット言語の文法は、演算子のオーバーロードを使用してc#で直接コーディングされ、文法構造を表現します。 Ironyのスキャナーおよびパーサーモジュールは、c#クラスとしてエンコードされた文法を使用して、解析プロセスを制御します。 c#クラスでの文法定義の例と、動作中のパーサーでの使用については、式の文法サンプルを参照してください。

6
Andy Dent

非常に型破りな文法がない限り、強く独自のレクサー/パーサーをロールしないことをお勧めします。

私は通常、C#のレクサー/パーサーが本当に不足していることに気付きます。ただし、F#にはfslexとfsyaccが付属しており、これらの使用方法を学ぶことができます このチュートリアルでは 。私はいくつかのレクサー/パーサーをF#で作成し、それらをC#で使用しましたが、非常に簡単です。

始めるにはまったく新しい言語を学ぶ必要があることを考えると、それは本当に貧乏人のレクサー/パーサーではないと思いますが、それは始まりです。

5
Juliet

Malcolm Croweには、C#用の優れたLex/YACC実装があります ここ 。 Lexの正規表現を作成することで機能します...

直接ダウンロード

2
Kieron

私の元の答えを変更します。

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

    ....
}

スタックからプッシュおよびポップして状態を維持します。

2
Chris S

私の WPF Convertersライブラリ のExpressionConverterを見ると、C#式の基本的な字句解析と解析が行われています。メモリからの正規表現は含まれていません。

0
Kent Boogaart

C#にはFlexとBisonを使用できます。

アイルランド大学の研究者は、次のリンクにある部分的な実装を開発しました: Flex/Bison for C#

プリプロセッサがない、「ぶら下がっているelse」の場合など、実装にまだいくつかの問題があるように思われるため、これは間違いなく「貧弱なマンレクサー」と見なすことができます。

0
the_e