Swiftでシンプルな自作言語のコンパイラを書いた

:::note info

これは [Swift Advent Calendar 2024](https://qiita.com/advent-calendar/2024/swift) の3日目の記事です。
一つ前の記事は [Ignite の新規機能紹介：Spotify 対応やモーダルの活用](https://qiita.com/KaitoMuraoka/items/3eaaa26ffcc5889f0f19) です。
次の記事は [`async` なメソッドは、宣言時に限定しない限り隔離領域を超えます](https://qiita.com/lovee/items/172d6b8e4fd9969e88b2) です。

:::


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/70a91ee7-2675-bb30-3df6-728d1609daa3.png)

[かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ](https://memo88.hatenablog.com/entry/2020/08/30/132314) 30番目の言語は Swift です。


# できたもの

https://github.com/sonota88/mini-ruccola-swift

不慣れな人が見様見真似で書いていますので、Swift のコードとして拙いところはご容赦ください。 Swift 自体に詳しくなるのは後回しにしてとにかく動くものを作るぞ、という方向性です。


## サイズ

```terminal
  $ 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
```

コンパイラの主なロジックの部分だけならこのくらい:

```terminal
  $ LANG=C wc -l mrcl_*.swift
  341 mrcl_codegen.swift
   54 mrcl_lexer.swift
  316 mrcl_parser.swift
  711 total
```

さらにコメントだけの行を除外するとこのくらい:

```terminal
  $ cat mrcl_*.swift | grep -v '^ *//' | wc -l
674
```





# 動作の例

```sh
  # 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 版から移植しています。

https://github.com/sonota88/vm2gol-v2




自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。

- コンパクト: コンパイラ部分は 1,000 行程度
  - 無理して短く書くようなことはせず、読みやすさ優先で素直に書いてこのくらい
- pure Ruby / 標準ライブラリ以外のライブラリ不要
  - ブラックボックスなしですべてを掌握できるように
- x86風の自作VM向けにコンパイルする
- ライフゲームのために必要な機能だけ
  - 変数宣言、代入、反復、条件分岐、関数定義
  - 演算子: `+`, `*`, `==`, `!=` のみ（優先順位は `(` `)` で明示）
  - 型なし（値は符号付き整数のみ）
- 作ったときに書いた備忘記事
  - [RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話](https://qiita.com/sonota88/items/f9cb3fc4a496b354b729)
  - [Rubyでかんたんな自作言語のコンパイラを作った](https://qiita.com/sonota88/items/2b95378b43a22109513c)
- [製作メモ](https://memo88.hatenablog.com/entry/2019/05/04/234516)
  - 作ったときの全過程を書いています
  - この通りにやれば誰でも同じものを作れる……のは確かだけど、いろいろ改善点が見えてきたので全体的に改訂したい
  - 凝ったことはしていないので Ruby を知らない人でも雰囲気くらいは分かるんじゃないかと
- [本体には含めていない後付けの機能など](https://memo88.hatenablog.com/entry/vm2gol_v2_additional_features)
    - 真偽値リテラル / break / if/else / 単項マイナス / パーサの別実装 / 簡単な静的型検査 / etc.
    - これらは後から追加できます
- [他言語への移植](https://memo88.hatenablog.com/entry/2020/08/30/132314)
    - 最低限の機能だけに絞ってコンパクトにしているので気軽に移植できます
    - コンパイラ部分のみ
    - Python, TypeScript, Julia, Dart, Haskell, Zig, V, R, Elixir など、2024-12-03 の時点では 30言語
- [セルフホスト版](https://qiita.com/sonota88/items/1e683276541cf1b87b76)
    - さらに育てていってセルフホスト（自作コンパイラで自作コンパイラをコンパイルする）もできました

---

これは [Mini Ruccola 言語で書いたライフゲーム](https://github.com/sonota88/vm2gol-v2/blob/main/gol.mrcl)をコンパイル＋アセンブルして（Ruby版の）VM で実行している様子。コンパイルだけなら今回作った Swift 版でも同様に行えます。

<!--
![](https://storage.googleapis.com/zenn-user-upload/52325d5e0d71-20240825.gif)
-->

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/98b4b4ad-da86-ca35-e578-20499277b42a.gif)



# メモ

入門したての人の解像度低めの感想など。

- オブジェクト指向 ＋ 型推論や Optional、switch文の網羅性チェックがあったりして、実用言語の発展の先端の一つっぽい雰囲気が味わえました。今っぽい。
  - 型推論などは取り入れているがそこまでML寄りでもない、みたいなバランス
  - TypeScript や Crystal あたりに近い？
- ひっかかったところ
  - サブストリングとインデックスは先にある程度知っておく必要あり（素朴な整数値指定ではない）
  - 正規表現の型まわりでコンパイラに怒られたり
  - あとは大きくひっかかるところはなくて割とサクサク書けました。よく分からないエラーが出て数時間悩んだり調べたりしても解消できず……みたいなのはなかったです。
- エラーハンドリング事情も気になっていたんですが今回は試せず。異常があったら即座に異常終了すればよいという方針なので出る幕がなかった……。
  - 参考: [Swiftのエラーハンドリングはなぜ最先端なのか](https://qiita.com/omochimetaru/items/c30f7a021fb9b8f0fa92)
- 関数の外部引数名・内部引数名
  - 「あー、そういえば外向けには別の名前にしたい、そういう機能欲しいと思ったことあったなー」というのが言語の機能になっていておもしろい。これは他の言語だと見たことなくて独自感ありますね。
- 開発環境は Mac ではなくいつも通り Ubuntu + Emacs（慣れてるので……）
  - 数百行程度の小さなプログラムなので swiftc が使えればOK


---

- let で宣言するとコレクションの要素も変更不可になる
  - 今回触った範囲では特に困ることはありませんでした。「ルートの参照だけ再代入不可」方式よりも、要素も含めてイミュータブルな方が心配することが減って嬉しい気がしますね。

```terminal
  $ 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 | 
```

---

（追記 2025-01-26）

[コメントで教えていただいた](https://qiita.com/sonota88/items/309753549677ccf8209d#comment-810884c94f9abf7caa8d)のですが、 Swift の配列は参照型ではなく値型なのだそうです。ドキュメントにも以下のような説明がありました。

[Structures and Classes | Documentation (docs.swift.org)](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures/)

> In fact, all of the basic types in Swift — integers, floating-point numbers, Booleans, strings, arrays and dictionaries — are value types, and are implemented as structures behind the scenes.




# この記事を読んだ人は（ひょっとしたら）こちらも読んでいます

<!-- 素朴な自作言語のコンパイラをCrystalに移植した -->
https://qiita.com/sonota88/items/275c2b5407986b100cc8

<!-- 素朴な自作言語のコンパイラをTypeScript（Deno）に移植した - memo486 -->
https://memo88.hatenablog.com/entry/2020/08/15/114754

<!-- 素朴な自作言語のコンパイラをRustに移植した - memo486 -->
https://memo88.hatenablog.com/entry/20210407_vm2gol_v2_rust