Zigでかんたんな自作言語のコンパイラを書いた

ブログに書いていたものを引っ越してきて一部手直ししました。元の記事公開日は 2021-01-07 です。
かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 11回目は Zig です。
ライフゲームのコンパイルが通ったのでヨシ、という程度の雑なものです。
Zig言語(ziglang)を触り始めて1週間くらいの人が、理解は後回しにしてとにかく動くものを作るぞ、という方向性で書いたもの(その程度のノリでかんたんに書けるコンパイラです)。
Zig のバージョンは 0.6.0。
(追記 2022-07-17: 0.9.1 に上げました)
(追記 2025-03-29: 0.14.0 に上げました)
できたもの
サイズはこんな感じ:
$ wc -l {lexer,parser,codegen}.zig lib/{types,utils,json}.zig
  189 lexer.zig
  630 parser.zig
  552 codegen.zig
  209 lib/types.zig
  210 lib/utils.zig
  144 lib/json.zig
 1934 合計
動かし方の例
echo '
  func add(a, b) {
    return a + b;
  }
  func main() {
    call add(1, 2);
  }
' | zig run lexer.zig | zig run parser.zig | zig run codegen.zig
# ↓アセンブリが出力される
  call main
  exit
label add
  push bp
  cp sp bp
  cp [bp:2] reg_a
  push reg_a
  cp [bp:3] reg_a
  push reg_a
  pop reg_b
  pop reg_a
  add_ab
  cp bp sp
  pop bp
  ret
label main
  push bp
  cp sp bp
  cp 2 reg_a
  push reg_a
  cp 1 reg_a
  push reg_a
  _cmt call~~add
  call add
  add_sp 2
  cp bp sp
  pop bp
  ret
# ... snip ...
移植元
<自作言語処理系(Ruby版)の説明用テンプレ>
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。
- 小規模: コンパイラ部分は 1,000 行程度
 - pure Ruby / 標準ライブラリ以外のライブラリ不要
 - x86風の自作VM向けにコンパイルする
 - ライフゲームのために必要な機能だけ
- 変数宣言、代入、反復、条件分岐、関数定義
 - 演算子: 
+,*,==,!=のみ(優先順位は()で明示) - 型なし(値は符号付き整数のみ)
 
 - 作ったときに書いた備忘記事
 - 
本体には含めていない後付けの機能など
- 真偽値リテラル / break / if/else / 単項マイナス / Racc などを使って書いたパーサの別実装
 
 - 
他言語への移植
- コンパイラ部分のみ
 - Python, Java, PHP, TypeScript, Julia, Dart, Haskell など、2022-07-17 の時点では 22言語
 
 - 
セルフホスト版
- さらに育てていくとセルフホストまでできます(できました)
 
 - さらに育てていくとセルフホストまでできます(できました)
 - 作り方は製作メモに全部書いています。凝ったことはしていないので Ruby 知らない人でも雰囲気くらいは分かるんじゃないかと。
 
<説明用テンプレおわり>
ベースになっているバージョン: tag:50 のあたり
(追記 2022-07-17: ステップ62 の修正まで適用しました)
(追記 2025-03-29: ステップ63 の修正まで適用しました)
メモ
- C と Go の中間、GC のない Go みたいな印象
 - sentinel-terminated な配列もなるほど便利そうと思って使ってみたが、通常の配列とは別の型になって煩雑なのと、サイズ固定な配列の出番が実はほとんどなかったので、後から普通の配列+スライスで書き直していった
 - 例外の扱いも Go とは違ったアプローチでおもしろい。最初は試しに使っていたけど、お遊びプログラムなのでまじめにやる必要ないかと思い直し、これも後から使わないように(Java でいえば検査例外をその場で RuntimeException に包んで投げ直す感じに)書き変えた。単に煩雑さを避けるのを優先した形なのでほんとはちゃんとハンドリングすべきと思います。
- エラーになったらその場で異常終了させるだけ、という作りです
 
 - 型まわりの理解がまだいまいち(配列や const が絡むあたりとか)
 - いろいろ用意するのが面倒なので Emacs + javascript-mode で済ませた
 - あと細かい話がいろいろあった気がするけど、作った後このエントリを書くまでしばらく他のことをしていたら忘れてしまった……思い出したら追記します
- その間に Zig のバージョンが 0.6 から 0.8 に上がっていた
 
 - 今回はあんまり余裕なかったので実験的な要素はなし
 
逆引き的なメモ
標準エラー出力に出力 (v0.9.1)
const std = @import("std");
pub fn main() !void {
    const x = 123;
    const file: std.fs.File = std.io.getStdErr();
    try file.writer().print("{}\n", .{x});
    // または catch で処理する
    // file.outStream().print("{}", .{x}) catch |err| {
    //     std.debug.panic("error ({})", .{err});
    // };
}
デバッグ用なら std.debug.print が手軽っぽい。
標準入力からの入力をすべて読む
→ Zig: 1バイトごとに読み書きするだけのcatコマンドを書いてみた
panic (v0.9.1)
const panic = @import("std").debug.panic;
pub fn main() !void {
    panic("PANIC {} {s}", .{ -123, "foo" });
}
C の sprintf 相当 (v0.9.1)
const std = @import("std");
pub fn main() !void {
    const s = "foo";
    const n = -123;
    var buf: [16]u8 = undefined;
    const slice: []u8 = try std.fmt.bufPrint(
        &buf,
        "{s} bar {}",
        .{ s, n }
    );
    std.debug.print("{s}\n", .{ slice });
}
整数 → 文字列 などの変換にも使えそう。
文字列を整数に変換 (v0.9.1)
const std = @import("std");
pub fn main() !void {
    const s = "-123";
    const n = try std.fmt.parseInt(i32, s, 10);
    std.debug.print("{}\n", .{ n });
}
Zig 0.6.0 → 0.7.1 にバージョンアップした際の変更点
(2021-03-17 に追記)
- 引数の型の変更: 
var→anytype 
Zig 0.7.1 → 0.8.0 にバージョンアップした際の変更点
(2022-07-17 に追記)
https://github.com/sonota88/vm2gol-v2-zig/commit/3635ba1eeb0a06fa
- フォーマット文字列の書式が変わった
- 文字列の場合は 
{s}と指定しないとコンパイルエラーになる 
 - 文字列の場合は 
 
- panic("Failed to parse ({})", .{str});
+ panic("Failed to parse ({s})", .{str});
- 
inStream,outStreamがそれぞれreader,writerに変わった 
- file.outStream().print(...
+ file.writer().print(...
Zig 0.8.0 → 0.9.0 にバージョンアップした際の変更点
(2022-07-17 に追記)
https://github.com/sonota88/vm2gol-v2-zig/commit/71d70661e38d6ded
- 未使用変数があるとコンパイルエラーになるようになった
 
↓この err なんかも使ってないと怒られるようになりました
-        var obj = allocator.create(Self) catch |err| {
+        var obj = allocator.create(Self) catch {
             panic("Failed to allocate", .{});
         };
この記事を読んだ人はこちらも(ひょっとしたら)読んでいます
Discussion