コードの変換と出力: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-L194 の CompilerInterface パイプラインを見るとわかりますが、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-L91 の run_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 回参照)— コンテキストを毎回再構築することなく、効率的に繰り返しのトラバーサルを実現します。
ループの前に一度だけ実行されるノーマライズパスでは、while を for ループに変換したり、const を let に変換したり、不要な "use strict" ディレクティブを削除したりして、最適化の余地を広げます。
Mangling と Codegen:AST から出力へ
識別子のマングリング
crates/oxc_mangler のマングラーは、識別子をできる限り短い名前に変換します。セマンティック解析で得られた Scoping データを使い、次のように動作します。
- マングリング対象のシンボルを特定 — グローバル変数、エクスポート、保持が必要な名前は除外
- スコープ単位でスロットを割り当て — 兄弟スコープ内の変数は同じ短い名前を共有可能
- base-54 エンコーディングを使用 —
base54関数がa、b、...、z、A、...、Z、aa... といった名前を生成
crates/oxc_mangler/src/lib.rs#L21-L39 の MangleOptions には debug モードがあります。このモードでは base-54 の代わりに slot_0、slot_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-L61 の CodegenReturn には、生成されたコード、オプションのソースマップ、抽出されたライセンスコメントが含まれます。
pub struct CodegenReturn {
pub code: String,
#[cfg(feature = "sourcemap")]
pub map: Option<oxc_sourcemap::SourceMap>,
pub legal_comments: Vec<Comment>,
}
プリンターは oxc_data_structures の CodeBuffer を使って文字列を効率的に構築し、余分なアロケーションを避けています。マングリングが有効な場合、プリンターはマングラーから受け取った 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-L135 で CompilerInterface パイプラインに組み込まれています。
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 回:31 クレートのワークスペース、3 層アーキテクチャ、
CompilerInterfaceパイプライン - 第 2 回:アリーナアロケーション、Drop を持たない
Box<'a, T>/Vec<'a, T>、ESTree から乖離した AST 設計 - 第 3 回:手書きの再帰降下パーサー、エラーリカバリー、スコープとシンボルを構築する
SemanticBuilder - 第 4 回:2 種類のトラバーサルシステム(読み取り用の
Visitと祖先情報付き変換用のTraverse)、およびast_toolsによるコード生成 - 第 5 回:Oxlint の並列 lint アーキテクチャ、
Ruleトレイト、LintContext、パフォーマンス最適化 - 本記事:transformer プリセット、固定点 minifier、codegen、フォーマッター、NAPI バインディング
全体を貫く一本の糸は、アリーナアロケーターと Scoping struct です。アリーナはアロケーションをほぼコストゼロにし、トラバーサルをキャッシュフレンドリーにします。Scoping struct はセマンティック情報をパイプラインの各ステージへと引き渡し、冗長な再解析を避けます。Oxc が実現しているパフォーマンス数値の裏側にあるのは、この 2 つの仕組みです。
ヒント: Oxc のパイプラインを理解する一番の近道は、実際に何か作ってみることです。まずは
CompilerInterfaceトレイトから始めて、必要なフックだけをオーバーライドしましょう。残りはデフォルト実装がすべて処理してくれます。compiler.rsのCompilerstruct は、まさにそのパターンの最小実装例です。