Unlambda インタプリタを書いてみた (改)
一年前に Ruby 1.9 で極力シンプルな Unlambda のインタプリタを書いたんですが、yhara さんにメールでバグを指摘されてしまったので、いろいろ修正しました。
まず、d (delay) と入出力のないシンプル版。
require "continuation" def unlambda_subset(s) t = ->(s) do c, *s = s r = case c when ?`; f, s = t[s]; g, s = t[s]; f[g] when ?s; ->(f){ ->(g){ ->(x){ f[x][g[x]] } } } when ?k; ->(x){ ->(y){ x } } when ?i; ->(x){ x } when ?c; ->(f){ callcc{|c| f[c] } } when ?v; v = ->(x){ v } when ?e; ->(x){ exit } else t[s] end [r, s] end t[s.split(//)] end
このコードは parse と eval を同時にやってるんですが、parse のときに Array#shift という破壊的な操作をしていて、そのせいで callcc で戻ったときに正しく再開できないのでした (``cii などで変になる) 。しょうがないので c, *s = s で非破壊的に head と tail に分けるようにして解決。
以下は全命令を実装したバージョン。
require "continuation" def unlambda(s) s = s.gsub(/#.*?$/m, "").chars b = nil a = ->(f, g){ f ? f[g[]] : ->(x){ a[g[], ->{ x }] } } o = ->(c){ ->{ ->(x){ print c; x } } } i = ->{ ->(x){ x } } v = ->{ ->(x){ v[] } } t = ->() do case s.next when ?`; f, g = t[], t[]; ->{ a[f[], g] } when ?s; ->{ ->(f, g, x){ a[a[f, ->{ x }], ->{ a[g, ->{ x }] }] }.curry } when ?k; ->{ ->(x, y){ x }.curry } when ?i; i when ?d; ->{ nil } when ?c; ->{ ->(f){ callcc {|c| a[f, ->{ c }] } } } when ?v; v when ?r; o["\n"] when ?.; o[s.next] when ?e; ->{ ->(x){ exit } } when ?@; ->{ ->(x){ b = $stdin.getc; a[x, b ? i : v] } } when ??; c = s.next; ->{ ->(x){ a[x, b == c ? i : v] } } when ?|; ->{ ->(x){ a[x, b ? o[b] : v] } } else t[] end end t[][] end
修正点は以下のとおり。
- コメントに対応した。# から行末までがコメントらしい。
- すべての適用で d が現れる可能性を考慮した (`dd や `cd などで変になってた) 。
- せっかくなのでリファクタリング。Proc#curry を使うようにしたり、Enumerator#next を使うようにしたり。
ちなみにこっちは parse と eval が分かれている *1 ので、破壊的に Enumerator#next を使ってよい。はず。
まあ直したとは言うけど本当に正しいかは自信ない。とりあえずこんな感じ。ちゃんとしたテストベンチがほしいです。
unlambda("``id`.Xi") #=> "" unlambda("``dd`.Xi") #=> "X" unlambda("``cd`.Xi") #=> "XX" unlambda("``.Y```si.Xdv") #=> "YX" unlambda("``.Y```si.Xiv") #=> "XY"
*1:最後の行の t[ ][ ] のうち、1 つ目の [ ] が parse で、2 つ目の [ ] が eval になってる感じ。