240 likes | 398 Views
静的型付け言語における 汎用的なユーザ定義演算子を 含む 式 の 構文 解析手法. 11M37011 市川 和央 千葉研究室. 言語内 DSL. 言語の中に作った小さな言語 目的に応じて最適な文法の言語を利用 データベースは SQL 風の文法、構文解析は BNF など より細粒度な例 : Map は連想配列、文字列のマッチは正規表現 実現には言語拡張が必要 もとの言語とは異なる字句規則 もとの言語では解析できない構文. 言語内 DSL の例. SQL + ユニットテスト + 正規表現 in Java ( 注 ) 普通の Java では書けない.
E N D
静的型付け言語における汎用的なユーザ定義演算子を含む式の構文解析手法静的型付け言語における汎用的なユーザ定義演算子を含む式の構文解析手法 11M37011 市川和央 千葉研究室
言語内DSL • 言語の中に作った小さな言語 • 目的に応じて最適な文法の言語を利用 • データベースはSQL風の文法、構文解析はBNFなど • より細粒度な例: Mapは連想配列、文字列のマッチは正規表現 • 実現には言語拡張が必要 • もとの言語とは異なる字句規則 • もとの言語では解析できない構文
言語内DSLの例 • SQL + ユニットテスト + 正規表現 in Java • (注) 普通のJava では書けない SQL @Test public void studentNumberTest() { ResultSetids = 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 型の変数 assertid matches [0-9]{2}(B|M|D)[0-9]{5}; Matcher 型を期待 => matches _ で解析 assertidmatches [0-9]{2}(B|M|D)[0-9]{5}; Regex 型を期待 => 正規表現の演算子で解析
解析手順 • 型のわからない部分は通常のルールで解析 • 代入文の左辺など • 型がわかる部分まで解析したら型解析 • 期待される型を特定 • 期待される型を返す演算子を使って構文解析 • 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 :: Num2 _:Int1 :: Num1 _:Int0 :: Num0 _:Num2 :: Num _:Num1 :: Num2 _:Num0 :: Num1 _:Int2 + _:Int1 :: Int2 _:Int1 * _:Int0 :: Int1 _:Int2 :: Int _:Int1 :: Int2 _:Int0 :: Int1
実装: 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 のような演算子で表現したい • ローカル変数宣言などのメタレベルの表現 • 演算子にメタレベルの挙動を持たせたい • 日本ソフトウェア科学会第28回大会口頭発表 「ユーザ定義演算子による内部DSLの構成法」 • PPL2011 ポスター発表 • ECOOP 2011 ポスター発表 • PPL2012 ポスター発表