ひとり勉強会

ひとり楽しく勉強会

NODE_ENSURE

ensure節のコードは、YARVでは複数箇所に複製される可能性があります。

  • begin の本体が正常終了する箇所
  • break などでensureを抜ける箇所
  • 例外で終わる場合の例外ハンドラとして

それぞれ、ensure節が終わったあとに実行するコードが違うので、ひとつにまとめてしまうと、ensure節の終わりでのジャンプが複雑になってしまうから、かな。

case NODE_ENSURE:{
  DECL_ANCHOR(ensr);

begin本体の後ろに繋げるための命令列はesnr変数にためます。宣言。

  VALUE ensure = NEW_CHILD_ISEQVAL(node->nd_ensr,
                   rb_str_concat(rb_str_new2
                                 ("ensure in "),
                                 iseq->name),
                   ISEQ_TYPE_ENSURE);

まずは、例外ハンドラとしてのコンパイル。ensure節の中身を別の命令列としてコンパイルして、変数ensureに保持します。

  LABEL *lstart = NEW_LABEL(nd_line(node));
  LABEL *lend = NEW_LABEL(nd_line(node));
  LABEL *lcont = NEW_LABEL(nd_line(node));
  struct ensure_range er = { lstart, lend, 0 };
  struct iseq_compile_data_ensure_node_stack enl = {
      node->nd_ensr,
      iseq->compile_data->ensure_node_stack,	/* prev */
      &er,
  };
  struct ensure_range *erange;

breakのところで見たように、ensureのネスト関係をちゃんとたどれるようにしておかないといけません。ここで、ネストを一段深くするための準備です。ensure_rangeというのはあとででてきます。

  COMPILE_POPED(ensr, "ensure ensr", node->nd_ensr);

ここでensure節の中身をコンパイル

  iseq->compile_data->ensure_node_stack = &enl;

  ADD_LABEL(ret, lstart);
  COMPILE_(ret, "ensure head", node->nd_head, poped);
  ADD_LABEL(ret, lend);

次に、本体部分をコンパイル。さっき新しく作ったensureのネスト関係をensure_node_stackに覚えてから再帰的にコンパイルしています。

  if (ensr->anchor.next == 0) {
      ADD_INSN(ret, nd_line(node), nop);
  }
  else {
      ADD_SEQ(ret, ensr);
  }
  ADD_LABEL(ret, lcont);

本体のうしろに、ensure節の中身を貼り付けます。空のときはnopを入れています。
続いて、本体部分で例外が発生したときに確実にensure節を実行するように、例外ハンドラの登録です。

  erange = iseq->compile_data->ensure_node_stack->erange;
  while (erange) {
      ADD_CATCH_ENTRY(CATCH_TYPE_ENSURE, erange->begin, erange->end,
		      ensure, lcont);
      erange = erange->next;
  }

なにやらループしてたくさんensure節を登録しています。これについてはすぐ下で詳しく読んでみます。とりあえずは

  iseq->compile_data->ensure_node_stack = enl.prev;
  break;
}

深くしたネスト情報を戻して、コンパイル終了。

ensure_range

なにやらたくさんCATCH_ENTRYを登録してるよ問題に戻ります。これは、「ensure節の中身を、breakのところにも複製してコンパイルしている」ことで必要となる処置です。こんなコードを考えてみましょう。

while true
  begin
    begin
      p "AAAA"
      break
      p "BBB"
    ensure #1
      p "CCC"
      raise "exception!!"
    end
  ensure #2
    p "DDD"
  end
end

breakの部分をコンパイルするときには、周囲のensure節の中身が全部展開されるのでした。

while true
  begin
    begin
      p "AAAA"
>>    p "CCC"
>>    raise "exception!!" <<!!!!
>>    p "DDD"
>>    jump loop_end
      p "BBB"
    ensure #1
      p "CCC"
      raise "exception!!"
    end
  ensure #2
    p "DDD"
  end
end
loop_end:

問題は、!!!! の行の例外です。この例外で外に飛ぶときは、#1のensureは実行しちゃまずいことになります。この部分はもともと#1のensureだったものを展開したコードなので、最初にこれを捕まえるensureはその1つ外の、#2になります。
展開後のコードで、例外が発生したとき、それを捕まえるensureはどれかを考えてみます。

#1    p "AAAA"
#2    p "CCC"
#2    raise "exception!!"
      p "DDD"
      jump loop_end
#1    p "BBB"

こうなりました。つまり、ensure #1 で例外を捕まえたい範囲は「beginの最初から最後まで」ではなくなってしまったのです。こういう風に、もっと細かい複数個の範囲になります。

beginの中を再帰的にコンパイルするときに、この細かい範囲の分割は、実はうまいこと処理済みです。処理済みでerange変数に入っていますので、

  erange = iseq->compile_data->ensure_node_stack->erange;
  while (erange) {
    ADD_CATCH_ENTRY(CATCH_TYPE_ENSURE,
                    erange->begin, erange->end,
                    ensure, lcont);
    erange = erange->next;
  }

こういう風に、全部のrangeに次々ハンドラとしてensureを登録していけば大丈夫です。

「実はうまいこと処理済み」...って、そんなコードあったっけ?実はあったのです。

add_ensure_iseq 他

ええとまず、rangeの初期値は、beginの最初から最後までです。

  struct ensure_range er = { lstart, lend, 0 };

これを、あとで段々細かく分割していきます。
細かくするのはbreakなどでensureを展開するところです。そこに秘密があります。さっき思いっきり簡略化といって省略した、add_ensure_iseq。実はこんなループだったのです。

static void
add_ensure_iseq(LINK_ANCHOR *ret, yarv_iseq_t *iseq)
{
  ...
  while (enlp) {
    DECL_ANCHOR(ensure_part);
    LABEL *lstart = NEW_LABEL(0);
    LABEL *lend = NEW_LABEL(0);
    add_ensure_range(iseq, enlp->erange, lstart, lend);

    iseq->compile_data->ensure_node_stack = enlp->prev;
    ADD_LABEL(ensure_part, lstart);
    COMPILE_POPED(ensure_part, "ensure part",
                  enlp->ensure_node);
    ADD_LABEL(ensure_part, lend);

    ADD_SEQ(ensure, ensure_part);
    enlp = enlp->prev;
  }
  ...
}

ひとつensureを展開するたびに、そのensure節のerangeに、展開した範囲(lstart,lend)をadd_ensure_rangeしています。add_ensure_rangeはこちら。

static void
add_ensure_range(yarv_iseq_t *iseq,
                 struct ensure_range *erange,
                 LABEL *lstart, LABEL *lend)
{
  struct ensure_range *ne =
    compile_data_alloc(iseq, sizeof(struct ensure_range));

  while (erange->next != 0) {
    erange = erange->next;
  }
  ne->next = 0;
  ne->begin = lend;
  ne->end = erange->end;
  erange->end = lstart;

  erange->next = ne;
}

whileループで、一番最後のerangeを探しています。最後のrangeから(lstart,lend)の範囲を取り除いて範囲を二つに分けるのが目的です。Rubyで書くとこういう多重代入で書けます。

最後.end, 新.begin, 新.end = lstart, lend, 最後.end

C言語だと見た目ややこしいですが、要するにこれと同じことをやってます。
というわけでめでたく、ensureのコンパイルができました。