【Firefox】クラスの再宣言時のエラーメッセージを改善した【SpiderMonkey】


はじめに

大学の実験でFirefoxのJavaScriptエンジン「SpiderMonkey」にコントリビュートする機会が得られたので、そのときに行った手順を具体的に残したいと思います。
SpiderMonkeyに変更を加えたい人の参考に少しでもなれば幸いです。

あわせて読みたい

この記事は実験で実際に解決したSpiderMonkeyのIssueのうち「その1」についてです。

変更内容

バグレポート

今回取り組んだIssueはこちら。
https://bugzilla.mozilla.org/show_bug.cgi?id=1282431

つまり、

js> class Foo {}; class Foo {};
SyntaxError: redeclaration of let Foo

となっていたものを

js> class Foo {}; class Foo {};
SyntaxError: redeclaration of class Foo

となるように変更しました。

パッチ

今回行った変更内容のdiffはこちら。
https://hg.mozilla.org/mozilla-central/rev/dafc1282a533

変数、クラス宣言の仕組み

2, 3日ソースコードを眺めただけのやつが言うことなので、間違っているところ、勘違いしているところは多々あるかと思うので、優しく指摘していただけると嬉しいです。

  • SpiderMonkeyでは、変数宣言をコンパイラがパースしてBytecodeと呼ばれる中間言語に変換する
  • インタプリタがそのバイトコードを解釈する
  • コンパイラがパースするときやインタプリタが解釈するときに何か例外が起こるとエラーが出力される
  • enum class DeclarationKindで宣言を種類分けしている(Let, Var, Constなど)
  • Parser.cppというファイルの関数classDefinition(...)でクラスを定義している
  • DeclarationKindで管理されている変数が再宣言されたとき、static const char* DeclarationKindString(DeclarationKind)という関数で再宣言に対応する文字列を生成する

なぜclassletとして扱われていたか

  • letで宣言した変数はDeclarationKind::Letで管理されていたが、classで宣言された場合も同じようにDeclarationKind::Letで管理されていたから
    • 今回の変更を加えたあとも、パースされたあとは従来通りclassletとして扱われるようにする必要がある

どう変更したか

具体的にどのような変更を行ったのか、先に簡単に紹介します。

  1. NameAnalysisTypes.hというファイルのenum class DeclarationKindcase Classを追加
  2. Parser.cppというファイル内の関数classDefinition(...)の中で呼ばれている関数noteDeclaredName(...)DeclarationKind::Classを渡すように変更(以前はDeclarationKind::Letを渡していた)
  3. それ以外の部分では、DeclarationKind::ClassDeclarationKind::Letと同様に扱われるようにした

どう手探ったか

それでは上記の変更箇所を具体的にどのように手探っていったかを残していきます。
手探る前に、DXRというサイトを紹介します。
DXRでは、SpiderMonkeyのコードを検索することができます。
関数・変数の定義元に飛べたり、関数・変数が使われているところの一覧を出すことができたりして、該当のコードを探すときに超便利です。
検索のテクニックとして、path:js/srcを頭につけて検索するとパスを制限できるので、応答が早くなります。
似たようなサイトでSearchfoxというのもあります。

  1. "redeclaration of "で検索
  2. js.msgというファイルに以下のエラーメッセージの定義が見つかる。JSMSG_REDECLARED_VARという識別子でこのエラーを呼んでいそうと分かる。

    MSG_DEF(JSMSG_REDECLARED_VAR, 2, JSEXN_SYNTAXERR, "redeclaration of {0} {1}")
    
  3. "JSMSG_REDECLARED_VAR"で検索

  4. Parser.cppというファイルの1149行目あたりにエラーを呼んでそうな以下の関数が見つかる

    errorWithNotesAt(Move(notes), pos.begin, JSMSG_REDECLARED_VAR,
                     DeclarationKindString(prevKind), bytes.ptr())
    
  5. ここで、Parser.cppの1149行目にブレークポイントを設定してデバッガを走らせてみる。手順は次のような感じ。lldbというデバッガを使った。詳しい使い方は別途調べてみてください。

    $ lldb ./dist/bin/js
    (lldb) b Parser.cpp:1149
    # ブレークポイントが設定される
    (lldb) r temp.js
    # さっきのブレークポイントで処理が止まる
    (lldb) p DeclarationKindString(prevKind)
    # => "let" となっていた! ここが"class"になればOK!
    
    temp.js
    class Foo {};
    class Foo {};
    
  6. 関数DeclarationKindString(...)の定義を見ると、enum class DeclarationKindの種類によって適切な文字列を返していた

    static const char*
    DeclarationKindString(DeclarationKind kind)
    {
      switch (kind) {
    
        ...
    
        case DeclarationKind::Var:
          return "var";
        case DeclarationKind::Let:
          return "let";
        case DeclarationKind::Const:
          return "const";
    
        ...
    
      }
      MOZ_CRASH("Bad DeclarationKind");
    }
    
  7. よって、enum class DeclarationKindcase Classを追加して、DeclarationKindString(DeclarationKind)ではDeclarationKind::Classに対して"class"を返すように変更すれば良さそうと分かる

  8. パースされたあとはDeclarationKind::Classは従来通りDeclarationKind::Letとして扱われるべきなので、様々なところのswitch文でのDeclarationKind::Classの扱われ方はDeclarationKind::Letと同じように変更する

  9. ここまでで、DeclarationKind::Classで登録されている変数に対しては適切に"class"を返すようになったが、クラスを宣言したときにメモリ(キャッシュ?)に登録される際にDeclarationKind::Letで登録されていることは変わりないので、次はクラスを宣言してメモリ領域に登録している部分を探す必要がある。

  10. DeclarationKind::Letがどのように使用されているかを探してみる。運の良い(?)ことに、10箇所くらいしか使われている部分がなく、探しやすかった。

  11. Parser.cppの8508行目に次のような記述があり、変数宣言を登録してそうな感じがする

        if (!noteDeclaredName(name, DeclarationKind::Let, namePos))
    
  12. noteDeclaredName(...)が使われている場所をDXRの機能である「Find references」を使って探す

  13. Parser.cpp7236行目のnoteDeclaredName(...)が呼ばれている関数の名前がclassDefinition(...)、7234行目に次の1行があり、これは勝った、って感じですね。

        if (classContext == ClassStatement) {
    
  14. ということで、7236行目のDeclarationKind::LetDeclarationKind::Classにして、晴れて変更完了!という感じです。

  15. 今回はクラス定義に関する変更を行ったので、js/src/tests/ecma_6/Class/にあるテストにパスすることを確認しました。

  16. 最後にパッチを作ってBugzillaに投げて、レビューをもらって無事取り込まれました!