Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

@inoue_ryuichi

WhiteSpaceをRubyに変換して実行しただけ

まえがき

平成最後のクリスマス・イブの夜ですが皆様いかがお過ごしでしょうか?1
今年のクリスマスはホワイトクリスマスにはならない感じですが、せめてエディタの上だけではホワイトクリスマスを感じたい…
今宵はそんなあなたのためにWhiteSpaceというプログラミング言語を紹介したいと思います

WhiteSpaceとは

難解プログラミング言語の一種で「スペース」「タブ」「改行」のみで命令を記述するプログラミング言語です
参考:Rubyist のための他言語探訪 【第 14 回】 Whitespace

ネタ言語難解プログラミング言語にしては珍しく(?)オンラインIDEもあります
参考:Whitelips the Whitespace IDE

Hello World

拡張子は.wsだそうです
驚きの白さですね

helloworld.ws

























ただ上記のコードはもしかすると正しく表示されないかもしれないので、その場合は上記のオンラインIDEで見るなどするほうが良いかもしれないです
え、黒い?仕様です

本題

これで締めると味気無さの極みのような記事になると思ったので、秋くらいに勉強のために書いたWhiteSpaceをRubyに変換してから実行するrubyプログラムの紹介をしたいと思います2

whitespace.rb
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

ざっくりやっていることを説明すると

  1. スペースsに、タブtに変換する(単純にデバッグの容易さを上げるためで本質的には不要な処理)
  2. s,t,改行で構成されたコードを正規表現を使ってWhiteSpaceの命令と対応するRubyメソッドに変換
  3. 変換されたコードをevalを使って実行

といった具合です(なんかオプションの処理とかやってますが…)

一応使い方ですが、下記で動くはずです

$ ruby whitespace.rb 何かしらのWhiteSpaceコードファイル

ちなみに-pオプション付きで変換完了時にRubyコードが出力されます

詳しい説明

goto文・関数呼出のあたりはあとで書くかもしれないです
とりあえずはcase文を使ってステートマシンっぽい仕組みでgoto文・関数呼出っぽいものを実現しているのがミソということだけ書いておきます

あとがき

これから近所の鳥貴族あたりで焼鳥食べる予定です


  1. 私は年末の大掃除をしてこの記事を書いています 

  2. Hello Worldと無限に整数を出力するプログラムしかテストしてないのでバグがあっても勘弁してほしい 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
0
Help us understand the problem. What are the problem?