これは Swift Advent Calendar 2024 の3日目の記事です。
一つ前の記事は Ignite の新規機能紹介:Spotify 対応やモーダルの活用 です。
次の記事は async
なメソッドは、宣言時に限定しない限り隔離領域を超えます です。
かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 30番目の言語は Swift です。
できたもの
不慣れな人が見様見真似で書いていますので、Swift のコードとして拙いところはご容赦ください。 Swift 自体に詳しくなるのは後回しにしてとにかく動くものを作るぞ、という方向性です。
サイズ
$ LANG=C wc -l main.swift mrcl_*.swift lib/{types,utils,json}.swift
14 main.swift
341 mrcl_codegen.swift
54 mrcl_lexer.swift
316 mrcl_parser.swift
132 lib/types.swift
83 lib/utils.swift
98 lib/json.swift
1038 total
コンパイラの主なロジックの部分だけならこのくらい:
$ LANG=C wc -l mrcl_*.swift
341 mrcl_codegen.swift
54 mrcl_lexer.swift
316 mrcl_parser.swift
711 total
さらにコメントだけの行を除外するとこのくらい:
$ cat mrcl_*.swift | grep -v '^ *//' | wc -l
674
動作の例
# bin/mrclc をビルド
$ rake clean build
# コンパイル
$ echo '
func add(a, b) {
return a + b;
}
func main() {
call add(1, 2);
}
' | bin/mrclc lex | bin/mrclc parse | bin/mrclc codegen
# ↓アセンブリが出力される
call main
exit
label add
push bp
mov bp sp
mov reg_a [bp:2]
push reg_a
mov reg_a [bp:3]
push reg_a
pop reg_b
pop reg_a
add reg_a reg_b
mov sp bp
pop bp
ret
mov sp bp
pop bp
ret
label main
push bp
mov bp sp
mov reg_a 2
push reg_a
mov reg_a 1
push reg_a
_cmt call~~add
call add
add sp 2
mov sp bp
pop bp
ret
(snip)
移植元
Ruby 版から移植しています。
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。
- コンパクト: コンパイラ部分は 1,000 行程度
- 無理して短く書くようなことはせず、読みやすさ優先で素直に書いてこのくらい
- pure Ruby / 標準ライブラリ以外のライブラリ不要
- ブラックボックスなしですべてを掌握できるように
- x86風の自作VM向けにコンパイルする
- ライフゲームのために必要な機能だけ
- 変数宣言、代入、反復、条件分岐、関数定義
- 演算子:
+
,*
,==
,!=
のみ(優先順位は(
)
で明示) - 型なし(値は符号付き整数のみ)
- 作ったときに書いた備忘記事
-
製作メモ
- 作ったときの全過程を書いています
- この通りにやれば誰でも同じものを作れる……のは確かだけど、いろいろ改善点が見えてきたので全体的に改訂したい
- 凝ったことはしていないので Ruby を知らない人でも雰囲気くらいは分かるんじゃないかと
-
本体には含めていない後付けの機能など
- 真偽値リテラル / break / if/else / 単項マイナス / パーサの別実装 / 簡単な静的型検査 / etc.
- これらは後から追加できます
-
他言語への移植
- 最低限の機能だけに絞ってコンパクトにしているので気軽に移植できます
- コンパイラ部分のみ
- Python, TypeScript, Julia, Dart, Haskell, Zig, V, R, Elixir など、2024-12-03 の時点では 30言語
-
セルフホスト版
- さらに育てていってセルフホスト(自作コンパイラで自作コンパイラをコンパイルする)もできました
これは Mini Ruccola 言語で書いたライフゲームをコンパイル+アセンブルして(Ruby版の)VM で実行している様子。コンパイルだけなら今回作った Swift 版でも同様に行えます。
メモ
入門したての人の解像度低めの感想など。
- オブジェクト指向 + 型推論や Optional、switch文の網羅性チェックがあったりして、実用言語の発展の先端の一つっぽい雰囲気が味わえました。今っぽい。
- 型推論などは取り入れているがそこまでML寄りでもない、みたいなバランス
- TypeScript や Crystal あたりに近い?
- ひっかかったところ
- サブストリングとインデックスは先にある程度知っておく必要あり(素朴な整数値指定ではない)
- 正規表現の型まわりでコンパイラに怒られたり
- あとは大きくひっかかるところはなくて割とサクサク書けました。よく分からないエラーが出て数時間悩んだり調べたりしても解消できず……みたいなのはなかったです。
- エラーハンドリング事情も気になっていたんですが今回は試せず。異常があったら即座に異常終了すればよいという方針なので出る幕がなかった……。
- 関数の外部引数名・内部引数名
- 「あー、そういえば外向けには別の名前にしたい、そういう機能欲しいと思ったことあったなー」というのが言語の機能になっていておもしろい。これは他の言語だと見たことなくて独自感ありますね。
- 開発環境は Mac ではなくいつも通り Ubuntu + Emacs(慣れてるので……)
- 数百行程度の小さなプログラムなので swiftc が使えればOK
- let で宣言するとコレクションの要素も変更不可になる
- 今回触った範囲では特に困ることはありませんでした。「ルートの参照だけ再代入不可」方式よりも、要素も含めてイミュータブルな方が心配することが減って嬉しい気がしますね。
(※コメントをいただきましたのでそちらも参照してください)
$ cat sample.swift
let xs = [1, 2, 3]
xs[0] = 11
$ swiftc sample.swift
sample.swift:2:3: error: cannot assign through subscript: 'xs' is a 'let' constant
1 | let xs = [1, 2, 3]
| `- note: change 'let' to 'var' to make it mutable
2 | xs[0] = 11
| `- error: cannot assign through subscript: 'xs' is a 'let' constant
3 |
この記事を読んだ人は(ひょっとしたら)こちらも読んでいます