Scm_VMCallCC (20)

そろそろ ReadingGauche 方面を出力して、test/dynwind.scm の中身をナニしたい。

前提

以下のインストラクションを前提に整理 (になるかどうか)。

gosh> (disasm (lambda () (list 1 2 (call/cc (lambda (c) (set1 cont c) 3)) 4)))
main_code (name=#f, code=0x80f9db0, size=12, const=2, stack=12):
args: #f
     0 CONSTI-PUSH(1)
     1 CONSTI-PUSH(2)
     2 PRE-CALL(1) 8
     4 CLOSURE #<lambda 0>      ; (lambda (c) (set1 cont c) 3)
     6 PUSH-GREF-CALL(1) #<identifier user#call/cc>; (call/cc (lambda (c) (set1 cont c) 3))
     8 PUSH
     9 CONSTI(4)
    10 LIST(4)                  ; (list 1 2 (call/cc (lambda (c) (set1 con ...
    11 RET
internal_closure_0 (name=#f, code=0x80f8ca0, size=8, const=2 stack=11):
args: #f
     0 PRE-CALL(2) 6
     2 GREF-PUSH #<identifier user#cont>; cont
     4 LREF0-PUSH-GREF-CALL(2) #<identifier user#set1>; (set1 cont c)
     6 CONSTI(3)
     7 RET
#<undef>
gosh>

上記 6 の call_entry: ラベルの直前のスタックの状態が以下。

     sp>|        |
   argp>|てつづき| ;; lambda な手続きオブジェクト
        +--------+
        |        | ;; base
        |        | ;; pc   next-pc は 8 の push
        |   2    | ;; size 
        |   *3   | ;; argp
        |   *2   | ;; env
   cont>|   *1   | ;; prev
        +--------+
        |   2    |
     *3>|   1    |
        +--------+
        |        |
*2(env)>|  ENV   |
        +--------+
        |        |
     *1>|  CONT  |
        +--------+

PUSH_GREF_CALL な分岐にて val0 に格納された closure を push して call/cc の global な束縛を解決して val0 に格納した所。cont が指す継続フレームは直前の PRE_CALL で push されてて、next-pc は 8 になっている模様。
この後、

  • 引数の数をカウントして argc に格納 (このケースだと 1)
  • SP に ARGP の値を格納
  • PC は RET を指す状態

にしておいて val0 に格納されている手続きオブジェクトを元に以下な命令を実行。

                    VAL0 = SCM_SUBR(VAL0)->func(ARGP, argc,
                                                SCM_SUBR(VAL0)->data);

上記は実質 Scm_VMCallCC 手続きの呼び出しになる。val0 に格納されている func な属性には stdlib.c で定義されている stdlib_call_with_current_continuation なポインタが格納されている (はず) で上図でてっぺんに push された手続きオブジェクトが Scm_VMCallCC に渡される模様。
で、まず上記なスタックが heap に退避される。継続フレームが退避される前に環境フレームも heap に退避。その上で R5RS な記述で言えば escape procedure なオブジェクトを生成して引数で渡された手続き (スタックに push された closure) に渡す。
こうして見るに Scm_VMCallCC という手続きは R5RS で言う脱出プロシージャ (escape procedure) を生成する手続きがほとんどで、生成のために継続フレームと環境フレームを heap に退避している事がわかる。3imp きちんと読まないといかんなぁ。

継続を補足してそれを呼び出した場合どうなるか。

gosh> (disasm (lambda () (cont -1)))
main_code (name=#f, code=0x80d6890, size=4, const=1, stack=4):
args: #f
     0 CONSTI-PUSH(-1) 
     1 GREF-TAIL-CALL(1) #<identifier user#cont>; (cont -1)
     3 RET 
#<undef>
gosh> 

引数を push して cont に束縛された手続きオブジェクトを val0 に格納して末尾呼び出し向けにスタックを調整 (詳細は略)。面倒臭いのでスタックのマンガを略しています。いかんなぁ。とは言え、このあと継続フレームが書きかわるはず。
とりあえず cont に束縛されている手続きオブジェクトの func 属性に格納されているのは throw_continuation です。上記 Scm_VMCallCC 手続きにてオブジェクトが生成。
中略しますが、ここから throw_cont_body 手続きが呼び出されて

  • PC は RET を指す状態
  • CONT は escape procedure なオブジェクトの cont 属性がセット
  • vm->handlers もナニ

された後に通常であれば引数を一つ取り出して戻る。

その後

PC が RET を指しているのでスタックから継続フレームを取り出してナニ。詳細は以下と見ています。

            int size__ = CONT->size;                                    \
            ARGP = SP = vm->stackBase;                                  \
            ENV = CONT->env;                                            \
            PC = CONT->pc;                                              \
            BASE = CONT->base;                                          \
            if (CONT->argp && size__) {                                 \
                ScmObj *s__ = CONT->argp, *d__ = SP;                    \
                SP += size__;                                           \
                while (size__-- > 0) {                                  \
                    *d__++ = *s__++;                                    \
                }                                                       \
            }                                                           \
            CONT = CONT->prev;                                          \

ちょっと微妙。ちょっとここで一旦止めます。

続けてみる

これってスタックの底に保存した積みかけの引数をロードしているように読める。あとはきっちりスタック (正確にはそうでないですが) が元の状態に戻って続きがナニ。
この状態で ReadingGauche にデータ投入を試みてみます。