yutopp's blog

サンドバッグになりたい

文鳥言語とBoost.Spirit.Qi Tips - C++ Advent Calendar 2013(11日目)

これは C++ Advent Calendar 2013 - PARTAKE 11日目の記事です。

この記事では俺々プログラミング言語の紹介と、C++処理系を作るにあたって便利そうなBoost.Spirit.QiのTipsを紹介します。
Boostは1.55.0が対象です。

文鳥言語とは

文鳥言語とは、私が趣味で作っているプログラミング言語です。
yutopp/rill · GitHub

C++は(人間の)プログラマが書いていて楽しく感じるように作られている言語ですが、文鳥言語は文鳥が書いていて楽しく感じられるように作られています。
文鳥 - Google 検索
カワイイヤッター!(?)

文鳥言語を支える技術

文鳥言語の実装には、全体的にC++11、構文解析器にBoost.Spirit.Qi、コード生成にはLLVMを用いています。
構文解析器とコード生成にライブラリを使っている時点で、言語を自分で作っている感は薄いのですが、常にそれなりに動く状態で遊べるのでとても楽しいのです。

今の段階では、このようなコードをコンパイルすることができます。単純なFizzbuzzです。

def main(): int
{
    print( "hello, bunchou lang on Linux!!!bunbun!\n" );
 
    val i = 1: int mutable;
    while( i < 100 ) {
        if ( i % 15 == 0 ) {
            print( "Fizzbuzz " );
        } else if ( i % 5 == 0 ) {
            print( "Buzz " );
        } else if ( i % 3 == 0 ) {
            print( "Fizz " );
        } else {
            extern_print_int( i ); print( " " );
        }
 
        i = i + 1;
    }
    print( "\n" );
    // Test
    overload( 2, 5 );
    overload( "bun!" );
 
    return 0;
}
 
def overload( val a: int, val b: int ): void
{
    print( "====================\n" );
    print( "test_scope\n" );
    print( "====================\n" );
 
    val i = 42: int mutable;
    {
        val i = 72: int;
        print( "inner: " ); print_int( i );
    }
    print( "outer: " ); print_int( i );
}
 
 
def overload( ref s: string ): void
{
    print( "====================\n" );
    print( s );
    print( "\n" );
}
 
//
extern def extern_print_int( val :int ): void "put_string2"; 
 
def print_int( val i: int ): int { extern_print_int( i ); print( "\n" ); }

出力バイナリの実行結果

hello, bunchou lang on Linux!!!bunbun!
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 Fizzbuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 Fizzbuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 Fizzbuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 Fizzbuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 Fizzbuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 Fizzbuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz 
====================
test_scope
====================
inner: 72
outer: 42
====================
bun!

かわいいですね。今は基礎部分に力を入れて開発をしています。いつか、これでリンカなど作りたいですね。

さて、この言語処理系を作るにあたって、Boost.Spirit.Qiが大活躍しています。

Boost.Spirit.Qi Tips

Boost.Spirit.Qi については、インターネッツを検索するとためになる資料が出てきますのでそちらを参考にして下さい。

さて、Qiについて色々調べていると、様々なつぶやきが散見されます。

  • とっつきにくい
  • コンパイル時間がBoostする
  • コンパイルエラーが意味不明
  • 非常に明快で分かりやすい

このコンパイルエラーが意味不明、というのがとっつきにくい最大の問題だと思います。

私が初めてBoost.Spiritに触れたのは中学3年生の頃(某とは関係ありません)でした。Boostは1.43.0でVC++2005な環境だった気がします。
その時はSpiritのコンパイルエラーに精神を破壊されたのですが、今ならSpirit.Qiをとても楽に使いこなせるはずです。

そう、Clangとならね。

Clangを使う

パーサを書くときは、Clangを使ってコンパイルするのがオススメです。
Clangはコンパイルエラーが優しいという事で有名ですが、この優しさがQiを使うときにも活きてくるためです。

例えば、以下のようなコードがあるとします。

#include <iostream>
#include <vector>
 
#ifndef BOOST_SPIRIT_USE_PHOENIX_V3
# define BOOST_SPIRIT_USE_PHOENIX_V3
#endif
#include <boost/spirit/include/qi.hpp>
 
#include <boost/fusion/include/adapt_struct.hpp>
 
struct wrapper
{
    std::vector<int> v;
};
 
BOOST_FUSION_ADAPT_STRUCT(
    wrapper,
    (std::vector<int>, v)
)
 
int main()
{
    namespace qi = boost::spirit::qi;
 
    std::string const test_input = "1, 2, 3, 4, 5";
 
    auto it = test_input.cbegin();
    auto const last = test_input.cend();
 
    wrapper result;
 
    auto const r = qi::int_ % qi::lit( ',' );
 
    auto const s = qi::phrase_parse( it, last, r, qi::ascii::space, result );
    assert( s );
    assert( it == last );
 
    for( auto const& i : result.v )
        std::cout << i << " ";
    std::cout << std::endl;
}

これはコンパイルエラーになるコードなのですが、ここで、Clang と GCC からのコンパイルエラーをご覧ください。(Wandboxありがとうございます)

GCCのコンパイルエラーはパッと見どの箇所がエラーの原因なのか分かりにくく、つらい感じがします(19行目)。
今回はシンプルなコードなので比較的特定しやすいのですが、構文のruleが増えてくると、この分かりにくさは地獄を生み出します。

比べてClangのコンパイルエラーの優しさといったらなんでしょう!11行目でバッチリ`value_type`が無いと教えてくれていますね。
これで、as でくくって要件を満たす一時オブジェクトを渡してやればいいんだな、とすぐに理解することが出来ます。

auto const r = qi::int_ % qi::lit( ',' );

auto const r = qi::as<std::vector<int>>()[ qi::int_ % qi::lit( ',' ) ];

に修正。やりました!

TemplateAliasesを使う

ruleをgrammerにまとめる場合はメンバにrule型の宣言を置くことになり、型を推論させることが出来ません。

なので、例えば

qi::rule<Iterator, ast::statement_ptr(), skip_grammer_type> statement_;

のような宣言を大量に書くことになるはずですが、Iterator や skip_grammer_type などを毎回書くのが面倒なので

template<typename T>
using rule = qi::rule<Iterator, T, skip_grammer_type>;

などとしておくと

rule<ast::statement_ptr()> statement_;

と書けるようになるので幸せだと思います。

これはVC++2013で使えるので使わない手はありませんね!
C++11/14 Core Language Features in VS 2013 and the Nov 2013 CTP - Visual C++ Team Blog - Site Home - MSDN Blogs

ruleを細かく分ける

ruleを細かく分割することによって、コンパイルエラーとなる箇所の特定がとても楽になります。
上記の2つと合わせて、rule を細かく分けるのがオススメです。

rule の 型をしっかり見る

rule<ast::statement_ptr()> statement_;

ruleに渡すast::statement_ptr()などの()は忘れがちでハマるので気をつけましょう。
引数を渡すruleを作ることが少ないので忘れがちになりますね…

Phoenix V3 を使う

Spirit関連のファイルをインクルードする前に

#define BOOST_SPIRIT_USE_PHOENIX_V3 1

とすることで、Boost.Phoenix 3が使われるようになります。
関数オブジェクトを作るのが楽になっているはずなので、Phoenix は 3 を使いたいところ。

make_shared を作る

文鳥言語では、ASTの組み変えが発生しそうなのでコピーコストが低いであろうshared_ptrを使ってASTを構築しています。
なので、SemanticActionにてオブジェクトを生成する必要があるのですが、Phoenixには相当の関数がデフォルトで無いようなので作ります。

template<typename T>
struct make_node_pointer_lazy
{
    typedef std::shared_ptr<T> result_type;

    template<typename... Args>
    auto operator()( Args&&... args ) const
        -> result_type
    {
        return std::make_shared<T>( std::forward<Args>( args )... );
    }
};

template<typename T, typename... Args>
auto make_node_ptr( Args&&... args )
    -> decltype( boost::phoenix::function<make_node_pointer_lazy<T>>()( std::forward<Args>( args )... ) )
{
    return boost::phoenix::function<make_node_pointer_lazy<T>>()( std::forward<Args>( args )... );
}

(Allocatorを考慮するの忘れていた…)
ひとまずコレで

some_rule_[qi::_val = make_node_ptr<HogeClass>( qi::_1, ... )]

のようにnodeが簡単に作れるようになるので、便利かもしれません。

参考: Boost.Phoenix V3 - 関数のbind化 - Faith and Brave - C++で遊ぼう

パース時に位置情報を取る

c++ - boost::spirit access position iterator from semantic actions - Stack Overflow
参考になるのでどうぞ…
(line_pos_iteratorの実装ってどうなんでしょう…ウーン)

Assertion `rhs.f && "Did you mean rhs.alias() instead of rhs?"' failed. is 何

未初期化のruleを直に代入すると、このassertionにひっかかります。

#include <iostream>
#include <vector>

#ifndef BOOST_SPIRIT_USE_PHOENIX_V3
# define BOOST_SPIRIT_USE_PHOENIX_V3
#endif
#include <boost/spirit/include/qi.hpp>

#include <boost/fusion/include/adapt_struct.hpp>

template<typename Iterator>
class code_grammer
    : public boost::spirit::qi::grammar<Iterator, int>
{
public:
    code_grammer()
        : code_grammer::base_type( a_ )
    {
        a_ = b_;

        b_ = boost::spirit::qi::int_;
    }

private:
    boost::spirit::qi::rule<Iterator, int> a_, b_;
};

int main()
{
    namespace qi = boost::spirit::qi;

    std::string const test_input = "42";

    auto it = test_input.cbegin();
    auto const last = test_input.cend();

    int result;
    code_grammer<decltype(it)> r;

    auto const s = qi::phrase_parse( it, last, r, qi::ascii::space, result );
    assert( s );
    assert( it == last );

    std::cout << result << std::endl;
}

code_grammerの

a_ = b_;

が、未初期化の b_ を代入しているのでダメですね。

解決策

  • 素直に a_ と b_ の代入位置を入れ替える
  • .alias() を使う
a_ = b_.alias();
  • %= を使う
a_ %= b_;

おわりに

ここまでBoost.Spirit.Qiを使って構文解析器を作った段階で踏み抜いた罠を紹介しました。
Spiritは実際便利なのであらゆる場所で使われているはずなのですが、少し初見殺しなところがある上に、罠から抜け出す術が中々載っていないので、Tipsが少しでも役に立てば幸いです。

では