静的型付け言語における 汎用的なユーザ定義演算子を含む式の 構文解析手法 11M37011 市川 和央 千葉研究室 言語内DSL • 言語の中に作った小さな言語 • 目的に応じて最適な文法の言語を利用 • データベースはSQL風の文法、構文解析はBNFなど • より細粒度な例: Mapは連想配列、文字列のマッチは正規表現 • 実現には言語拡張が必要 • もとの言語とは異なる字句規則 • もとの言語では解析できない構文 言語内DSLの例 • SQL + ユニットテスト + 正規表現 in Java • (注) 普通の Java では書けない SQL @Test public void studentNumberTest() { ResultSet ids = select id from students; for (String id : ids.toList()) { assert id matches [0-9]{2}(B|M|D)[0-9]{5}; } ユニットテス } 正規表現 ト ユーザ定義演算子 • 演算子として新しい構文を導入する構文拡張手法 • ユーザ定義二項演算子: Scala など • mixfix operators: Agda など • 静的型付け言語では composable • 似た構文を持つ演算子を一緒に使っても安全 • 型によるオーバーロードを持つため • 表現力が弱い • 特定の形式を持つ演算子しか導入できない • 字句レベルの拡張はできない mixfix operators • infix, prefix, postfix, outfix 演算子の総称 • infix : _ op1 _ op2 _ ... _ opn _ • prefix : op1 _ op2 _ ... _ opn _ • postfix: _ op1 _ op2 _ ... _ opn • outfix : op1 _ op2 _ ... _ opn _ はオペランドに対応 : hole と呼ぶ opi はオペレータに対応 : name part と呼ぶ SQL の select _ from _ mixfix operators の限界 • infix, prefix, postfix, outfix 以外は表現できない • 連続するオペランドを含む構文は表現できない • 字句規則は変えられない • ユーザ定義リテラルは作れない 正規表現リテラルとして解析する必要 for (String id : ids.toList()) { assert id matches [0-9]{2}(B|M|D)[0-9]{5}; } assert _ _ は mixfix に含まれない 提案: protean operators • 演算子のパターンは hole と name part の並び • assert _ _ のような演算子を含む • mixfix operators を包含 • 演算子ごとに異なる字句規則 • 正規表現の演算子は正規表現のルールを持つ • ユーザ定義リテラルも表現可能 ナイーブな解析手法では時間がかかりすぎる 提案: 期待される型を利用した再帰下降構文解析 • 字句・構文・型解析器が連携 • 期待される型の情報を構文解析器にフィードバック • 利用する演算子によって字句規則を変える • 型のあわない演算子は解析に使わない • 期待される型の情報から演算子を特定 • 不要な解析パスを枝刈り • メモ化を利用してバックトラックのコストを最小化 • 左再帰を許す packrat parsing を利用 具体例 assert id matches [0-9]{2}(B|M|D)[0-9]{5}; void 型を期待 => assert _ _ で解析 assert id matches [0-9]{2}(B|M|D)[0-9]{5}; String 型を期待 => String 型の変数 assert id matches [0-9]{2}(B|M|D)[0-9]{5}; Matcher 型を期待 => matches _ で解析 assert id matches [0-9]{2}(B|M|D)[0-9]{5}; Regex 型を期待 => 正規表現の演算子で解析 解析手順 1. 型のわからない部分は通常のルールで解析 • 代入文の左辺など 2. 型がわかる部分まで解析したら型解析 3. 期待される型を特定 4. 期待される型を返す演算子を使って構文解析 • name part は対応するトークンを読み込む字句解析 • hole は対応する型を期待する型として再帰的に解析 演算子の解析順序 • 同じ型を返す演算子の間には順序関係が必要 • どちらの演算子が special case であるか • if _ then _ より if _ then _ else _ を優先 • + と * の間の演算子優先順位とは異なる • 構文解析器はこの順序に従って解析を試す • 成功するまでバックトラックを繰り返す • 成功した時点で終了 スライド中ではコードの上にある方を優先するものとする 演算子優先順位・結合性 • コンパイル時の型の書き換えにより実現 • 優先順位を含む型を作る • 結合性に従って型情報を書き換える _:Int + _:Int :: Int (優先順位 2, 左結合) _:Int * _:Int :: Int (優先順位 1, 左結合) 引数の型 _:Int2 + _:Int1 :: Int2 _:Int1 * _:Int0 :: Int1 _:Int2 :: Int _:Int1 :: Int2 _:Int0 :: Int1 返り値の型 サブタイプ関係 • 演算子の追加により実現 • サブタイプをスーパータイプに関連付け • 演算子優先順位を考慮 Int <: Num _:Int + _:Int :: Int (優先順位 2, 左結合) _:Int * _:Int :: Int (優先順位 1, 左結合) _:Int2 + _:Int1 :: Int2 _:Int1 * _:Int0 :: Int1 _:Int2 :: Int _:Int1 :: Int2 _:Int0 :: Int1 _:Int2 :: Num2 _:Int1 :: Num1 _:Int0 :: Num0 _:Num2 :: Num _:Num1 :: Num2 _:Num0 :: Num1 実装: ProteaJ • Java 1.4 に protean operators を導入した言語 • コンパイラを Java で実装 • http://www.csg.ci.i.u-tokyo.ac.jp/~ichikawa/ProteaJ.tar.gz • 演算子モジュールと呼ばれるモジュール機構を持つ • using 節によりインポートして利用できる • 各モジュールが言語内DSLを表現 議論: protean operators の解析の難しさ • 複数のDSLの利用は文法規則の曖昧性を発生させる • 曖昧性は型チェックで解決する必要 • 文法の非決定性も多数発生 • 字句規則の追加は特に曖昧性を発生させやすい • 例: e には 変数、自然対数の底、正規表現リテラル等の解釈がある • 字句規則は衝突が起こりやすい ナイーブな解析手法 • 字句・構文解析でありうる構文木を全て列挙 • それぞれの構文木に対して型チェック ナイーブな手法の計算量 • 構文木の列挙にかかるコストは O(|G|*n3) • |G| は文法サイズ、n は入力長 • 字句・構文規則の追加は |G| を大きくする • n = 文字数なので、 n3 は非常に大きくなる • 型チェックすべき構文木の数は爆発しやすい • 多くの部分木に影響する曖昧性があると構文木の数が増えやすい • 字句規則は多くの部分木に影響しやすい DSL の composability が低い 本手法の計算量 • 本手法はほとんどの場合に O(n) で解析可能 • 解析コストは通常 |G| に依らない • 期待される型で演算子を限定するため • 同じ型を返す演算子の個数に比例するが、通常これは十分小さい • 左再帰を許す packrat parsing は大抵の場合に O(n) • 現実的な文法では O(n) となることが知られている 関連研究: 構文マクロ • ポピュラーな言語拡張手法の1つ • Lisp, Template Haskell, Nemerle, Boo, ScalaMacros, ... • コンパイル時に構文木を書き換える • もとの言語と異なるセマンティクスを与えることができる • 構文規則は変えられない • composable でない • 同じ構文木に対して複数のマクロを定義すると正しく動作しない 関連研究: リードマクロ • 強力な言語拡張手法の1つ • Common Lisp, Template Haskell, Converge, ... • 特定の構文内では字句・構文解析器を切り替える • 字句・構文規則は自由 • 特殊な構文を必要とする • composable でない • リードマクロ中で別のリードマクロを使えない可能性 関連研究: ユーザ定義の構文 • Isabelle, OBJ3 • _ _ のような演算子を定義可能なプログラミング言語 • 字句規則は変えることができない • ナイーブな解析手法であるため composability が低い • Nemerle • ユーザ定義の構文を定義可能なプログラミング言語 • 構文の最初の1単語はユニークな識別子でなければならない 関連研究: 構文解析手法 • Metaborg [Bravenboerら OOPSLA '04] • 構文規則だけでなく字句規則の拡張も許す • ナイーブな解析手法であるため composability が低い • Type-oriented island parser [Silkensenら '12] • 部分木に対して型チェックしつつボトムアップ解析 • 構文解析コストが |G| に比例しない • 字句規則は変えることができない まとめ • 期待される型を利用した再帰下降構文解析を提案 • 型情報を利用して解析パスを枝刈り • ユーザ定義の protean operators を解析可能 • ほとんどの場合に O(n) で解析できる • protean operators • hole と name part の並びで表現される演算子 • 演算子ごとに異なる字句規則を持つ 今後の課題 • ジェネリクスや期待される型の推論 • 代入文を _:TypeName[T] _:Id = _:T のような演算子で表現したい • ローカル変数宣言などのメタレベルの表現 • 演算子にメタレベルの挙動を持たせたい 発表など • PPL2011 ポスター発表 • 日本ソフトウェア科学会 第28回大会 口頭発表 • ECOOP 2011 ポスター発表 「ユーザ定義演算子による内部DSLの構成 • PPL2012 ポスター発表 法」
© Copyright 2024 ExpyDoc