自作言語に型推論を

つけたい話

社内用

誰?

  • 自作言語でfizzbuzzできるとこまでやった

  • LLVM-IRが最終出力産物

  • 発生するエラーのハンドリングが厳しい

  • 型を手で入力させるも非常に辛い

背景

  というわけで

型推論を実装してみよう

ついでに色々と挑戦してみよう

成果物

r-type-check

自作言語の型推論と型検査をする。評価はできない

url:    https://github.com/rchaser53/r-type-check

型推論 is 何

文脈から型を推論すること

// TypeScriptのファイルと考えてほしい

const abc = 13;         // number

const strFunc = (a) => {
    return a + "hoge"     // string
};                                    // strFuncは引数stringを取り、戻り値stringを取る関数
  1. 手製のLexer, Parserからの脱却

  2. 宣言時に型を明示させない

  3. 型関連の知識の強化

今回やりたかったこと

  • Lexer: 字句解析
    • 文字列を1文字ずつ分析していって字句を作る
    • ex. 123, "abc", true, false
  • Parser: 構文解析
    • Lexerが作ったものを受け取りASTを作る
  • AST (Abstract Syntax Tree: 抽象構文木)
    • ソースファイルを扱いやすいように木構造にした物
    • 型解析やLint、Fomarterなど色々なことに使われる
    • V言語(笑)みたいにASTを作らない言語もある

Lexer, Parserって?

https://ts-ast-viewer.com/ 使ってASTのサンプル見せること
  • combineを使用

  • combine = parser combinator

  • 慣れるまで時間かかったが便利

  • 前回までは構文の修正にかなり時間がかかったが、cominbeにより相当短縮できた

1. 手製のLexer, Parserからの脱却

  • 制限はあるがある程度は作れた

    • 引数、戻り値の型は省略できる
    • 再帰は大丈夫
    • let多相性は作れた?

2. 宣言時に型を明示させない

  • demo1

    • 引数、戻り値の型は省略できる

2. 宣言時に型を明示させない

  • demo2

    • 再帰は大丈夫

2. 宣言時に型を明示させない

  • demo3

    • let多相性は作れた?

2. 宣言時に型を明示させない

多相性 is 何?

  • 1つのコードで複数の型を扱えるようにした物

    • パラメータ多相(genericsとか)
    • アドホック多相(オーバーロードとか)
    • let多相性(今回実装した物)
  • 逆に1つの型しか扱えないものを単相型という

let多相性をもうちょい

  • 多相性をトップレベルのlet束縛に限定する

  • 演算子や式の構造から型を判断する

  • 判断した結果を保存する

  • 不明な変数は残す

  • 不明な変数はワイルドカードのように扱う

  • 整合性が取れなかったらエラー

実装の方針

  • 基本的に上から順に式の解決をしていく

  • 変数などの型情報は保存するが      関数は毎回型を解析している

  • 「fn (a) { return a }」みたいなケースのため

多相と再帰

let abc = fn(a) { return a; } in (
  abc(22);     // ここで「引数intをとり、intを返す関数と保存してしまうと
  abc("aa");  // ここで引数stringが渡されるのでエラーになる(本来ならばOKのはず)
)
このやり方だと

再帰の場合止まらない…

  • 呼び出し元の関数の型情報を保存しておき、同じ関数から呼び出されていた場合使用する

  • このやり方だと相互再帰は止まらない

多相と再帰

// 相互再帰
let abc = fn(a) {
  if (a <10) {
    def(a+1);
 }
}
def = fn(b) {
  if (b<10) {
    abc(b+1);
  }
}
in (
  abc(0);  // stack over flow!
)
 呼び出すパターンを保存する?

良い方法が思いつかないので別途調査

  • 呼び出し元の関数の型情報を保存しておき、同じ関数から呼び出されていた場合使用する

  • このやり方だと相互再帰は止まらない

どうするべきだった?

// 相互再帰
let abc = fn(a) {
  if (a <10) {
    def(a+1);
 }
}
def = fn(b) {
  if (b<10) {
    abc(b+1);
  }
}
in (
  abc(0);  // stack over flow!
)
 呼び出すパターンを保存する?

良い方法が思いつかないので別途調査

  • 健全性、完全性の保証

  • 相互再帰

  • エラーメッセージの正確な表示

  • 構文エラーのエラーメッセージ表示

  • hashmapの型推論の完全なサポート

今回できなかったこと

  • 構文や型を明確に定義できていない

    • となると健全性も完全性もクソもない

    • 定義を明確にするために何かのサブタイプにを作り、それに対して型推論機を作った方が勉強としては良さそう

健全性、完全性

正しく型付けされた項は行き詰まりにならず、次の状態のどちらかである

  • 値である

  • 評価規則によって評価を進められ、評価後も正しく型付けされている

    • a+a みたいなイメージ

健全性(安全性)

型システム入門には以上のように書かれているが難しい…

型付けできたら正しく動く

くらいの認識で良いと思う(合ってるかは怪しい)

正しいプログラムは必ず型が推論できる

  • 型システム入門に定義らしい定義が見つからなかった
  • 完全性を保つのは難しいので、健全性だけ保つケースが多い

完全性

  • 読むと思考停止していた型関連の文章と 戦えるようにはなった

  • 大量に出てきた単語や公式もある程度は 理解できたと思う

  • Rustの型周りの実装の理解が進むはず…

3. 型関連の知識の強化

  • 楽しい

  • 言語仕様をもう少し決めてから作るべき

  • ケースが多い。開発手順から改善するべき

    • ex. モンキーテスト=> バグ発見 => テスト追加
  • 文章だけではイメージが湧かないので  もっとソースを読むべき

  • PRでも投げながら学習していきたいと思う

まとめや感想

参考資料

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

社内用

By rchaser53

社内用

ASTとかJser向けとか色々直す

  • 1,169