Oteto Blogのロゴ

空白言語WhiteSpaceとは?Rubyで実装してみた

WhiteSpaceとは?

Whitespace(ホワイトスペース)は、プログラミング言語のひとつであり、またそれを動作させるインタプリタを指している。WhitespaceはGPLにより配布されている。実用言語ではない難解プログラミング言語のひとつ。

Whitespace - Wikipedia

WhiteSpaceは空白に相当する文字のみで構成されたプログラミング言語です。

WhiteSpaceのコード

上記のものは空白やタブ、改行のみで構成されていますが、WhiteSpaceの場合これもコードです。

$ ruby whitespace.rb count.ws
1
・
・
10

ちなみに処理としては1から10までの文字を出力するもので、実際に実行しても正常に動作していますね。

仕様

WhiteSpaceは下記3つの要素で構成されています。

  • IMP
  • コマンド
  • パラメーター

IMPは一言で表すなら「どんな種類の処理をしたいか指定するもの」です。処理の種類は5つあり、どのIMPを指定するかによって選べるコマンドとパラメーターも決まってきます。

スタック操作

その名の通りスタックを操作するものです。IMPは[Space]

IMPコマンドパラメーター詳細
[space][Space]数値パラメーターで指定した数値をスタックに積む
[space][LF][Space]-スタックの一番上の値を複製する
[space][LF][Tab]-スタックの上から2つの値を入れ替える
[space][LF][LF]-スタックの一番上の値を削除する

演算

スタックの上に積まれてる2つのリテラルを取り出しそれらで演算を行い、結果を再びスタックに積みます。IMPは[Tab][Space]

IMPコマンドパラメーター詳細
[Tab][Space][Space][Space]足し算
[Tab][Space][Space][Tab]引き算
[Tab][Space][Space][LF]掛け算
[Tab][Space][Tab][Space]割り算
[Tab][Space][Tab][Tab]剰余

ヒープ操作

その名の通りヒープに値を保存したり取り出したりし、変数のような扱いができます。IMPは[Tab][Tab]

IMPコマンドパラメーター詳細
[Tab][Tab][Space]-スタックの一番上を値としその次のものをアドレスとし、ヒープ上のそのアドレスに対応する位置にその値を保存する
[Tab][Tab][Tab]-スタックの一番上をアドレスとし、ヒープ上のそのアドレスに対応する位置にある値をスタックに積む。

制御命令

指定した箇所にマークをつけるたりそこにジャンプしたりすることができます。これにより繰り返しなどが実現できます。IMPは[LF]

IMPコマンドパラメーター詳細
[LF][Space][Space]ラベルマークする
[LF][Space][Tab]ラベル関数を呼ぶ
[LF][Space][LF]ラベルパラメーターで指定したマークにジャンプする
[LF][Tab][Space]ラベルスタックの一番上が0の場合にジャンプする
[LF][Tab][Tab]ラベルスタックの一番上が負の値の場合にジャンプする
[LF][Tab][LF]-関数の実行を終了し呼び出し元に戻る
[LF][LF][LF]-プログラムを終了する

入出力

スタックの一番上の値を取り出し出力したり、スタックからアドレスを取り出しそのアドレスの位置に入力された文字を書き込んだりします。IMPは[Tab][LF]

IMPコマンドパラメーター詳細
[Tab][LF][Space][Space]-スタックの一番上の値に等しい文字コードに対応する文字を出力する
[Tab][LF][Space][Tab]-スタックの一番上の数値を文字列として出力する
[Tab][LF][Tab][Space]-入力された文字に対応するアヒープ上のドレスの位置に、スタックの一番上の値を積む
[Tab][LF][Tab][Tab]-入力された数値に対応するヒープ上のアドレスの位置に、スタックの一番上の値を積む

大まかな概要はこんな感じ。詳細は公式のチュートリアルをご参照ください。

WhiteSpaceをRubyで実装してみる

1. 字句解析

def tokenize # 字句解析
  result = []
  scanner = StringScanner.new(@code)
  while !(scanner.eos?) # スキャンが最後尾まで達したか
    para = nil # パラメーターを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]]
    case imp
   #-----スタック操作-----
    when :stack
      unless scanner.scan(/ |\n[ \t\n]/)
        raise Exception, "undefined cmd of stack"
      end
      cmds = {
        " " => :push,
        "\n " => :duplicate,
        "\n\t" => :swap,
        "\n\n" => :discard
      }
      cmd = cmds[scanner[0]]
      if cmd == :push # pushの場合パラメータを取得
        unless scanner.scan(/[ \t]+\n/) # \nが来るまではパラメーター
          raise Exception, "undefined parameter of stack"
        end
        para = scanner[0]
      end

   #-----演算-----
    when :arithmetic
      unless scanner.scan(/ [ \t\n]|\t[ \t]/)
         raise Exception, "undefined cmd of arithmetic"
      end
      cmds = {
        " " => :addition,
        " \t" => :subtraction,
        " \n" => :multiplicate,
        "\t " => :integer_division,
        "\t\t" => :modulo
      }
      cmd = cmds[scanner[0]]

   #-----ヒープ操作-----
    when :heap
      unless scanner.scan(/ |\t/)
       raise Exception, "undefined cmd of heap"
      end
      cmds = {
        " " => :store,
        "\t" => :retrieve
      }
      cmd = cmds[scanner[0]]

   #-----制御命令-----
    when :flow
      unless scanner.scan(/ [ \t\n]|\t[ \t\n]|\n\n/)
        raise Exception, "undefined cmd of flow"
      end
      cmds = {
        " " => :mark,
        " \t" => :call,
        " \n" => :jump_uncondionally,
        "\t " => :jump_zero,
        "\t\t" => :jump_negative,
        "\t\n" => :end_and_transfer,
        "\n\n" => :end,
      }
      cmd = cmds[scanner[0]]
      unless (cmd == :end_and_transfer) || (cmd == :end) #end_and_transferもしくはend以外のコマンドの場合パラメータを取得
        unless scanner.scan(/[ \t]+\n/) # \nが来るまではパラメーター
          raise Exception, "undefined parameter of flow"
        end
        para = scanner[0]
      end

   #-----入出力-----
    when :io
      unless scanner.scan(/ [ \t]|\t[ \t]/)
        raise Exception, "undefined cmd of io"
      end
      cmds = {
        " " => :output_character,
        " \t" => :output_number,
        "\t " => :read_character,
        "\t\t" => :read_number
      }
      cmd = cmds[scanner[0]]
    end
    result << [imp,cmd,para] # それぞれを配列に格納
  end
  result
end

まず字句解析をします。scanメソッドはStringScannerクラスのもので、eos?メソッドを使ってトークンが最後尾まで達したかを判定しています。

2. 構文解析

def evaluate
  @tokens = []
  @tokens_sub =tokenize # パラメータを数値に変換するための配列
  @stack = []
  @call_stack = []
  @heap = {}
  pc = 0 # カウンター
  @tokens_sub.each do |im,cm,pa| # パラメーターを全て数値に変換
    pa2 = pa
    if pa #パラメーターがnilでない場合のみ
      pa2.gsub!(/ /,"0")
      pa2.gsub!(/\t/,"1")
      sign= pa2.slice!(0) # 符号を取得
      pa2 = pa2.to_i(2) # 10進数に変換
      if sign == "1"
        pa2 = pa2 * (-1) # 負の値に
      end
    end
    @tokens << [im,cm,pa2] # 本命の@tokensに格納
  end
  count=0
  while true do
    imp , cmd , para = @tokens
    count = count +1
   (省略)

そして構文解析。変数pcはマークする際やジャンプする際に使用するカウンターのようなものです。

次にIMPそれぞれの操作を書いていきます。

3. スタック操作

case imp
  #-----スタック操作-----
  when :stack
    pc = pc + 1
    cmds = {
      " " => :push,
      "\n " => :duplicate,
      "\n\t" => :swap,
      "\n\n" => :discard
    }
    case cmd
    when :push
      @stack.push(para)
    when :duplicate
      @stack.push(@stack.first)
    when :swap
      top_second = @stack.delete_at(@stack.size-2) # topの2つを入れ替える
      @stack.push(top_second)
    when :discard
      @stack.pop
    end

4. 演算

  #-----演算-----
  when :arithmetic
    pc = pc + 1
    elm = @stack.pop
    elm2 = @stack.pop
    case cmd
    when :addition # 和
      @stack.push(elm2 + elm)
    when :subtraction # 差
      @stack.push(elm2 - elm)
    when :multiplicate # 積
      @stack.push(elm2 * elm)
    when :integer_division # 商
      @stack.push(elm2 / elm)
    when :modulo # 剰余
      @stack.push(elm2 % elm)
    end

5. ヒープ操作

  #-----ヒープ操作-----
  when :heap
    pc = pc + 1
    case cmd
    when :store # ハッシュに保存
      value = @stack.pop
      key =@stack.pop
      @heap.store(key,value)
    when :retrieve # スタックに追加
      key = @stack.pop
      @stack.push(@heap[key])
    end

6. 制御命令

#-----制御命令-----
  when :flow
    case cmd
    when :mark
      pc += 1
    when :jump_uncondionally # ジャンプ
      n = 0
      @tokens.each {|im,cm,pa|
        if cm == :mark && pa == para
          pc = n+1
        end
        n += 1
        }
    when :call
      @call_stack.push(pc) # スタックに現在の位置をプッシュ
      n=0
      @tokens.each {|im,cm,pa|
        if cm == :mark && pa == para
          pc = n+1
        end
        n += 1
      }
    when :jump_zero # スタックの先頭が0ならジャンプ
      if @stack.pop == 0
        n = 0
        @tokens.each {|im,cm,pa|
          if cm == :mark && pa == para
            pc = n+1
          end
          n += 1
        }
      else
        pc += 1
      end
    when :jump_negative # スタックの先頭が負ならジャンプ
      if @stack.pop < 0
        n = 0
        @tokens.each {|im,cm,pa|
          if cm == :mark && pa == para
            pc = n+1
          end
          n += 1
        }
      else
        pc += 1
      end
    when :end_and_transfer
      pc = @call_stack.pop
      pc =pc +1
    when :end # 終了
      exit
  end

7. 入出力

    #-----入出力-----
    when :io
      pc = pc + 1
      case cmd
      when :output_character # 文字を出力
        print @stack.pop.chr
      when :output_number # 数値を出力
        puts @stack.pop.to_s
      when :read_character # 文字コードの入力
        key = @stack.pop
        print("入力=>")
        value =STDIN.gets.ord # 文字コードに変換
        @heap.store(key,value)
      when :read_number #数値の入力
        key = @stack.pop
        print("入力=>")
        value =STDIN.gets.to_i # 整数に変換
        @heap.store(key,value)
      end

これでIMPそれぞれの処理は書けました。あとはtokenizeevaluateを呼び出してあげるだけです。

require "strscan"

class WhiteSpace
  def initialize
    @code = ""
    @file_name = ARGV[0]
    @file = open(@file_name) # コマンドライン引数で渡されたファイルの内容を取得
    @file.each do |line|
      @code << line
    end
    tokenize()
    evaluate()
  end
(中略)
end
WhiteSpace.new

これで完成です 🎉