⛩️

モダンJSの一時代を築いたBabelから学ぶトランスパイル

2022/07/23に公開
1

はじめに

今回この記事を書こうと思ったのは、JavaScriptのパーサ関連の記事が定期的に投稿される中で、
ESTreeの話はよく出てきていて反響がありそうだけれど、それを利用したツールは実際にどのようにコードを解析しているかが詳しく書かれている記事が少ないなあと思ったためです。
この記事では、トランスパイラとしてモダンJSの一時代を築いたBabelを例にトランスパイルについて、詳しく解説できたらと思います。

詳しい仕様などは日本語訳がされたハンドブック もありますが、
あいまいな訳になっているため、英語版のハンドブックをわたしはお勧めします。
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook

記事の対象者

  • JavaScriptやTypeScriptを利用している
  • JavaScriptなどのコードは書けても実際にブラウザでどのように実行されているか気になる
  • JavaScriptなどのパーサや言語処理、トランスパイラ、コンパイラに興味がある .etc

Babelとは

まず、Babelとは

Babel (pronounced "babble")

と書かれているように読みは、「バブル」と読み、(知らなかった...)
※ 上記の件についてコメント欄に追記しました。(2022/07/25)

BabelはJavaScriptのための包括的な多目的コンパイラです。Babelを利用すると、あなたは次世代のJavaScriptを体験するだけではなく、次世代JavaScript用ツールを利用(または作成)することが出来ます。

とのことです。

実際のところ今後については、IE11のサポート終了が施行されたこと、EdgeのベースにChromeライクのChromiumが採用されていること、次世代のビルドツールであるGo製のesbuildやRust製のswcなど、Node以外の言語で作成されたより高速なフロントエンドビルドツールが登場してきたことなどによって、次第に日の目を見る機会は少なくなっていくのかなあと個人的に感慨に耽っています。
※ Chrome, Edge, Safari, Firefoxは自動更新がブラウザデフォルトとなっているため基本的には常に最新版のブラウザであるという想定が実質のところ可能となっています。

ただやはり、JavaScriptの過渡期において偉大な実績を残しているBabelの根幹である言語処理を学ぶことの意義は計り知れないです。

トランスパイルについて

まず、コードがトランスパイルされるまでの過程を示した図を下記に記します。

解析(Parse)

まず、与えられた文字列を解析して、AST: Abstract Syntax Tree(抽象構文木)を作成します。
JavaScriptコードからASTを作成するライブラリとしては、はじめて触れたようにESTree が有名です。また主な処理フェーズとして、下記で解説する字句解析フェーズ構文解析フェーズを持ちます。

字句解析(Lexical Analysis)

字句解析フェーズでは与えられたJavaScript(TypeScript)コードを解析し、トークンのIDと種別を識別できるトークンストリーム生成し格納した配列を生成します。

以下のJavaScriptコードを例に考えます。

test.js
const square = (n) => {
  return n * n
}

字句解析フェーズでは、test.jsを与えると以下のようなトークンストリームが配列にフラットに格納されて得られます。

testLexicalAnalysis.js
[
  { type: 'Program', start: 0, end: 40  },
  { type: 'VariableDeclaration', start: 0, end: 40 },
  { type: 'VariableDeclaration', start: 6, end: 40 },
  { type: 'Identifier', start: 6, end: 12, name: 'square' },
  { type: 'ArrowFunctionExpression', start: 15, end: 40, expression: false, generator: false, async: false },
  ...
]

余談で、この時点で自明ではありますが type を識別することで、走査(Traversal) を行うことが可能になっています。例えば。type: VariableDeclaration | Identifier | ...etcについては、コードブロックにネストが存在する可能性がありますね。

構文解析(Syntactic Analysis)

字句解析フェーズで生成された配列の情報を利用して、トランスフォーム処理がしやすいように構造を最適化し、ASTを生成します。余談で話しましたが、typeを識別し必要であれば識別できる形でネストした先にノードを配置します。

最終的には、test.jsが下記のようなASTで表現されます。
気になる方はWeb上で簡易に試すことができる AST Explorerを利用してみてください。

testAST.js
{
  "type": "Program",
  "start": 0,
  "end": 40,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 40,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 40,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 12,
            "name": "square"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 15,
            "end": 40,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [
              {
                "type": "Identifier",
                "start": 16,
                "end": 17,
                "name": "n"
              }
            ],
            "body": {
              "type": "BlockStatement",
              "start": 22,
              "end": 40,
              "body": [
                {
                  "type": "ReturnStatement",
                  "start": 26,
                  "end": 38,
                  "argument": {
                    "type": "BinaryExpression",
                    "start": 33,
                    "end": 38,
                    "left": {
                      "type": "Identifier",
                      "start": 33,
                      "end": 34,
                      "name": "n"
                    },
                    "operator": "*",
                    "right": {
                      "type": "Identifier",
                      "start": 37,
                      "end": 38,
                      "name": "n"
                    }
                  }
                }
              ]
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

トランスフォーム(Transform)

解析フェーズで生成されたASTをもとに、コンパイラやトランスパイラごとに処理の手法も違うためこのフェーズでASTを利用するトランスパイラ、コンパイラの処理に最適化します。
(ex. ES2022 -> ES2015 などへの対応関係をASTに反映するなど
Babelのハンドブックの方にも、

This is by far the most complex part of Babel or any compiler.

とあるようにこのフェーズがどのコンパイラ、トランスパイラでも一番複雑というところでしょう。

生成(Generate)

トランスフォーム処理で最適化された、ASTをもとにトランスパイル先のコードを生成します。
生成については今までのまとめのようなものなのでここまでくれば安心です。

おわりに

この記事ではコードがどのようにトランスパイルされるか具体的に解説できたかと思います。
次回の記事では解析(Parse)に焦点を絞って、
どういう解析手法があるか、どういう解析が主流でその理由などの解説をできたらと思います。
なにかご指摘や質問などあればコメントしていただけると幸いです。

Discussion

からころからころ

反響が大きかったので追記ですが、

Babelは「pronounced babble」という前提で、
babbleの発音記号は 「bˈæbl 」なので「バブル」です。

通常のbabelだと、発音記号では「béɪb(ə)l 」となり「バベル」となります。