Racc で Brainf*ck 、その場で実行の為に

対応する括弧を行き来する感覚を大事にする為にその場で実行する方向で考える。ソースを行き来できるようにするわけだ。その為に、ソースとソース上の位置を示すポインタを導入する。

class BrainF_ckParser
rule# class BrainF_ckParser

expression :
           | expression TERMINAL { send val[1] }

end # class BrainF_ckParser

---- inner
  def initialize(source)
    @source = source.split //
    @source_pointer = 0
    @tape = []
    @pointer = 0
  end # def initialize(source)

  def parse(opts = nil)
    @yydebug = opts.yydebug if opts.respond_to? :yydebug
    yyparse self, :scan
  end # def parse(opts = nil)

  def right
    @pointer += 1
  end # def right

  def left
    @pointer -= 1
  end # def left

  def plus
    @tape[@pointer] = (@tape[@pointer] ||= 0) + 1
  end # def plus

  def minus
    @tape[@pointer] = (@tape[@pointer] ||= 0) - 1
  end # def minus

  def put
    print (@tape[@pointer] ||= 0).chr
  end # def put

  def get
    c = $stdin.getc
    @tape[@pointer] = c.respond_to?(:ord) ? c.ord : c
  end # def get

  def bra
    if (@tape[@pointer] ||= 0) == 0 then
      @source_pointer = corresponding_bracket(@source_pointer) - 1
    end # if (@tape[@pointer] or 0) == 0
  end # def bra

  def ket
    unless (@tape[@pointer] ||= 0) == 0 then
      @source_pointer = corresponding_bracket(@source_pointer) - 1
    end # unless (@tape[@pointer] or 0) == 0
  end # def ket

  private
  def scan
    while c = @source[@source_pointer] do
      case c
        when '>' then yield [:TERMINAL, :right]
        when '<' then yield [:TERMINAL, :left]
        when '+' then yield [:TERMINAL, :plus]
        when '-' then yield [:TERMINAL, :minus]
        when '.' then yield [:TERMINAL, :put]
        when ',' then yield [:TERMINAL, :get]
        when '[' then yield [:TERMINAL, :bra]
        when ']' then yield [:TERMINAL, :ket]
      end # case c
      @source_pointer += 1
    end # while c = @source[@source_pointer] do
    yield nil
  end # def scan

  Correspondings = {
    '[' => ['[', ']', +1 ],
    ']' => [']', '[', -1 ],
     }
  def corresponding_bracket(s_pointer)
    bra_ket, ket_bra, sign = Correspondings[@source[s_pointer]]
    s_pointer += sign
    corr = 0
    until corr == 0 and @source[s_pointer] == ket_bra do
      raise unless (0...@source.size).include? s_pointer
      corr += 1 if @source[s_pointer] == bra_ket
      corr -= 1 if @source[s_pointer] == ket_bra
      s_pointer += sign
    end # until corr == 0 and @source[s_pointer] == ket_bra do
    s_pointer
  end # def corresponding_bracket(s_pointer)

---- header
# -*- coding: utf-8; -*-
Version = '$Id: brainf_ck_source_pointer.y 3298 2009-03-07 02:50:25Z hs9587 $'
# $Date: 2009-03-07 11:50:25 +0900 (土, 07 3 2009) $

---- footer
require 'optparse'
require 'ostruct'
opts = OpenStruct.new
opts.yydebug = false

ARGV.options do |opt|
  opt.on('--[no-]yydebug', 'debug mode (from racc -t) on/off'){ |v| 
opts.yydebug = v }
  opt.release = 'TERMINAL acts directly'
  opt.banner += "\n\tBrainf*ck interpreter.\nOptions:"
  opt.parse!
end # ARGV.options do |opt|

BrainF_ckParser.new((ARGV.size > 0 ? ARGF : $stdin).read).parse(opts)

もう文法要素は最低限、終端記号も一種類。アクションも単純、そして send で分岐している。
ソース @source は配列にしてみた、文字列のままでも良かったのだけど、1.8系と 1.9系で文字列の要素へのアクセスが変わっているのでちょっと避けた('String'[n])。
corresponding_bracket はちょっと複雑かな、まあ対応する括弧を探してるだけといえばそれだけ、括弧のネストを数えるのが少し煩雑。この中ではテープからはみ出るのチェックしている。あとはこういったソース位置の計算と、スキャナ内でのソース位置の変更(ひとつ先に進む)のタイミングを合わせないといけなかったり。