🚩

静的型付けスクリプト言語「kirl」

5 min read

本記事は 言語実装 Advent Calendar 2021 22日目の記事です。

私が今年開発したプログラミング言語「kirl」の紹介です。

https://github.com/White-Green/kirl-lang

概要

タイトルに書いてある通り、kirlは静的型付けスクリプト言語で、これ自体はRustで実装されています
ちなみにkirlという命名に特に意味はありません(アルファベット数文字で入力しやすく、検索で類似のものがヒットしないぐらいの条件で適当に選びました)
構文はRustに、型システムはTypeScriptに影響を受けています
静的型付けなので、もちろん以下のようなプログラムは起動時にエラーになります

kirl
fn function(num: Number) {
    // Number(数値)型を引数に取る関数
}

// に、文字列(String型)を渡すのはエラー
function("This is String!");

さらに、このようにある程度まともな型検査を起動時に行っているにも関わらず起動時間は十分高速です

https://twitter.com/White_Green2525/status/1462449469216071682?s=20
型検査やコード生成が律速になりそうな小さいコードで比較すると、Pythonよりも高速なことがわかると思います
尤も、コード生成時の最適化をほぼ行っていないため今後の開発によって遅くなりそうですが
この規模のプログラムであればコード生成が長くとも数十ms程度で済む水準を保つよう機能拡張をする予定です

開発経緯

https://twitter.com/White_Green2525/status/1402072087368437762?s=20
TypeScriptの型システムにおいては、構造体のフィールドが部分的に一致していれば部分型とみなすようなStructural Subtyping(構造的部分型付け)が採用されています
(これを知ったのがTweetの6月です)
例えば、これがエラーにならないということですね
TypeScript
type A = {
    member_a: string,
    member_b: number,
}
type B = {
    member_a: string,
}
const VALUE_A: A = { member_a: "type A", member_b: 10 };
const VALUE_B: B = VALUE_A;

一方、PythonやJavascript等の広く使われているスクリプト言語ではメンバの存在を実行時に確認するDuck Typingが採用されています
一般的にDuck Typingの言語は表現力が高い(自由に書ける)と言われますが、構造的型付けを採用すれば表現の自由さを減らすことなく型検査だけできるのでは?と思ったのが開発のきっかけです
あわよくば静的型付けスクリプト言語が流行ってPython滅びないかなーと

蛇足ですが、型検査ができるとtypo程度の簡単なミスが実行前にわかったり、エディタでの入力補完やJump to definitionのようなコーディング支援機能が型検査機構をもとに完全なものを作成できるます
なので、データ分析などのプログラムを書き換えたり実行したりを繰り返す用途でスクリプト言語を使う場合型検査されるということの利点は十分にあると思っています

設計思想

このような経緯で開発した言語なので、Python等のスクリプト言語に比べて書きにくくなることをできるだけ避けるような言語仕様(型システムを含む)の設計をしています
ユースケースとしては

  • 高機能なシェルスクリプト(よくあるsetup.pyみたいな)
  • 手計算だと面倒な程度の計算や集計(kirlで計算処理を書くのではなくRustで書いたライブラリを呼び出す)
  • 他プログラムのプラグインシステムとして

あたりを想定しています

言語仕様

現在実装されている仕様のうち比較的特殊なものをいくつか紹介します

シャドーイング

同じ名前の変数を上書きして宣言できます
Rustにある機能が元ネタです
個人的に全言語実装してほしい機能です

kirl
var data: Number = 10;               // 変数`data`はNumber型
var data: String = data.to_string(); // 変数`data`はString型
var data: [String] = [data];         // 変数`data`はStringの配列型

もちろんブロックも分かれます

kirl
var data: Number = 0;
if data == 0 {
    var data: String = "some_string_value";
    // do something
}
// ここでdata == 0

UFCS

統一関数呼び出し構文(Uniform Function Call Syntax)が使えます
開発中はNimの機能だと思ってたんですがD言語にもあるらしいですね

kirl
std::io::println("hello!");
"hello".std::io::println();

map(function, iterable)よりiterable.map(function)のほうが可読性高い(特に複数の処理を連ねる場合)ので実装しました

型推論

C#と同じぐらいの強さの型推論が載っています
将来的にはVSCodeのプラグインとかで推論した型を表示できるようになると良いですね

kirl
var num1 = 0;        // 変数`num1`はNumber型(数値リテラルはNumber型なので)
var num2 = num1 + 1; // 変数`num2`はNumber型(Number型と数値リテラルの足し算はNumber型になるので)
var msg = "hello";   // 変数`msg`はString型(文字列リテラルはString型なので)

タプル型、無名構造体型

最近の言語には結構実装されているタプル型と、TypeScriptの{num: number, str: string}みたいな無名構造体型を実装しています
あと代入時に分解もできます 書きやすくてとても便利

kirl
// 型注釈は省略可能
var tuple: (Number, String) = (0, "string");
tuple.0th.println(); // 0
tuple.1st.println(); // string
var (num, str) = tuple; //分解代入
num.println(); // 0
str.println(); // string
kirl
// 型注釈は省略可能
var object: #{num: Number, str: String} = #{num: 0, str: "string"};
object.num.println(); // 0
object.str.println(); // string
var #{num, str: string} = object; //分解代入
num.println();    // 0
string.println(); // string

OR型

TypeScriptでいうところのUnion型が使えます
Rustのif-let、while-letのような構文で動的に型検査しつつ分解して使えます

kirl
var data: String | Number = 10;
if some_condition {
    data = "some_condition is true";
}

if var data: String = data {
    // data is String
}

while var data: Number = data {
    // data is Number
}

標準ライブラリの実装

kirlは他のプログラムのプラグインとして使うことを想定しているので、プラグインのホスト側の関数等をkirl側から呼ぶ場合の定義をそこそこ簡単にできてほしいです
ところで、これはkirlの標準ライブラリをRustで実装する場合と状況が同じですね?
というわけで、kirl側から呼び出せる関数の定義を関数にattributeを付けるのみでできるようにしています

実例はこのへんです

https://github.com/White-Green/kirl-lang/blob/4e818d64c3c3c1eee5d0bedcf3fe8ed82a277062/kirl_stdlib/src/lib.rs#L300-L304

kirl_function attributeが関数定義用のもので、その関数の型だけ与えています
これは中身がproc_macroになっていて、同名の構造体とそれに対するtraitの実装を自動生成しています
定義はkirl_common_macro内にあります

プログラム例

markdownを変換してスライド形式のhtmlにするプログラムをkirlで書いたものがあります
紹介した以外の言語仕様が見たければこちらも読んでいただけると

kirl製スライド作成プログラム
入力サンプル

処理系

kirlの処理系はおおまかには以下のようになっています
実際はimportを通じて複数ファイル読みこむ場合があるのでもうちょっと複雑です

flowchart TD
    プログラム -- 字句解析 --> 字句列
    字句列 -- 構文解析 --> 構文木
    構文木 -- 構文木解析 --> HIR
    HIR -- 名前解決,型検査など/コード生成 --> LIR
    LIR -- コード生成 --> VMコード

図中のHIR/LIRはHigh-level Intermediate Representation(高レベル中間表現)とLow-level --(低レベル--)の略です
字句解析はDFA、構文解析はLR(1)アルゴリズムを使っていて、どちらもジェネレータを自作してます(もうちょっとまともな実装にしたいですね)

https://github.com/White-Green/Yet-Another-Rust-Parser
これで生成したVMコードをインタプリタ実行しています

今後の開発

issueに今後開発したい内容を書いているのですが、これを見てわかる通り現状だと実用には全然足りないです

https://github.com/White-Green/kirl-lang/issues
特に、型システム周りに結構穴がありそうだということがわかっているので(例えばこれとか)
https://twitter.com/White_Green2525/status/1463375905607147524?s=20
このあたりに適当な落としどころを見つけないとといったところです

ということで、とりあえずTaPLを真面目に読みます...

さいごに

このkirl言語ですが、
U-22 プログラミング・コンテスト2021で経済産業大臣賞(テクノロジー)を頂きました

https://twitter.com/White_Green2525/status/1462365795304886274?s=20

Discussion

ログインするとコメントできます