Rust製TypeScriptコンパイラstcの現状と今後

この記事は、Merpay Tech Openness Month 2023 の2日目の記事です。

メルペイFrontendエンジニアの@togami2864です。普段はPartner Platformというチームで加盟店申込みフォームや審査・管理を行うためのMerchant Supportツールの開発・運用を担当しています。

本記事ではRust製TypeScriptコンパイラであるstcについて筆者の観測範囲での概要、開発状況、課題等を紹介します。なお、内容は全て2023年5月時点のものです。また、本記事の一部は Node学園 41時限目 書籍について で発表したものと重複していることをご了承ください。

概要

stcは2022年10月にオープンソース化されたRust製のTypeScriptコンパイラです。

https://github.com/dudykr/stc

製作者はRust製のトランスパイラswcの作者であるkdy1氏で、Rustとparallelな解析によってTypeScriptのビルドとイテレーションを短縮して DX を改善することを目的としています。1
また、tscの動作に準拠したコンパイラ(drop in replacement)を目指すという立場をとっており、tscの挙動を仕様として追従していく予定です。

一時はRustの採用を諦め、Goで開発していた時期もあったようですが、最終的にはRustで作ることを決定しました。

2022/01/26 元々Rustで作っていたが、tscが多くの共有可変性やGCに依存していることを理由にRustの採用を見送り。ZigとGoで実験した結果Goを採用2
2022/10/10 tscを実直にGoで行ごとに移植していたものの、量が膨大すぎるためTypeScriptコンパイラのコードを行ごとにGoに変換し、コンパイラを生成するコンパイラを考案3
2022/10/27 Goを使っているとはいえ、コンパイラを通して生成したGoのコードには非効率なものが多く含まれること、不要な部分の移植も必要なため結局Rustに戻すことを決定4

移植難易度の高さ

言うまでもなく、tscの他言語への移植は非常に困難で挑戦的なプロジェクトです。その主な理由の一つは、他のプログラミング言語と異なり、TypeScriptには明確な仕様書が存在しない点です。5そのため、stcはtscの挙動を仕様とみなしています。また、開発に際しては以下の3つのリソースを参考にしています。

1. 機能が追加された時のPRを見る

TypeScriptには、基本的な型に加えて、conditional types、mapped types、template literal typesといった独自の型が存在します。これらの機能に関する詳細な説明やエッジケースはPRに書いてあります。
ちなみに大きな機能追加のほとんどはTypeScriptの共同創案者であるAnders Hejlsberg氏のものです。彼のPRは詳細な説明を書いている上に、テストケースも豊富に書いているため非常に重要です。

例:
conditional types
unknown type
variadic tuple

2. テストケースを参考にする

TypeScriptのリポジトリには、tests/casesディレクトリに多数のテストケースがあります。これらのテストケースの入力・出力とコメントを参考にして開発が進められます。また、stcではcompilerとconformanceディレクトリ内のテストケースを流用してテストが実施されています。

3. tscのソースコードとコメントを読み解く

これが最も確実な方法でありながら、非常に高難易度です。TypeScriptのコンパイラのコードベースは10年以上にわたる開発が続けられているため巨大かつ複雑です。

https://twitter.com/kdy1dev/status/1652531146138464259

結構有名な話ですが型検査のコードがあるchecker.tsのみでファイルサイズは約2.7MBあり、GitHub上で表示できません。

GitHub上のchecker.ts
GitHub上のchecker.ts 引用: microsoft/TypeScript

仕組み

次のようなシンプルなTypeScriptコードに対し、型チェックを行うとしましょう。

const foo: number = 1 + 1

定数fooを宣言し、型注釈としてnumber型を指定しています。値として1 + 1を代入しています。
型チェックを行うためにまずソースコードを字句解析、構文解析を通してASTにする必要があります。stcではTypeScriptのコードをASTにするためのlexerとparserはswcを使っています。あくまでstcが担当するのは型チェックのみです。
そこで生成されたswcのASTを使って型検査を行ないます。簡略化したASTは次のようになります。

生成されたASTの図

stcではVisitor patternを実装しています。
VisitorとしてAnalyzerという構造体が用意されており、各ASTのタイプに対応するvisitメソッドが実装されています。ASTをたどりながらAnalyzerに実装されている操作を呼び出し、そのタイプごとに独自の処理を行います。

このサンプルコードでは、単純に型注釈に対して右の式の結果の型が代入可能であるか(つまり部分型であるかどうか)をチェックします。
明示的な型注釈により、変数の型がnumberと判断されます。次に式1 + 1ですが、BinaryExpressionに到達したときに演算子が+であることがわかります。その後、leftとrightの式の型が分かれば、結果がどのようになるかチェックできます。もし+演算が適用できない型同士であれば、ここでエラーが出されます。

式の結果の型を型注釈への代入を表す図

今回は両方ともnumber型の値なので、式の結果もnumber型になることがわかります。
number型に対してnumber型は代入可能ですから無事に型チェック完了です。

現在の開発状況

stcは現在TypeScript5.0のブランチのconformance testをもとに開発されています。

基本的な型、構文、演算子、builtin typesのサポート

基本的な型に加え、Generics、オーバーロード、mapped types、conditional typesといった高度な型もサポートしており、2023年4月現在TypeScript4.9のsatisfies operatorまでサポートしています。また、ES2022までのbuiltin typesの解析が可能です。

tscとの互換性

stcはTypeScriptとの互換性を重視して開発されています。そのため、TypeScriptとの動作の違いを把握することが重要です。そこで、stcのリポジトリには tsc-stats.rust-debug というファイルが用意されています。

Stats {
    required_error: 3538,
    matched_error: 6497,
    extra_error: 771,
    panic: 74,
}

このファイルでは、本家tscが出力した結果とstcが出力した結果を比較して、エラーの一致数やパニックの発生数などを集計しています。tscのリポジトリからコピーした/conformanceディレクトリ内のテストケースを使って集計されています(stcにはconformanceテスト以外にもテストケースがありますが、このファイルの数値には含まれていません)。

required_error (false-negative)

これは、tscがエラーを出しているのに対して、stcがエラーを出していないケースの数を示しています。現在、3538箇所存在しています。この値はできるだけ減らしたいものです。

matched_error (true-positive)

これは、tscとstcの両方が正しくエラーを表示できている箇所の数を示しています。現在、6497箇所存在しています。この値はできるだけ増やしたいものです。

extra_error (false-positive)

これは、tscではエラーを出していないのに、stcだけが誤ってエラーを表示している箇所の数を示しています。この値は最優先で減らすべきです。 現在、771箇所存在しています。

理想的には0になってほしい値ではありますが、現状では多くのエッジケースが含まれており、どこまでサポートするかは今後の課題となります。

panic

この項目は、panicによってプログラムが終了するケースの数を示しています。これらのケースは主にparser (swc) の問題や、解析中のオーバーフローが原因となっています。

これらの数値は、4月まで https://stc.dudy.dev/ で週に1回進捗が共有されていました。しかし、最近の大きなタスクや容易に修正できる部分がほぼ解決されたため、更新頻度が月1回に変更されています。

@typesパッケージの解析

@types/node や @types/react といった有名なツールの型定義ファイルは、普段の開発でほぼ必須となります。stcも実用段階に達するためには、これらのパッケージを解析できることが必須でしょう。
ただし、namespaceを使用している部分が並列解析できなかったり(特に@types/nodeはnamespaceを多用している)、単純なプロパティの多さからくるデバッグの難しさなどの理由で、まだ十分な進捗がありません。

未対応のケース

現状では、基本的な型の多くは解析できますが、対応できていないケースも存在しています。

例):
https://github.com/dudykr/stc/blob/7c76ed2314a82040efba2f82db951eee6c2c88bb/crates/stc_ts_type_checker/tests/conformance/controlFlow/controlFlowAliasing.ts#L6-L14

// @strict: true
// @declaration: true

// Narrowing by aliased conditional expressions

function f10(x: string | number) {
    const isString = typeof x === "string";
    if (isString) {
        let t: string = x; // 本当はエラーにならないのにTS2322が表示
    }
    else {
        let t: number = x; // TS2322
    }
}

変数xがif句、else句内でそれぞれnarrowingされることが期待されますが、現在のstcではif-else句内でもxを(string | number)と判断しています。そのため、TS2322: Type '(string | number)' is not assignable to type 'number'.というエラーが誤って表示されます。

おそらく、式typeof x === "string"の結果の型を判定する際に、その式がifステートメントの条件として使用されていない場合、変数xをnarrowingする処理が行われていないものと思われます。

このようなfalse-positiveケースが多数存在しており、特にclass構文周りではfalse-positiveが多いようです(メンバーやメソッドなどの解析の順番を決めるのが難しいため)。

false-positiveをどこまで妥協するか

false-positiveは極力減らすべきです。しかしながら、false-positiveの中には現実的なユースケースとして本当に現れるのかというケースも大量にあります。例えば次のコードは現在のfalse-positiveのケースの一つです。
https://github.com/dudykr/stc/blob/main/crates/stc_ts_type_checker/tests/conformance/classes/members/privateNames/privateNameComputedPropertyName3.ts

// @target: esnext, es2022, es2015

class Foo {
    #name;

    constructor(name) {
        this.#name = name;
    }

    getValue(x) {
        const obj = this;

        class Bar {
            #y = 100;

            [obj.#name]() { // <----------- Umimplemented
                return x + this.#y;
            }
        }

        return new Bar()[obj.#name]();
    }
}

console.log(new Foo("NAME").getValue(100));

TypeScriptのコードとしては不正ではないものの、重箱の隅をつついてくるようなfalse-positiveのテストケースが大量に存在しておりそれらをどう扱うかははっきりしていません。

今後の動き

まだまだ開発途中であり、決まっていることは少ないですが、アルファ版へのロードマップは https://stc.dudy.dev/docs/roadmap で公開されています。

assign ruleの改善

tscの挙動を仕様とし、互換性や@typesパッケージの解析のために改善が続けられるでしょう。またGenerics推論の改善や解析順序の改善が予定されています。

VSCode拡張

2023年4月に開発が始まったようです。現在は開発者向けのVSCode拡張機能の開発が進行しています。

開発中のVSCode拡張
開発中のVSCode拡張機能 引用:This week in stc, 23

独自の構文拡張はあり得るか

(筆者の意見ですが)非常に可能性は低いと考えられます。なぜなら、作者のkdy1氏は標準遵守の意識が強く、swcでもその姿勢を維持しているからです。
また、bunがJSXの独自構文を導入した際にもかなり難色を示していました。

https://twitter.com/kdy1dev/status/1609013152590725120?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1609013152590725120%7Ctwgr%5Ed9b51ff7ef2db59201ba768191816a2788474fff%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fkdy1.github.io%2Fpost%2F2023%2F2%2Fstc-ethics%2F

独自の破壊的変更によるコミュニティの混乱を避けるため、stcがTypeScriptに独自の拡張を導入する可能性は非常に低いと考えられます。6

まとめ

stcの概要について紹介しました。高速なtscと聞くと非常に魅力的に聞こえますが、まだ鋭意開発中であり、うまくいくかどうかはtscの複雑さも相まって全くもって未知数です。また、実際に高速に動作するのか正確なベンチマークが用意されていないため、その点も確認できません。7
しかし、swcのASTを中心としたエコシステムの一員として、個人的には非常に期待しています。

脚注

[1] Rewriting TypeScript in Rust? You’d have to be… https://www.totaltypescript.com/rewriting-typescript-in-rust
[2] I’m porting tsc to Go https://kdy1.dev/posts/2022/1/tsc-go
[3] Status update of my tsc port https://kdy1.dev/posts/2022/10/tsc-port-status
[4] Open-sourcing the new TypeScript type checker https://kdy1.dev/posts/2022/10/open-sourcing-stc
[5] 正確にいうと、TypeScript v1.8までの仕様書は存在しています。しかしながら、それ以降の更新はなくこのissueを見る限りほぼ放置されていると思って良いでしょう。
[6] stc의 윤리적 문제 https://kdy1.github.io/post/2023/2/stc-ethics
[7] csstypesの解析がtscの57倍高速であったという報告や、@types/reactの解析にわずか0.174秒しかかからなかったという報告があります。これらから期待はできますが、実行環境や条件が明確でないため、参考程度に留めておくのが良いでしょう。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加