言語理論とコンパイラ
第七回:
下向き構文解析
2011 年 5月 27日
http://www.sw.it.aoyama.ac.jp/2011/Compiler/lecture7.html
Martin J. Dürst
© 2005-11 Martin
J. Dürst 青山学院大学
目次
- 前回のまとめと宿題
- 文法の種類、例、作り方
- 構文解析の要点
- 解析木と構文木
- 上向き構文解析と下向き構文解析
- 下向き構文解析
- 再帰的下向き構文解析とその例
前回のまとめ
- 正規表現やそれと同等の表現力を持つツールには限界
(例: 括弧の入れ子構造など)
- 正規表現などは構文解析には使えないが、字句解析には有効
- コンパイラで字句解析と構文解析を分けると構造が鮮明、実行が高速
- (プログラム)
言語などの文法は文脈自由文法で記述可能
- 文脈自由文法はプッシュダウンオートマトンで受理可能
- プッシュダウンオートマトンでは決定性より非決定性のものが強力だが、実装は複雑で遅い
前々回からの宿題: Flex
前回の宿題
C
プログラム言語など知っている言語やデータ形式の文法を調べなさい
(提出不要):
C プログラム言語の文法の例 (yacc
形式; typedef
に要注意)
Java プログラム言語の文法
(BNF 形式)
Ruby プログラム言語の文法
(図)
文法規則と BNF
文法規則には色々な書き方がある:
- 一番単純な書き方: 矢印だけ
- 左側に同じ被終端記号を持つものを複数組み合わせて
|
で選択を表す
⇒ 根本的に 1. と変わらない (syntactic sugar/糖衣構文)
- 正規表現の
?
の様なもの (あり/無し) の追加
(よく {
...}
[
...]
で書く)
⇒ 二つの構文規則に分けることが可能
- 正規表現の
*
の様なもの (よく
[
...]
{
...}
で書く)
⇒ 書き換えが可能
上記の拡張を含む文法規則の書き方は BNF (Backus-Naur Form),
EBNF (Extended...), ABNF (Augmented...) などという
EBNF の書き換え
M → a [{N}] b
⇒
M → a b | a NList b
NList → N | NList N
文法の記述の種類
単純な文法
- 例: C
の文法
- メタ記号は
→
/:
と |
だけ
- 理論で使う文法規則と同じ
(A|E)BNF
- 例: Java
の文法
- メタ記号は
→
/:
(Java
の文法では改行)、
|
、{}
、[]
など (Java
の文法では斜体と正体の差に注目)
- 書き換え規則の右側に正規表現みたいなもの
文法の作成
- 言語の簡単な実例を記述
- 実例をトークンの種類に変更 (字句解析の結果)
- 実例の現象を命名 (例: ...式、...文など)
- 書き換え規則の案
- もうちょっと複雑な例で使えるかどうかを検討、修正
文法作成の例
- 目的:
5 + 3 * (7 + 2)
のような式のための文法
構文解析の目的
- 入力の受理・非受理の判定
- 入力の処理:
- エラーの分かりやすい報告
構文解析の難しさ
- 決定性と非決定性のプッシュダウンオートマトンの受理能力が違う
- 一般の文脈自由文法を受理できるアルゴリズムは
O(n3)
- O(n)
のアルゴリズムが好ましいが、そのために文法の制限が必要
- 構文解析の研究では長年、様々な解析方法が研究されてきた:
- 人間に分かりやすい文法
- 制限が少ない文法
- 手作業で解析機が実装できる文法
- 自動で解析機が実装できる文法
構文解析の結果: 解析木と構文木
- 解析木 (parse tree, concrete syntax tree):
- 葉は終端記号と (分析の途中の場合)
被終端記号
- 節は非終端記号
- 文法や解析方法の研究に使われる
- 構文木 (抽象構文木、abstract syntax tree):
- 葉は終端記号の一部 (識別子、定数など)
- 節は被終端記号の一部に相当するが、終端記号の一部
(演算子、予約語) などをラベルに使う
- 一部の終端記号は無視 (括弧類など)
解析木と構文木の例
文法:
E → E '+' E
(Expression, 式)
E → E '*' E
E → '(' E ')'
E → integer
入力の例:
5 + 3 * (7 + 2)
解析の実装: 下向き解析と上向き解析
- 下向き構文解析 (top-down parsing):
- 解析木を上から (初期記号から) 作る
- 上向き構文解析 (bottom-up parsing):
- 解析木を下から (終端記号から) 作る
- 途中に複数の (小さな) 解析木がある
下向き解析の一般概要
- 文法を初期記号から展開する
- 選択肢があれば順番に試してみる
- 終端記号まで展開したらこれを入力と比べる
- 合ったら続く
- 合わなかったら戻って
(バックトラック、backtracking)、違う選択を試す
バックトラックの要点
バックトラックの遅さへの対策:
- 文法の書き方に注意:
バックトラックが少ないように文法を書きかえる
- 途中の結果の記憶 (packrat parser)
次のトークンしか見なくてよい文法に限定したい
再帰的下向き構文解析
- 非終端記号ごとに関数一つを作成
- 文法に再帰的な要素がある
例 (代入式/assignment operation): A → variable '=' A |
integer
下向き構文解析の実装: 簡単な手作りコンパイラ
プログラム: scanner.h, scanner.c, parser1.c
手造り下向き構文解析の詳細: 字句解析
(scanner.c 参照)
- 不変条件 (invariant):
- 次に検討すべき文字が
nextChar
に保持
(one-character lookahead)
- 文字の消化後、すぐ次の文字を
nextChar
に読み込む
- 構文解析からの使い方:
initScanner
で初期化
getNextToken
で次々とトーケンを読む
nextChar
がグローバル変数だが、グローバル変数なしの実装も可能
getNextToken
の実装:
- 一文字のトークンはその場で判断
- 複数文字のトークンは一文字目で判断、専用のの関数で読み終わる
手造り下向き構文解析の詳細
(parser1.c 参照)
- 不変条件 (invariant):
- 次に検討すべきトークンが
nextToken
に保持
(one-token lookahead)
- トークンの消化後、すぐ次のトークンを
nextToken
に読み込む
- 全体的な使い方:
initScanner
と getNextToken
で準備
- 文法の初期記号に相当する関数 (例:
Expression()
) を呼び出す
- その結果 (構文木又は評価結果) をさらに処理
nextToken
がグローバル変数だが、グローバル変数なしの実装も可能
来週への宿題