前言#
トークナイザーの構築手順については詳しく述べませんが、リポジトリ内のトークナイザーを参照するか、他のタイプのものを自分で作成して、トークナイザーが返すトークンとパーサー内のトークン(ここではSymbol
クラスのインスタンス)が接続できるようにすれば大丈夫です。
このプロジェクトはドラゴンブック [1] に基づいて実装されており、Parser 内のかなりの部分の詳細はドラゴンブックに対応する部分が見つかるので、例としてドラゴンブックの理解を助けることができます。
このプロジェクトは Python 言語を使用しています。なぜなら、Python の自由度により、プログラムのロジックの実装に最大限集中でき、文法の細かい部分に煩わされることがなく、アイデアを実践するのに非常に適しているからです。また、型注釈を利用して静的チェックを実現できます。将来的にパフォーマンスが要求される場合は、これを基に C++ バージョンのプログラムを構築することも可能です。
最後に、こんなに難しい実験課題を出してくれた授業の先生に感謝します。そうでなければ、こんなことをすることは思いつかなかったでしょうし、約 1 週間ほど夢中になって取り組むこともなかったでしょう。
路線#
準備#
開発環境は Windows 11 + Jetbrains Pycharm + Python 3.9
- 使用する新しいバージョンの Python の特徴は以下の通りです:
- Python 3.6 以降、組み込み型
dict
の挿入が順序付きになり、項集族を明確に印刷するために使用されます。 - Python 3.7 以降でサポートされている
from __future__ import annotations
の使用法は、型注釈に使用され、クラスの定義順序を緩和し、後で定義されたクラスを前で型注釈として使用できます。
- Python 3.6 以降、組み込み型
- プログラミング習慣:
- 定数はできるだけ統一されたファイルに置くようにし、魔法の数字や魔法の文字列を避ける。
- 型注釈を多く使用し、変数の型を明確にし、静的なエラーを排除する。
- クラス内で
debug_mode
スイッチを定義し、デバッグ情報を印刷する。 - ユニットテストを行う。
- コード内の単体テストは
tester.py
を参照。
- コード内の単体テストは
- 外部メインプログラム main.py からデバッグするには?
- 外部メインプログラムに
create_cfg
メソッドを実装し、その中で文脈自由文法を定義および変更する。 - テスト用のプログラムテキストを準備する(例:test.c)。
- サンプル CFG 文法は test.c 内の
c_generators
を参照。
- サンプル CFG 文法は test.c 内の
- Tokenizer インスタンスを定義し、すべてのトークンを読み込む。
- Parser インスタンスを定義し、デバッグモードを開き、構造を初期化する。
- Tester インスタンスを使用してテストプロセスをカプセル化することができる。
- 出力を観察および分析し、実装を調整し続ける。
- 確認のために、出力をファイルにリダイレクトすることができる。
- 外部メインプログラムに
シンボル、生成規則、項、項集などのモデリング#
シンボル、生成規則、項、項集などのモデリングはgrammar.py
ファイルにあります。
- 文法シンボル ->
Symbol
クラス- メンバー変数
sType
: トークンのタイプ- 終端シンボルと非終端シンボルの 2 つのカテゴリに分類。
name
: トークンのタイプ- 数値定数、識別子などに分類。
- 例えば、識別子に対応する
name
はconstants
。
content
: トークンの文字列表現- 例:
int a;
の中でa
は識別子型で、属性name
は "constants"、属性content
は "a"。
- 例:
- メンバー関数
__str__
:文字列表現に変換し、印刷を容易にする。__hash__
と__eq__
:集合型の使用を容易にする。fromStr
:str
型からSymbol
型に変換し、外部で終端シンボルセットを指定する必要がある。
- 空シンボルはどう表現するか?
constants.py
に定数を定義し、ε
で表す。Symbol
型またはstr
で定義することができる。
- メンバー変数
- 生成規則 ->
Generator
クラス- メンバー変数
start
:生成規則の左側の非終端シンボル。end
: 生成規則の右側の文法シンボル列。
- メンバー関数
__str__
:文字列表現に変換し、印刷デバッグを容易にする。
ComposedGenerator
は何のためにあるのか?- 手動で CFG 文法を定義する際、文字列を書く方が
Symbol
を定義するよりもはるかに便利なので、ComposedGenerator
で変換の層を作成した。 - また、同じ生成規則の左側が複数の生成規則の右側に対応する場合(例:
A -> a | b
)、ComposedGenerator
内のメンバーend
の型注釈はlist[list[str]]
となり、"Composed" を示す。- 例:A -> a | b
ComposedGenerator
として定義すると{start: 'A', end: [['a'], ['b']]}
。Generator
に変換すると 2 つのGenerator
型のインスタンスが得られる。{start: 'A', end: ['a']}
{start: 'A', end: ['b']}
- 例:A -> a | b
- 手動で CFG 文法を定義する際、文字列を書く方が
- メンバー変数
- LR (1) 項 ->
Item
クラス- メンバー変数
start
: 生成規則の左側の非終端シンボル。end
: 生成規則の右側の文法シンボル列。point
: 生成規則内の点、読み込まれた位置を示す。lf_symbols
: 前方参照のシンボル、LR (1) は 1 つだけ見る。
- メンバー関数
__hash__
と__eq__
:インスタンスを区別し、集合型の使用を容易にする。__str__
:文字列表現に変換し、印刷を容易にする。- ここでは生成規則が空である場合を特別に扱っている。
- メンバー変数
- LR (1) 項集 ->
ItemSet
クラス- メンバー変数
_items
:すべての項。
- メンバー関数
add
:項を項集に追加し、重複は自動的に無視される。union
:他のItemSet
型と統合する。toList
: リストに変換する。- 後続の LR オートマトンの状態を数字でインデックスするのに便利。
__hash__
と__eq__
:インスタンスを区別し、集合型の使用を容易にする。__iter__
: 内部イテレータを返し、for
ループでの反復に使用する。__len__
: サイズを返す。Sized
を継承する必要がある。
- なぜ項集をモデリングする必要があるのか?
set(Item)
を直接使用することはできないのか?- いい質問です、最初は私もそう思いました。
- 問題は LR (1) 標準項集族を求めるときに発生します。
- 新しい項集が追加されていないかを判断する必要があり、最初の反応は
set(Item)
の外側にもう一層重ねること、set(set(Item))
です。 - しかし、
set(Item)
には偏序と等価のデフォルト定義がなく、複合型です。- "複合型" についてはプログラミング原本 [2] 第十二章を参照。
- したがって、項集のために手動で偏序と等価を定義する必要があります。
- 新しい項集が追加されていないかを判断する必要があり、最初の反応は
- また、印刷デバッグなどの要件があるため、項集をモデリングする必要があります。
- メンバー変数
- 文脈自由文法 (CFG) ->
GrammarCFG
クラスstart_mark
: 開始シンボルのリテラル。end_mark
: 終端シンボル。- 一般的にはスタックの底の '$'。
end_symbols
: 終端シンボルの集合。generators
: 生成規則の集合。- ここから非終端シンボルの集合を得ることができる。
FIRST 集合と FOLLOW 集合の求め方の実装#
このステップから Parser の実装を開始し、対応する参考実装はparser.py
にあり、対応するドラゴンブックのページ番号はコードに詳細な注釈があります。
- 参考コード中の関連関数
get_first
get_chain_first
(文法シンボル列の first を求める)get_follow
_get_follow_once
_get_follow_all
- 参考コード中の関連メンバー変数
self._map_follow
self._map_first
- なぜ最初に first 集合と follow 集合を求めるのか?
- first 集合と follow 集合を求めることは後続のアルゴリズムの基礎です。
- さらに、文法をモデリングしているので、ドラゴンブックのプロセスに従って実装すればよく、文法シンボルと生成規則に関わるだけです。
- 簡単な操作から始めて、繰り返しデバッグすることで、プログラミングの状態を徐々に見つけることができます。
- どのように求めるべきか?
- first 集合を求めるのに特に問題はなく、関数の呼び出しプロセスは木構造です。
- 例えば:
A -> B
とB -> b
の場合、first(A)
を求めるためにfirst(B)
を呼び出します。first(B)
を求めると{b}
が得られます。
- 例えば:
- しかし、follow 集合を求める場合、関数の呼び出しプロセスには環があります。
- 例えば:
A -> aB
とB -> bA | b
の場合、- follow 集合の第三のルールに基づいて、
follow(A)
を求めると、follow(A)
はすべてfollow(B)
にあります。follow(B)
を求めると、follow(B)
はすべてfollow(A)
にあります。
- 参考実装の解決方法は比較的単純で粗暴です。
- すべての非終端シンボルの follow 集合を事前に計算し、
dict
に保存してグローバル変数self._map_follow
にします。 - すべての非終端シンボルの follow 集合をスキャンし続け、新しい終端シンボルが追加されなくなるまで繰り返します。
- 判断方法は、例えば 2 つの非終端シンボルの follow 集合を統合し、
len
に変化があるかどうかを確認するなどです。
- 判断方法は、例えば 2 つの非終端シンボルの follow 集合を統合し、
- すべての非終端シンボルの follow 集合を事前に計算し、
- より良い実装を探してみることができます。
- 例えば:
- first 集合を求めるのに特に問題はなく、関数の呼び出しプロセスは木構造です。
CLOSURE と GOTO の求め方の実装#
first 集合と follow 集合を求めるツールができたので、次のステップは LR (1) 項に関連するアルゴリズムに取り組むことができます。対応する参考実装はparser.py
にあり、対応するドラゴンブックのページ番号はコードに詳細な注釈があります。
- 参考コード中の関連関数
get_closure
get_goto
- 書籍で言及されていない部分は?
- 依存順序に従って、まず closure を実装し、その後 goto を実装します。
- closure 集
- 重複を避けることに注意が必要です。
- 一度の遍歴で新しく追加された項を別に保存し、次回は新しく追加された項だけを遍歴します。
- 新しく追加された項を最後に加えることに注意し、イテレータの無効化を防ぎます。
- 参考コード中の私の実装は非常に劣っており、今思うと偶然動いているだけで、当時酔っていた可能性があるため、参考にしないことをお勧めします。
- goto 集には特に問題はありません。
- 次のシンボルが X で、点を移動させるだけで追加できます。
- 最後に closure を求めて返します。
LR (1) 項集の求め方の実装#
拡張文法の最初の生成規則から closure を求め始め、goto ができたので LR (1) 項集を求めることができます。goto を求め続け、新しい項集が追加されなくなるまで繰り返します。対応する参考実装はparser.py
にあり、対応するドラゴンブックのページ番号はコードに詳細な注釈があります。
- follow 集合を求めるのと似た方法で「新しい項集が追加されていない」ことを判断できます。
- 参考コード中の関連関数
get_lr1_items
- このステップを順調に完了すると、goto のプロセスを印刷し、各項集を状態として見ることで、実際に LR (1) 項集のオートマトンを完成させたことがわかります。このステップで既に成功の半分を達成しています。
LR (1) の解析表の実装#
- 参考コード中の関連関数
get_lr
- 必要なこと
- アクションをモデリングし、解析表に埋め込む内容とする。
- メンバーには以下を含める必要があります。
- 実行するアクションのタイプ。
- アクションを実行するために必要な情報。
- 例えば:
- どの生成規則を使って還元するか。
- 次にどの状態に進むか。
- 例えば:
- メンバーには以下を含める必要があります。
- LR (1) 解析表をモデリングし、1 つずつ埋め込む。
- 対応する
LRSheet
。 - 一次元は状態のインデックス、一次元はシンボル。
- 対応する
- 標準 LR (1) 項集族をすべての状態として使用する。
- アクションをモデリングし、解析表に埋め込む内容とする。
- 参考コードのようにルールに従ってアクションを 1 つずつ埋め込むことも、まとめてアクションを埋め込むこともできます。
- 埋め込む過程で 1 つのセルに 2 回埋めた場合は?
- 実装にエラーがあります。
- 文法が LR (1) 文法ではありません。
- その他
- 参考コード中、空生成規則の還元はエラー後にスタックのトップに空シンボルを試して置くことで実現されています。
LR (1) の構造の実装#
- 構造をモデリングする。
- すべての入力が既知であると仮定する。
- ここでは 158 ページ [1] の定義に厳密には従っていません。
- スタック内の入力と残りの入力に加え、スタック内の状態インデックスもあり、下から上への処理過程をデバッグおよび観察しやすくします。
- 参考コード中では、1 ステップのアクションを実行することも構造に組み込まれており、状態を印刷しやすくしています。
- このステップで基本的に完成します。
簡単な SDD 構文導出解析の追加#
ここでの SDD は S 属性の SDD に限られ、create_cfg()
内で再帰的なサブルーチンの形式で定義され、アクションが生成規則の還元操作のときにのみトリガーされます。生成規則の中間やアクションが移入操作のときにトリガーされるように自分で修正してみることができます。
- SDD を追加するための手順は以下の通りです:
- Symbol などのモデリングを拡張し、さまざまな意味属性を持たせる。
- 簡単な型宣言を実装する。
- 簡単な式評価を実装する。
- 簡単な型チェックルールを実装する。
- 制御フローの SDD を実装する。
- 注意
- Python 内部ではオブジェクトは浅いコピーであるため、
forward
で意味アクションを実行する前に深いコピーを行い、他のSymbol
インスタンスの SDD を上書きしないようにします。
- Python 内部ではオブジェクトは浅いコピーであるため、
- どのように拡張するか?
- 再帰的なサブルーチンの形式を定めます。例えば:
- パラメータリストはすべて
(input_symbols: list[grammar.Symbol], reduction_symbol: grammar.Symbol, st: parser.SymbolTable)
。 - 属性の名前を
constants.py
で定義します。- これは定数を統一して定義する利点を反映しています。
- リテラルを何度も手打ちする必要がなく、誤字が発生しやすく、予期しないバグを引き起こす可能性があります。
- パラメータリストはすべて
- 各生成規則を定義する際に、関数を再帰的なサブルーチンとしてバインドします。
Item
クラスにもこのパラメータを追加します。- Parser クラスのさまざまなメソッドで新しい
Item
インスタンスを作成する際に、バインドされた関数も一緒に渡します。 forward
で 1 ステップの還元アクションを実行する際に意味アクションを実行します。- 詳細は参考コードの Git 差分を参照してください。
- 再帰的なサブルーチンの形式を定めます。例えば:
存在する問題#
- モデリングの初期段階で
Symbol
の等価性の問題を十分に考慮しなかった。- 例えば:
a
とb
は生成規則内でどちらもidentifier
であるため、それらを比較すると等しい結果が得られる。 - 最終的には生成規則内の表現のみを比較し、文字列の内容は比較しないことにした。
- 例えば:
- なぜ
sType
とname
の 2 つが分かれているのか?両方ともトークンタイプを表しているのに、統合するのは良くないのか?- 当初、これら 2 つを分けたのはインターフェースとの接続の問題があったため。
- トークナイザー部分のタイプは終端シンボルと非終端シンボルに分けられておらず、5 つのカテゴリに細分化されていた。
- 最後に外部で CFG 文法を定義する際に再度終端シンボルと非終端シンボルに分けたため、処理が確かに良くなかった。トークナイザーで分けるべきだった。
- Parser 内部のいくつかの関数は公開されている。
- 単体テストを容易にするためだが、あまり整然としていない。
if-else
の処理がうまくいっておらず、特別な空の else を使用している。- 実際には文法定義で解決できるため、ドラゴンブック 134 ページを参照。
- 真のパニックモードを実装しておらず、必要なトークンが現れるまで単に捨てているだけ。
- 実装の効率性の問題を考慮していない。
- 例えば、follow 集合と LR (1) 標準項集族を求める際、新しい項または項集が追加されていないかを判断する操作は非常に単純で粗暴である。
- SDD、三アドレスコードの実装が不規則。
constants.py
と外部のcreate_cfg
の結合度が高い。ItemSet
は変更後のハッシュを求める問題を十分に考慮していない。- closure を求める際の重複排除の実装が良くない。
- 例えば、
cur_items
を使用して重複を排除するのではなく、f_list
を使用して重複を排除している。 f_list
は前方参照のシンボルと生成規則の右側を混在させており、潜在的なバグがある。
- 例えば、
参考資料#
- [1] コンパイラ原理 第 2 版(Compilers: Principles, Techniques and Tools, Second Edition)、(米)Alfred, V.A. など著;赵建华など訳、—— 北京:機械工業出版社、2009.1
- [2] プログラミング原本(Elements of Programming)、(米)Stepanov, A.、(米)McJones, P. 著;裘宗燕訳。—— 北京:機械工業出版社、2011.12