制約と時代から読み解くTypeScriptコンパイラ設計史

2.5K Views

May 23, 26

スライド概要

tskaigi 2026 day2
https://2026.tskaigi.org/talks/38

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

T S K a i g i 2 0 2 6 DAY 2 #tskaigi_leverages 制約と時代から読み解く TypeScript コンパイラ設計史 Yoshiaki Togami 2026 /05/23

2.

2025 03/11 typescript-go 爆誕 A 10x faster TypeScript — with Anders Hejlsberg #tskaigi_leverages 2/52

3.

Why Go? 前提: rewriteではなくport 型システムが独特なのに仕様がないのでport必須 循環参照のデータ構造に強く依存している AST Nodeへ後付け書き込みもある 既存の実装をそのまま移せてかつパフォーマンスが出せる言語が適している Rustだとborrow checkerと噛み合わせが悪い microsoft/typescript-go #411 / TypeScript is being ported to Go | interview with Anders Hejlsberg #tskaigi_leverages 3/52

4.

Why Go? ネイティブFirst 共有メモリ並行性 GC 循環参照を素直に持てる Line by Lineで移植しやすい Classをほぼ使わずClosureベース → Goのstruct + methodに機械変換が楽 ts-to-goという変換スクリプトで一括変換 microsoft/typescript-go #411 / TypeScript is being ported to Go | interview with Anders Hejlsberg #tskaigi_leverages 4/52

5.

そもそも🧐 Rustとの相性以前にコンパイラとしては少し独特 なぜこうなっている? 循環参照のデータ構造に強く依存している AST Nodeへ後付け書き込みもある #tskaigi_leverages 5/52

6.

Agenda 01 02 03 04 速習 TypeScript Compiler 独特な内部設計 時代背景と JS の制約 Go port による改善 話さないこと 型システムそのものや checker.ts の実装 flowNodeなど制御フロー解析周りのこと #tskaigi_leverages 6/52

7.

01 速習 TypeScript Compiler #tskaigi_leverages 7/52

8.
[beta]
TypeScript Compiler
Source
function add(
a: number,
b: number,
) {
return a + b;
}
add(1, "2");

Scanner

Parser

FunctionKeyword
Identifier
OpenParenToken
Identifier
ColonToken
NumberKeyword
CommaToken
Identifier
...

SourceFile
FuncDecl

Binder

ExprStmt

Ident(add)Params Block CallExpr
Return Args
...
...

SourceFile.locals
add
Symbol(add)
add.locals
a
Symbol(a)
b
Symbol(b)
Symbol(add):

Checker

Symbol → Type

a : number
b : number
add :

(a: number, b: number)
=> number

name:

"add"

add(1,

flags:

Function

decls:

[Node]

Argument of type 'string'
is not assignable to type
'number'.

→ FuncDecl in AST

"2" )

#tskaigi_leverages 8/52

9.
[beta]
Source -> Scanner -> Parser
Source
function add(
a: number,
b: number,
) {
return a + b;
}
add(1, "2");

Scanner

Parser

FunctionKeyword
Identifier
OpenParenToken
Identifier
ColonToken
NumberKeyword
CommaToken
Identifier
...

SourceFile
FuncDecl

ExprStmt

Ident(add)Params Block CallExpr
Return Args
...
...

普通のコンパイラと同じく、tokenize → AST 生成 までは共通
#tskaigi_leverages 9/52

10.
[beta]
Source -> Scanner -> Parser

ASTでは構文的に正しいが意味的にNGなケースは捕まえにくい
function add(a: number, b: number) { return a + b; }
add(1, "2"); // Argument of type 'string' is not assignable to parameter of type 'number'.

TypeScript に判断して欲しいのはセマンティクスエラーの方
AST だけでは判断できない → もう一工夫必要🤔

#tskaigi_leverages 10/52

11.

Symbol 識別子からそれが指している宣言や種類、スコープへとアクセスするための基本単位 どんな名前か どこで宣言されたか、元の AST ノードへの参照 ( declarations ) どんな種類の宣言か ( BlockScopedVariable , Function , Class , etc.) (ES6のSymbolとは別物) #tskaigi_leverages 11/52

12.

Binder ASTとSymbolを結びつけるコンポーネント Symbol、SymbolTableの作成 flowNodeの構築 この段階では型は確定しない 全てCheckerによる遅延評価 #tskaigi_leverages 12/52

13.
[beta]
Checker

AST と Symbol を使って型を計算・検査するコンポーネント
function add(a: number, b: number) { return a + b; }
add(1, "2"); // Argument of type 'string' is not assignable to parameter of type 'number'.

1. add という名前 → どの宣言か? を Symbol から引く

2. その宣言から パラメータの型 を計算 ( number , number )

3. 実引数の型 ( 1: number , "2": string ) と 比較してエラー

#tskaigi_leverages 13/52

14.

02 独特な内部設計 #tskaigi_leverages 14/52

15.
[beta]
問題は。。
コレ👇
Source
function add(
a: number,
b: number,
) {
return a + b;
}
add(1, "2");

Scanner

Parser

FunctionKeyword
Identifier
OpenParenToken
Identifier
ColonToken
NumberKeyword
CommaToken
Identifier
...

SourceFile
FuncDecl

Binder

ExprStmt

Ident(add)Params Block CallExpr
Return Args
...
...

SourceFile.locals
add
Symbol(add)
add.locals
a
Symbol(a)
b
Symbol(b)
Symbol(add):
name:

"add"

flags:

Function

decls:

[Node]

→ FuncDecl in AST

Checker

Symbol → Type

a : number
b : number
add :

(a: number, b: number)
=> number

add(1,

"2" )

Argument of type 'string'
is not assignable to type
'number'.

#tskaigi_leverages 15/52

16.

特徴その1 ASTが semantic情報を背負う Binderが AST に直接後から書き込む parent Nodeへのポインタも後から書き込む node.symbol = symbol // Symbol node.locals = ... // SymbolTable node.flowNode = ... // CFA node.parent = ... // Pointer to parent 構文木 + 意味解析要素が1つの可変ツリーに同居 immutableな層を持たず、parentごとこの1枚に直書きする #tskaigi_leverages 16/52

17.

特徴その2 循環参照 ASTが親 <=> 子で循環参照 AST <=> Symbol間で循環参照 Symbol同士が親 <=> 子で循環参照 メリット ASTから自分の親、子、Symbol、スコープなにアクセスができる #tskaigi_leverages 17/52

18.

なぜこうなっている? #tskaigi_leverages 18/52

19.

03 時代背景と JS の制約 #tskaigi_leverages 19/52

20.

周辺の出来事を見る🧐 2004年4⽉ 2008年9⽉ Gmail 公開 2011年頃 V8 / Chrome Erich Gamma が MS ⼊社 VS Code プレビュー VS Online "Monaco" 2006年10⽉ 2004 2015年4⽉29⽇ 2013年11⽉ 2014年 Docs & Spreadsheets Atom Shell / ピボット 2016 2016年4⽉ VS Code 1.0 GA 2005年2⽉ Google Maps 2009年5⽉ Node.js 初公開 2010年頃 内部開発開始 (Strada) 2012年10⽉ TS 0.8 公開 (GOTO) 2014年4⽉3⽇ TS 1.0 公開 #tskaigi_leverages 20/52

21.

2004 ~ 2006年 Google から Gmail, Google Map, Google Docsなどが公開 いわゆるAjax革命 Gmail (2004) Google, "Hitting send on the next 15 years of Gmail", The Keyword, 2019 Google Maps (2005) Version Museum, "22 Years of Google Maps Website Design History" (peterme.com, 2005) #tskaigi_leverages 21/52

22.

2008年 - 2010年 2008年9月GoogleのV8の公開や Chromeのリリース IE7のおおよそ約15倍 JSエンジン戦争により高速化が進んだ Node.jsの公開(2009) JavaScript Performance Rundown #tskaigi_leverages 22/52

23.

2010年 at Microsoft IEのChakraもパフォーマンスを上げる 高速化によりJSでより大規模なソフトウ ェアが書かれる流れ MS内部でOfficeなどの製品のWeb移 植が迫られていた TypeScript Origins: The Documentary(1m14s〜) #tskaigi_leverages 23/52

24.

しかし、、 当時のJSツーリングは非常に貧弱 他言語では普通な開発体験が全くできない Luccoは開発のためのエディタから作り始める 7000行あたりでのバグ追いに半日溶かす経験を繰り返す #tskaigi_leverages 24/52

25.

アプローチ 1. JSを遠ざける 別言語で書いて JS にトランスパイル Script#, CoffeeScript など MS内部の一部チームでは Script# を使っており、当初はそれを採 用しようとした 2. JSを置き換える Google の Dart 2011年10月発表 / 2013年11月 安定版リリース #tskaigi_leverages 25/52

26.

第3の選択肢 ターゲット言語からそこまで離れるくらいなら、 JS自体を直すべきでは🧐 #tskaigi_leverages 26/52

27.

Strada 誕生 既存JSのスーパーセットとして型を足す LuccoはTypeScriptの開発に専念 Editorは別のチームへ引き継がれる Visual Studio Online 'Monaco' 'Monaco' Workbench(内部用) 後の VSCode VS Code Day Keynote with Erich Gamma (3:41~) #tskaigi_leverages 27/52

28.

Dogfooding体制の確立 I made them agree to one thing, which is: when we have enough TypeScript that you could actually use it, please try to use it in building VS Code and give us feedback because we desperately need feedback. TypeScript で VS Code を作る VS Code で VSCode自身とTypeScript を作る #tskaigi_leverages 28/52

29.

初期のMonaco Workbench(VSCodeの前身) Behind The Scenes: The Making of VS Code (4:30~) #tskaigi_leverages 29/52

30.

TypeScriptに課せられた要件 JavaScriptのスーパーセットであること 型が実行時に消えること ツーリング/IDE体験を成立させること #tskaigi_leverages 30/52

31.

Editor / IDE 独特の要件 ソースコードが頻繁かつ局所的に変わる 同じ木に対して複数の関心事(補完 / hover / 診断 など)が同時並行にアクセスする 応答要件 200ms超えると遅い #tskaigi_leverages 31/52

32.

それを満たすための条件 1. イミュータブルである サブツリー再利用 インクリメンタルパース 並行読み取り 2. 親へのアクセスができる 上向き操作 3. 位置情報、コメントなどのTriviaの保持 #tskaigi_leverages 32/52

33.

カーソル位置から文脈へ エディタ操作の起点はカーソル位置のト ークン / 小さな構文ノード Hover: この位置は型情報は何? Breadcrumb: 今どの構文階層にい る? Find All References: このSymbolは 他のどこに使われている? #tskaigi_leverages 33/52

34.

親アクセスとImmutableの両立ができない 同じノードを複数の親で共有できない 部分木を再利用すると parent が旧ツリーを指す 構造共有もインクリメンタルパースも素直には書けない immutability と親アクセスを両立する仕組みが要る #tskaigi_leverages 34/52

35.

C# Roslyn 同じ Anders Hejlsberg が関わった C# コンパイラ Roslyn 従来コンパイラはバッチ処理のブラックボックス Roslyn は中間解析結果に API でアクセスできる設計 複数スレッドから解析結果を共有 IDE統合前提 -> そこで生まれたのが Red-Green Tree dotnet/roslyn #tskaigi_leverages 35/52

36.

red green tree(赤緑木) rust-analyzer や swiftでも採用 Green Tree parentを持たない、widthだけ持つ、純粋にimmutableで共有可能 Red Tree Greenをラップして、parentと絶対位置を遅延計算で乗せる "木のImmutablityに保ちたい"と"文脈情報(親や絶対位置)をたどりたい"を"両立 #tskaigi_leverages 36/52

37.

赤緑木で全部解決! #tskaigi_leverages 37/52

38.

ちょっとまった! #tskaigi_leverages 38/52

39.

問題その1 immutability をあまり活かせない 2010年ごろのJS ES5が出たばかり ES6はこの5年後 Node.jsのバージョンは0.1 ~ 0.2 worker APIは皆無 #tskaigi_leverages 39/52

40.

問題その2 メモリ消費 RUST pub struct GreenNode { kind: u16, // 2 bytes width: u32, // 4 bytes } // total 8 bytes(padding込み) GO type GreenNode struct { kind uint16 // 2 bytes width uint32 // 4 bytes } // total 8 bytes(padding込み) C# internal struct GreenNode { public ushort Kind; // 2 bytes public int FullWidth; // 4 bytes } // total 8 bytes(padding込み) #tskaigi_leverages 40/52

41.

JSの場合 class GreenNode { constructor( public kind: number, // 4 bytes public width: number, // 4 bytes ) {} } // ≈ 数十 bytes オブジェクトには数十バイトのヘッダがつく データレイアウトの制御が難しい news.ycombinator.com/item?id=46719899 V8やChakraで試したけど合わなかった おそらく現代と比べるとまだJITの最適化が未熟で試したけどあまり合わなかったと推察 #tskaigi_leverages 41/52

42.

その結果 永続データ構造としての immutability を捨てる ツーリング体験の実現とその機能の応答速度優先 => メモリの消費は犠牲 親ポインタを持つ incremental parseはもっと粗い粒度で実現 #tskaigi_leverages 42/52

43.

単一のAST + binder による段階的詰め込み parse → AST → binder → checker ↓ parent / symbol / flowNode を AST に直接貼り付ける 単一ツリーで構文と意味解析結果を兼ねる symbol / locals / flowNodeなどを AST に直接貼り付ける parent pointer は最初から(lazy 生成なし) red-greenでいうred木のみ。greenのようなイミュータブルな層はない 実行時性能への影響がなくIRを別で最適化する動機も薄かった #tskaigi_leverages 43/52

44.

JS環境に最適化してきたが限界が パフォーマンス優先の実装なのに時間がかかるレベル😵 もちろんメモリの消費も多い #tskaigi_leverages 44/52

45.

04 Go port による改善 #tskaigi_leverages 45/52

46.

なぜ Go で早くなる? 約 10x の内訳 3x: ネイティブ化 3x: 共有メモリ・マルチスレッド化 → 合計 約 10x TypeScript is being ported to Go | interview with Anders Hejlsberg #tskaigi_leverages 46/52

47.

単一バイナリ ランタイムのウォームアップに時間をショートカット 従来: tsc(JavaScriptで書かれたコンパイラ) -> Node.jsプロセス(V8) -> V8がJITコンパ イル -> 機械語を実行 Go: tsgoの単一バイナリ -> 機械語を実行 #tskaigi_leverages 47/52

48.

値型 JS: 子ノードや属性が別オブジェクト 辿るたびにヒープ上のバラバラな場所へジャンプ -> キャッシュミス (ただし現代ならかなり最適化が入っている) Go: struct をフィールドや配列にインラインで埋め込める 言語仕様レベルで連続配置が保証される 一回の読み取りで周辺の値が同じキャッシュラインに乗る #tskaigi_leverages 48/52

49.

値型化 typescript-go/internal/ast/ast.go type Node struct { Kind Kind Flags NodeFlags Loc core.TextRange id atomic.Uint64 Parent *Node data nodeData } // 値(インライン) // 値(インライン) // struct を値で埋め込み // 循環参照のためポインタは残る スカラ・小さい struct は値で埋め込み 循環参照が必要なところはポインタ維持 #tskaigi_leverages 49/52

50.

共有メモリ・マルチスレッド 2010年くらいのJS(node.js)にworker APIはない 後にworker threads + SharedArrayBufferで共有+並列処理できるが、、 オブジェクトをそのまま渡せず煩雑 参照だらけの巨大なオブジェクトグラフを表現するのが現実的でない #tskaigi_leverages 50/52

51.

共有メモリ・マルチスレッド #tskaigi_leverages 51/52

52.

まとめ 時代の都合で出発点が既存のJSの拡張路線だった JSで書くかつEditor/IDE統合を最初から念頭に置いた結果独特の仕組みになった その独特な仕組みの上にトリッキーな型システムが構築されてきた JS自体の制約から来ていた限界がGo Portで解消された memory layout 並列処理 etc. #tskaigi_leverages 52/52