まえがき
平成最後のクリスマス・イブの夜ですが皆様いかがお過ごしでしょうか?1
今年のクリスマスはホワイトクリスマスにはならない感じですが、せめてエディタの上だけではホワイトクリスマスを感じたい…
今宵はそんなあなたのためにWhiteSpaceというプログラミング言語を紹介したいと思います
WhiteSpaceとは
難解プログラミング言語の一種で「スペース」「タブ」「改行」のみで命令を記述するプログラミング言語です
参考:Rubyist のための他言語探訪 【第 14 回】 Whitespace
ネタ言語難解プログラミング言語にしては珍しく(?)オンラインIDEもあります
参考:Whitelips the Whitespace IDE
Hello World
拡張子は.wsだそうです
驚きの白さですね
ただ上記のコードはもしかすると正しく表示されないかもしれないので、その場合は上記のオンラインIDEで見るなどするほうが良いかもしれないです
え、黒い?仕様です
本題
これで締めると味気無さの極みのような記事になると思ったので、秋くらいに勉強のために書いたWhiteSpaceをRubyに変換してから実行するrubyプログラムの紹介をしたいと思います2
require 'optparse'
# ----------------------------------------------------------------------------
# ---------------------------------------WSVM---------------------------------
# ----------------------------------------------------------------------------
# ---------- Whitespaceで扱えるジャンプ命令以外の命令を実装したVMっぽいクラスです ---------
# ----------------------------------------------------------------------------
module Stack
def _show_call_stack # Debug用
p @stack
end
# 引数に指定された値をスタックにプッシュする(即値のプッシュ)
def stack_push(number)
@stack.push number
end
# スタックの上からn番目をコピーし,スタックの一番上に積む
def stack_dup_n(n)
stack_push @stack[-n]
end
# スタックの上から1番目をコピーし,スタックの一番上に積む
def stack_dup
stack_push @stack.last
end
# スタックの上から1番目をキープしつつ,上から(2 + 引数)番目を捨てる
def stack_slide(n = 0)
@stack.delete_at(-(2 + n))
end
# スタックの上から1番目と2番目を交換する
def stack_swap
tmp = @stack.pop(2)
tmp.reverse.each{|e| stack_push e }
end
# スタックの上から1番目を捨てる
def stack_discard
@stack.pop
end
end
module Operand
# スタックの上から1番目と2番目をポップし,
# それを加算(2番目 + 1番目)した結果を最上段にプッシュする
def arith_add
tmp = @stack.pop(2)
stack_push tmp.reduce(:+)
end
# スタックの上から1番目と2番目をポップし
# それを減算(2番目 - 1番目)した結果を最上段にプッシュする
def arith_sub
tmp = @stack.pop(2)
stack_push tmp.reduce(:-)
end
# スタックの上から1番目と2番目をポップし
# それを乗算(2番目 * 1番目)した結果を最上段にプッシュする
def arith_mul
tmp = @stack.pop(2)
stack_push tmp.reduce(:*)
end
# スタックの上から1番目と2番目をポップし
# それを除算(2番目 / 1番目)した結果を最上段にプッシュする
def arith_div
tmp = @stack.pop(2)
stack_push tmp.reduce(:/)
end
# スタックの上から1番目と2番目をポップし
# その剰余(2番目 % 1番目)をとった結果を最上段にプッシュする
def arith_mod
tmp = @stack.pop(2)
stack_push tmp.reduce(:%)
end
end
module Heap
# スタックの上から1番目と2番目を取り出し,2番目をヒープのアドレスとし
# そこに1番目の値を書き込む(heap[2番目] = 1番目)
def heap_store
tmp = @stack.last(2)
@heap[tmp[0]] = tmp[1]
end
# スタックの上から1番目を取り出し,1番目をヒープのアドレスとし
# そのアドレスの値をスタックの最上段にプッシュする( stack_push(heap[1番目]); )
def heap_load
stack_push @heap[@stack.last]
end
end
module InOut
require 'io/console'
# スタックの上から1番目を取り出し,その数値に対応するascii文字を出力
def io_put_char
print @stack.last.chr
end
# スタックの上から1番目を取り出し,その数値をascii文字として出力
def io_put_num
print @stack.last.ord
end
# スタックの上から1番目を取り出し,その値をヒープのアドレスとし
# そこに標準入力から1文字読み込む
def io_read_char
@heap[@stack.last] = STDIN.getch.to_s
end
# スタックの上から1番目を取り出し,その値をヒープのアドレスとし
# そこに標準入力から数値を読み込む
def io_read_num
@heap[@stack.last] = STDIN.getch.to_i
end
end
class WSVM
include Stack
include Operand
include Heap
include InOut
attr_reader :source_file
def initialize(file)
@source_file = file
@stack = []
@call_stack = []
@heap = {}
__state = nil
begin
# みんな大好きeval
eval @source_file
rescue StandardError => e
puts '-------- Error! ---------'
puts __state
puts '-------- stack ---------'
p @stack
puts '------ @call_stack ------'
p @call_stack
puts '--------- heap ---------'
p @heap
puts '-------- Message ---------'
raise e
end
end
# プログラムを終了する
def flow_halt
exit
end
end
# ----------------------------------------------------------------------------
# -------------------------------------WS2Ruby--------------------------------
# ----------------------------------------------------------------------------
# --------- WhitespaceソースをWSVMクラスで使えるコードに変換するためのクラスです -----------
# ----------------------------------------------------------------------------
class WS2Ruby
def initialize(str)
@source = str
@_source = @source.dup
@codes = ['while true', 'case __state', 'when nil']
convert_ws_to_stl
convert_stl_to_ruby
@codes.push('end', 'break', 'end')
end
def generated_code
@codes * "\n"
end
private
def convert_ws_to_stl
@_source.gsub!(/\S/, '')
@_source.gsub!(/[ ]/, 's')
@_source.gsub!(/[\t]/, 't')
@_source.gsub!(/[\n]/, 'l')
end
def convert_stl_to_ruby
while @_source != ''
case @_source
when /^ss[^l]+l/
code_push(/^ss[^l]+l/, processing_immediate('stack_push', /^ss[^l]+l/, /^ss/))
when /^sts[^l]+l/
code_push(/^sts[^l]+l/, processing_immediate('stack_dup_n', /^sts[^l]+l/, /^sts/))
when /^sls/
code_push(/^sls/, 'stack_dup')
when /^stl[^l]+l/
code_push(/^stl[^l]+l/, processing_immediate('stack_slide', /^stl[^l]+l/, /^stl/))
when /^slt/
code_push(/^slt/, 'stack_swap')
when /^sll/
code_push(/^sll/, 'stack_discard')
when /^tsss/
code_push(/^tsss/, 'arith_add')
when /^tsst/
code_push(/^tsst/, 'arith_sub')
when /^tssl/
code_push(/^tssl/, 'arith_mul')
when /^tsts/
code_push(/^tsts/, 'arith_div')
when /^tstt/
code_push(/^tstt/, 'arith_mod')
when /^tts/
code_push(/^tts/, 'heap_store')
when /^ttt/
code_push(/^ttt/, 'heap_load')
when /^lss[^l]+l/
flow_set_label(/^lss[^l]+l/, /^lss/)
when /^lst[^l]+l/
flow_go_sub(/^lst[^l]+l/, /^lst/)
when /^lsl[^l]+l/
flow_jump(/^lsl[^l]+l/, /^lsl/)
when /^lts[^l]+l/
flow_bez(/^lts[^l]+l/, /^lts/)
when /^ltt[^l]+l/
flow_bltz(/^ltt[^l]+l/, /^ltt/)
when /^ltl/
flow_endsub(/^ltl/)
when /^lll/
code_push(/^lll/, 'flow_halt')
when /^tlss/
code_push(/^tlss/, 'io_put_char')
when /^tlst/
code_push(/^tlst/, 'io_put_num')
when /^tlts/
code_push(/^tlts/, 'io_read_char')
when /^tltt/
code_push(/^tltt/, 'io_read_num')
when /^tsls/ # debug
code_push(/^tsls/, '_show_call_stack')
else
puts 'syntax_error!'
p @codes
p @_source
exit(1)
end
end
end
def code_push(pattern, code)
@codes.push(code).flatten!
@_source.sub!(pattern, '')
end
def processing_immediate(code, pattern, cmd_pttrn)
if @_source =~ pattern
word = $&.dup
word.sub!(cmd_pttrn, '')
return "#{code}(#{stl_argument_to_binary(word)})"
end
end
def pick_immediate(pattern, cmd_pttrn)
if @_source =~ pattern
word = $&.dup
word.sub!(cmd_pttrn, '')
return "#{stl_argument_to_binary(word)}"
end
end
def stl_argument_to_binary(str)
str.sub!(/l$/, '')
str.gsub!(/s/, '0')
str.gsub!(/t/, '1')
return "0b#{str}"
end
# flow
# 引数に指定されたラベルを定義する
def flow_set_label(pattern, cmd_pttrn)
label = pick_immediate(pattern, cmd_pttrn)
code = "when :label_#{label}"
code_push(pattern, code)
end
# 引数に指定されたラベルをサブルーチンとして呼び出す(呼び出し位置を記憶してジャンプ)
def flow_go_sub(pattern, cmd_pttrn)
label = pick_immediate(pattern, cmd_pttrn)
code = ["@call_stack.push(:return_#{@codes.count})", "__state = :label_#{label}", "next", "when :return_#{@codes.count}"]
code_push(pattern, code)
end
# 引数に指定されたラベル位置にジャンプする
def flow_jump(pattern, cmd_pttrn)
label = pick_immediate(pattern, cmd_pttrn)
code = ["__state = :label_#{label}", "next"]
code_push(pattern, code)
end
# スタックの上から1番目を取り出し,その値が0ならば引数に指定されたラベル位置にジャンプする
def flow_bez(pattern, cmd_pttrn)
label = pick_immediate(pattern, cmd_pttrn)
code = ["__state = (@stack.last == 0)? :label_#{label} : :b_#{@codes.count}", "next", "when :b_#{@codes.count}"]
code_push(pattern, code)
end
# スタックの上から1番目を取り出し,その値が0より小さいならば引数に指定されたラベル位置にジャンプする
def flow_bltz(pattern, cmd_pttrn)
label = pick_immediate(pattern, cmd_pttrn)
code = ["__state = (@stack.last < 0)? :label_#{label} : :b_#{@codes.count}", "next", "when :b_#{@codes.count}"]
code_push(pattern, code)
end
# サブルーチンの呼び出し元にジャンプする(関数のreturn)
def flow_endsub(pattern)
code = ["__state = @call_stack.pop", "next"]
code_push(pattern, code)
end
end
is_print = false
opt = OptionParser.new
opt.on('-p', '--print', 'Print converted ruby code.') { is_print = true }
opt.parse!(ARGV)
if ARGV.size == 1
source = ARGF.map{|line| line } * ''
converter = WS2Ruby.new(source)
if is_print
puts '-----------start generated code-----------'
puts converter.generated_code
puts '------------end generated code------------'
end
WSVM.new(converter.generated_code)
end
ざっくりやっていることを説明すると
-
スペース
をs
に、タブ
をt
に変換する(単純にデバッグの容易さを上げるためで本質的には不要な処理) -
s
,t
,改行
で構成されたコードを正規表現を使ってWhiteSpaceの命令と対応するRubyメソッドに変換 - 変換されたコードを
eval
を使って実行
といった具合です(なんかオプションの処理とかやってますが…)
一応使い方ですが、下記で動くはずです
$ ruby whitespace.rb 何かしらのWhiteSpaceコードファイル
ちなみに-pオプション付きで変換完了時にRubyコードが出力されます
詳しい説明
goto文・関数呼出のあたりはあとで書くかもしれないです
とりあえずはcase文を使ってステートマシンっぽい仕組みでgoto文・関数呼出っぽいものを実現しているのがミソということだけ書いておきます
あとがき
これから近所の鳥貴族あたりで焼鳥食べる予定です