ひとり勉強会

ひとり楽しく勉強会

この前のScheme or YARVが面白かったので、なんとなく楽しくなって、Toy Lispを作ってらっしゃる日記を検索して見て回ったりしてました。

リスト

2007-03-14 - HaskellでLispを書く日記 - haskell ではHaskellLisp。ちゃんとしたS式パーザがとてもコンパクトに書かれていて、Parsec便利そうですねえ。そちらで

やっぱりlisp(LISt Processing)というからには、car,cdr,consが使えないと。

と書かれていて、これはその通りだと思ったので、真似してみることにしました。consセルは2要素の配列で表現しちゃうことにすると

def cons(a,d); [a,d];  end
def car(lst);  lst[0]; end
def cdr(lst);  lst[1]; end

これで完成。関数にしないで全部その場でコンパイルすることにすると

          when :cons
            scm_compile arg[0]; scm_compile arg[1]; newarray 2
          when :car
            scm_compile arg[0]; putobject 0; send :[], 1
          when :cdr
            scm_compile arg[0]; putobject 1; send :[], 1

          when :nil?
            scm_compile arg[0]; putnil; send :==, 1
          when :list
            arg.each{|a| scm_compile a}
            putnil
            arg.each{|a| newarray 2}

こうですね。空リストはnilということにしました。

scheme <<EOS
  (define (sum x)
     (if (nil? x) 0 (+ (car x) (sum (cdr x)))))
EOS

p scheme("(list 1 2 3)")       # ==> [1,[2,[3,nil]]]
p scheme("(sum (list 1 2 3))") # ==> 6

末尾再帰

突然ですがRubyでSchemeのサブセットを実装します。継続と末尾再帰最適化ありの。その1:データ構造とパーサ - <s>gnarl,</s>技術メモ”’<marquee><textarea>¥ という記事では

末尾再帰最適化もないような代物にSchemeを名乗る資格はございませんことよ。

と書かれていて、これもまた実にその通りだと反省しました。

YARVにも末尾再帰の最適化は予定はされているらしく、VM_CALL_TAILCALL_BIT というフラグは定義されています。が、特に実装はされていません(たぶん)。というわけで、手製の超絶手抜き末尾再帰を自分で実装してみます。

まず、コンパイル時に関数の末尾かどうかを気にしながらコンパイルするようにします。

  def scm_compile(s,tailpos=false)

関数を定義するときだけ、そのフラグをtrueにします。↓こんな感じ。

          when :define
            mname, *mparam = arg[0]
            putnil
            definemethod mname, YASM.method(mname, mparam) {
            _ :method_head              # <--
              scm_compile arg[1], true  # <--
              leave
            }
            putnil

あと、末尾再帰するわけなので、メソッド先頭にジャンプで戻るためのラベルを置いておきました。
if文全体が末尾位置なら、そのthen部とelse部も末尾です。というわけでtailposフラグをスルーパス

          when :if
            scm_compile arg[0]
            branchunless :else_part
            scm_compile arg[1], tailpos  #<--
            jump :end
          _ :else_part
            scm_compile arg[2], tailpos  #<--
          _ :end

あとは、末尾の再帰呼び出しをジャンプへとコンパイルするだけです。

          else
            if tailpos && op.to_s==@name         # <--
              arg.each{|a| scm_compile a}        # <--
              @vars.reverse.each{|v| setlocal v} # <--
              jump :method_head                  # <--
            else
              putnil
              arg.each{|a| scm_compile a}
              call op, arg.size
            end

YASM::SimpleDataBuilderのインスタンス変数に触ってるのと、あとlet文作ってないので変数は引数しかないという手抜き仕様を大いに活用しているのがダメなところです。よい子のみんなは真似しないでね。
ま、ともかく、

scheme <<EOS
  (define (wa n)
     (if (== n 0) 0 (+ n (wa (- n 1)))))
EOS

p scheme("(wa 10000)"); # ==> stack level too deep (SystemStackError)

これは末尾再帰じゃないのでスタックがあふれますが。

scheme <<EOS
  (define (wa2 n a)
     (if (== n 0) a (wa2 (- n 1) (+ n a))))
EOS

scheme <<EOS
  (define (wa3 n)
     (wa2 n 0))
EOS

p scheme("(wa3 10000)") # ==> 50005000

これは末尾再帰なので無事計算できるようになりました。やった!

勢いでlambda

簡単な言語のインタプリタ/コンパイラを作るときに一番面倒なのは、変数などなどの名前の管理だと個人的には思っています。ここまでは、「ローカル変数を定義するlet文が無い」「変数とindexの対応関係はyasmが管理してくれている」という二つの理由で、この部分を思いっきり手抜きできていました。もし前回のScheme on YARVが簡潔に見えたとしたら、それが理由の99.9%です(たぶん)。

前置きはともかく、関数型言語ならlambdaとか高階関数くらい作れないとねー、と思いました。これをやると変数の範囲にスコープができたり色々一気に大変になります。が、ちゃんとしたものを作る気は最初からないので、ごまかせる範囲で適当に作ります。手抜きばんざいです。

(lambda (x y) (+ x y))

          when :lambda
            putnil
            call :proc, 0, block(arg[0]) {
              scm_compile arg[1]
              leave
            }

無名関数はこんな感じでprocオブジェクトにするのが自然ですかね。
さてこうすると、ブロックローカル変数はgetlocalではなくgetdynamic命令でアクセスする必要があるので、変数アクセス式のコンパイルに手を加えないといけないです。

      when Symbol
        if @type==:block && @vars.include?(s)
          getdynamic s
        else
          getlocal s
        end

変数の管理とか全然やってないので、yasmから即手に入る情報でごまかします。この実装だとネストしたブロックローカル変数にはさわれません。
あと、lambdaを入れる以上、関数を関数名以外で呼び出せないとダメですね。これも適当にごまかして

            if Array===op || @vars.include?(op)
              scm_compile op
              arg.each{|a| scm_compile a}
              send :call, arg.size
            else
              if tailpos && op.to_s==@name
                ...
              else
                ...
              end
            end

こんな感じでProc#callにコンパイルします。

scheme <<EOS
  (define (map f x)
     (if (nil? x)
         (list)
         (cons (f (car x)) (map f (cdr x)))))
EOS

scheme "(p (map (lambda (x) (+ x 1)) (list 1 2 3)))"
  # ==> [2, [3, [4, nil]]]

こんなもんで。