web-dev-qa-db-ja.com

レクサーを2D配列および巨大なスイッチとして実装する理由

私は学位を取得するためにゆっくり作業しています。今学期はCompilers 101です。 the Dragon Book を使用しています。コースの少し前に、字句解析と、決定論的有限オートマトン(以下、DFA)を介してそれを実装する方法について説明します。さまざまなレクサー状態を設定し、それらの間の遷移を定義します。

しかし、教授と本はどちらも、巨大な2D配列(1つの次元としてさまざまな非終端状態、もう1つの次元として可能な入力シンボル)となる遷移テーブルと、すべての終端を処理するswitchステートメントを介してそれらを実装することを提案しています。非終了状態の場合は、遷移テーブルにディスパッチします。

理論はすべてうまくいきますが、実際に何十年もコードを書いている人として、実装は下劣です。それはテスト可能ではなく、保守不可能であり、読み取り不可能であり、デバッグするのは骨の折れる作業です。さらに悪いことに、言語がUTF対応である場合、それがリモートでどのように実用的であるかはわかりません。非最終状態ごとに100万程度の遷移テーブルエントリがあると、急いで不屈になります。

それで、契約は何ですか?なぜこの主題についての決定的な本はこのようにすることを言っているのですか?

関数呼び出しのオーバーヘッドは本当にそれほどですか?これはうまく機能するか、または文法が事前に知られていない場合に必要ですか(正規表現?)あるいは、より具体的なソリューションがより具体的な文法でよりうまく機能する場合でも、すべてのケースを処理する何かでしょうか?

注:重複の可能性 " 巨大なswitchステートメントの代わりにOOアプローチを使用する理由は? "は近いですが、私はオブジェクト指向については気にしていません。スタンドアロンの関数を使用する機能的なアプローチまたはより重要な命令的なアプローチでさえ問題ありません。)

例として、識別子のみを持ち、それらの識別子が[a-zA-Z]+。 DFA実装では、次のようなものが得られます。

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(ファイルの終わりを正しく処理するものでも)

私が期待するものと比較して:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

DFAの最初から複数の宛先があると、NextTokenのコードが独自の関数にリファクタリングされます。

25
Telastyn

実際には、これらのテーブルは、言語のトークンを定義する正規表現から生成されます。

number := [digit][digit|underscore]+
reserved_Word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

Lex が作成された1975年以来、字句アナライザを生成するユーティリティがありました。

あなたは基本的に正規表現を手続き型コードに置き換えることを提案しています。これにより、正規表現のいくつかの文字が数行のコードに拡張されます。中程度に興味深い言語の字句解析のための手書きの手続き型コードは、非効率的であり、維持するのが難しい傾向があります。

17
kevin cline

次の内部ループ:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

パフォーマンス上の利点がたくさんあります。すべての入力文字に対してまったく同じことを行うので、そこにはまったくブランチがありません。コンパイラーのパフォーマンスは、レクサー(入力のすべての文字のスケールで動作する必要があります)によって制御できます。ドラゴンブックが書かれたとき、これはさらに真実でした。

実際には、レクサーを学ぶCSの学生以外に、transitionテーブルを作成するツールに付属するボイラープレートの一部であるため、その内部ループを実装(またはデバッグ)する必要はありません。

7
Ben Jackson

特定のアルゴリズムの動機は、主にそれが学習課題であるため、DFAの概念に近づき、コード内で状態と遷移を非常に明示的に保つことです。原則として、とにかく実際にこのコードを手動で作成する人は誰もいません。ツールを使用して文法からコードを生成します。そして、そのツールはソースコードではなく、文法の定義に基づく出力であるため、コードの可読性を気にしません。

あなたのコードは、手書きのDFAを保守している人にとってはすっきりしていますが、教える概念から少し離れています。

7
psr

記憶から、-本を読んでから久しぶりで、最新版を読んでいないと確信しています。確かに、Javaのようなものを覚えていません-それは一部はテンプレートを意図したコードで書かれており、テーブルはLexのようなレクサージェネレーターで埋められています。それでもメモリから、テーブル圧縮に関するセクションがありました(これもメモリからですが、テーブル駆動パーサーにも適用できるように記述されているため、おそらく本書ではこれまでに見たものよりもさらに進んでいます)。同様に、私が覚えている本は8ビット文字セットを想定していたため、おそらくテーブル圧縮の一部として、後の版ではより大きな文字セットの処理に関するセクションを期待しています。 SOの質問への回答としてそれを処理する別の方法 を指定しました。

最新のアーキテクチャでタイトループデータを駆動することでパフォーマンスが確実に向上します。これは、非常にキャッシュフレンドリーであり(テーブルを圧縮している場合)、ジャンプ予測は可能な限り完全です(語彙の最後に1つミス、おそらく1つ)シンボルに依存するコードにディスパッチするスイッチがありません。これは、テーブルの解凍が予測可能なジャンプで実行できることを前提としています)。そのステートマシンを純粋なコードに移動すると、ジャンプ予測のパフォーマンスが低下し、おそらくキャッシュプレッシャーが増加します。

5
AProgrammer

以前にドラゴンブックを使用してきたが、テーブル駆動のレバーとパーサーを使用する主な理由は、正規表現を使用してレクサーを生成し、BNFを使用してパーサーを生成できるようにするためです。この本では、Lexやyaccなどのツールがどのように機能するかについても説明し、これらのツールがどのように機能するかを理解できるようにします。さらに、いくつかの実用的な例を通して作業することが重要です。

多くのコメントにもかかわらず、40年代、50年代、60年代に書かれたコードのスタイルとは何の関係もありません。それは、ツールがあなたのために何をしているか、そしてあなたが持っているものを実際に理解することと関係があります。それらを動作させるために行うこと。これは、コンパイラーがどのように機能するかを理論的および実用的な観点から根本的に理解することとすべて関係があります。

うまくいけば、インストラクターはLexとyaccの使用も許可します(それが大学院レベルのクラスであり、Lexとyaccを記述できる場合を除く)。

2
Robert Baron

パーティーの後半:-)トークンは正規表現と照合されます。それらの多くがあるので、あなたは巨大なDFAであるマルチ正規表現エンジンを持っています。

「さらに悪いことに、言語がUTF対応であった場合、それがリモートで実用的であるかどうかはわかりません。」

それは無関係です(または透明です)。 UTFのほかに、Niceプロパティがあるため、そのエンティティは部分的にさえオーバーラップしません。例えば。文字「A」を表すバイト(ASCII-7テーブルから)は、他のUTF文字には再び使用されません。

したがって、レクサー全体に対して単一のDFA(マルチ正規表現)があります。 2D配列よりもそれを書き留める方が良いですか?

0
greenoldman