なぜレクサーを2Dアレイと巨大なスイッチとして実装するのですか?


24

私は学位を取得するためにゆっくりと取り組んでおり、この学期はCompilers 101です。DragonBookを使用ています。まもなくコースに入り、語彙分析と、決定論的有限オートマトン(以下、DFA)を介してそれを実装する方法について説明します。さまざまなレクサーの状態を設定し、それらの間の遷移を定義します。

しかし、教授と本は両方とも、巨大な2D配列(1つの次元としてのさまざまな非終端状態、および他の可能性のある入力シンボル)に相当する遷移テーブルと、すべての端子を処理するswitchステートメントを介して実装することを提案していますまた、非終端状態の場合は遷移テーブルにディスパッチします。

理論はすべて良好で優れていますが、実際にコードを何十年も書いた人として、実装は下手です。それはテスト可能ではなく、保守可能でもなく、読み取り可能でもなく、デバッグするのに苦労します。さらに悪いことに、その言語がUTFに対応していれば、どのように実用的であるかわかりません。非終端状態ごとに100万程度の遷移テーブルエントリがあると、急いで扱いにくくなります。

それで、取引は何ですか?主題に関する決定的な本が、このようにそれをするように言っているのはなぜですか?

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

注:可能性のある重複「なぜ巨大なswitchステートメントの代わりにオブジェクト指向アプローチを使用するのか?」は近いですが、オブジェクト指向については気にしません。機能的アプローチ、またはスタンドアロン関数での賢明な命令型アプローチでも問題ありません。)

また、例のために、識別子のみを持つ言語を考えてみましょう[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'));
}

NextTokenDFAの開始から複数の宛先を取得したら、コードを独自の機能にリファクタリングします。


5
古代(1977)の遺産コンパイラ設計の原則?40年前、コーディングスタイルは非常に異なっていた
ブヨ

7
DFA状態の移行をどのように実装しますか?そして、端末と非端末についてこれは何ですか、「非端末」は通常、文法の生成規則を指します。これは字句解析の後に来ます。

10
これらのテーブルは人間が読めることを意図したものではなく、コンパイラーが使用でき、非常に高速に実行されることを意図しています。入力を先読みするときにテーブルを飛び回るのは簡単です(たとえば、左再帰をキャッチするために、実際にはほとんどの言語はそれを避けるために構築されています)。

5
刺激の一部が、より良い仕事をする方法を知り、あなたが好むアプローチに対するフィードバックや感謝を得る能力を欠いていることから来ている場合-業界の数十年はフィードバックと時には感謝を期待するように私たちを訓練しているので-より良い実装を作成し、CodeReview.SEに投稿して、あなた自身の安心のためにその一部を取得する必要があります。
ジミーホッファ14年

7
簡単な答えは、レクサーは通常、有限状態マシンとして実装され、文法から自動的に生成されるためです。そして、驚くことではないが、状態テーブルは最も簡単かつコンパクトに表として表されます。人間があるため、オブジェクトコードと同じように、それはと仕事への人間のため容易ではないという事実は無関係ですしていないそれに取り組みます。ソースを変更し、新しいインスタンスを生成します。
ケシュラム14年

回答:


16

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

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

lexが書かれた1975年以来、字句解析器を生成するユーティリティがありました。

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


4
私はその卸売を提案しているのか定かではありません。正規表現は、任意の(通常の)言語を扱います。特定の言語で作業するとき、より良いアプローチはありませんか?この本は予測的アプローチに触れていますが、例ではそれらを無視しています。また、C#の素朴なアナライザーを数年前にやったことがありますが、メンテナンスがそれほど難しくありませんでした。非効率的な?確かに、しかしそれほどひどくはないので、当時の私のスキルを考えてみてください。
テラスティン14年

1
@Telastyn:テーブル駆動のDFAよりも速く進むことはほとんど不可能です。次の文字を取得し、遷移テーブルで次の状態を検索し、状態を変更します。新しい状態がターミナルの場合、トークンを発行します。C#またはJavaでは、一時的な文字列を作成するアプローチは遅くなります。
ケビンクライン14年

@kevincline-もちろん、しかし私の例では一時的な文字列はありません。Cでさえ、それは単なるインデックスまたは文字列をステップスルーするポインタになります。
テラスティン14年

6
@JimmyHoffa:はい、コンパイラーのパフォーマンスは間違いなく重要です。コンパイラーは、地獄に戻って最適化されているため高速です。マイクロ最適化ではなく、不要な一時オブジェクトの作成や破棄などの不必要な作業は行いません。私の経験では、ほとんどの商用テキスト処理コードは、最新のコンパイラの10分の1の作業を行い、それを行うのに10倍の時間がかかります。ギガバイトのテキストを処理しているときのパフォーマンスは非常に大きくなります。
ケビンクライン14年

1
@Telastyn、あなたはどのような「より良いアプローチ」を考えていましたか?また、どのように「より良い」と期待しますか?十分にテストされたレキシングツールが既にあり、非常に高速なパーサーを生成することを考えると(他の人が言ったように、テーブル駆動DFAは非常に高速です)、それらを使用するのは理にかなっています。lex文法を書くことができるのに、なぜ特定の言語に対して新しい特別なアプローチを発明したいのでしょうか?lex文法はより保守性が高く、結果のパーサーはより正確である可能性が高くなります(lexおよび同様のツールが十分にテストされていることを考えると)。
DW

7

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

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


7

以下の内部ループ:

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

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

実際には、レクサーを勉強しているCS学生以外に、transitionテーブルを構築するツールに付属している定型文の一部であるため、誰もその内部ループを実装(またはデバッグ)する必要はありません。


5

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

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


2

以前にDragon Bookを使用したことがありますが、テーブル駆動のレバーとパーサーを使用する主な理由は、正規表現を使用してレクサーを生成し、BNFをパーサーを生成できるようにするためです。この本は、lexやyaccのようなツールがどのように機能するかについてもカバーしており、これらのツールがどのように機能するかを理解するために順を追って説明しています。さらに、いくつかの実用的な例を検討することが重要です。

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

うまくいけば、あなたのインストラクターがlexとyaccを使用できるようになります(大学院レベルのクラスで、lexとyaccを書くことができる場合を除きます)。


0

パーティーに遅れて:-)トークンは正規表現と照合されます。それらの多くがあるので、マルチ正規表現エンジンがあります。これは巨大なDFAです。

「さらに悪いことに、その言語がUTFに対応していれば、どのように実用的であるかわかりません。」

無関係(または透明)です。UTFに加えて、そのエンティティは部分的にも重複しない優れたプロパティを持っています。たとえば、文字「A」(ASCII-7テーブルから)を表すバイトは、他のUTF文字には再び使用されません。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.