最強のC++実装TOMLパーサーが完成した

ここ1, 2週間費やしていた作業が完了し、めでたくtoml11のバージョン2.0.0をリリースした。

github.com

このバージョンアップで、

  • コードが凄まじく美しくなり、
  • エラーメッセージが最強になり、
  • TOML v0.5.0 (最新) に対応した。

せっかくなのでこの記事で何をしたのか書いていこうと思う。宣伝ついでに、自分でパーサを書く人(最近はフルスクラッチ自作コンパイラが流行っているので)の一助になれば良いのだが。

TOML 0.5.0への対応

TOML v0.5.0では、それなりの数のアップデートが入った。toml11 v2.0.0ではその全てに対応している。順を追って説明していこう。

dotted keys

まず、以前からネストされたテーブルの名前は.で繋げていた。

[a]
n = 10
[a.b] # <- これ
m = 20
# {"a" : {"n":10, "b":{"m":20}}} (in JSON) と同等

さらに、TOMLではテーブルも他の型の値と特に差はない。インラインテーブルすら存在する。通常の=で区切られたkey-valueペアと、テーブルの名前とコンテンツのペアの間に特にセマンティクス上の差異はない。

# 上と同等
a = {n = 10, b = {m = 20}}

すると、テーブルの名前と同じ書き方が通常のkey-valueペアのkeyに使えないのは不自然だ。

# 上と同等
a.n = 10
a.b.m = 20

というわけで、上記の書き方が許されるようになった。

integer prefix

一般の入力ファイルで整数が10進表記しかいらないということはありそうにない。というわけで、TOMLではhex, oct, binaryの3つの文法が追加された。0xなどのプレフィックスが必要になる。

bin = 0b01000011
oct = 0o775
hex = 0xDEADBEEF

順当だろう。

special floating point

TOML v0.5.0ではinfnan浮動小数点数の入力として使えるようになった。まあ必要なこともあるのだろう。あるいは、表現できる値を全て入力可能にしたほうがいいという判断かも知れない。

a = inf
b = -inf
c = nan

ただ、v0.4.0までTOMLは浮動小数点数の内部表現を定めておらず、処理系定義だった(64-bit precision expectedとは言われている)。だが実質、現代でIEEE 754に従わない処理系はほぼないだろう。あまり意味もなくゆるくし過ぎると、ユーザーが何も仮定できず、使う際に様々なエラーに対応する必要が生まれてしまう(あるいは、実質の標準、のようなあまり健全でない状態が生まれる)。というわけで、TOML v0.5.0ではIEEE 754のbinary64表現を用いる、と定められた。

datetime

元々TOML v0.4.0では日時が一級市民だったが、年月日からタイムゾーンまで全て書かなければならなかった。

dob = 1979-05-27T07:32:00-08:00

ローカル時間でいいよ、という場合にこれは面倒だし、日付さえあれば時間帯はどうでもいい、という場合も困る。 というわけで日時オブジェクトが緩和されて、local_date, local_time, local_datetime, offset_datetimeに分離された。日付のみ、時刻のみ、日時のみ、日時+タイムゾーンに対応する。

date = 1979-05-27
time = 07:32:00
datetime = 1979-05-27T07:32:00

toml11ではそれらを全て別の型として扱い、読み込み時に勝手にタイムゾーンを足したりしない。が、自前で日時計算を実装するのは地獄なので、std::chronoの型に変換できるようにして、変換してから使うことを推奨している。変換時に必要ならタイムゾーンが調べられ、足される。このとき、local_timeだけは「時点」ではなく「時間」なので(日付がわからないとepochから伸びる数直線上の1点に固定できない)、これだけはsystem_clock::time_pointに変換できない。ので、任意のdurationに変換できるようにした。日付の0時0分に足すとその時刻になるような時間に変換される(ユーザー定義durationの精度が足りていれば)。

エラーメッセージを最強にする

自分でTOMLを使って設定ファイルを書いていると、ちょいちょいミスをしてしまう。例えば、よくわからない記号(;など)を足してしまう。そういうとき、パーサが無言で落ちるとつらい。以前は行数と何が失敗したかだけ出すようにしていたが、もっとRustのコンパイラみたいなカッコいいエラーメッセージを出したかった。

ちなみにRustはエラーメッセージのフォーマットまでRFCにしている(rfcs/1644-default-and-expanded-rustc-errors.md at master · rust-lang/rfcs · GitHub)。 なので狙いは逸れない。これを真似ればいい。

というわけで出来上がったのが以下だ。

[error] toml::parse_table: invalid line format
 --> example.toml
 1 | a = "value";
   |            ^- expected newline, but got ';'.

「指している場所で改行が入ることを期待していたのに、;とかいうよくわからない記号が出てきた」というエラーメッセージだ。文法ミスを的確に指摘してくれる。行数も付いているし、ファイル名もある。最高ではないか。

また、ファイルが長くなってくると同じ名前のものを間違って定義してしまうこともあるだろう。そういうとき、パーサに2つ同じものが出てきたと言われると、「もう一個はどこだよ! 知ってるんだろ! 吐けよ!」となるのではないだろうか。

toml11は知っている。そして親切に教えてくれる。

[error] toml::insert_value: value ("a") already exists.
 --> duplicate-value.toml
 1 | a = 3.14
   |     ~~~~ value already exists here
 ...
 2 | a = 42
   |     ~~ value inserted twice

テーブルを2回定義してしまっても大丈夫だ。

[error] toml::insert_value: table ("table") already exists.
 --> duplicate-table.toml
 1 | [table]
   | ~~~~~~~ table already exists here
 ...
 3 | [table]
   | ~~~~~~~ table defined twice

値を書き忘れた? 大丈夫、わかってますよ。

[error] toml::parse_key_value_pair: missing value after key-value separator '='
 --> missing-value.toml
 2 | a = 
   |     ^ expected value, but got nothing

型が異なるArrayを作ってしまった? 個々の値のフォーマットが間違っていなくても、それは文法違反です。

[error] toml::parse_array: type of elements should be the same each other.
 --> inhomogeneous-array.toml
 1 | a = [42, "value"]
   |     ~~~~~~~~~~~~ inhomogenous types

親切なのは読み込み時だけではない。TOMLは読んで終わりではなく、読んだ値を元にアプリケーションの設定をするのだ。なので、テーブルに何かの値が入っていることを期待する。つまり、値の取り出しが発生する。その時、型を間違えたり、存在しないフィールドを読もうとしたり、様々なエラーがあり得るだろう。

toml11のtoml::get<T>を使うと、ある型Tとしてフィールド上の値を取り出せる。例えば、以下の例はtitleというフィールドが実際は文字列が入っているのに整数だと思って取り出そうとする例だ。

const auto data = toml::parse("example.toml");
const auto title = toml::get<int>(data.at("title"));

titleは実際には文字列なので、エラーが出る。そのエラーは以下のようになる。

[error] toml::value bad_cast to integer
 --> example.toml
 3 | title = "TOML Example"
   |         ~~~~~~~~~~~~~~ the actual type is string

また、toml::find<T>を使って値を探した時、

const auto data = toml::parse("example.toml");
const double a = toml::find<double>(data.at("table"), "alpha");

フィールドが存在しなければstd::out_of_rangeが投げられ、そのwhat()には以下が入っている。

[error] key "alpha" not found
 --> example.toml
 1 | [table]
   | ~~~~~~~ in this table

どこで定義されていて実際の型が何と判定されたか、どのテーブルのフィールドを見たのか、およそ必要な全てがそこにある。これで何が間違っているかわからない人間はいない。C++コードまでは面倒を見られないので、自分で書いたコードを覚えていなかったら別だが。

ちなみにコンソールに出力されるかどうかはわからないので色はついていない。色付きでファイルに出力した場合、ログファイルが凄まじく汚くなってしまうので、避けた。あとPOSIXWindowsで分けるのが面倒だった。

実装

どうやって上のようなメッセージを作るのか? 必要な情報はわかっている。パーサーによるエラーの説明と理由、ファイル名、行数、落ちた位置あるいは間違っている領域だ。それをパース中、あるいはパースが終わってからも覚えていればよいのだ。

パーサによるエラーの説明は、その場でパーサが生成できる。また、落ちた位置または問題のある領域は、パーサが作ることもできるが、後でtoml::getからも出したいことを思うと覚えておく簡便な方法を持っておくべきだろう。ファイル名と行数は通常忘れてしまうものなので、何か方法を考えないといけない。

幸い、toml::parseは通常ファイル名を受け取るので、これを覚えればよい。ストリームが渡された場合は、仕方ない。情報が無いのに取り出すことはできない。必要ならユーザーに渡してもらえばいい。ユーザーがエラーメッセージにファイル名は必要ないと判断したなら、それはそれで仕方ない。

toml11では、これまでイテレータを受け取っていたパーサが、イテレータをラップした構造体を受け取るようになった。その構造体は、ファイルのバイト列へのshared_ptrとファイル名、そして現在の位置(これまで生で受け取っていたイテレータと同等)を覚えている。ファイルのバイト列を覚えているので、必要になれば行数をカウントできるし、前後の改行文字を探せば現在いる行全体を取得できる。ファイル名もある。そして当然現在の位置がある。これで大体情報は揃う。

// イメージ図
struct location
{
    std::shared_ptr<std::vectpr<char>> source_;
    std::string name_;
    std::vector<char>::const_iterator iter_;
};

これを受け取って場所を進めながらパースする。また、これとほぼ同じregionという構造体も用意し、そちらで領域を表すようにする。

// イメージ図
struct region
{
    std::shared_ptr<std::vectpr<char>> source_;
    std::string name_;
    std::vector<char>::const_iterator first_, last_;
};

これを持っていれば、値についてエラーが生じたとき、対応する領域を表示できる。パース開始時のlocationと終了時のlocationからregionは簡単に作れる。

あとは、これらとエラーの説明から先のエラーメッセージを生成する関数を作り、それを使ってエラーを投げればよい。フォーマット自体は決まっているので簡単だ。前後の改行文字を探したり、先頭から現在地までの改行の回数をカウントしたり、幅を揃えたりするのが面倒なだけだ。

コードを綺麗にする

前提として、TOMLはまだバージョン1になっていない。これからも文法がちょいちょい変わるだろう。折角素晴らしい機能を実装しても、その際コードが汚すぎてバージョンアップに追従できなかった場合、詰む。

というわけで、主にパーサとtoml::valueの実装を綺麗にした。

パーサの実装

パーサは、これまで値が正しいフォーマットになっているかどうかを確認しないまま読み進め、変換処理とフォーマットのチェックを同時並行でやっていた。この場合、やっている処理の意味が入り乱れてしまう。さっきまでフォーマットのチェックをやっていたのに次の行ではデータの変換の準備をしていて、その次の行でまたチェックをしている、となると大変にわかりにくい。フォーマットが間違っていたらエラーで返せばよいし、フォーマットが正しければよっぽど(64bit整数の範囲を超えるとか)でないとエラー処理は必要ない。なのでこの2つは分離した方が、個々のコードがやっていることの意味がはっきりして、結果読みやすくなるはずだ。

というわけでparserをlexerparserに分けた。lexerは成功すればエラーメッセージの話に出てきたregionを返し、そうでなければ「何を期待していて、何が現れたか」を返す。parserは正しいフォーマットのものしか来ないと思ってサクサク処理ができる。言語処理系の歴史を一人で再現してしまっている感じがする。これによって、例えbooleanのパーサーは以下のようになった。

// 実際のコード
template<typename Container>
result<std::pair<boolean, region<Container>>, std::string>
parse_boolean(location<Container>& loc)
{
    const auto first = loc.iter();
    if(const auto token = lex_boolean::invoke(loc))
    {
        const auto reg = token.unwrap();
        if     (reg.str() == "true")  {return ok(std::make_pair(true,  reg));}
        else if(reg.str() == "false") {return ok(std::make_pair(false, reg));}
        else // internal error.
        {
            throw toml::internal_error(format_underline(
                "[error] toml::parse_boolean: internal error", reg,
                "invalid token"));
        }
    }
    loc.iter() = first; //rollback
    return err(format_underline("[error] toml::parse_boolean", loc,
            "token is not boolean", {"boolean is `true` or `false`"}));
}

返り値はresult型で、RustのResultと同じだ。成功値(std::pair<boolean, region>)かエラー値(std::string)のどちらかが入っている。成功した場合にregionを返す理由はエラーメッセージの実装の節を読めばわかる。

している処理は、lex_booleanが成功すればtruefalseかを調べる。それ以外の値でlex_booleanが成功していればそれはバグなので、パース失敗のエラーを返すなどと悠長なことはせず、即座にinternal_errorを送出して落とす。そもそもlex_booleanが失敗していれば、エラーを返す。通常は次に別の値、例えば文字列として読んでみて成功したらそちらを返すので、位置をロールバックしておく。

自明なコードだ。パーサーはこのような自明なコードが大量に組み合わさって動く。たまに挟まる非自明なこと(a.b.c = 42のようなときにトップレベルからa.bというテーブルを再帰的に探し、なければ挿入し、conflictの際のエラーメッセージを生成するなど)は関数を分けて、さらに随所にコメントを入れた。

ではフォーマットチェックの仕事を押し付けられたlexerはどうか。そこが結局難しくなっては仕方がないので、これも簡単にする方法を考えた。TOMLはabnfによる表現が提供されていて(それが規格になるわけではない、と注意を促されてはいるが)、許されるパターンがわかりやすい。正規表現的なパターンでフォーマットを書けるなら、それぞれの単位パターンを受理するルーチンを書いて、合成すればよい。正規表現エンジンみたいなものだ。ただし汎用である必要はない。TOMLに使う最低限度だけでよい。

というわけで、combinator.hppにそのようなものを作った。すると、個々のlexerは以下のようになる。例えば、以下は10進表記の整数のパターンだ。

// digit | nonzero 1*(digit | _ digit)
using lex_unsigned_dec_int = either<sequence<lex_nonzero, repeat<
    either<lex_digit, sequence<lex_underscore, lex_digit>>, at_least<1>>>,
    lex_digit>;
// (+|-)? unsigned_dec_int
using lex_dec_int = sequence<maybe<lex_sign>, lex_unsigned_dec_int>;

パターンをコンパイル時に構築しておいて高速化するという目論見があるので、全てテンプレートになっている。つまり上のコードのusingは全てエイリアスで、typedefに等しい。それに、動的に追加していると実行パスを追わないとパターンがわからないのでつらいということもある。パターン文字列から自動生成するのはやり過ぎで、この部分はユーザーに見せる気はないので便利である必要がない。内部実装なので、実装の簡単さが優先される。そもそも文字列のパースのための機能を文字列をパースして生成するというのは意味がわからない。無限後退か?

それぞれの型がコメントに書いてあるパターンと対応する。さらに、新しく定義したパターンも全く同様のコンビネータなので、さらに繋げることができ、再利用もできる。これで許されるパターンが変わった時も、これを少しいじれば済む。正規表現より読みやすいかは人の好みがあるだろうが、正規表現パターンのアップデートと似たような難易度にしかならない。

ただし、再帰的なものを作ってしまうと無限に長くなって詰む(実装を上手くやればごまかせはするが)。つまり、テーブルの値としてテーブルが許されるので、テーブルのパターンは自分自身を内部に含む。そのようなパターンは素直な実装(今回のような)では無限に長いパターンになり、コンパイラがスタックオーバーフローするか、早めに失敗する。なのでlexerにはテーブルのパターンはなく、テーブルの内容(key-value pairの列)はパーサーがparse_keyparse_valueを交互に呼びながら処理している。

というわけでparserとlexerを分けることでコードが最高に読みやすくなった。エラー処理もしやすい。変更にも対応しやすいロバストなコードになった。

valueの実装

valueは、数あるTOML型のどれか一つになる。そういうものは、モダンにはstd::variant、古狩人はunionを使う。toml11はC++17を要求しないので、std::variantは使えず、古い方法を使わなければならない。unionはテンプレートで個数を変えたりできない点と、どの値がactiveかを示すフラグが自動ではついてこないことが問題だ。だが、今回は値の数は固定だし、フラグは自分で足せばいい。なので普通に使える。

unionの使い方は面倒なので言わない。そもそもそこは前までの実装と変わっていない。今回変更したのは、以前までテンプレートを使って無理やり行数を減らしていたのをやめて、むしろコードが3倍くらいになってもいいから、奇妙なトリックを使わないようにした。ある意味実践的になったとも言える。ある程度はテンプレートとSFINAEも使っているが(整数型かどうかの判定をSFINAEでなくintlongunsignedに、と一つ一つ手で作った温かみが伝わってくるオーバーロードでやるのは気が狂っている)。なので、様々な型からtoml::valueを構築するためのコンストラクタと代入演算子がそれぞれ定義されている。大半が同じコードだが変換の部分が少しずつ違っている。8割は同じコードだから無理やり一般化しようとした当時の私の気持ちもわかるが、ここは多分全部書いていた方が結果的にわかりやすいし、後で追加もしやすかろう。

便利関数たち

そういえば便利関数も足した。toml::get<T>という便利関数があり、Tに好きな型を入れると、toml::valueに変換してくれるというものだ。

auto answer  = toml::get<std::int64_t    >(data.at("answer"));
auto pi      = toml::get<double          >(data.at("pi"));
auto numbers = toml::get<std::vector<int>>(data.at("numbers"));

これの機能を少し強化して、TOMLのテーブルに入っているフィールドの型が全て同じ(全部文字列、とか)場合はmapごと一気に置き換えられるようにしたり、

// [tab]
// key1 = "foo" # all the values are
// key2 = "bar" # toml String
const auto tab = toml::get<
    std::map<std::string, std::string>>(data.at("tab"));

TOMLでは許されているArray of arraysの各Arrayで型が違う値をstd::vectorのペアで取り出せるようにしたり、

// aofa = [[1,2,3], ["foo", "bar", "baz"]] # toml allows this
const auto aofa = toml::get<
    std::pair<std::vector<int>, std::vector<std::string>>
    >(data.at("aofa"));

フィールドが見つからない場合のエラーメッセージを強化したtoml::findを追加したり、

// 期待しているexample.toml
// [table]
// num = 42

const auto data = toml::parse("example.toml");
const auto num  = toml::find<int>(data.at("table"), "num");

/* 値がなかったら以下のエラー
terminate called after throwing an instance of 'std::out_of_range'
  what():  [error] key "num" not found
 --> example.toml
 3 | [table]
   | ~~~~~~~ in this table
*/

Rust流のresultを返すtoml:: expectを追加したりした。

const auto value = toml::expect<int>(data.at("number"))
    .map(// function that receives expected type (here, int)
    [](const int number) -> double {
        return number * 1.5 + 1.0;
    }).unwrap_or(/*default value =*/ 3.14);

あとがき

というわけで最強のライブラリができた。C++らしい、裏側が透けて見えるシンプルさと様々な用途をサポートする強力さが両立したインターフェース、Rustのようなわかりやすく見目麗しいエラーメッセージ、分離され構造化されたコード、と私の思う理想のライブラリ(に近いもの。当然だがいくつかの妥協が含まれるし、テストから漏れたミスも多分残っていると思う)ができた。気分が盛り上がっているので手前味噌でも褒める手は止めない。全員これに乗り換えてくれねえかな

脳を破壊するFortranコード

今日、後輩と色々試していて面白いことに気付いた。Fortranのキーワードは、予約語ではない。Fortrandoとかifとかといったキーワードを持っているが、これらは予約語ではない。

ググると規格書のセクション番号込で情報が出てくる。

Keywords in Fortran Wiki

ISO Fortran90 standard § 2.5.2 "Keyword"では、以下のような記述がある。

These keywords are not reserved words; that is, names with the same spellings are allowed.

「これらのキーワードは予約語ではない。つまり、同じ綴りの名前は許される」らしい。

規格書のドラフトはここで見ることができる。

https://wg5-fortran.org/

質問している人もいるが、「キーワードを変数名として使うのはバッドプラクティスだ」という(当たり前の)答えが返ってきている。

software.intel.com

どういうことかというと、以下のコードが合法だということだ。

program main
    integer :: do, end=0
    do do=1, 10
        end = end + do
    end do
    write(*,*) end
end program main

ついでに、以下のようなコードも合法である。

program main
    logical::if=.true.
    if (if) if = then()
    write(*,*) if
contains
    function then()
        logical then
        then = .true.
    end function then
end program main

こんなコードが出てきた日には発狂すること請け合いであるが、Fortranコードのメンテをさせられて発狂寸前の人はこのような爆弾をコードに仕込んでみても面白いのかも知れない。

ifを関数にすることもできて、以下のコードはokを出力する。

program main
    if(if(if(if(.true.)))) write(*, *) 'ok'
contains
    function if(x)
        logical::x
        logical::if
        if = x
    end function if
end program main

何一つokなところがない。

これだとif関数が何もしていないからまだいいが、もっと悪意の強い関数にもできる。

program main
    if(if(.true.)) then
        write(*, *) 'ok'
    else
        write(*, *) 'not ok'
    end if
contains
    function if(x)
        logical::x
        logical::if
        if = .not. x
    end function if
end program main

このコードはnot okを出力する。確かにnot okだ。if関数が定義されているばかりか、その関数がlogicalな値を受け取ってそれを反転するので。

ちなみにgfortran-5.5.0-Wall-Wextraつきでコンパイルしても、上記のコードに警告は出ない(規格上予約語ではないとしても、警告くらいは出していいと思うのだが)。というわけで静かに爆弾を仕込むことができるので、使ってみてはいかがだろうか。

GitHubのシンタックスハイライトを直した話

GitHubは自動でシンタックスハイライトがされるが、言語仕様が今も変わっていくような言語(大体の使われている言語はそうだ。Goのように意図的に固定している例外を除いて)ではたまにそれが古いままで、公式ページのハイライトがおかしいことがある。

TOMLがその良い例で、TOML v0.5.0で追加された一部の機能が真っ赤にハイライトされていた。例えば、dotted keys(キーを繋いで階層構造を作る)や整数プレフィックス0x0bなど)、特殊な浮動小数点数infnan)などだ。

ところで私は既にv0.5.0対応のTOMLライブラリを一つ書いており(toml11は書き換え中)、それを使ったツールのサンプルが真っ赤になるのは悲しかった。

github.com

なので直したいと思い立ち、調べてみることにした。有名エンジニアである犬さんのブログが真っ先に引っかかる。

rhysd.hatenablog.com

これによると、GitHubシンタックスハイライトは以下のレポジトリによって行われている。

github.com

このレポジトリはエディタ用の文法定義を再利用するようになっている。TextMateAtomSublimeや……といったエディタの設定ファイルをメンテしているレポジトリを覚えておいて、数週間かおきにpullしてきてアップデートしつつ使うというようになっている。それぞれのシンタックス定義は別レポジトリになっているので、linguist自体にPRが山ほど飛んでくるという事態を避けることができる。

このディレクトリにそのモジュールの一覧がある。

linguist/vendor at master · github/linguist · GitHub

ここから直したいレポジトリに飛んで、そこにPRを投げればいいわけだ。今回はTOMLなので、TextMateなるエディタの設定ファイルを編集することになる。

github.com

ところで私はTextMateというものを使ったことがない。ちらりとTOML用のレポジトリの中身を見てみたが、正規表現でマッチした部分に名前を付け、名前によってハイライトしていくものであるようだ。XMLで書かれているので読めないこともないが、つらい。

動作確認のこともあり、TextMateが必要だろうと思った。まず、ググるmacOS用のエディタだということがわかる。私はWindowsマシンは持っていないがmacは持っているので、インストールできた。

TextMate: Text editor for macOS

上のメニューからバンドルを開いて追加すればよいと書かれているのでやってみる。ちなみにやったのが数週間前(後述するがPRが閉じられるまでそれくらいかかった)なのでよく覚えていない。

インストール後、TextMateの Bundles > Edit Bundles > TOML > Language Grammers > TOML と開いていくと、別ウインドウでシンタックスの定義ファイルを編集しながら結果を見ることができるではないか。しかもXMLではなくJSONっぽい(けど少し違う)形式を使っているので、より目にやさしい。Ctrl-Sを押すごとにハイライトが更新されるので、保存のタイミングでこの形式からXMLに変換しているのだろう。実際、Applications/みたいなディレクトリにXML形式のファイルがある。

さて、書き方はわかったが、次は何を編集したらどういう影響が出るのか調べねばならない。以下は試しながらの憶測だったが、まあ多少の編集を経たとはいえマージされたので大外しはしていなかったのだろう。上記のようにして開くと以下のような設定が見える。以下のコードは(TextMateによる変換を経たとはいえ)https://github.com/textmate/toml.tmbundleからの引用になる。

patterns = (
    {   begin = ‘([A-Za-z0-9_-]+)\s*(=)\s*‘;
        end = ‘(?<=\S)(?<!=)|$’;
        captures = {
            1 = { name = ‘variable.other.key.toml’; };
            2 = { name = ‘punctuation.separator.key-value.toml’; };
        };
        patterns = ( { include = ‘#primatives’; } );
    })

ここでbeginend正規表現がある。これらによって囲まれた領域にマッチするものと思われる。基本重要な部分は大抵beginに書いてあったのでそこをいじった。で、そのグループの登場順序によって番号が振られ、capturesの中の番号に対応する名前を書いておくと、その名前としてハイライトされるようである。多分、カラースキームのようなものが別にあって、実際の色はそちらで構文要素の名前から決められるのだろう。この例では、bare-keyに対応する1つめのグループ([A-Za-z0-9_-]+)variable.other.key.tomlという名前が割り振られ、次のグループ=punctuation.separator.key-value.tomlなる名前がついている。

つまり、正規表現を拡張して新機能にマッチするようにして、グループ毎の名前を適切につければよいわけだ。そのような方針で編集して動くことを確認し、内容をPRにまとめて送りつけた。以下がそれだ。

allow dotted keys by ToruNiina · Pull Request #9 · textmate/toml.tmbundle · GitHub

allow nan, inf, hex, oct, bin values by ToruNiina · Pull Request #10 · textmate/toml.tmbundle · GitHub

数週間ほど放置されていて少しやきもきしたが、今日晴れてマージされた。いや正確にはマージされたというより、いくらかの編集を経てコミットが追加され、クローズされた。

追加された変更は、例えば数値にマッチする正規表現が長すぎたので分割する(最初は整数と浮動小数点数が同じくくりでマッチされていたので、そういう方針なのかと思って尊重していたのだが、単に短いから構わないという扱いだったのだろうか)とか、ハイライト時の分類を直すとか(ここは純粋にどうするべきかわかっていなかった)、いくつかのエッジケースを直す(ありがたい)などだった。

というわけで、しばらくすればGitHubのハイライトも修正されるだろう。今までもそれなりに使ってもらえるツールは作っていたが、これまでとは比べ物にならない規模の人数が目撃するソフトウェアの端っこに自分の小さな爪痕を残したというのは、結構気分がいいものだ。

これでtoml.tmbundleのレポジトリのコントリビュータは3人になった。プライマリ作者と、TOMLそのものの作者Tom Preston-Werner(@mojombo)と、私である。ヤバい人々でビビるが、それ故に嬉しさもひとしおというところだ。

みなさんもGitHubのハイライトに不満があれば、PRを投げつけてみてはいかがだろうか。

CとC++のIdent

今日、妙な話を聞いた。CかC++かでコードを書いていたところ、変数名を長くすると32文字だかそこらでtruncateされてしまい、前半が同じ名前の変数を使っているとコンパイルエラーになってしまったというのだ。私は(少なくともC++で)変数名に文字数制限があるとは思っていなかったので、かなり驚いた。むしろコンパイラを疑った。

というわけで規格書にあたってみることにした。C規格としてN1256(C99)を、C++規格はN3337(C++11)を参照する。

まず、Cでは§6.4.2.1の Language Lexical elements > Identifiers > General > Implementation limits に、

an implementation may limit the number of significant initial characters in an identifier ... Any identifiers that differ in a significant character are different identifiers. If two identifiers differ only in nonsignificant characters, the behavior is undefined.

との記述がある。要するに、処理系は識別子の最初の何文字か以降は無視してしまって構わないということだ。そしてもし無視されない前半部分が重複していて無視される後半部分のみが違う識別子の組が登場した場合、その挙動は未定義となる。

これは驚きだ。識別子として使える(意味を持つ)文字数は処理系定義なのか。ここしか見ていないのでわからないが、もしかしてこの文字数は1文字でも規格準拠なのだろうか……標準ライブラリのほとんど全てが使えないが。

ちなみにGCCでは文字数に制限はない(以下参照)。まあそりゃそうだ。

Using the GNU Compiler Collection (GCC): Identifiers implementation


ではC++ではどうだろう。§2.11のLexical convensions > Identifiersには、

An identifier is an arbitrarily long sequence of letters and digits.

と書かれている。任意の長さが許されるようだ。話が終わってしまった。まあ、安心するべきことではある。

まとめると、Cを書いている時に長い変数名を使って妙なことが起きた場合(そしてそれが純粋に変数名が長いことによる問題だった場合)、コンパイラが悪いとは言えない。しかしもしC++で識別子の後半が無視された場合は、コンパイラが悪い。プログラマは自信を持ってIssueを報告して構わないということだ。

std::ratioについて

C++にはコンパイル時の有理数計算ライブラリがある。個人的な理解では、これは<chrono>のために入ったライブラリで、<chrono>durationを綺麗で速い(使いやすいとは言っていない)やり方で実装するためのものだ。コンパイル時に比率を計算できるので、実行時にかかるオーバーヘッドはせいぜい整数か浮動小数点数の掛け算くらいしか残らないことになる。

ご存知の通り、浮動小数点数は我々の知る実数とは少しだけ異なる。有限のメモリ領域を使ってできる限り広い範囲の数を表現しようとしてはいるが、もちろん完璧ではない。特に、我々は指が10本なので単位系を10の倍数ベースに作っており、そして悲しいことに0.1は2進数では循環少数になる。なので計算途中に丸めが入ることになる。これを避けるためには、整数同士の比で計算するしかない。整数は(実行時に伸びる多倍長整数を使えば)事実上無限の長さを扱えるので、有理数で計算している限り丸め誤差は入る余地がない。まあ、コンパイル時に計算する場合は固定長でなければならないのだが。コンパイル時多倍長演算を実装するという手は置いておいて。

なのでstd::ratioは2つのstd::intmax_tを取る。それぞれが分子と分母に相当する。そして演算もサポートされていて、ratio_addratio_subtractratio_multiplyratio_divideが用意されている。これらを使えば、約分まで済んだstd::ratioが得られる。また、比較も用意されていて、ratio_equalその他がそれぞれ定義されている。

また、主目的が単位換算なので、SI接頭辞(std::kilostd::milliなど)も定義されている。

cpprefjp.github.io

さて、単位には非常に大きな数値が登場するものがあったりする。例えば、分子量は6.02e+23の大きさがあり、原子1個あたりの重さは例えば12 / 6.02e+23グラムになったりする。ところで、log10(264)の値は20に届かない。なのでおそらくこの単位はほとんどの環境でオーバーフローを引き起こしてしまい、上記の枠組みに乗らなくなってしまうだろう。

となると、この際丸め誤差には目をつぶって、コンパイル時に浮動小数点演算を行うしかない。幸いなことにC++11以降ではconstexprがあるので、浮動小数点演算を行うことが可能だ。すると、既存のstd::ratioと相互運用可能なfratioとでも言うべきものを作る必要がある。

面倒なことにC++のnon-type template argumentは浮動小数点数を持てないので、static constexpr double value = /**/;のようにする必要がある。

struct mole
{
    static constexpr double value = 6.02e22;
};

template<typename Numer, typename Denom>
struct fratio
{
    static constexpr double num = Numer::value;
    static constexpr double den = Denom::value;
    static constexpr double value = num / den;
};

しかしながらstd::ratiostd::ratio::valueを持っていないので、共通で使うには::valueを使ってはいけない。とはいえfratioの中で::valueを使わざるを得ない以上(再帰的に定義できた方が便利だ)、共通のインターフェースを用意しておく必要がある。

template<typename T>
struct value_of
{
    static constexpr double value = T::value;
};
// std::ratioへの部分特殊化
template<std::intmax_t N, std::intmax_t D>
struct value_of<std::intmax_t<N, D>>
{
    // 有理数をdoubleに変換する
    static constexpr double value = static_cast<double>(N) / D;
};

これを噛ませれば、std::ratioを共通で使っていくことが可能になる。

正確な数値という利便性を捨ててしまうことになるが、多少の丸め誤差が問題ない場合はこれでことが足りる。もしどうしても正確な数値が必要なら、std::intmax_tのペアもしくは可変長引数を取ることによって多倍長を実現しても構わないのではあるが、使うのがとても難しくなってしまうので、難しいところだ。

Permission denied (publickey) on Travis.CI

タイトルで察した方は帰って結構です。

git submoduleというのがある。これは、別レポジトリをレポジトリの一部として管理できる機能で、要するに依存しているレポジトリのURLと対応するコミットをコードの一部として管理するものだ(大雑把すぎる)。すると依存レポジトリのバージョンをgitで一括で管理できるようになる。

$ git submodule add <repo URL>
# その後
$ git submodule init
$ git submodule update
# または、上記を一括で
$ git submodule update --init
# submodule の submodule まで再帰的に
$ git submodule update --init --recursive

submoduleも一つの独立したgitディレクトリとして振る舞うので、そこでpullしてから上でaddすることによって「依存関係にあるレポジトリのバージョンを上げる」という変更をgitで記録できる。

さて、このようなことを実践したレポジトリをpushしたところ、Travisが落ちた。Permission denied (publickey)と言われている。gitのsshプロトコルでアクセスした場合、sshの鍵がないとダウンロードできない(sshでログイン出来ないので)。もちろん、自分の秘密鍵を公開レポジトリに置くような馬鹿はいない。よって、submoduleをダウンロードできず、CIは失敗する。

さてどうしたものか。 とりあえずググると、Gistに解決策が転がっていた。まず、Travisが自動で行うgit clone --recursiveをやめさせる(git: submodules: false)。続いて、sedgit@github.comhttps://github.comに変換する。力技だ。

Travis-CI submodules · GitHub

確かにこれはLinuxでなら動くが、OS Xだと動かない。OS XUnixでありLinuxではないので、コマンドの挙動がほんの少しずつ異なる。以下に同じ問題に苦しめられた人間の怨嗟の声がある。

Sed: 'sed: 1: invalid command code R' on Mac OS X

何が起きているかというと、sed -iはファイルをその場で(in-place)書き換える。これは怖いので、sedはバックアップファイルを作りたいと思っている。そのバックアップファイルの拡張子を-iオプションの引数として要求されているのだが、我々はそれを渡さず、代わりにsedスクリプトと変更して欲しいファイルを渡した。するとsedスクリプトをバックアップファイルの拡張子と思い込み、続くファイル名をsedスクリプトだと思って実行しようとして、「そんなコマンドはない」と返しているというわけだ。

回避するには、適当な拡張子をくれてやればいい。あるいは、空の文字列を渡せばそもそもバックアップファイルは作られない。

# この""が必要
$ sed -i "" 's/hoge/foo/g' foobar.dat

あとはこれで、Travisで環境を見て分岐してから実行して終わりだ。