はじめに
最近、typescript のコンパイラーのコードをサラサラーっと読んでいたのですが、なんとなく区切りがついたので、簡単にまとめておこうと思います。いつも通りではありますが、この記事で間違ったことを書く可能性は大いにあるため、正確な情報が必要であれば、参考に記載している参照元 URL を確認してください。
参考
typescript のコンパイルに関するソースコードがあるディレクトリです。
https://github.com/microsoft/TypeScript/tree/main/src/compiler
typescript の型チェックに関するソースコードです。超巨大なので typescript のリポジトリをクローンしてから、ローカルのエディターで開きましょう(汗)。
https://github.com/microsoft/TypeScript/blob/main/src/compiler/checker.ts
typescript のメンテナーがコンパイラーに関してまとめてくれています。
https://github.com/microsoft/TypeScript-Compiler-Notes
typescript のメンテナーがミニマムの Typescript を実装しています。
https://github.com/sandersn/mini-typescript
コンパイラーの流れ
ものすごくざっくりとコンパイラーの流れを分割すると、以下の3つになります。
- テキスト解析(parsing text)
- 型チェック(Type Checking)
- コード出力(Emit)
各工程について記載していきます。
テキスト解析(parsing text)
テキスト解析の最終的な目的はシンタックスツリーを作成することです(シンタックスツリーについては後述)。
シンタックスツリーを作成するために、まずは、コンパイル対象のコードをテキストとして読み取り、キーワード毎に分割していき、分割されたキーワードに対して名称を与えます。
例えば、以下のコードをキーワード毎に分割し、名称を与えると...
function hello() {
console.log("Hi");
}
以下になります。
// 分割対象`名称`
function`FunctionKeyword`
`WhitespaceTrivia`
hello`Identifier`
(`OpenParenToken`
)`CloseParenToken`
`WhitespaceTrivia`
{`OpenBraceToken`
`NewLineTrivia`
`WhitespaceTrivia`
console`Identifier`
.`DotToken`
log`Identifier`
(`OpenParenToken`
"Hi"`StringLiteral`
)`CloseParenToken`
;`SemicolonToken`
`NewLineTrivia`
}`CloseBraceToken`
`NewLineTrivia`
`EndOfFileToken`
上記の例を見ていくと、関数の宣言は FunctionKeyword
と言う名称が与えられ、関数名の hello は Identifier
になっています。また、console も Identifier
です。javascript の構文にない hello や console は固有の名称ということで Identifier
になります。
これらの処理は、typescript の playground に Ts Scanner というプラグインを入れることでも確認できます。
https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABACwKYBt1wBQEpEDeAUIohAgM5zqoB0WA5tgEQASMzuA3EQL5A
次は、これらに階層の情報を付与され、シンタックスツリーが作成されます(ここで作成されるシンタックスツリーが超重要で、この後の処理でもずっと使われます)。
シンタックスツリーの全量を書くのはしんどいので、省略しますが、ざっくり以下になります。
SourceFile:
- statements: [
Function Declaration
- name: Identifier
- body: Block
statements: [
ExpressionStatement
expression: CallExpress
...
]
]
ソースファイルがルートにあり、その一階層下に関数宣言(Function Declaration
)があり、関数宣言の一階層下に、関数名を表す name(Identifier
)があります。こんな感じでシンタックスツリーが作成されています。
※ 本当は、関数宣言(Function Declaration
)に name 以外にも色々な属性が付与されていますが、上記のざっくりしたシンタックスツリーでは、属性は省略して記載しています。属性とは、例えば、関数宣言の開始位置、終了位置があり、これらは vscode 等のエディターでエラー箇所をハイライトするために使われたり...他にも色々と属性はありますが、これらは後続の処理で使用されます。
これらの処理は、Typescript の playground の設定から AST Viewer を有効することでも確認できます。
型チェック(Type Checking)
型チェックは、宣言された "もの" に対して実行されていきます。"もの" は、変数や関数、インターフェイスなどです。
"もの" はテキスト解析でシンタックスツリーとして表現されており、シンタックスツリーの頭からつま先まで、型チェックは行われます。
<!-- これらを頭からつま先まで型チェック -->
SourceFile:
- statements: [
Function Declaration
- name: Identifier
- body: Block
statements: [
ExpressionStatement
expression: CallExpress
...
]
]
例えば、以下のコードがあります。
function hello() {
console.log("Hi");
}
上記に宣言された関数は、シンタックスツリーでは、hello(Identifier
)と言う名前の関数(Function Declaration
)で表現されており、型チェックでは、hello(Identifier
)と言う名前の関数(Function Declaration
)が重複して存在しないかがチェックされます。もし、存在すれば Duplicate function implementation.
と言うエラーが得られます。
次に、以下のコードがあります。
const num: number = 'A'
// error message : Type 'string' is not assignable to type 'number'.
このコードは、Type 'string' is not assignable to type 'number'.
というエラーが得られます。
このコードのシンタックスツリーを見ると以下のようになっています(関連のある属性のみ表示)。
VariableDeclaration:
- initializer: StringLiteral
- type: NumberKeyword
VariableDeclaration
は num
という名前の変数を宣言していることを意味しており、この変数の初期値(initializer
)は A
という StringLiteral
になっていますが、型(type
)は NumberKeyword
となっています。
型チェックにて、初期値(initializer
)と型(type
)が整合していないため、先の Type 'string' is not assignable to type 'number'.
というエラーメッセージを作成します。
コード出力(Emit)
.js
, .d.ts
, .map
ファイルが出力されます。コード出力では、テキスト解析で得られたシンタックスツリーを元に、tsconfig
に合わせて新しいシンタックスツリーを作成します。例えば、tsconfig
のターゲットが es2015
であれば、ESNext
, ES2020
, ES2019
, ES2018
, ES2017
, ES2016
, ES2015
のシンタックスは変換される必要があります。
まとめ
かなり粒度は荒くしていますが、簡単にコンパイラーの処理の流れのメモとして置いておきます。以上終わり。