nim-lang/Nim に issue を建ててちょっと恥ずかしい思いをした話

TL;DR

  • Nim で C++ の std::set のラッパーを書いた
  • 奇妙なコンパイラのバグを踏んだのでNimにissueを建てた
  • 4年前に建てられていたissue と同様のものだったために close された、恥ずかし〜

はじめに

私は Nim が好きでNimをよく書いています。 最近AtCoderでstd::setを使う問題がよく出題されており、 Nimでも C++ の std::set を使えるようにラッパーを作っていました。

github.com

ところが、これを使用した方からコンパイルエラーが出るという報告をいただきました。

コンパイルエラー

type CSet {.importcpp: "std::set", header: "<set>".} [T] = object
proc cInitSet(T: typedesc): CSet[T] {.importcpp: "std::set<'*1>()", nodecl.}
proc insert[T](self: var CSet[T],x:T) {.importcpp: "#.insert(@)", nodecl.}
proc `==`[T](x,y:CSet[T]):bool{.importcpp: "#==#", nodecl.}
var (S1,S2) = (cInitSet(int),cInitSet(int))
S1.insert 1
if S1 == S2: echo "wrong"
else: echo "correct"

このコードをコンパイルすると、以下のエラーがでて、NimからC++に変換した後のC++コンパイルに失敗します。

Error: execution of an external compiler program 'clang++ -c  -w  -~/.choosenim/toolchains/nim-0.20.0/lib -I~/codes/nim/ -o ~/.cache/nim/t_d/t.nim.cpp.o ~/.cache/nim/t_d/t.nim.cpp' failed with exit code: 1

~/.cache/nim/t_d/t.nim.cpp:153:7: error: invalid argument type 'TY_89ajMAHL4D29bP2aGRstu1mQ' (aka 'set<long long>') to unary expression
                if (!S1_X5vbOHyI0SI13LVkxEIzEQ==S2_q2YrwwKwgOf4xdemDnRTYA) goto LA4_;
                    ^~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

ほう、という感じですね、

このC++コードの全文は以下です。

N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
    tyTuple_WVskh7WUQHL0ugdByxngAw T1_;
    nimfr_("t", "/path/to/t.nim");
    nimln_(8, "/path/to/t.nim"); 
    T1_.Field0 = std::set<NI>();
    T1_.Field1 = std::set<NI>();
    S1_X5vbOHyI0SI13LVkxEIzEQ = T1_.Field0;
    S2_q2YrwwKwgOf4xdemDnRTYA = T1_.Field1;
    nimln_(9, "/path/to/t.nim"); 
    S1_X5vbOHyI0SI13LVkxEIzEQ.insert(((NI) 1));
    nimln_(10, "/path/to/t.nim"); 
    {
        if (!S1_X5vbOHyI0SI13LVkxEIzEQ==S2_q2YrwwKwgOf4xdemDnRTYA) goto LA4_;
{       echoBinSafe(TM_earX20ePV9cxitomwdUMS7g_2, 1);
}   }
    goto LA2_;
    LA4_: ;
    {
        nimln_(11, "/path/to/t.nim"); 
        echoBinSafe(TM_earX20ePV9cxitomwdUMS7g_4, 1);
    }
    LA2_: ;
    popFrame();
}
}

このエラーを見ると分かるように、要は if S1 == S2: ... の条件分岐を C++ に翻訳した時に、 if (!S1 == S2 ) goto LA4_; になってしまっているようです。 条件分岐をミスっている様子で、これは正解は if (!(S1 == S2 )) goto LA4_; となるべきですね。

つまり、if A == B: X else: YC++に翻訳する際に、 if (!(A == B))Y; else X; となるべきところを、 if (!A == B)Y; else X; としてしまっているわけです。どうして否定が入っているのかはわかりませんが、条件分岐のときにだけコンパイルが失敗するので、最適化か何かの都合で混入したバグなのでしょう。

nim-lang/Nim に issue を建てる

このバグは最新のNim0.20 でも発生します。流石にこれを放っておくわけにもいかないので issue を建ててみます。 勿論、(これはOSSを使うときには今までもよくあることなのですが) すでに issue で議論されていないか確認すべきです。 今回のissueに関連するワード equal function if override の単語で検索し、既に議論されていないか確認します。 無さそうだったので、早速 issue を建てます。

f:id:CHY72:20190913191300p:plain

こんな感じで、Bug Report のissueを建てようとすると、指針としてテンプレートが既に書かれています。便利ですね。 これに従って書いていけばよさそうです。

Think about the title, twice

と初めに書いてあります。タイトルは大事ですからね。 普段技術文書を英語で読むことはよくあるのですが、それに対して英語を書くのはとても苦手なので最低限文法が間違っていないかをgrammarly でチェックします。

そしてこれは戒めなのですが、最低限文法が通っているにしても意味が最低限間違っていないかをチェックするために Google 翻訳に2回かけるやつもやっておくべきでした...(英語→日本語→英語 or 日本語→英語→日本語 をして意味が崩れないかチェックするやつ)

例えば 建てた issue の冒頭、

Overriding equal function causes incorrect behaviour. Nim compiler translates !(a == b) into !a == b and fails to compile.

と拙い英語で書いていたのも、

Overriding an equal function will cause incorrect behavior. Nim compiler converts !(a == b)! to a == b and fails to compile.

になって、なんとなくこっちのほうがいい感じな気がします(非ネイティブ並の感想...)*1

なんやかんやあって issue を建てました。 github.com

回答が来る

f:id:CHY72:20190913192953p:plain

要は # == # (#は出てきた順にその番目の引数を表す) ではなくて、 (# == #) と定義してくれとのこと。なるほど *2Low Priority のタグが付きます。

実は duplicate issue だった

f:id:CHY72:20190913193456p:plain

その後、これは duplicate issue だよ、と指摘されます。あちゃ〜やっちまった〜〜。 そういうわけで4年前に建てられたissueと同一の内容であったので close されました。 Low Priority なので 4年間放置されていたバグであっても仕方がないですね。

恥ずかし〜〜〜〜〜〜〜〜〜〜

どうすれば duplicate issue を回避できたか、 equal function if override で検索したのに加えて C++ compile error でも検索する、もしくは Label:C++ codegen を探しておく、が正解だったっぽいですね。

う〜 。

未来永劫 duplicate issue を建てた者としてnim-lang/Nimに記録が残るのはちょっと悲しいですね〜〜〜。

総計 12028 件の Nimのissueのうち duplicate を含む issue は たった 502 件 しかなくてそのうちの一つになっちゃったな〜〜〜〜。

うひ〜〜〜。

あ〜〜〜〜〜〜〜〜。

おわり。

*1:というか function って可算名詞ですね。

*2: ちなみに、Nimの流儀として全ての二項演算をこのように (#+# ではなく (#+#) と書く) 定義するといった指針はありませんのでこれは特殊な書き方と言えます。