Upload
yak1ex
View
2.960
Download
1
Embed Size (px)
DESCRIPTION
For Boost Study Meeting #5 at Nagoya
Citation preview
非実用的 Boost Spirit Qi 入門
2011/5/14 Boost 勉強会名古屋
@yak_ex / 新康孝
自己紹介
• 氏名: 新康孝 (あたらし やすたか)
• Twitter ID: yak_ex
• Web: http://yak3.myhome.cx:8080/junks
• C++ / Perl が主戦場
• 現在、仕事でコードに触れていないので競技プログラミング(TopCoder、Codeforces)で潤い補充
• 闇の軍団に憧れるただの C++ 好き– 初めて Spirit 使った経験を元に発表
Spirit との馴れ初め
• PDF 用 Susie プラグインがライセンスの関係でずっと公開停止になってる
→ライセンスの問題がない PDF Susie プラグインを作ろう!
→最低限、画像だけ抜いて来られればいいや
→PDF フォーマットは割とテキストベース
→構文解析が必要
※プラグインは axpdf--.spi β版として公開中
• せっかくだから俺はこの Spirit を選ぶぜ!→ Spirit を初めて使用した経験を元に発表
Boost Spirit とは?
• Boost 公式サイトの記述:
– LL parser framework represents parsers
directly as EBNF grammars in inlined C++
–構文解析器を、C++ 内で直接 EBNF 文法を書く事で作れるフレームワーク
• はぁ?
Boost Spirit とは?
• 世間での評判
–変態なことで有名なBoost::Spiritを…http://zo3kirin3.net/?p=82
–変態的と名高い(?) Boost.Spirit で解析。http://ja.doukaku.org/comment/6518/
– …同じく変態(褒め言葉)と名高いboost::spirit を使って実装することにした。http://d.hatena.ne.jp/Hossy/20080407
結論: Boost Spirit =変態
これが Spirit の力だ!• // #include と using namespace 省略
int main(void){typedef std::map<std::string, std::string> Config;Config config;
std::string input("Boost.Spirit = extraordinary ¥n C++er = ...");phrase_parse(input.begin(), input.end(),
*(lexeme[*(graph - char_('='))] >> lit('=') >> lexeme[*graph]),space, config);
BOOST_FOREACH(Config::value_type &kv, config) {std::cout << kv.first << '=' << kv.second << std::endl;
}
}
• 出力Boost.Spirit=extraordinaryC++er=...
<key>=<value>が std::map に突っ込まれる
型と出力変数定義
入力定義
出力
へ ん た い
高度に発達したC++は魔法と区別がつかない
アーサー・C++・クラーク
Boost Spirit とは?
• Boost ライブラリの中でも最高峰の一つ(超私見)
– Optional, Variant, Fusion, Proto, Phoenix, MPL 等、他の Boost ライブラリをふんだんに使用
– C++ でできることのベンチマーク的位置づけ• どんなものか知っとくだけでも意味はあるかと
• Qi, Karma, Lex で構成
– Qi: 構文解析(文字列→データ構造)
– Karma: 出力(データ構造→文字列)
– Lex: 字句解析(文字列→トークン列)
←今回のテーマ
Spirit Qi 入門
Tutorial嫁
アジェンダ
• 概要紹介
–限定された使い方のみ
• 拡張性
– Directive を自作してみる
– Customization point
• 蛇足
Boost Spirit とは?
• Boost 公式サイト:
– LL parser framework represents parsers
directly as EBNF grammars in inlined C++
–構文解析器を、C++ 内で直接 EBNF 文法を書く事で作れるフレームワーク
構文解析と EBNF
• 構文解析(文字列→データ構造)
–あるルールに則った文字列を解析例: 式
1 0 + 2 0 * ( 3 0 - 4 0 )
10 20 30 40
-*
+
構文解析と EBNF
• EBNF = Extended Backus Normal Form
– 「あるルール」=文法の表記方法
• 選択 |
• 0回以上の繰り返し *、1回以上の繰り返し +
–例: 式
• <Expression> ::= <Term> ((„+‟ | „-‟ ) <Term>)*
• <Term> ::= <Factor> ((„*‟ | „/‟ ) <Factor>)*
• <Factor> ::= <Integer> | „(„ <Expression> „)‟
Boost Spirit の rule
• EBNF による式の表現
– <Expression> ::= <Term> ((„+‟ | „-‟ ) <Term>)*
– <Term> ::= <Factor> ((„*‟ | „/‟ ) <Factor>)*
– <Factor> ::= <Integer> | „(„ <Expression> „)‟
C++ の式として有効• Spirit での式の表現– expr = term >> *(char_(“+-”) >> term);
– term = factor >> *(char_(“*/”) >> factor);
– factor = int_ | lit(„(„) >> expr >> lit(„)‟);
Boost Spirit の rule
• 構成要素
– Parser(基本要素)
– Directive(修飾)
– Operator(結合)
Boost Spirit の rule
• Parser(基本要素)
char_ 任意の1文字を読む
char_(„a‟, „b‟) a~bの範囲の1文字を読む
char_(“abc”) abc いずれか1文字を読む
int_ 整数値を読む
lit(„a‟) a 1文字を読む
lit(“abc”) 文字列 abc を読む
graph isgraph() が true な1文字を読む
etc.
Boost Spirit の rule
• Directive (修飾:Parser の挙動を変える)
lexeme[p] p 先頭で空白をスキップ、p 内部では空白をスキップしない
no_case[p] p 内部で大文字、小文字を区別しない
repeat(N)[p] p の N 回の繰り返し
repeat(N,M)[p] p の N~M 回の繰り返し
repeat(N,inf)[p] p の N 回以上の繰り返し
etc.
Boost Spirit の rule
• Operator (結合)
a >> b 連接(普通に繋げる)
a | b 選択(a あるいは b)
*a p の 0 回以上の繰り返し
+a p の 1 回以上の繰り返し
-a p が 0 or 1 回
a - b b でない a
a % b b で区切られた a の繰り返し
etc.
これが Spirit の力だ!• // #include と using namespace 省略
int main(void){typedef std::map<std::string, std::string> Config;Config config;
std::string input("Boost.Spirit = extraordinary ¥n C++er = ...");phrase_parse(input.begin(), input.end(),
*(lexeme[*(graph - char_('='))] >> lit('=') >> lexeme[*graph]),space, config);
BOOST_FOREACH(Config::value_type &kv, config) {std::cout << kv.first << '=' << kv.second << std::endl;
}
}
• 出力Boost.Spirit=extraordinaryC++er=...
<key>=<value>が std::map に突っ込まれる
へ ん た い
Boost Spirit の rule
• *(
lexeme[
*(
graph - char_('=')
)]
>> lit('=')
>> lexeme[
*graph
])
0回以上の繰り返し
内部スキップなし
0回以上の繰り返し
= 以外の表示文字
=
内部スキップなし
表示文字の 0 回以上の繰り返し
<key>=<value>の繰り返し
で、出力は?
で、出力は?• Parser には「属性」があってその属性型の値を返す
– int_ → int / char_ → char
• 修飾、結合されたものは?
→Fusion シーケンス or STL コンテナが属性になる– char_ >> int_ >> double_
→ tuple<char, int, double>
– *int_ → vector<int>
– int_ >> int_ は?→ どっちでもOK
※tuple, vector は代表で Fusion シーケンス / コンテナなら何でも良い
※詳細は「Quick Reference」 の「Compound Attribute Rules」を参照
で、出力は?• pair はアダプタによって Fusion シーケンスと見なせる
(ヘッダの #include が必要)
• 構造体も Fusion シーケンスと見なせる– BOOST_FUSION_ADAPT_STRUCT
– BOOST_FUSION_DEFINE_STRUCT で構造体定義とADAPT を一気にできる
※Fusion は前回勉強会の cpp_akira さんの発表も参照
BOOST_FUSION_DEFINE_STRUCT(
(yak)(pdf),
indirect_ref,
(int, number)
(int, generation)
)
構造体 yak::pdf::indirect_ref がtuple<int, int> 相当になる
これが Spirit の力だ!• // #include と using namespace 省略
int main(void){typedef std::map<std::string, std::string> Config;Config config;
std::string input("Boost.Spirit = extraordinary ¥n C++er = ...");phrase_parse(input.begin(), input.end(),
*(lexeme[*(graph - char_('='))] >> lit('=') >> lexeme[*graph]),space, config);
BOOST_FOREACH(Config::value_type &kv, config) {std::cout << kv.first << '=' << kv.second << std::endl;
}
}
• 出力Boost.Spirit=extraordinaryC++er=...
<key>=<value>が std::map に突っ込まれる
へ ん た い
出力先
• lexeme は skip の仕方が変わるだけで属性は変化しないので消す
→ lit は無属性(unused_type)、graph, char_ はchar、pa – pb は pa の属性
→ vector<tuple<vector<char>, vector<char> > > or vector<vector<char> >
※vector<char> とstring は互換→これらも互換
*(*(graph - char_('=')) >> lit('=') >> *graph)
*(*char >> *char)
• std::map<std::string, std::string>
→ std::pair<std::string, std::string> のコンテナ
→ std::string, std::string の Fusion シーケンスのコンテナ
Ruleの属性
代入先
*(lexeme[*(graph - char_('='))] >> lit('=') >> lexeme[*graph])
Spirit.Qi 入門まだ Spirit の変態フェイズは
終了してないぜ!!
アジェンダ
• 概要紹介
–限定された使い方のみ
• 拡張性
– Directive を自作してみる
– Customization point
• 蛇足
拡張性
• Spirit (の変態性)を支える重要な要素
–後から拡張できるような仕掛けがしてある
• Proto を使っているので自前のコンポーネント(Parser
等)を作成できる→Directive の自作
• 処理にフックが用意してある→Customization point
Boost Spirit の rule (再掲)
• Directive (修飾:Parser の挙動を変える)
lexeme[p] p 先頭で空白をスキップ、p 内部では空白をスキップしない
no_case[p] p 内部で大文字、小文字を区別しない
repeat(N)[p] p の N 回の繰り返し
repeat(N,M)[p] p の N~M 回の繰り返し
repeat(N,inf)[p] p の N 回以上の繰り返し
etc.
Directive の自作
• Parser の自作は boost-spirit.com に記事有りhttp://bit.ly/ikdvQt
• 作る物: delimited(delimiter)[parser]
–例: delimited(std::string(“endstream”))[char_]endstream という文字列にぶつかるまで char を読み出す
–せっかくなので char 非限定で作成• std::vector<int> delim(2, -1);
delimited(delim)[int_]で -1 -1 にぶつかるまで int を読み出す
Directive の自作
• 自作コンポーネントの要素
–終端記号の宣言
– Parser 本体の定義
• 属性の型
• 実際の解析処理
– Parser generator の定義(rule → Parser の変換)
–有効化
• コンポーネント自身
• Semantic action ※今回 semantic action には全く触れませんので Tutorial 参照
Directive の自作
• 終端記号の宣言
namespace mine {
BOOST_SPIRIT_TERMINAL_EX(delimited)
}
Directive の自作• Parser 本体
namespace mine {
// 内部 parser が 1 つの parser 型を定義 (CRTP を利用)template<typename Subject, typename Delimiter>struct delimited_parser
: boost::spirit::qi::unary_parser<delimited_parser<Subject, Delimiter> > {
// メンバテンプレートクラス attribute を定義(type が属性の型)template<typename Context, typename Iterator>struct attribute {typedef typename boost::spirit::traits::build_std_vector<
typename boost::spirit::traits::attribute_of<Subject, Context, Iterator>::type>::type type;
};
// コンストラクタ(parser generator から呼ばれる)delimited_parser(Subject const &subject, Delimiter const &delimiter): subject(subject), delimiter(delimiter) {}
※Directive 内部のParser の型
delimited(delimiter)[parser]
Directive の自作• Parser 本体
namespace mine {
// 実際の解析ルーチンtemplate<typename Iterator, typename Context, typename Skipper, typename Attribute>bool parse(Iterator &first, Iterator const &last,
Context &context, Skipper const &skipper, Attribute &attribute) const {
// 詳細略 subject.parse() を使ってKMP法的に処理。// 汎用的にやるならコンテナ操作には traits を使う。// traits::container_value<Attribute>::type// traits::container_iterator<const Delimiter>::type// traits::begin(delimiter);
}
template <typename Context>boost::spirit::info what(Context& context) const {
return boost::spirit::info(“delimited”, subject.what(context));}
Subject subject; // Directive 内部のパーサー保持用Delimiter delimiter; // デリミタ保持用
};
※属性
Directive の自作• Parser generator の定義(rule → Parser の変換)
namespace boost { namespace spirit { namespace qi {
template <typename Delimiter, typename Subject, typename Modifiers>struct make_directive<terminal_ex<mine::tag::delimited,
fusion::vector1<Delimiter> >, Subject, Modifiers> {
// Parser の型typedef mine::delimited_parser<Subject, Delimiter> result_type;
// Parser を返す operator()template <typename Terminal>
result_type operator()(Terminal const& term, Subject const& subject, unused_type) const {
return result_type(subject, fusion::at_c<0>(term.args));}
};
}}}引数の 0 番目要素
Directive の引数の型:Delimiter 1つ
delimited(delimiter)[parser]
Parser のコンストラクタ呼び出し
Directive の自作• 有効化(Proto に認識させる)
namespace boost { namespace spirit {
// 通常用template<typename Delimiter>struct use_directive<qi::domain,terminal_ex<mine::tag::delimited,fusion::vector1<Delimiter> > > : mpl::true_ {};
// Phoenix (Lambda みたいなもの)用template<>struct use_lazy_directive<qi::domain,mine::tag::delimited, 1 // arity
> : mpl::true_ {};
Directive の自作• // #include と using namespace 省略
int main(void){typedef std::map<std::string, std::string> Config;Config config;
std::string input("Boost.Spirit= extraordinary ¥n C++er= ...");phrase_parse(input.begin(), input.end(),
*(lexeme[delimited(std::string(“=“))[graph]] >> lexeme[*graph]),space, config);
BOOST_FOREACH(Config::value_type &kv, config) {std::cout << kv.first << '=' << kv.second << std::endl;
}
}
• 出力Boost.Spirit=extraordinaryC++er=...
※入力を微妙に変更
ね、簡単でしょう?
代替手段
• 引数(Inherit attribute)付きルールを定義
rule<Iterator, std::string(std::string)> delimited;
delimited = *(graph_ - _r1) >> omit[_r1];
• 使い方phrase_parse(input.begin(), input.end(),
*(lexeme[delimited(“=“)] >> lexeme[*graph]),
space, config);
引数 引数値を捨てる
引数の型属性
非実用的入門
※最悪計算量的にはO(nm) v.s. O(n+m)
Customization point
• いちいち Parser とか Directive とか作ってらんないけど挙動をカスタマイズしたい
→それ、Spirit ならできるよ!
→traits を特殊化することで挙動を変更
Customization point• boost::spirit::traits 以下に用意されている。
SFINAE 用の Enabler は省略
– is_container<Container> >> 系用• 関連: container_value<Container> etc.
– handles_container<Component, Attr> >> 系用– transform_attribute<Exposed, Transformed, Domain> rule 系用– assign_to_attribute_from_iterators<Attr, Iterator> 汎用– assign_to_attribute_from_value<Attr, T> 汎用– assign_to_container_from_value<Attr, T> 汎用– push_back_container<Container, Attr> 繰返系用– clear_value<Attr> 繰返系用– etc.
– create_parser<T> auto_用cf. http://slashdot.jp/~Yak!/journal/525693
Customization point の関係例) Foo >> *Bar の結果の戻し先として型 Qux の変数 qux が渡された場合
※ Foo >> *Bar の属性が Qux 型であるとは限らない!※大枠だけ図示、::type や ::call も省略
is_container<Qux> handles_container<Foo>
handles_container<*Bar>
push_back_container<Qux,Foo_temp>(qux, foo_temp);
foo_temp ← Foo 読み出し
container_value<Qux>::type foo_temp;
true truequx ← Foo 読み出し
false
true
qux ← *Bar 読み出し
※ * ならデフォルト true
false なら↑と同様の処理
clear_value<Bar>
push_back_container<Qux,Bar_attr>
を内部で利用
Customization point の関係例) Foo >> *Bar の結果の戻し先として型 Qux の変数 qux が渡された場合
※ Foo >> *Bar の属性が Qux 型であるとは限らない!※大枠だけ図示、::type や ::call も省略
is_container<Qux> is_container<QA>
transform_attribute<QA, Foo_attr>::type foo_temp =
transform_attribute<QA, Foo_attr>::pre(qa);
false true前ページと同様の処理
falseQux は 2 要素のFusion シーケンスtuple<QA, QB>とする
foo_temp ← Foo 読み出し
transform_attribute<QA, Foo_attr>::post(qa, foo_temp);
QB と *Bar の属性について上と同様の処理
Foo は何?
rule や attr_cast
Foo_attr foo_temp;
他
※デフォルトは内部で assign_to を使用
※内部で assign_to_* 族を使用
assign_to(foo_temp, qa);
foo_temp ← Foo 読み出し
アジェンダ
• 概要紹介
–限定された使い方のみ
• 拡張性
– Directive を自作してみる
– Customization point
• 蛇足
Spirit Qi とうまく付き合うために
• コンパイル時間が Boost!!!
→コーヒーでも飲んで優雅に待つといいよ!※axpdf--.spi だとフルビルドに約 10 分
→Spirit 関連部分を分離して文法が変わらない限り再コンパイル不要にする
– Parser はあくまで parser に徹して構文解析結果に対する処理は別に分けるとか
Spirit Qi とうまく付き合うために
• エラーメッセージ量が Boost!!!
→例: 3.3MB
見えてる範囲
20%縮小表示
画像補正しないと薄くて文字が分からないレベル
Spirit Qi とうまく付き合うために
• エラーメッセージ量が Boost!!!
→例: 3.3MB
• テンプレートのインスタンス化情報がほとんど
–まずは error で検索
error で検索
ココ
20%縮小表示
Spirit Qi とうまく付き合うために
• エラーメッセージ量が Boost!!!
→例: 3.3MB
• テンプレートのインスタンス化情報がほとんど
–まずは error で検索
–後はどこが真因かインスタンス化情報を遡る…前に
–一度エラー箇所を見てみるといいことがあるかも
• ケース1. static_assert
– エラーメッセージ
spirit7.cpp:30: instantiated from here
/usr/local/include/boost/spirit/home/qi/nonterminal/gra
mmar.hpp:75: error: no matching function for call to
„assertion_failed(mpl_::failed************
(boost::spirit::qi::grammar<Iterator, T1, T2, T3,
T4>::grammar(const boost::spirit::qi::rule<Iterator_,
T1_, T2_, T3_, T4_>&, const std::string&) [何か一杯]::incompatible_start_rule::************)(何か一杯))‟
Spirit Qi とうまく付き合うために
• ケース2. コメント– エラーメッセージ
spirit7.cpp:39: instantiated from here/usr/local/include/boost/spirit/home/qi/nonterminal/rule.hpp:277: error: no match for call to „何か一杯’
– /usr/local/include/boost/spirit/home/qi/nonterminal/rule.hpp
// If you are seeing a compilation error here stating that the// forth parameter can't be converted to a required target type// then you are probably trying to use a rule or a grammar with// an incompatible skipper type.if (f(first, last, context, skipper))
Spirit Qi とうまく付き合うために
277行目
• 先生!コンパイル通ったけど思った通りに動きません!
Spirit Qi とうまく付き合うために
→プログラムは思った通りに動かない。書いたとおりに動く。
→debug(rule); を使うと構文解析の様子がトレースできるhttp://boost-spirit.com/home/articles/doc-
addendum/debugging/
– 属性に operator<< が必要
まとめ
•Spirit は変態真面目に Spirit やりたい人は
• Tutorial を読みながら一通り実行する• boost-spirit.com の記事を読む•随時 Quick Reference を参照する
ご静聴ありがとうございました