siquare weblog

ねこが好きです。

授業でFirefoxにコントリビュートした話

本記事は東京大学工学部電子情報工学科の「大規模ソフトウェアを手探る」という実験の報告記事です

学校の授業でOSSにコントリビュート?

東京大学工学部電子情報工学科にはあらゆる工学系学科の例に漏れず「実験」の授業があります。 実験の内容はいくつかのテーマの中から選択することができ、例えば

  • OpenCV/OpenGLによる映像処理」
  • 「情報可視化技術とデータ解析」
  • 「AIロボットを作ろう」

などのテーマが用意されています。 私はその中から 「大規模ソフトウェアを手探る」 というものを選択し、 その結果としてFirefoxJavaScriptエンジンである SpiderMonkeyにコントリビュートすることができました。

ところで、実験といえば実験の概略・原理・方法・結果・考察をまとめた実験レポートに頭を悩まされることが付きものです。 しかしなんとこのテーマではブログ記事を実験レポートとして代用することができます!(結局同じようなことを書くことになるけど) というわけで、本記事では実験の詳細およびコントリビュートの内容を実験レポートとしてご報告しようと思います。

「大規模ソフトウェアを手探る」

「大規模ソフトウェアを手探る」という本実験は、2コマ(210分)x10日の35時間で、変更したいOSSの選定と変更を行います。 初日は大規模ソフトウェアの例としてgnuplotが選ばれ、実際にgnuplotを変更する手順が紹介されます。 2日目は変更したいOSSの調査とチーム分けが行われ、3日目にチーム(2名 or 3名)での開発がスタートします。

私は @hakatashi とペアになって「SpiderMonkey に Pipeline Operator を実装する」というテーマを選びました。

コントリビュートの内容

JavaScriptとは

私たちが手を加えたSpiderMonkeyというソフトウェアはFireFoxJavaScriptエンジンです。 JavaScriptの実装はブラウザごとにいろいろありますが、 それぞれの規格がばらばらではそれに対応させられるエンジニアはやっていられません。 そこで、TC39という標準化委員会がECMAScriptという標準規格を作成しています。

ある仕様がECMAScriptに取り入られるためにはいくつかの段階を経る必要があります。 段階はstage-0(ただのアイデア)からstage-4(少なくとも2つの実装を要する)までの5つに分かれています。 私たちが実装したPipeline Operatorは初めはstage-0でしたが、実装の途中でいつのまにかstage-1になっていました。

Pipeline Operatorとは

Pipeline Operatorは次のような文法です。

1 |> print

このコードは次のコードと等価になります。

print(1)

この例では面白くないですが、Pipeline Operatorを複数の関数に連続的に適用していくと少し面白くなります。

function doubleSay (str) {
  return str + ", " + str;
}

function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}

function exclaim (str) {
  return str + '!';
}

let result = "hello"
  |> doubleSay
  |> capitalize
  |> exclaim;

result //=> "Hello, hello!"

"hello" |> doubleSay |> capitalize |> exclaim の方が exclaim(capitalize(doubleSay("hello"))) よりわかりやすい気がしませんか?? 通常の関数呼び出しは引数のほうが先に評価されるにも関わらず、 コードを書く際は関数の方を先に書かなければなりません。 関数の戻り値を次の関数の引数としているような場合は、 括弧の深さに注意しながら一番深いところから順番に見ていかないと行けないのです。 一方でPipeline Operatorはコード上で登場する順番で評価も行われます。 複数の引数を取る関数だとまた事情が違ってきますが、 この記法はなかなか便利そうだと思います。

TC39のページ *1ではより実用的な例がいくつも紹介されています。

テーマ選択のモチベーション

私は普段からJavaScriptを使いWebサービスを作っているので、 JavaScriptという親しみを持てるOSSの開発に抵抗はありませんでした。 しかしながら、「JavaScriptに未実装の仕様を使いたいときはBabel *2 を使えば一発で解決できる」という事情もあり、 JavaScript本体へのコントリビューションへのモチベーションを持てずにいました。

このような思いを持っていたにも関わらずこのテーマを選択したのは、 ただただひとえに「授業のTAにSpiderMonkeyのコミッターがいたこと」が理由です。 授業に参加してみて本当に驚いたのですが、TAがコミッターだったのです。 しかもかなりアクティブに開発に参加されている方だったので、レビューもできればマージもできる権限を持った方のようでした。 OSSにコントリビュートする際、そのOSSのコミュニティとコミュニケーションを取るのは、 私のようにOSSの開発に参加したことがない(あと英語も下手な)人間に取ってはかなりのハードルです。 しかし、SpiderMonkeyをテーマにすればその心理障壁が取り除かれ、OSSにコントリビュートできるかもしれないと思いました。 かねてからOSSにコントリビュートしてみたいと思っていた私にはSpiderMonkeyは最高のテーマだったのです。

また別の理由として、TC39のプロセスで提案されている機能には仕様がついているから実装しやすいというのもあります。 まったくゼロの状態から何かの機能を実装するよりかなりとっつきやすくなっていたと思います。

また、提案された手法は誰かしらから実現を望まれている機能であることが保証されています(かなり少ない人数かもしれませんが…)。 需要という点でもJavaScriptの提案仕様をテーマに選択するのは理にかなっているのです。

コントリビュートの方法

それではここからは実際に行ったコントリビュートの手順を紹介します。 コントリビュートは次の手順で行いました。

  1. 環境構築
  2. コード変更
    1. Tokenizer の変更
    2. Parser の変更
    3. BytecodeEmitter の変更
    4. テストの追加
    5. こまごましたこと
  3. パッチを投げる
  4. レビューを受ける
  5. レビューを元にコードを修正する
  6. 終わり
    1. 取り込まれたり
    2. 取り込まれなかったり

環境構築

基本的にクローンしてきてmakeするだけで動きます。 ただしリポジトリがめちゃくちゃでかいのでクローンはもちろんgit statusすらそこそこ遅いです。 最終的にコミュニティに送るのはただのパッチファイルなので変更に関連するファイル以外はgit管理から外しても良いかもしれません。

コンパイルに関しては詳しい手順がMozillaWiki *3 に書かれているので特に難しいことはありませんでした。

cd spidermonkey/js/src
cp configure.in configure && chmod +x configure # or autoconf2.13 or autoconf-2.13
mkdir build_DBG.OBJ 
cd build_DBG.OBJ 
../configure --enable-debug --disable-optimize
make # or make -j8
cd ..

コードの変更

JavaScriptのように仮想マシン上で動作するプログラミング言語は、 プログラマが書く言語(JavaScript)から仮想マシンのための言語(Bytecode)に翻訳されます。 今回の実装方針としては、Pipeline Operatorに対応するバイトコードを新しく定義する方針と、 Pipeline Operatorを既存のバイトコードのみでうまい具合に表現するという方針の二つが考えられました。 前者の実装方針の場合はバイトコードコンパイラにも手を加える必要があるため過剰に面倒です。 また、Pipeline Operatorはようは関数を呼び出すだけの演算子なので既存のバイトコードだけで簡単に表現できるはずです。 以上の理由から後者の実装方針を採用することにしました。

さて、そのような方針の場合、変更は次のような手順で行うことになります。

  1. Tokenizer の変更
  2. Parser の変更
  3. BytecodeEmitter の変更
  4. テストの追加
  5. こまごましたこと

Tokenizerはコード上の意味的なかたまりを識別子として認識するための仕組みです。 今回は|>という記法を新たに追加するので、これを新たなTokenとして登録します。 Tokenizerはコードを頭から読んでいき、|>を識別したときにこれに対応するTokenを生成します。

js/src/frontend/ParseNode.h

/* \
* Binary operators. \
* These must be in the same order as TOK_OR and friends in TokenStream.h. \
*/ \
+F(PIPELINE) \
F(OR) \

js/src/frontend/TokenStream.cpp

case '|':
  if (matchChar('|'))
    tp->type = TOK_OR;
+#ifdef ENABLE_PIPELINE_OPERATOR
+ else if (matchChar('>'))
+   tp->type = TOK_PIPELINE;
+#endif

ParserはTokenizeされたコードをAST(Abstract Syntax Tree)という形式に変換します。 SpiderMonkeyのParserは二項演算子をParseする場合は適当に優先順位を定義するだけでいい感じのASTにしてくれます。 あるJavaScriptのコードがどのようなASTに変換されるかはparse関数を使うことで知ることができるので、 これを使って正しいASTが出力されているかどうかを確認します。

js> parse('1 |> print')
(STATEMENTLIST [(SEMI (PIPELINE [1
                                 print]))])

js/src/frontend/Parser.cpp

static const int PrecedenceTable[] = {
-    1, /* PNK_OR */
-    2, /* PNK_AND */
...
+    1, /* PNK_PIPELINE */
+    2, /* PNK_OR */

次にASTからBytecodeを生成します。 Bytecodeを生成する物体はBytecodeEmitterと呼ばれています。 関数を呼び出すBytecodeはcall命令なので、適当に引数を評価した後にcall命令を呼び出してあげればうまく動きそうです。 どのようなバイトコードが出力されるかはdis関数によって知ることができるので、これを使って試行錯誤していきます。 最終的なバイトコードは次のようになりました。

js> dis('1 |> print')
loc     op
-----   --
main:
00000:  one                             # 1
00001:  getgname "print"                # 1 print
00006:  gimplicitthis "print"           # 1 print THIS
00011:  pick 2                          # print THIS 1
00013:  call 1                          # print(...)
00016:  setrval                         #
00017:  retrval                         #

Source notes:
 ofs line    pc  delta desc     args
---- ---- ----- ------ -------- ------
  0:    1    17 [  17] xdelta
  1:    1    17 [   0] colspan 10

肝はpick命令で、これを使うことで引数を評価した後に関数を評価しても、関数をスタックの後ろに回すことができます。 この部分はこの命令があることに気づいてしまえば簡単に実装できてしまうのですが、実装中はこの命令の存在になかなか気づかず、 もしかしてインタプリターにも手を加えなければならないのかと暗い気持ちになっていました。 ちなみにBabelの実装はただ単に演算子を関数呼び出しに置換するだけなので、評価順という点では仕様どおりになっていません。 この点も、この機能を実装したいモチベーションの一つです。

さて、最低限以上のような変更を施せばPipeline Operatorが実装できます。 そこで、次に書かなければならないのがテストです。 SpiderMonkeyのテストはテストスクリプト(JavaScriptで書ける!)に対して実行バイナリを渡すという形で実行します。 Pipeline Operatorはまだstage-1の仕様なので、まだ決められていないことが多いです。 現段階では取り敢えずもっとも有り得そうな仕様 *4 を仮定して進めるしかないので、テストもそのように書いていきます。 ちなみに、テスト用のJavaScriptには当然まだPipeline Operatorは実装されていないわけですから、 テストコードに素直に演算子を書くとその時点でSyntax Errorが出てしまいテストになりません。 これを回避するためには、テスト全体をevalで囲んでやる必要があります。 なかなか気持ち悪い気もしますが、しかたありません。

パッチを投げる

変更した内容からgit format-patchを使ってパッチファイルを作成します。 その後、Bugzilla というサイトから提案をバグとして報告し、パッチファイルを送ります。

1405943 - Implement Pipeline Operator |>

f:id:siquare:20171025015349p:plain

レビューを受ける

パッチに対するレビューを受けます。 今回はTAの藤澤さんがメンターになってくれたので、かなりスムーズにレビューを受けることができました。

f:id:siquare:20171025015412p:plain

レビューを元にコードを修正する

修正します。 今回はSpiderMonkeyのコードフォーマットに従っていないという指摘が中心で、 根本的な実装方針に関する指摘はありませんでした(相談しながら進めたので当然ですが)。 なので、修正もスムーズに進めることができました。

おわり

以上でコントリビューションのプロセスは終わりです。 最終的に変更がコミュニティに受け入れられれば変更は取り込まれますし、そうでなければ取り込まれません。

結果

冒頭で紹介したように、無事、私たちの変更はSpiderMonkey本体に取り込まれることになりました 🎉 *5

コミットのログとしては私とペアの@hakatashiとのものに別れていますが、 実際にはどちらの内容も二人で協力しながら行ったものです。 コミットログに残す際にキリが良いようにこのような形のログを作り直したというだけです。

感想

この授業の歴史の中でも、授業中に行った変更がOSS本体に取り入られた例はこれ以外に1件あるだけということなので、 今回はかなりうまく言った方なのだと思います。 それもひとえにTAの藤沢さんとペアの@hakatashiのおかげです。 また、このような機会を与えてくれたこの授業そのものにも心から感謝しています。 来年度以降もこのような素晴らしい取り組みが続いていくことを心から願っています。

また、個人的なこととしては、今回学んだOSSへの取り組み方を活かして他にもいろいろなOSSへコントリビュートしてみたいと思っています。 具体的には普段から使っているOSSであるRailsやReactに興味があります。 それぞれのコミュニティには特色があるのでしょうから、結果がどうなっていくか見当もつきませんが精一杯やってみようと思います。

それではだいぶ長くなってしまいましたが、ここで報告を終わりといたします。 ここまでお読みいただきありがとうございました。

追記

ペアの @hakatashi のブログ記事も公開されました。 実験のタイムラインや社会的な活動についてこの記事よりかなり詳しく書かれています。

hakatashi.hatenadiary.com

*1:https://github.com/tc39/proposal-pipeline-operator

*2:BabelはECMAScriptに取り込まれていない新仕様を先取りしたいというモチベーションで開発されたプロダクトで、 新仕様の文法を現行の仕様のJavaScriptに書き換えてくれるトランスパイラみたいなやつです。 ちなみにPipeline OperatorもBabelを使えば使用可能です。 また、後から聞いた話ですが、現場のエンジニアがJavaScriptエンジンの開発を積極的に進めて欲しい理由として、 TypeScriptなどBabelを使わない環境でも新仕様を使いたいというのもあるそうです。 確かに、まったくそのとおりでした。

*3:JavaScript:New to SpiderMonkey - MozillaWiki

*4:specが提案されていたのでこれを参考に実装しました https://github.com/tc39/proposal-pipeline-operator/pull/51

*5:Pipeline Operatorは現在stage-1なので将来的に仕様が変更される可能性があり、デフォルトでは有効になっていません。 configure時にフラグとして--enable-pipeline-operatorを含めると使用できるようになります。