💎
WhitespaceのインタプリタをRubyで書いた
#tech
#Ruby
#esolang
2024-1-27
年末年始が忙しくて,1月中にブログのネタを1本も書けないのではないかという懸念があったため,雑に生活を破壊してWhitespaceのインタプリタを書いた.
Q. 何故Ruby?
A. そういえば自分のGitHubで公開しているリポジトリにRuby製のものが無いなと思ったから
Q. 何故Whitespace?
A. どうせやるならesolang&テストコードが簡単に用意できる[1]
Whitespaceは,esolang(難解プログラミング言語)の1つであり,ソースコードがコメントを除き全て空白であることが大きな特徴である.
Whitespaceでは,次の3種の文字以外は全てコメントと解釈される.
- 半角スペース
- TAB文字
- 改行文字(LF)
他は全てコメントなので,適当な場所に適当なこの3つに該当しない文字を書いても良い.
Whitespaceはスタック指向の手続き型言語とみなすことができ,各命令はIMP コマンド (引数)
のような形式をしている.
IMPには,
- スタック操作
- 数値演算
- ヒープへのアクセス
- I/O
- フロー制御
の5種類がある.それぞれは
- [SP]
- [TAB][SP]
- [TAB][TAB]
- [TAB][LF]
- [LF]
で表され,各IMPの後にはコマンドと,必要に応じて引数が続く.例を挙げるとスタック操作のコマンドには
- スタックへのpush([SP](param))
- スタックのトップにある値を複製してpush([LF][SP])
- スタックの上2つの値をswapする([LF][TAB])
- スタックのpop([TAB][TAB])
- スタックの引数番目にある値をコピーしてpush([TAB][SP](param))
- スタックの2番目以降の値を引数の個数だけ削除する([TAB][LF](param))
がある.ここでparam
で表されている引数は,最初に符号を[SP]=正,[TAB]=負で表してから[SP]=0,[TAB]=1の2進数で表記するようになっている.
他のIMPに属するコマンドも似たようなものなので,これらは割愛する.esolangs.orgによくまとまっている.
再掲だが各命令はIMP コマンド (引数)
のような形をしているので,これらのタプルのリストのようなものを命令列として切り出してやれば良い.これを行うには例えばstrscan
のStringScanner
を用いて
scanner = StringScanner.new(@code)
while !(scanner.eos?)
param = nil
unless scanner.scan(/\A( |\n|\t[ \n\t])/)
raise Exception, "undefined imp"
end
imps = {
" " => :stack,
"\t " => :arithmetic,
"\t\t" => :heap,
"\n" => :flow,
"\t\n" => :io,
}
imp = imps[scanner[0]]
の様にして,以降はimp
に応じてcase-whenすればよい.
例えば,フロー制御を行う:flow
については次のようにする.
when :flow
unless scanner.scan(/ [ \t\n]|\t[ \t\n]|\n\n/)
raise Exception, "undefined cmd of flow"
end
cmds = {
" " => :mark,
" \t" => :call,
" \n" => :jmp,
"\t " => :jz,
"\t\t" => :jmi,
"\t\n" => :ret,
"\n\n" => :exit,
}
cmd = cmds[scanner[0]]
unless (cmd == :ret) || (cmd == :exit)
unless scanner.scan(/[ \t]+\n/)
raise Exception, "undefined parammeter of flow"
end
param = scanner[0]
end
:ret
か:exit
以外であれば,その後に引数を取るはずであるので,そのような場合はパラメーターを追加で読み出す.
トークナイズが完了した時点でIMP列が取得できているから,あとはやるだけ.仕様に則ってスタックやプログラムカウンタの管理をしっかりとしてやる.
例えばIMP列の要素を1つ取得して,もしスタック関連のIMPであった場合の各操作の内容を示す.
imp, cmd, param = @tokens[pc]
count += 1
case imp
when :stack
pc += 1
case cmd
when :push
@stack.push(param)
when :duplicate
@stack.push(@stack.last)
when :swap
@stack[-1], @stack[-2] = @stack[-2], @stack[-1]
when :discard
@stack.pop
when :copy
@stack.push(@stack[-param.to_i(2) - 1])
when :slide
elm = @stack.pop
param.to_i(2).times do
@stack.pop
end
@stack.push(elm)
end
この点,Pietなどを題材にするとめんどい ↩︎