言語理論とコンパイラ
第七回:
下向き構文解析
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
    がグローバル変数だが、グローバル変数なしの実装も可能 
来週への宿題