Wado言語の開発はずっと続けていて、最近は3-4日くらいで(Wadoの開発のために契約した)Claude Code Max 20x のweekly limitに達してしまってちょっと物足りないくらいです。
まあとにかく、けっこう面白い機能が揃ってきたので紹介します。
imort構文
「このモジュールは標準ライブラリなのかサードパーティなのかローカルなのか、そしてこのライブラリの導入でどういうシンボルが導入されるのか」というのはけっこう言語によって判断が変わるところで、たとえばRustは Cargo.toml に宣言したライブラリを宣言なく使うことができて、しかも wildcard import があるので「どのシンボルがどのライブラリから来ているか」というのがパッとみた感じわかりにくいです。LSPの力を借りればできますが、逆にいえばLSP前提の言語設計といえます。
importに関しては、メジャーな言語でいうとES modulesはかなり優れていて、どのシンボルがimportされるか、どこからきたモジュールかが比較的わかりやすいんですね。こんなふうに:
// JavaScript ES modules
// nodeの標準モジュール
import fs from "node:fs";
// サードパーティのモジュール
import base64 from "base64";
// ローカルのモジュール
import { foo } from "./foo.js";
ただ、ES Modulesは "default export" という概念を導入してしまったせいで、パッとみてnamespace importなのかdefault importなのかがわからなという問題があります。
そこで、Wadoもほぼ同じ構文を採用しつつ(import というキーワードを別の意味で使いたいので use ですが)、default exportを排除した設計です:
// Wado
// ローカルのモジュール
use { foo } from "./foo.wado";
// 標準ライブラリのcore:json。この構文はかならず namspace import
use json from "core:json";
さらに、ES modulesのimport構文は後ろにオプションをとれるので、それもES modulesからそのまま拝借してカスタマイズできるようにしました。いまのところできるのは .wasm & .wat をimportすることと:
// Wado標準ライブラリの一部
// 数学関数はlibm crateをリンクしてつかう
use {
libm_sin as f64_sin, libm_cos as f64_cos, libm_tan as f64_tan,
// (省略)
} from "../libm.wat" with { type: "wat" };
それと、任意のファイルから.wadoを生成してimportすることです。マクロがない代わりですが、成果物がファイルシステム上にWadoファイルができるのでデバッグ時は単に生成されたファイルを読むだけです:
// Wado
// ANTLR4の文法ファイルからWado製パーサーを生成してimportする
use ts from "./grammars/TypeScriptLexer.g4"with {
generator: {
module: "../src/generator.wado",
inputs: ["./grammars/TypeScriptParser.g4"],
options: { highlight: false },
output_dir: "tests/generated/typescript",
},
};
// later: ts::parse(&ts_source) -> CST
importは基本的な機能ながらなかなかしっくりする言語が少ないので、Wadoの仕様には結構満足しています。
assert構文
assertもいろいろ流儀がありますね。構文か、関数か。リリースビルドで消すか残すか。エラーのときの振る舞い。
Wadoはこうです:
- 構文
- リリースビルドで消すオプションは無し
- エラーのときはpower assert相当の振る舞い
とくに、assertを消すオプションがないのは賛否があるかもしれません。
しかし、たとえばリリースビルドで「assertを消す」という選択肢を取るとき、それは暗黙のうちに「リリースビルドにはバグが含まれないことを仮定する」ということに他ならないと思うのですよ。でもそんなはずはないですよね。バグのないプログラムはありえない。そうだとすると、「assertを消す」には別の含意が生まれます。それは「プログラムの契約が守られない(=状態が不正)まま動作し続けることを許容する」ということ。
Wadoでassertを消すオプションをつけないのはまさにこのためです。すなわち、リリースビルドにもバグはあるし、プログラムの契約が守られないまま動作し続けることは認められない、という表明です。
ついでに、assertがコケたときにpower assert相当の振る舞いもします。たとえば次のプログラム:
#!/usr/bin/env wado test
test {
let x = 3;
let y = 4;
assert x + y > 10;
}
これを wado test で走らせるとコケて次のようなメッセージを出します:
Assertion failed in __test_0 at assert_failure.wado:6 condition: x + y > 10 x: 3 y: 4 x + y: 7
x, y, そして x + y の値が出力されます。オリジナルのpower assertと違って、人間にとっての読みやすさはそこまで重視していませんが、コンパクトで情報量が多いようなメッセージになるように工夫しています。
このように、振る舞いとしていろいろな工夫があるために、関数ではなく構文にしています。
effect system
エフェクトシステムは、IOなどの外部リソースの操作や非決定性を構文として表現できるようにする機能です。たとえば、次のHello worldプログラムの "Stdout" は、stdoutに対して操作するということをシグネチャで表現するものです:
#!/usr/bin/env wado run
use { println, Stdout } from "core:cli";
export fn run() with Stdout {
println("Hello, world!");
}
これ、導入するとあらゆる関数にeffectがついてまわるので、かなり煩雑です。人間相手のプログラミング言語だったら、煩雑すぎて導入をためらうレベルです。
しかしWadoはcoding agentsのために設計された言語です。であれば、煩雑であっても明示的で堅牢なほうがよい、という判断で入れてます。
Wadoの場合はさらに、effectと WASI (Wasm世界のシステムコール)が紐づいていて、さらにeffect handlerの差し替え構文により、ちょっと面白い感じではあるなあと思っています。
effect handlerはこういう感じ:
#!/usr/bin/env wado run
use { println, Stdout } from "core:cli";
effect Counter {
fn next() -> i32;
}
struct CounterState {
value: i32,
}
impl Counter for CounterState {
fn next(&mut self) -> i32 {
self.value += 1;
resume self.value;
}
}
export fn run() with Stdout {
let mut c = CounterState { value: 0 };
with Counter => &mut c do { // このブロック内部では Counter effect の実装は CounterStateのインスタンス c が担う
let a = Counter::next(); // c.next() が呼ばれる
let b = Counter::next();
println(`a={a}, b={b}, final={c.value}`); # a=1, b=2, final=3
}
}
言語機能としてのeffect handlerの振る舞いは、traitそのものです(WadoのtraitはRustのtraitとほぼ同等)。「effect handlerとは、特定のtraitを実装した何らかの型である」と言えます。
WASI = effectを提供するもの、という統合とtraitシステムとの統合でいい感じにeffect & effect handlerを作れたのではないかなと思っています。まあ人間が書くときはそこそこ大変なんですけど。
とまあ、あらゆる言語機能を考えに考えた末に「これしかない!」と思うような形にしているので、これが楽しくないわけがないんですね。