博多電光

blog.hkt.sh

Firefoxにパイプライン演算子を実装した

主に大学とバイトで忙しくオープンソース的な活動がしばらくできていない@hakatashiだが、大学の実験でSpiderMonkeyの開発に参加し、なおかつ本家にマージされるという貴重な経験を得たので、これまでの経緯とかをまとめておく。

かつ、本実験では実験レポート代わりにブログ記事を1つ書くことになっている (すごい) ので、そちらのレポートも兼ねている。

相方のブログエントリはこちら:

siquare.hatenablog.com

SpiderMonkeyとは

SpiderMonkeyとは、JavaScriptの処理系の一つである。フロントエンドエンジニアならばまず知らない人はいない。主にMozillaが保守・開発を行っており、Firefoxブラウザに標準で搭載されている。あまり知られてないが当初は Netscape Browser に搭載するために実装されたJSエンジンであり、同時に世界で初めてブラウザに実装されたJSエンジンでもある。

SpiderMonkeyの処理系としての大きな特徴は、JavaScriptコードを実行する際に、一度中間言語であるバイトコードに変換してからインタプリタに通すという点である。この点で、直接マシンコードに落とし込んで実行するV8などの他のJSエンジンとは大きく異なる。これはある意味プラットフォームに応じた最適化がやりづらいとも言えるが、一方でコンパイラのコードがフロントエンドとバックエンドにはっきりと分離できるため、可搬性が高く、開発にかかるコストが低いのも特徴である。今回SpiderMonkeyに新しい機能を実装する際にも、この構成に大いに助けられた。

実装までの経緯

本来ならば、こんな大規模プロジェクトに、しがないフロントエンドエンジニアであるところの僕が関わる機会はまず無いのだが、どういうわけかガッツリと関わりを持ってしまった。これもいろんな偶然と人との出会いがあった故である。

僕が通っている電気系の学科で行われる実験の一つに、「大規模ソフトウェアを手探る」というタイトルのものがある。これは一言で言うと「大規模なOSSプロジェクトを一つ選び、何かコントリビュートする」という実験 (?) で、シンプルながら非常に意欲的な内容である。オープンソース主義者であるところの僕も当然ワクワクする実験だったのだが、さらに驚くべきはこの実験を担当するTAの一人がMozillaのコミッターであり、Firefoxリポジトリに日常的にパッチを投げ続ける、まさに「JavaScriptのプロ」であったという点である。実験中にもレクチャーがあったが、実験課題としてSpiderMonkeyを選択した場合、なんとその場で彼がコードレビューして、本体にマージすることも可能であるとのこと。

実験を通して@siquareとペアを組んだ僕は、2人で相談して「SpiderMonkeyに新しい言語機能を追加する」という課題に取り組むことに決めた。お互いJSエンジンへのコントリビュート経験もなくやや敷居の高そうな内容だったが、TAの一人がMozillaのコミッターであること、2人ともJavaScriptには日常的に世話になっていること、そしてこんな機会はめったに巡ってこないであろうことを考えて、すこし背伸びをしてみることにした。

タイムライン

実験に割り当てられた時間は10日間あった。タイムラインは以下のような感じである。

  • 1日目: 実験に関する大まかなレクチャー。
  • 2日目: 実験で苦楽を共にするペアを組む。取り組む課題を決める。環境構築。パイプライン演算子のドラフトを読む。
  • 3日目: 環境構築。トークナイザの修正。
    • この時点で@siquareがすでにトークナイザを完成させてASTを吐けるようになっていた。はっやーい。
    • そのころ@hakatashiはいまだに環境構築をしていた。
  • 4日目: バイトコードエミッタの修正。
    • ASTをバイトコードに変換する処理。ソースコードで言うところのBytecodeEmitter.cppに相当する。
    • 既存の関数呼び出しのバイトコードを吐いてる所を流用して書いたら、一応それっぽいものが動いた。
      • 提案書に書いてあるサンプルコードとかもちゃんと正しく動いたので、この時点でもう勝った気になっていた。
  • 5日目: 演算子の優先順位の修正。テストの記述。
    • 演算子の優先順位をより仕様に沿った形に修正。および仕様へのフィードバック (後述)。
      • この作業にだいぶ詰まって時間を溶かす。
    • テストは書き慣れたJavaScriptで記述する。楽しい!!
  • 6日目: テストを完成させる。優先順位問題の解決。評価順問題について検討。
    • 一応リグレッションがないように他のテストも全て走らせたところEC2の4コアマシンで8時間程度かかった。世の中のFirefox開発者の開発用マシンはどうなっているのだろうか。
  • 7日目: 評価順問題の解決。Reflect.parse API の実装。Bugzillaにパッチを投げる。
    • Reflect.parseは、SpiderMonkeyのJS部分から利用できるAPIで、JSコードの文字列をパースしてASTをJSオブジェクトとして取得できる関数である。
      • これのデバッグ自体にパイプライン演算子を使ったら、とてつもなく便利だった。誰だこれ実装したの。
      • 例: '10 |> parseInt' |> Reflect.parse |> (_ => JSON.stringify(_, null, ' ')) |> print
    • 実際に投げたパッチはこれ 1405943 - Implement Pipeline Operator |>
  • 8日目: コードレビュー (1回目)。
    • ありがたいことに翌日にはBugzilla上でTAからコードレビューを受けることができたが、不真面目な学生なので実験当日まで放置していた。
    • TA以外からもコメントを受け、configure flag の下にコードを隠すようにしたのもこのタイミング。
    • 2人がかりで修正し、再度パッチを投げる。
  • 9日目: コードレビュー (2回目)。
    • 本家masterへのrebase作業や、パッチ分割作業など。
    • 再度修正し、パッチを投げる。
  • 10日目: 最終発表
    • ⋯⋯があったらしいが、@hakatashiは今年最大級の絶起をキメてしまったため痛恨の欠席。
    • https://pbs.twimg.com/profile_images/3422504765/31886cb104d4d32f9a8e5110dba047eb.png

パイプライン演算子とは

JavaScriptは現在、仕様に関する議論が最も盛んな言語である。新しい機能や構文を追加するためのドラフトが日夜更新され、それを正式な仕様に組み込むための議論や実装が日々進められている。僕らが今回実装した「パイプライン演算子」も、そうしたドラフトの仕様の一つである。

「パイプライン演算子」は、関数呼び出しの新しい文法を定義する演算子で、執筆時点ではstage-1の提案である。

通常、JavaScriptの関数呼び出しは以下のように記述される。

print('hoge');

なんのことはない、他の言語でも見慣れた、丸括弧を用いた記法である。この文法の問題点は、関数呼び出しの多重ネストを行った時にコードの可読性が大きく下がるというものだ。例えば、

print(Boolean(parseInt(getChars(100))));

という感じである。処理の順番としては getCharsparseIntBooleanprint となるはずなのに、コード上では記述する順番が逆転してしまう。これは見方にもよるだろうが、確かに読みづらいかもしれない。

これを解決するために提案されているのが、パイプライン演算子である。上のコードをパイプライン演算子で記述すると、以下のようになる。

100 |> getChars |> parseInt |> Boolean |> print;

見ての通り、関数と引数の順番が逆転している。これにより関数呼び出しを「実際に処理される順番に」記述することができ、コードの可読性が上がる⋯⋯というのがこの演算子が提案された所以である。

関数呼び出しというプログラムの根幹に関わる変更で、個人的に言わせてもらえばヤバい文法といった印象だが、実はこのパイプライン演算子は他の言語でも意外と実装されている。有名どころで言うと Julia, OCaml, F#, Elixir, さらにはLiveScriptやElmといったAltJS系の言語でもこの演算子は実装されている。こうした背景にはLiveScriptやElmのようにJavaScript関数型言語化を推し進めようという流れがあり、pappの陽な仕様を提案しているコミュニティと深いつながりがあったりなかったりするのだが⋯⋯ともかく、関数呼び出しを便利に書ける演算子だということが一番重要なポイントである。

詳しい話は以下あたりの記事が詳しい。

qiita.com

abouthiroppy.hatenablog.jp

今回、SpiderMonkeyに実装する新機能として、このパイプライン演算子の先行実装を選択した。理由はいくつかあるが、主なものは、

  1. 「新しい演算子」という、ユーザーの目に見える部分の実装で、個人的にモチベーションが高かったため
  2. SpiderMonkey (や、その他ブラウザ) で、既に提出されているパッチや実装がなかったため
  3. 関数呼び出しという、既に存在する機能に対する文法なので、バイトコードから先の実装は変更しなくてよく、実装が軽そうに見えたため
  4. この仕様を先行実装するBabelプラグインが存在し、実装も数十行程度とシンプルだったため

という感じである。

開発

実際の開発は、相方の@siquareと協力して淡々と進めていった。最終的に本家に送信したパッチは、実装部分が@siquare, テスト部分が僕という分割がされているが、実際には特にどちらがどちらを担当したということはなく、お互いが実装とテストを行ったり来たりしながら開発を進めていった。

実装に関して、細かい話は@siquareのブログエントリに書いてあるので省略するが、特に難しかったというか、意外なつまづきポイントだったのが「関数と引数の評価順の問題」だった。

例えば、以下のコード

print(hoge);

と、

hoge |> print;

は、ほぼ同じ動作をするが、厳密には等価ではない。これは実装している最中にパイプライン演算子のドラフト仕様を読んでいて発見したことだが、通常の関数呼び出しでは「関数」→「引数」という順番 (printhoge) で評価が行われるのに対し、パイプライン演算子では「引数」→「関数」という順番 (hogeprint) で評価が行われることになっている。要するにコードに記述した順番に評価が行われるのだ。

確かにコードを書く側からすればそっちのほうが直感的かもしれない。しれないが実装する側からするとこれは少し考えものである。というのも僕らの実装ではSpiderMonkey中間言語であるバイトコード自体には手を付けず、パイプライン演算子を既存の関数呼び出しのバイトコードに変換するという手法を取っていた。それなのに厳密には直接の関数呼び出しとは違うとなるとだいぶ困ってしまう。これを厳密に「仕様に忠実に」実装するために大いに悩んだが、最終的にはpickというバイトコードの命令を用いて、「引数」→「関数」の順番でスタックに積んだあと、スタックを回転してから関数呼び出しの命令を実行することで解決した。

仕様へのフィードバック

今回開発を進める上で僕が特に注力したのは、実際のパイプライン演算子の実装というよりもむしろ、社会的な活動のほうだった。実験中にTAも仰っていたとおり、まだ仕様として固まっていないドラフトをJSエンジンに先行実装する意義とは、実際に実装を行ってみて得られた経験や、ユーザーが新しい仕様を使った時の知見を、フィードバックとして仕様に還元することにある。パイプライン演算子に関する議論はおもにGitHubで今も激しく行われているが、実際にJSエンジンに実装してみたのは (おそらく) 僕らが最初ということもあって、実際に実装して初めて得られた疑問や知見などを積極的に仕様にフィードバックしていこうと考えていた。

パイプライン演算子の優先順位問題

そんな中で特に難しい問題だったのは、演算子の優先順位の問題である。

パイプライン演算子の優先順位をどのように定義するかに関して、執筆時点ではまだ仕様が確定していないが、なるべく優先度を低くするという点で見解は概ね一致しているようである。というのも、パイプライン演算子は複数連ねて書くので他の演算子と組み合わせた時に括弧無しで書けるのが望ましく、また見た目にも「大きい」演算子なので、優先順位が低いほうが直感的であるというのが主な理由である。

このような流れもあって、当初はこの意向に従い、「あらゆる演算子の中で最も優先順位が低い」という実装を行う予定だった。しかし実際に実装してみると、思わぬ壁にぶちあたった。

今回はじめて知ったことだが、実はJavaScriptの演算子の優先順位は、「単項演算子」→「二項演算子」→「三項演算子」という順番で並んでいる。ふだんJavaScriptを書いていて意識したことはなかったが、言われてみればたしかにそうである。SpiderMonkeyではこの仕様を受けて、演算子が取る引数の数を最初に見て、まずはその順に優先順位を確定させてしまうという実装になっていた。

今回僕らは三項演算子 (?:) よりもパイプライン演算子の優先順位を低く設定しようとしたため、SpiderMonkeyのコードに大きな変更を加えなければいけないことに気がついた。これには多大な労力を伴う上に、「単項演算子」→「二項演算子」→「三項演算子」というJavaScript演算子の優先順位のルールを崩すことになってしまう。果たして本当にパイプライン演算子の優先順位は「全ての演算子の一番下」であるべきかと疑問に思った僕らは、パイプライン演算子の提案に対してその旨フィードバックし、実装は「二項演算子の一番下」で行うことにした。

(なお、ドラフトの仕様では最初から「二項演算子の一番下」という優先順位になっている。)

スプレッド演算子との組み合わせ

もう一つ、関数呼び出しのコードを手探っていて気になったのは、スプレッド演算子に対する対応である。

ES2015で定義された比較的新しい関数呼び出しの文法に、スプレッド演算子が存在する。例えば、以下のコード

const array = [1, 2, 3];
print(...array);

は、

print(1, 2, 3);

とほぼ等価である。

これをパイプライン演算子に応用するとどうなるのかを考えたところ、以下のような文法が考えられるのではないかと考えた。

...array |> print;

ヤバいを通り越してバイオハザードのコードだが、いちおう論理的には辻褄が合っている⋯⋯かもしれない。

こういった応用手法に関して突っ込んだ議論は見られなかったので、実装していて考えたことをいくつかまとめて、新たにIssueを立てた。さすがにあまりポジティブな反応は得られなかったが、パイプライン演算子の提案を前に進めるためにいちおう一定の貢献はできたのではないかと考えている。

本家コードへのマージ

そんなこんなで紆余曲折を経て、SpiderMonkeyに仕様どおりのパイプライン演算子を実装することができた。

stage-1の提案というかなり実験的な機能だということもあって、当初は本家のコードへのマージは現実的でないと考えていたが、TAの尽力もあって、configure flag 付きという条件のもとで本家ブランチへのマージができるようになった。要するに現時点では配布版のFirefoxのビルドには含まれないし、この演算子を試すにはソースからビルドし直さないといけないけれど、ソースコードには残して貰えるということである。

2回のコードレビューを経て、約束通り本家ブランチにマージして頂き、コミット履歴に僕と@siquareの名前を残すことができた。我々エンジニアにとってはこれ以上ない栄誉である。

hg.mozilla.org上のコミット:

GitHub上のコミット:

というわけで、僕も@siquareも、今では立派な (?) Firefoxコントリビューターである。

今回実装したパイプライン演算子を今すぐ試したい場合は、このページの手順に従ってSpiderMonkeyをビルドする必要がある。その際、./configureのフラグに、--enable-pipeline-operatorをつけるのをお忘れなく。

全体を通しての感想

まず何よりも、本実験を統括してくださった田浦先生、ならびに実験を通して非常にお世話になったTAの藤澤さんに深い感謝を。ありがとうございます。お世辞抜きで最高の実験でした。最終日は本当に申し訳ありませんでした。

加えて、実験ペアの@siquareにも大きな感謝を。最初から最後まで迷惑かけっぱなしで、最終発表に加えてこの実験レポートの執筆も遅れに遅れてしまったので本当に反省したい。謝謝。

そして何度でも書くが、今回の実験でSpiderMonkeyにコントリビュートするという貴重な経験を得ることができて、感激の至りである。やっぱり普段の活動ではこういったプロダクションレベルの大規模OSSに関わるチャンスは少なく、特にSpiderMonkeyのような基盤的なソフトウェアには逆に苦手意識すらあったので、これを機に他のOSSにも手を出していけたらと思っている。

実験全体を通して、レポート代わりのブログ記事や、毎回の進捗発表の時間など、先進的な取り組みを多く取り入れていて、参加して非常に楽しい実験だった。EEICの名物実験と銘打ってもいいくらいだと思う。もしこの記事を読んで実験に興味を持った東大生がいたら、ぜひ電気系に進学してほしい。というわけで、#進振りはEEICへ