静的型検査自作入門

  • 名前: r-chaser53

  • 所属: LINE株式会社

  • 何やってる人?

    • 分類はフロントエンジニア

    • なんかいろいろやってます

誰?

  • 自作言語を作っていて静的型検査が欲しくなった

  • とりあえず単体で動くものを作った

  • 知見共有という名の感想

はじめに

今回作ったもの

名前: primary-type-checker

説明: JavaScriptのprimitive型の型検査のみ行うmodule

  • ASTの作成には@babel/parserを使用
  • Lexer、Parserを自作しなくていいのは楽
  • 自作と比べてAST綺麗すぎて笑える

demo

方針

適当なParserでASTを作り、対象のASTを2回走査する

  1. 変数をScopeに登録

  2. 型検査したい項目を調べエラーがあれば記録

  3. エラーを出力

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的に問題ないことも含んでいる

  1. 代入の左辺と右辺は同じ型

  2. 配列参照添え字は整数

  3. 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なのだろう…?

参考資料

言語実装パターン

 

ご静聴ありがとうございました

Made with Slides.com