Zig で簡単な Lisp インタプリタを書いてみた。
はじめに
先日 Zig で JSON パーサを書いて Zig がどういう言語なのか、だいたい理解できたので Lisp インタプリタを実装してみた。
簡単な実装なので四則演算と変数定義、関数呼び出しくらいしか実装してない。
ちゃんと文法チェックなどをやってないので、変な Lisp コードを渡すと落ちる。これはいずれ直す予定。
実装中に知った Zig の記法
blk
名前付きブロックで値が返せる。
const std = @import("std");
pub fn main() anyerror!void {
std.log.warn("{}", .{
blk: {
const d: u32 = 5;
const e: u32 = 100;
break :blk d + e;
},
});
}
便利かどうかは置いておいてカッコいい。
error handling if
関数の戻り値が error と union されている場合は if で処理ができる。
if (std.fmt.parseInt(i64, bytes.items, 10)) |num| {
// 正常時の処理
} else |err| {
// 異常時の処理
}
最初これを知らなくて遠回りな書き方をしてしまっていた。
for に else が書ける
var i: i32 = 0
for (i < 100): (i+=1) {
// なにか
} else {
// 回り切った時に実行される
}
賛否両論あるけど使ったら意外と便利だった。
今後気を付けたいところ
Zig はアロケータが全て。例えば struct に可変文字列 []const u8
を持つ場合、後で同じアロケータを使ってメモリを解放しないとリークする。これはこれでC言語ぽさがあって好きだけど、便利さは下がる。例えば Zig のハッシュマップに動的に項目を足す場合、キーも動的に生成する事になるが、このキーも解放時に同じアロケータで解放しなければならない。
var key = try parseString(a, br);
defer key.deinit();
try m.put(key.toOwnedSlice(), value);
for (m.keys()) |key| {
allocator.free(key);
}
m.deinit();
ちなみにメモリリークしているかどうかは、アロケータとして std.testing.allocator
を渡してテストすると勝手にリーク検査をやってくれる。とても便利。
若干の不満
ベンチマーカーが標準に無い
Zig には標準でベンチマーカーが付属していない。Go に慣れてしまっているので標準でベンチマークが付いていて当然になってしまった。
インタフェースが無い
Zig は全て struct なので、メソッドをシグネチャのチェックとして使える Go や Java の様なインタフェースが存在しない。代わりに anytype
を渡してコンパイル時にチェックされる。これはこれでいいのかもしれないが、安心感が無い。どうしても型チェックしたい場合には型を書くしかないが、標準ライブラリに同梱されている型は異常に長い。
例えばバイト列を読み込めるピーク可能なストリームの型は以下の様に宣言しないといけない。
const ByteReader = std.io.PeekStream(std.fifo.LinearFifoBufferType{ .Static = 2 }, std.io.Reader(std.fs.File, std.os.ReadError, std.fs.File.read));
まぁ別名が付けられるだけマシではある。
今回実装した物の紹介
今回実装した Lisp は整数の四則演算ができる。
(print (+ 1 2))
変数が設定できる。
(setq a (+ 2 3))
(print (- 2 (+ 3 a)))
関数が作れる。
(defun foo (a b) (+ a b))
(print (foo 1 2))
雑な実装なので参照するスコープなどはおかしいかもしれない。
おわりに
数日前に書いた JSON パーサの時よりも、だいぶ Zig の理解が出来た気がする。
ところで新しいプログラミング言語を学ぶ時は Lisp を書いたりしているが、その実装をリストアップしておくので、見たい人は見て下さい。
Zig 完全に理解した。
Discussion