名前のとおりまだexperimentalではあるけど、これは紹介せずにはいられない!というわけで。
import { parseSync } from "oxc-parser";
const CODE = `
// ...
`;
const ret = parseSync("test.js", CODE, {
preserveParens: false, // default `true`, for ESTree compat
experimentalRawTransfer: true, // 👈🏻
});
Rustベースのツールの課題
XXXをRustで書き換えたら実行速度が劇的に改善された!って話は、巷にいろいろある。
速さは正義であり、それはそれでよい話。少なくとも、それだけで完結しているうちは。
問題は、そのツールを既存のJSエコシステムと組み合わせたいってなった場合にどうするか。 たとえば、Linterのプラグインを自作したいだとか、ESTreeのASTを利用する既存のツールで再利用したいとか。
こうなると問題になるのは、RustでパースしたASTを、JS側にどうやって送る?ってこと。
一番わかりやすい解としては、serde
なんかでJSONにフォーマットして、JS側でJSON.parse()
するってのがある。
けど、そうなるとこの変換コストがかかって普通に遅い。JSで全部書いたほうが速いってなる。
さて、どうしたものか。
OXC以外のツールでは
Biomeでは、LinterのプラグインをGritQLで書けるようデザインしてる。
RFC: Biome Plugins Proposal · biomejs/biome · Discussion #1762 https://github.com/biomejs/biome/discussions/1762
JS/TSでプラグインを書ける道も、まだ諦めたわけではないようではあるけど。
Denoでは、もう既にJS/TSでLinterプラグインが書けるらしい。
Deno 2.2: OpenTelemetry, Lint Plugins, node:sqlite https://deno.com/blog/v2.2#javascript-plugin-api
Rust-JS間の問題に対しては、ASTのフォーマットを工夫してるとのこと。
Speeding up the JavaScript ecosystem - Rust and JavaScript Plugins https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-11/
簡単にまとめると、
- ASTのデータ構造を、ツリーではなくフラットにして
- 冗長な文字列もマップで管理して、後から参照するように
- なおかつ、
NodeFacade
を用意してgetter
で遅延で評価するように
という、効率よく変換するという感じのアプローチ。
OXCでは
子曰く、「Rust-JS間でのフォーマット変換がボトルネックなのであれば、そもそも変換しなければいい」。
え、どういうこと?そんなんできるん?ってなるけど、それをやってのけちゃうのがこの人たち・・・。
feat(ast/estree): raw transfer (experimental) by overlookmotel · Pull Request #9516 · oxc-project/oxc https://github.com/oxc-project/oxc/pull/9516
せっかくブログを書くのなら、具体的にこういう処理をやってそれを実現してる!ってことを本当は書きたかったけど、高度すぎて正直よくわからんかったです。
こっちの元Issueにも詳しい説明があります。
Faster passing ASTs from Rust to JS · Issue #2409 · oxc-project/oxc https://github.com/oxc-project/oxc/issues/2409
ざっくりまとめると、
- OXCではArenaアロケーターですべてのメモリを管理してる
- だから速い
- そしてこのメモリを、
napi-rs
を通してそのままJS側に持っていき、(Array)Buffer
から触れるようにした- これはメモリのポインタだけのやり取り
- メモリレイアウト上のどこに欲しいものがあるか?は、JS側で解決する
- これはスキーマで事前に決められてるので、マッピングするだけ
- やろうと思えば、その逆方向の機構も作れる
なるほど、わからん。
興味を持った人向けに、リポジトリのどこにコードがあるかの取っ掛かりだけ置いておきます。
- npmで
oxc-parser
として公開されるもの napi-rs
を使った実装はこっちで、表向きのJSとそれを支えるRustの両方があるexperimentalRawTransfer: true
は、parseSync()
の第3引数のParseOption
として指定するparseSync(fileName, code, { experimentalRawTransfer: true })
- TSの型はまだない
- これを有効にすると、内部で呼ばれるバインディングされたAPIが変わる
parseSyncRaw()
- https://github.com/oxc-project/oxc/blob/109d06626d0114c360dd3b319fe813c12bdee981/napi/parser/index.js#L70
Uint8Array
を作ってRust側に渡せるようにする
parseSyncRaw()
のRust側はここ- https://github.com/oxc-project/oxc/blob/109d06626d0114c360dd3b319fe813c12bdee981/napi/parser/src/raw_transfer.rs#L78
- メモリのポインタをごにょごにょしてる
- ついでにASTとソース文字列との位置対応も、UTF-16にちゃんと補正される
- JS側に戻ってきたら、各ASTノードとしてメモリ上からマッピングする
- この
deserialize-
は自動生成で、そのスクリプトはここ
という感じ。
氏は過去にSWCにもcontributeしてたことがあって、その頃からの構想だったらしく、ドラマティックな展開だわ。
おわりに
厳密にベンチしたわけではないので明言は避けるけど、手元の数値だと、より大きいファイルでその速度差を実感できる感じやったし、小さいファイルでもJSと遜色ないくらい。
ちゃんとベンチした結果とかも、そのうち公開されるかな?乞うご期待。
JSだけでなくJSXも(ASTの形はさておきTSも)パースができて、しかも遅くないっていうのが偉いし、Acornのアウトプットと比較したテストにも100%通ったESTreeになっててさらに偉い。
本当にすごい!偉業!