Read OSS

コードの変換と出力:Transformer・Minifier・Codegen

上級

前提知識

  • 第 1〜4 回:アーキテクチャ・AST・Parser/Semantic・Visitor/Traverse
  • Babel トランスフォームとコード minification の基本概念に慣れていること

コードの変換と出力:Transformer・Minifier・Codegen

第 1〜4 回では、Oxc パイプラインの入力側、つまりソーステキストを AST へパースし、セマンティック情報を付与するところまでを見てきました。最終回となる本記事では、出力側を扱います。具体的には、互換性のための AST 変換、サイズ削減のための minification、そしてソースコードへの逆変換です。あわせて、Prettier 互換フォーマッターと、Oxc を Node.js エコシステムに公開する NAPI バインディングも見ていきましょう。

Transformer:Babel 互換のコード変換

crates/oxc_transformer/src/lib.rs の transformer は、第 4 回で解説した Traverse システムを使って Babel 互換のトランスパイルを実現しています。ECMAScript 2015〜2026 のプリセット、TypeScript の型除去、JSX 変換、デコレーターをサポートしています。

transformer は、Babel の構造を踏襲したプリセットモジュールで構成されています。

flowchart TB
    subgraph "Transformer Presets"
        Common[Common transforms]
        TS[TypeScript]
        JSX[JSX / React]
        Decorator[Decorators]
        ES2015[ES2015]
        ES2016[ES2016]
        ES2017[ES2017]
        ES2018[ES2018]
        ES2019[ES2019]
        ES2020[ES2020]
        ES2021[ES2021]
        ES2022[ES2022]
        ES2026[ES2026]
        RegExp[RegExp]
    end
    
    TO[TransformOptions] -->|"target selection"| Common
    TO --> TS
    TO --> JSX
    TO --> ES2015
    TO --> ES2022

各プリセットは Traverse トレイト(第 4 回参照)を実装した struct です。つまりそれぞれの変換は、TraverseCtx 経由で祖先ノードの情報にフルアクセスしながら enter_*/exit_* フックを利用できます。transformer はこれらを一つのトラバーサルパスにまとめて実行します。

エントリーポイントの build_with_scoping は、セマンティック解析が生成した Scoping struct(第 1 回で CompilerInterface のフローとして見たもの)を受け取り、更新済みの scoping を含む TransformerReturn を返します。

pub struct TransformerReturn {
    pub errors: std::vec::Vec<OxcDiagnostic>,
    pub scoping: Scoping,
    pub helpers_used: FxHashMap<Helper, String>,
}

ターゲット選択には EngineTargets を使います。たとえば Chrome 100 をターゲットに指定すると、Chrome 100 が既にサポートしているフィーチャーの変換はスキップされます。Babel の @babel/preset-env や、oxc-browserslist クレート経由の browserslist クエリとも互換性があります。

ヒント: 既存の Babel 設定をそのまま取り込むには BabelOptions::from_json() が使えます。Oxc の transformer は、同じ設定インターフェースでそのまま置き換えられるよう設計されています。

プラグイン変換:Inject と Replace

メインの transformer パスの後に、2 つのプラグイン変換を追加で実行できます。これらは crates/oxc_transformer_plugins で定義されており、ビルドツール向けのユースケースに対応しています。

  • ReplaceGlobalDefines — グローバル識別子を定数値に置き換えます(Webpack の DefinePlugin や esbuild の --define に相当)
  • InjectGlobalVariables — グローバル変数用の import 文を注入します(@rollup/plugin-inject に相当)

crates/oxc/src/compiler.rs#L169-L194CompilerInterface パイプラインを見るとわかりますが、transformer によってスコープツリーが古くなる可能性があるため、これらのプラグインを使う際はセマンティックデータの再構築が必要です。

// Symbols and scopes are out of sync.
if inject_options.is_some() || define_options.is_some() {
    scoping = SemanticBuilder::new()
        .with_stats(stats)
        .build(&program)
        .semantic
        .into_scoping();
}

ReplaceGlobalDefines の実行後、minifier が無効の場合はデッドコード削除(DCE)が実行されます。定数への置き換えによって生じた到達不能なブランチを除去するためです。たとえば if (process.env.NODE_ENV === 'production')if (true) になったような箇所が対象です。

Minifier:固定点ピープホール最適化

crates/oxc_minifier/src/compressor.rs の minifier コンプレッサーは、Google Closure Compiler にインスパイアされた固定点最適化ループを実装しています。考え方はシンプルです。ピープホール最適化を実行し、何か変化があればもう一度実行する。安定するまでこれを繰り返します。

flowchart TD
    Start[Input AST] --> Normalize[Normalize Pass]
    Normalize -->|"convert while→for,\nconst→let"| Loop{Peephole Loop}
    Loop -->|"changed=true"| PO[PeepholeOptimizations]
    PO --> Check{Changed?}
    Check -->|Yes, iteration < max| Loop
    Check -->|No, or max reached| Done[Optimized AST]
    
    style Loop fill:#ff9

compressor.rs#L69-L91run_in_loop メソッドがその核心です。

fn run_in_loop(
    max_iterations: Option<u8>,
    program: &mut Program<'a>,
    ctx: &mut ReusableTraverseCtx<'a>,
) -> u8 {
    let mut iteration = 0u8;
    loop {
        PeepholeOptimizations.run_once(program, ctx);
        if !ctx.state().changed {
            break;
        }
        if let Some(max) = max_iterations {
            if iteration >= max {
                break;
            }
        } else if iteration > 10 {
            debug_assert!(false, "Ran loop more than 10 times.");
            break;
        }
        iteration += 1;
    }
    iteration
}

注目すべきポイントは以下のとおりです。

  • MinifierState.changed — 最適化が一つでも実行されたかどうかを追跡します。何も変化がなければループは終了します。
  • 安全上限 10 回 — 無限ループを防ぐためのガードです。実際のコードは 2〜3 回のイテレーションで安定することがほとんどです。
  • ReusableTraverseCtx(第 4 回参照)— コンテキストを毎回再構築することなく、効率的に繰り返しのトラバーサルを実現します。

ループの前に一度だけ実行されるノーマライズパスでは、whilefor ループに変換したり、constlet に変換したり、不要な "use strict" ディレクティブを削除したりして、最適化の余地を広げます。

Mangling と Codegen:AST から出力へ

識別子のマングリング

crates/oxc_mangler のマングラーは、識別子をできる限り短い名前に変換します。セマンティック解析で得られた Scoping データを使い、次のように動作します。

  1. マングリング対象のシンボルを特定 — グローバル変数、エクスポート、保持が必要な名前は除外
  2. スコープ単位でスロットを割り当て — 兄弟スコープ内の変数は同じ短い名前を共有可能
  3. base-54 エンコーディングを使用base54 関数が ab、...、zA、...、Zaa... といった名前を生成

crates/oxc_mangler/src/lib.rs#L21-L39MangleOptions には debug モードがあります。このモードでは base-54 の代わりに slot_0slot_1... といった読みやすい名前が生成されるため、マングル後の出力をデバッグする際に役立ちます。

コード生成

crates/oxc_codegen/src/lib.rs の codegen プリンターは、AST を JavaScript のソースコードに変換します。使用するトレイトは 2 つです。

  • Gen — 優先度コンテキストが不要なノード(ステートメント、宣言など)向け
  • GenExpr — 親ノードの演算子優先度を考慮して括弧を挿入する必要がある式向け
sequenceDiagram
    participant AST as Program AST
    participant CG as Codegen
    participant CB as CodeBuffer
    participant SM as SourcemapBuilder
    
    AST->>CG: build(program)
    loop For each AST node
        CG->>CG: call Gen/GenExpr trait method
        CG->>CB: write bytes
        CG->>SM: record source mapping
    end
    CG-->>AST: CodegenReturn { code, map }

lib.rs#L48-L61CodegenReturn には、生成されたコード、オプションのソースマップ、抽出されたライセンスコメントが含まれます。

pub struct CodegenReturn {
    pub code: String,
    #[cfg(feature = "sourcemap")]
    pub map: Option<oxc_sourcemap::SourceMap>,
    pub legal_comments: Vec<Comment>,
}

プリンターは oxc_data_structuresCodeBuffer を使って文字列を効率的に構築し、余分なアロケーションを避けています。マングリングが有効な場合、プリンターはマングラーから受け取った Scoping を使って変換後の識別子を出力します。

フォーマッター:Prettier 互換の出力

crates/oxc_formatter/src/lib.rs のフォーマッターは、codegen とは根本的に異なるアプローチを採っています。codegen が AST を直接出力するのに対し、フォーマッターはいったん AST を FormatElement の中間表現(IR)に変換し、その IR を出力します。

pub struct Formatter<'a> {
    allocator: &'a Allocator,
    options: FormatOptions,
}

impl<'a> Formatter<'a> {
    pub fn build(self, program: &Program<'a>) -> String {
        let formatted = self.format(program);
        formatted.print().unwrap().into_code()
    }
}

IR に含まれる主な要素は次のとおりです。

  • Group — 1 行にまとめて(フラット)か複数行に展開するかを選択できるコンテンツ
  • LineMode — ハード改行・ソフト改行・スペースの区別
  • FormatElement — テキスト、インデント、デデント、アライン

この IR ベースのアーキテクチャは Prettier と同じ設計で、単純な再帰的プリンターでは実現できないグローバルな折り返し判断を可能にします。各 AST ノード型に Format トレイトが実装されており、生の文字列ではなく FormatElement を生成します。

NAPI バインディング:Node.js との橋渡し

Oxc は napi-rs を通じて NAPI バインディングを提供し、Node.js からツールを利用できるようにしています。napi/parser/src/lib.rs のパーサーバインディングは、バイナリ転送プロトコルという点で特に興味深い実装です。

Raw Transfer

64 ビットリトルエンディアンのシステムでは、NAPI パーサーは「raw transfer」をサポートします。これは JSON シリアライズを完全に排除するプロトコルです。AST を JSON にシリアライズして JavaScript 側でデシリアライズする代わりに、パーサーは既知のレイアウトでバイナリデータとして AST ノードを直接書き込み、JavaScript 側は DataView でそれを読み取ります。

// Only enabled on 64-bit little-endian platforms
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
mod raw_transfer;

#[napi]
pub fn raw_transfer_supported() -> bool {
    cfg!(all(target_pointer_width = "64", target_endian = "little"))
}

これにより、ネイティブ→JS ブリッジのパフォーマンスを左右するシリアライズ・デシリアライズのコストをゼロにできます。AST 型に付与された #[repr(C)] レイアウト注釈(第 2 回で解説した #[ast] マクロが追加するもの)がここで重要な役割を果たします。バイナリレイアウトを予測可能かつ安定させるために必要だからです。

flowchart LR
    subgraph Rust
        Parser --> AST[AST in Arena]
        AST -->|raw binary| Buffer[SharedArrayBuffer]
    end
    subgraph JavaScript
        Buffer -->|DataView| JSAST[JS AST Objects]
    end
    
    style Buffer fill:#ff9

Transform バインディング

napi/transform/src/lib.rs の transform NAPI バインディングは、transformer と isolated declarations(.d.ts 出力)の両方を Node.js に公開しています。Rolldown などのビルドツールが Oxc のトランスフォームパイプラインを利用しているのは、このバインディング経由です。

Isolated Declarations

oxc_isolated_declarations クレートは、TypeScript の型チェッカーを必要とせずに .d.ts 型宣言ファイルを生成します。実装の詳細を取り除きながら型シグネチャを保持する処理を、AST に対して直接行います。compiler.rs#L133-L135CompilerInterface パイプラインに組み込まれています。

if let Some(options) = self.isolated_declaration_options() {
    self.isolated_declaration(options, &allocator, &program, source_path);
}

パイプライン全体の振り返り

6 回にわたる連載を通じて、JavaScript ファイルが Oxc ツールチェーン全体をどのように流れるかを追いかけてきました。

flowchart LR
    Source[Source Text] --> Alloc[Arena Allocator]
    Alloc --> Parser
    Parser --> AST[AST]
    AST --> Semantic[SemanticBuilder]
    Semantic --> Scoping
    
    Scoping --> Linter
    Linter --> Diagnostics
    
    Scoping --> Transformer
    Transformer --> Plugins[Inject/Define]
    Plugins --> Compressor[Minifier]
    Compressor --> Mangler
    Mangler --> Codegen
    Codegen --> Output[JavaScript Output]
    
    AST --> Formatter
    Formatter --> Formatted[Formatted Output]
  1. 第 1 回:31 クレートのワークスペース、3 層アーキテクチャ、CompilerInterface パイプライン
  2. 第 2 回:アリーナアロケーション、Drop を持たない Box<'a, T>/Vec<'a, T>、ESTree から乖離した AST 設計
  3. 第 3 回:手書きの再帰降下パーサー、エラーリカバリー、スコープとシンボルを構築する SemanticBuilder
  4. 第 4 回:2 種類のトラバーサルシステム(読み取り用の Visit と祖先情報付き変換用の Traverse)、および ast_tools によるコード生成
  5. 第 5 回:Oxlint の並列 lint アーキテクチャ、Rule トレイト、LintContext、パフォーマンス最適化
  6. 本記事:transformer プリセット、固定点 minifier、codegen、フォーマッター、NAPI バインディング

全体を貫く一本の糸は、アリーナアロケーターと Scoping struct です。アリーナはアロケーションをほぼコストゼロにし、トラバーサルをキャッシュフレンドリーにします。Scoping struct はセマンティック情報をパイプラインの各ステージへと引き渡し、冗長な再解析を避けます。Oxc が実現しているパフォーマンス数値の裏側にあるのは、この 2 つの仕組みです。

ヒント: Oxc のパイプラインを理解する一番の近道は、実際に何か作ってみることです。まずは CompilerInterface トレイトから始めて、必要なフックだけをオーバーライドしましょう。残りはデフォルト実装がすべて処理してくれます。compiler.rsCompiler struct は、まさにそのパターンの最小実装例です。