自作言語 msaw を作った

WebAssembly にコンパイルする言語 msaw を作りました。minimini_lang の続きで、関数・分岐・ループ・ローカル変数・ホスト関数の import を備えています。Wasm 向け言語開発のテストを兼ねていて、今後作る言語のベースにする予定です。

msaw Playground でブラウザから試せます。

例

import env.print_i32 as fn print_i32(i32);

fn main() {
    print_i32(_gcd(-12, 18)); // 6
}

fn _gcd(a: i32, b: i32) -> i32 {
    if a < 0 { a = -a; }
    if b < 0 { b = -b; }
    loop {
        if b == 0 {
            return a;
        }
        let r = a % b;
        a = b;
        b = r;
    }
}

言語仕様

字句

  • 文は ; で終わる。改行に意味はない
  • コメントは //
  • 識別子: [A-Za-z_][A-Za-z0-9_]*
  • 整数リテラル: 10進数。負数は単項マイナスで書く
  • 文字列リテラル: "..."。エスケープなし

型

i32 のみ。比較の結果は 1(真) / 0(偽)、if の条件は 0 以外が真。

トップレベル

import env.print_i32 as fn print_i32(i32);          // 関数の import
import env.buffer as memory(1);                      // メモリの import
global let start = import env.start_string as i32;  // グローバルの import
global let len = 12;                                 // グローバル変数
data(start, "hello world!");                         // メモリの初期値
fn main() { ... }                                    // 関数定義

定義の順番は自由です。

  • import: パスは モジュール名.名前 の2段。関数の引数は型のみ書く。memory(n) の n は最小ページ数
  • global let: 右辺は整数・負の整数・他のグローバルのいずれか。関数から代入されているグローバルは自動でミュータブルになる (mut キーワードはない)。import したグローバルには代入できない
  • data: 指定オフセットに文字列を置く。文字列リテラルはここでしか使えない
  • fn: _ で始まる関数は非公開、それ以外は export される。引数には代入できる
fn add(a: i32, b: i32) -> i32 { return a + b; }  // export される
fn _helper() { }                                  // export されない

文

文意味
let x = 式;ローカル変数の宣言 (初期値必須)
x = 式;代入
式;評価して値は捨てる
if 条件 { } else if 条件 { } else { }分岐
loop { }無限ループ
break; / continue;内側の loop を抜ける / 先頭へ戻る
return 式; / return;関数から戻る
  • 代入は文。x = y = 1; や f(x = 1); は書けない
  • if と loop の本体は { } 必須
  • while / for はない

演算子

優先順位演算子結合
1-x右
2* / %左
3+ -左
4== != < <= > >=連鎖不可
  • 符号付き 32bit 演算。オーバーフローは折り返す
  • / は 0 方向へ切り捨て、% の符号は左辺に従う。0 除算はトラップ
  • a < b < c は構文エラー
  • && || ! は未実装

関数呼び出しの引数の数、戻り値のない関数を値として使うことはコンパイル時にチェックされます。

スコープ

let の変数は宣言した { } の終わりまで有効です。同じ名前で let するとシャドーイングになります。

let x = 1;
let x = x + 1;      // 右辺の x は 1
if x > 0 {
    let x = x * 10; // if の中だけの x (20)
}
// ここの x は 2

return のチェック

戻り値のある関数で、末尾まで処理が流れ落ちる可能性があるとコンパイルエラーになります。

fn f() -> i32 { if 1 { return 1; } }
// error: `f` may reach its end without returning a value

break のない loop は抜けないものとして扱います。冒頭の _gcd が末尾に return なしで通るのはこのためです。

実行環境

ホストから env として以下を import できます。

import型内容
env.print_i32fn(i32)数値を出力
env.print_stringfn(i32)start_string から n バイトを文字列として出力
env.buffermemory(1)共有メモリ
env.start_stringi32文字列の置き場所 (100)
import env.print_string as fn print_string(i32);
import env.buffer as memory(1);

global let start_string = import env.start_string as i32;
data(start_string, "hello world!");

fn helloworld() {
    print_string(12);
}

プレイグラウンドでは関数と引数を指定して実行します。無限ループは停止ボタンで止められます。