ひとり勉強会

ひとり楽しく勉強会

NODE_WHILE 再び

case NODE_OPT_N:
case NODE_WHILE:
case NODE_UNTIL:{
  LABEL *prev_start_label = iseq->compile_data->start_label;
  LABEL *prev_end_label = iseq->compile_data->end_label;
  LABEL *prev_redo_label = iseq->compile_data->redo_label;
  VALUE prev_loopval_popped =
    iseq->compile_data->loopval_popped;

  struct iseq_compile_data_ensure_node_stack *enlp =
    iseq->compile_data->ensure_node_stack;

さっきまとめたところによると、whileで管理しておきたいのは

  • break, redo, next のジャンプ先
  • whileの時点でのpopedフラグ
  • ensureで囲まれてる場合はそのensureの命令列

でした。まさにその5項目がiseq構造体のメンバとして保存されています。ここは、これからこの5項目を書き換えるので、whileループのコンパイルが終わったら戻すために一時退避しているコードです。
続いて、その5項目を新しくセットアップするコードです。

LABEL *next_label = iseq->compile_data->start_label
  = NEW_LABEL(nd_line(node));   /* next  */
LABEL *redo_label = iseq->compile_data->redo_label
  = NEW_LABEL(nd_line(node));   /* redo  */
LABEL *break_label = iseq->compile_data->end_label
  = NEW_LABEL(nd_line(node));   /* break */
LABEL *end_label = NEW_LABEL(nd_line(node));

iseq->compile_data->loopval_popped = 0;
iseq->compile_data->ensure_node_stack = 0;

break, redo, next のジャンプ先ラベルは新しく作っています。新しくwhileで囲ったことで、「この中でbreakなどしてもensureを実行する必要はない状態」にいったん戻るので、ensure_node_stack は 0 クリアされています。loopval_popped は... なんで 0 なんでしょ?

前回読んだ部分をもいちど見直してみると...

jmp next_label
redo_label:
  本体
next_label:
  条件判定
end_label:
  putnil
break_label:
  if(poped) pop

breakでは常に値がpushされることを前提にして、要らないならwhile側でpopする、というコードに現在はコンパイルされてるようでした。なので、とりあえずbreakには常にpushさせるloopval_popped = 0 になっているのかな、と思います。もしかしたら将来的に最適化される予定なのかも。

この下には、前回やったwhile文のコード生成が続きます。中に出てくるbreakなどは、ここで再帰的にコンパイルされるはずです。で、それが終わると後処理です。

ADD_CATCH_ENTRY(CATCH_TYPE_BREAK, redo_label, break_label,
  0, break_label);
ADD_CATCH_ENTRY(CATCH_TYPE_NEXT | 0x10000, redo_label,
  break_label, 0, iseq->compile_data->start_label);
ADD_CATCH_ENTRY(CATCH_TYPE_REDO, redo_label, break_label, 0,
  iseq->compile_data->redo_label);

「break while 例外」で説明したように、ブロック内での break, next, redo を例外として捕まえるための、ハンドラ登録をしています。読み方はだいたい、たとえば

ADD_CATCH_ENTRY(CATCH_TYPE_BREAK, redo_label, break_label,
  0, break_label);

なら「redo_labelからbreak_labelの範囲でCATCH_TYPE_BREAKタイプの例外が発生したら、break_labelにジャンプせよ」という意味で登録されてるようです。CATCH_TYPE_NEXT に or されている 0x10000 がとてもマジカルな感じですが、whileのnextとブロックのnextで処理を変えなきゃいけないせいで、その識別用、かな。あとでset_exception_table (compile step 4.2)を読むときに真剣に調べるかもしれません。とりあえずスルーで。
あと、ここの iseq->compile_data->start_label は簡単に redo_label でいいんじゃないかと思うのですが、どうなんだろ。

iseq->compile_data->start_label = prev_start_label;
iseq->compile_data->end_label = prev_end_label;
iseq->compile_data->redo_label = prev_redo_label;
iseq->compile_data->loopval_popped = prev_loopval_popped;
iseq->compile_data->ensure_node_stack = enlp;
break;

最後に、最初で保存した変数を戻しています。これでネストもばっちりです。