Christopher Strachey の GPM

かの有名なる GPM (General Purpose Macrogenerator) 互換のマクロ・プロセッサを和田先生が Scheme で書いていらっしゃいます。

http://parametron.blogspot.jp/2013/12/cristopher-stracheygpm_11.html

それを Ruby に翻訳しました。env と args はペアを使ったリストのままにしています。ペアには 2 要素の Array オブジェクトを使い、car を first、cdr を last メソッドに機械的に置き換えてあります。gpm 手続きのレキシカル・スコープは GPM インスタンスにして、さらに、Scheme の末尾呼び出しを利用したループを、while ループに書き直しています。なお、readstring 手続きでコンマかセミコロンを実引数にコンスアップしているのに対して、 Ruby 版の expand_string メソッドでは実引数の後ろに付けるように変更しています。

class StracheyGPM
  def initialize(str, env, args)
    @str = str
    @env = env
    @args = args
    @index = 0
  end

  def expand
    t = ""
    while true
      str = expand_string
      return t + str if String === str
      t << str.first << str.last
    end
  end

  def expand_string
    t = ""
    while @index < @str.size
      ch = getch
      if ch == ',' || ch == ';'
        return [t, ch]
      elsif ch == '<'
        t << expand_quote
      elsif ch == '~'
        ch = getch
        (ch >= '0' and ch <= '9') or raise ArgumentError
        t << lookup_argument(ch.ord - '0'.ord, @args)
      elsif ch == '$'
        t << expand_macro
      else
        t << ch
      end
    end
    t
  end

  def expand_quote
    t = ""
    level = 1
    while level > 0
      ch = getch
      if ch == '>'
        level -= 1
      elsif ch == '<'
        level += 1
      end
      t << ch if level > 0
    end
    t
  end

  def expand_macro
    a = a0 = [nil, nil]
    begin
      t, ch = expand_string
      if ch == ',' || ch == ';'
        a[1] = [t, nil]
        a = a[1]
      end
    end until ch == ';'
    macrocall(a0[1])
  end

  def macrocall(actuals)
    if actuals.first == "def"
      @env = [actuals.last, @env]
      ""
    else
      str = lookup_macro(actuals.first, @env)
      StracheyGPM.new(str, @env, actuals).expand
    end
  end

  def getch
    begin
      ch = @str[@index]
      @index += 1
    end while ch == " " || ch == "\n"
    ch
  end

  def lookup_argument(n, args)
    n >= 0 or raise ArgumentError
    while not args.nil?
      return args.first if n == 0
      n -= 1
      args = args.last
    end
    nil
  end

  def lookup_macro(key, env)
    while not env.nil?
      return env.first.last.first if key == env.first.first
      env = env.last
    end
    nil
  end
end

def gpm(s)
  StracheyGPM.new(s, nil, nil).expand
end

puts gpm "$1+,7,$def,1+,<$1,2,3,4,5,6,7,8,9,10,$def,1,<~>~1;;>;;"