静的型検査自作入門
-
名前: r-chaser53
-
所属: LINE株式会社
-
何やってる人?
-
分類はフロントエンジニア
-
なんかいろいろやってます
-
誰?
-
自作言語を作っていて静的型検査が欲しくなった
-
とりあえず単体で動くものを作った
-
知見共有という名の感想
はじめに
今回作ったもの
説明: JavaScriptのprimitive型の型検査のみ行うmodule
- ASTの作成には@babel/parserを使用
- Lexer、Parserを自作しなくていいのは楽
- 自作と比べてAST綺麗すぎて笑える
demo
方針
適当なParserでASTを作り、対象のASTを2回走査する
-
変数をScopeに登録
-
型検査したい項目を調べエラーがあれば記録
-
エラーを出力
1. 変数をScopeに登録
-
Global Scopeからスタート
-
関数やIf文などが存在する度、新しいScopeを作成
-
変数を宣言する箇所を見つける度、現在のScopeに登録
1. 変数をScopeに登録
Scopeと変数定義
/* ここまで処理 */
let a = 'str';
let b = 4;
{
let a = true;
a = false;
b = 'str2';
a = c;
}
const scopes = [
/* Global Scope */
{
id: 1,
/* Global Scopeはnull */
parentId: null,
/* 変数の定義情報 */
defs: [],
},
/* Global Scope */
}
ソースコードを上から処理していく
ソースコード
1. 変数をScopeに登録
Scopeと変数定義
let a = 'str';
let b = 4;
/* ここまで処理 */
{
let a = true;
a = false;
b = 'str2';
a = c;
}
const scopes = [
/* Global Scope */
{
id: 1,
/* Global Scopeはnull */
parentId: null,
/* 変数の定義情報 */
defs: [
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
/* Global Scope */
}
ソースコードを上から処理していく
ソースコード
1. 変数をScopeに登録
Scopeと変数定義
let a = 'str';
let b = 4;
{
let a = true;
/* ここまで処理 */
a = false;
b = 'str2';
a = c;
}
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
Blockがあったら新しいScopeを作る
ソースコード
1. 変数をScopeに登録
Scopeと変数定義
let a = 'str';
let b = 4;
{
let a = true;
a = false;
b = 'str2';
a = c;
}
/* ここまで処理 */
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
宣言でない場合は何もしない
ソースコード
2. 型検査したい項目を調べエラーがあれば記録
以下のような検査項目を1つずつ調べていく(※)
検査項目に違反したらエラーオブジェクトをstackに登録する
以下はJavaScript的に問題ないことも含んでいる
-
代入の左辺と右辺は同じ型
-
配列参照添え字は整数
-
If 条件部はBoolean etc...
※ 同時でも問題はないとは思う
今回はこれだけやる
2. 型検査したい項目を調べエラーがあれば記録
Scopeと変数定義
/* ここまで処理 */
let a = 'str';
let b = 4;
{
let a = true;
a = false;
b = 'str2';
a = c;
}
1と同じように上から処理していく
ソースコード
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
2. 型検査したい項目を調べエラーがあれば記録
Scopeと変数定義
let a = 'str';
let b = 4;
{
let a = true;
/* ここまで処理 */
a = false;
b = 'str';
a = c;
}
宣言は無視して飛ばす
ソースコード
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
2. 型検査したい項目を調べエラーがあれば記録
Scopeと変数定義
let a = 'str';
let b = 4;
{
let a = true;
/*
「a = false;」 の処理中
1. ScopeのIdは2
2. a(左)はdefs内に存在
3. a(左)はBoolean
4. false(右)はBoolean
5. 型は同じセーフ!
*/
a = false;
b = 'str2';
a = c;
}
a = false; の型は問題なし
ソースコード
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
2. 型検査したい項目を調べエラーがあれば記録
Scopeと変数定義
let a = 'str';
let b = 4;
{
let a = true;
a = false;
/*
「b = 'str2';」 の処理中
1. ScopeのIdは2
2. b(左)はdefs内に存在しない
3. ScopeのparentIdを使い、親Scopeを参照
4. b(左)はdefs内に存在
5. b(左)はNumber
6. 'str2'(右)はString
7. 型が違う。エラー!
*/
b = 'str2';
a = c;
}
b = 'str2'; は型エラー。エラー情報を記録する
ソースコード
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
2. 型検査したい項目を調べエラーがあれば記録
Scopeと変数定義
let a = 'str';
let b = 4;
{
let a = true;
a = false;
b = 'str2';
/*
「a = c;」 の処理中
1. ScopeのIdは2
2. a(左)はdefs内に存在
3. a(左)はBoolean
4. c(右)はdefs内に存在しない
5. ScopeのparentIdを使い、親Scopeを参照
6. c(右)は親のdefs内にも存在しない
7. 親のparentIdはnull。これ以上ない!
8. c(右)は未定義のエラー
*/
a = c;
}
a = c; は未定義のエラー。エラー情報を記録する
ソースコード
const scopes = [
{
id: 1,
parentId: null, /* Global Scopeはnull */
defs: [ /* 変数の定義情報 */
{
name: 'a',
type: 'String',
},
{
name: 'b',
type: 'Number',
}
],
},
{
id: 2,
parentId: 1,
defs: [
{
name: 'a',
type: 'Boolean'
}
]
}
}]
3. エラーを出力
2で登録したエラーオブジェクトを元にエラーを出力する
- エラーオブジェクトの情報次第でエラーメッセージは変化
- 行数、列数が分かればエラーの場所が明示できる
- いい感じに作れば良いのでは?
単なるオモチャでは?
それを言ってはいけない
でもこの方針でも以下のような事には対応できる
- オブジェクトの型 (Classもいける)
- 関数の型
- TypeScriptのようなInterface, Typeの導入(※)
1と2の対象とロジックを改良してやれば良い
※ この辺やるとParserを自作する羽目になると思う
実際のところ難しい?
正直結構難しい。この程度でもすごい勢いで複雑になった
でもNodeによって処理は分けられる
だからある程度どうにかなりそう
以下のような事は今回は無視している
- 型の昇格
- 再帰型 etc.
やりがいのある内容だと思う
感想
- 楽しい
- TypeScriptや他の言語の型システムの実装が気になる
- パターンマッチが欲しい。enumが弱い
- テストないと死ぬ
- デバッグ用のエラーメッセージ便利
- テスト用の簡易CLI書くといいかも
- テスト対象やlogのレベル指定できるやつ
備考
let a = 3; みたいなものを宣言と書いているのは
@babel/parserが出力するASTのnodeのtypeがVariableDeclarationのため
正直今回の場合、全部定義と書いた方が良いと思うのだけど
何故VariableDeclarationなのだろう…?
参考資料
ご静聴ありがとうございました
静的型検査自作入門
By rchaser53
静的型検査自作入門
JavaScript製のJavaScriptの静的型検査機を作った話 https://github.com/rchaser53/primary-type-checker
- 1,767