発端
先日、gfxさんの自作プログラミング言語の記事を読みました。
それで自分も自作プログラミング言語作りをやってみたくなり、CodexやClaudeをしばくこと幾晩。ちょっとしたプログラミング言語とコンパイラを作って、その言語で小さなウェブサイトをデプロイするところまでいったので、記事に残したいと思います。言語の名前はTunaScriptといいます1。
コンセプト
今回作った言語のコンセプトをざっくりいうと、他の言語や形式との親和性を構文レベルで改善することにあります。
- SQLとの親和性。ソースコード内にSQLを埋め込みます。ORマッパーやクエリビルダーは不要です
- JSONとの親和性。Zodのようなスキーマ定義が不要です
- HTMLとの親和性。TypeScriptのJSXと同様の構文を埋め込むことができます
SQL親和性
Rustのsqlxというライブラリをよく使っているのですが、これは以下のような感じでマクロを利用してSQLを型安全に埋め込むライブラリです。
let countries = sqlx::query!(
"
SELECT country, COUNT(*) as count
FROM users
GROUP BY country
WHERE organization = ?
",
organization
)
.fetch_all(&pool) // -> Vec<{ country: String, count: i64 }>
.await?;
これで型推論と型チェックもちゃんと利きますし、直接SQLを書けるのでやりたいことは何でも直感的にできます。ただし、微妙な部分もありました。
- SQL部分の構文ハイライトが効かない
- SQL部分のコードフォーマットが効かない
- 言語サーバーがテーブル構造を取得するのに実データベースへのアクセスが必要
それならば、もうプログラミング言語側にSQLを取り込んでしまえばいいのでは?と思いました。今回作ったTunaScriptでは、テーブルの定義を、SQLのCREATE TABLEほぼそのままの構文で、ソースコード中に直接書きます。
create_table users {
user_id
name
organization TEXT NOT NULL,
country
}
また、そのテーブルを参照するSQLを、以下のようにソースコード中にそのまま書くことができます。
const countries = fetch_all {
SELECT country, COUNT(*) as count
FROM users
GROUP BY country
WHERE organization = {organization}
}?
コードの字面がとてもすっきりしていることがわかります。また先ほど挙げた問題点もすべて解決しています。
- SQL部分も含めて、まるごとコードフォーマッターが効きます
- SQL部分も含めて構文ハイライトが効きます2
- 実際のRDBへの接続は不要で、コンパイラによってテーブル名やカラム名の検証が効きます。うっかりテーブル名や型を書き間違えてもコンパイルエラーになります。
まだ実験的な実装なので、中身のデータベースはSQLiteです。でももうちょっと頑張れば、たぶんPostgresQLのようなデータベースにも対応できるでしょう。
JSON親和性
APIでやり取りするデータ形式ではJSONは事実上の標準です。私の場合、RustではSerde、TypeScriptではZodやValibotを使ってJSONとのデータの相互変換を扱うことが多いです。たとえば、Valibotの場合は以下のようにJSONスキーマを定義し、スキーマからInferOutputを使って型を導出します3。
const LoginSchema = v.object({
email: v.string(),
password: v.string(),
});
// { email: string; password: string }
type LoginData = v.InferOutput<typeof LoginSchema>;
const output1 = v.parse(LoginSchema, { email: '', password: '' });
これはこれでいいのですが、以下のような問題は残ります。
- 型定義とスキーマが別々になってしまう
- 型のドキュメントコメントが書きづらい
- 再帰する型だと
lazyが必要になってちょっとわかりづらい
TunaScriptでは、データ型さえ定義すれば自動的にJSONとの相互変換が可能になります。以下のように変換先の型名を指定してparse関数を呼ぶだけです。
type LoginData = { email: string, password: string }
const data = parse<LoginData>(`{ email: '', password: '' }`)
TypeScriptではparseはanyを返すので、たとえ型が一致しなくてもそのまま処理が進んでしまい、予期しないところでランタイムエラーになることがあります。しかしTunaScriptのparseは入力をJSONとして構造を読み取るだけでなく、その構造が定義された型と一致することを検証します。このため、予期しないところでランタイムエラーが起きることはありません。
HTML親和性
TypeScriptなどと同様に、JSXも構文に組み込まれています。ウェブアプリケーションを作る言語であればもはや必須といっていいでしょう。
export function LoginData(props: LodingData): JSX {
const { email, password } = props
return <form>
<input value={email} />
<input type="password" value={password}/>
</form>
}
サンプルコード
create_table todo {
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL
}
const users = fetch_all {
SELECT id, name, email
FROM user
}
return <ul>{
users.map(function(user){
return <li>
<span>Name:{user.name} Email:{user.email}</span>
</li>
})
}</ul>
デモ
わざわざ自分の環境にTunaScriptをインストールして試すような人はいないと思うので、ブラウザで動くプレイグラウンドを作りました。このプレイグラウンドのバックエンド自体がTunaScriptで作られています4。
一応、コードフォーマットや構文ハイライトも利くようになっています。
そのほか
TunaScriptの基本的な構文はTypeScriptに寄せてあります。これはコードを見た人が馴染みやすいようにと、LLMによっても理解の正確さが上がりそうな気がするからです。それに、Qiitaのようなサイトにコード片を書くときにも、TypeScriptを指定してあげればそこそこちゃんと構文ハイライトが効きます。
コンパイラの実装にはGoを使っています。コンパイラのコードはほぼ100%をAIが書いています。人間が書かないのでどんな言語でも構わなかったのですが、Goだとビルドが早そうみたいな素朴な理由もありますし、LLMがts-goを参考に実装できる可能性があるというのがひとつの理由です。
コンパイルターゲットはWASMです。これについては深い理由はないのですが、ウェブブラウザで実行出来たら面白いかな、というくらいです。実際、簡単なプログラムであればブラウザでも実行できるWASMを出力できます。ただし先に述べたようなSQLとの相互呼び出しは専用のランタイムでないと実行できません。
TunaScriptはエラーハンドリングもやや特徴的で、エラーを返す関数はT | Errorで表現します。RustやElmのResult型などと同じように返り値の型にエラーを含めるタイプですが、成功してTを返すか、失敗してErrorを返すか、という非常に自明な表現になっていて、JSONとの相互変換もそのまま書けます。
Rustなどと似た構文の?で、T | ErrorからTを取り出す構文も用意してあります。これによって、エラーの時にその関数から簡単に抜け出せるようになるなど、構文として見やすく改善しています。
さいごに
TypeScriptは高度な型システムを持ち、ZodやValibotはそれを駆使して複雑なスキーマ言語を実現します。また、Rustのような言語はマクロであらゆることが実現しまいます。それでも、異なる言語や形式との境界ではミスマッチが生じます。それを今回、AIを駆使して欲しい構文をガンガンぶちこんでしまうことで改善できないかと試みました。
TunaScriptでは簡単なサンプルプログラムの作成しかしていませんし、現状では実用レベルにはほど遠いです。今回はひとまずコンパイラとコードフォーマッター、vscodeの構文ハイライトとプレイグラウンドを作ったのですが、実用的なプログラミング言語にするには必要なものがまだまだたくさんあります。
- 言語サーバーと各種のエディタ向けのコード補間やリファクタリングツールなどの拡張
- ドキュメントジェネレーター
- パッケージマネージャー
- Linter
- テスティングフレームワーク
- オプティマイザやベンチマーク
- チュートリアルなどのドキュメント
- そして熱意のあるコミュニティと豊富なライブラリ
自分は以前にも自作プログラミング言語をやったことがあるのですが、そのときとは速度感が大きく変わりました。こういう構文が欲しい、と思ったら雑にAIに投げるだけで実装が済んで、すぐさま使い勝手を試せるのはとても面白い感じがあります。
AIの台頭に伴い、今後は人間がコードを書く頻度は大きく減っていくでしょう。今回TunaScriptで実装してみたような構文レベルの改善が、実際の生産性に繋がることはあまりないとは思います。それでも自作プログラミング言語というのはソフトウェア開発者のひとつの夢ですし、それが容易に実現したことは面白い時代が来たことを感じさせます。
参考
- TunaScriptのリポジトリ https://github.com/aratama/tunascript
- 同じくgfxさんの記事を読んで作ったというTerraformライクなインフラ管理ツール https://mizzy.org/blog/2026/01/24/1/