Transcript Get Slide

静的型付け言語における
汎用的なユーザ定義演算子を含む式の
構文解析手法
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 ポスター発表
法」