20日目: rescueとensureをコンパイルする
前回はrescueとensureのノードを書き換えたので、今回はそれらをコンパイルできるようにしていきましょう。
バイトコードを眺める
compile.cをいじるまえに、rescueやensureがどのようなバイトコードになるのかを確認しておきましょう。
begin a rescue Ex1 => e1 e1 rescue Ex2, expr2, Ex2_2 => e2 e2 else b ensure c end
このコードに対応するバイトコードは次の通りです。
== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(11,3)> == catch table # 複数のrescueで1つのcatch tableとISeqになる | catch type: rescue st: 0000 ed: 0003 sp: 0000 cont: 0008 | == disasm: #<ISeq:rescue in <main>@test.rb:3 (3,0)-(6,4)> | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) | [ 1] "$!"@0 # 発生した例外を表す特殊変数 # 1つめのrescue # `rescue Ex1` | 0000 getlocal_WC_0 "$!"@0 ( 3) | 0002 opt_getconstant_path <ic:0 Ex1> | 0004 checkmatch 3 | 0006 branchunless 15 # `=> e1`による代入 | 0008 getlocal_WC_0 "$!"@0[Rs] | 0010 setlocal_WC_1 e1@0 # rescueのbodyである`e1` | 0012 getlocal_WC_1 e1@0 ( 4)[Li] | 0014 leave ( 3) # 2つめのrescue # `rescue Ex2` | 0015 getlocal_WC_0 "$!"@0 | 0017 opt_getconstant_path <ic:1 Ex2> ( 5) | 0019 checkmatch 3 ( 3) | 0021 branchif 40 # `rescue expr2` | 0023 getlocal_WC_0 "$!"@0 | 0025 putself ( 5) | 0026 opt_send_without_block <calldata!mid:expr2, argc:0, FCALL|VCALL|ARGS_SIMPLE> | 0028 checkmatch 3 ( 3) | 0030 branchif 40 # `rescue Ex2_2` | 0032 getlocal_WC_0 "$!"@0 | 0034 opt_getconstant_path <ic:2 Ex2_2> ( 5) | 0036 checkmatch 3 ( 3) | 0038 branchunless 47 # `=> e2`による代入 | 0040 getlocal_WC_0 "$!"@0 ( 5)[Rs] | 0042 setlocal_WC_1 e2@1 # rescueのbodyである`e2` | 0044 getlocal_WC_1 e2@1 ( 6)[Li] | 0046 leave ( 3) # いずれのrescueでもハンドルできなかった場合は例外を投げる | 0047 getlocal_WC_0 "$!"@0 | 0049 throw 0 # retryようのcatch table | catch type: retry st: 0003 ed: 0008 sp: 0000 cont: 0000 # ensureに対応するcatch tableとISeq | catch type: ensure st: 0000 ed: 0008 sp: 0001 cont: 0012 | == disasm: #<ISeq:ensure in <main>@test.rb:10 (10,2)-(10,3)> | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) | [ 1] "$!"@0 # rescueのbodyである`c` | 0000 putself ( 10)[Li] | 0001 opt_send_without_block <calldata!mid:c, argc:0, FCALL|VCALL|ARGS_SIMPLE> | 0003 pop # 最後に例外を投げる | 0004 getlocal_WC_0 "$!"@0 | 0006 throw 0 |------------------------------------------------------------------------ # begin ... end全体に対応する local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] e1@0 [ 1] e2@1 0000 putself ( 2)[Li] 0001 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 pop ( 3) 0004 putself ( 8)[Li] 0005 opt_send_without_block <calldata!mid:b, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0007 nop ( 3) 0008 putself ( 10)[Li] 0009 opt_send_without_block <calldata!mid:c, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0011 pop 0012 leave
今までと雰囲気がだいぶ違いますね。 もとのコードと各ISeqの対応をまとめると以下のようになります。

- メインとなるISeqの他にrescue, retry, ensure用のcatch tableとISeqが生成される
- メインとなるISeq:
- 本体に当たる部分(
a)とelseの部分(b)とensureの部分(c)に対応する命令がその順番で並ぶ - elseの部分は例外が発生しないときに実行するので、メインのISeqにだけ存在している
- ensureの部分は例外が発生してもしなくても実行するので、メインのISeqにも存在する必要がある
- 本体に当たる部分(
- rescueに対応するISeq:
- 複数の
rescueがある場合でも1つのISeqに命令がまとめられる - これは上から順番にチェックして、最初にマッチするrescue句が実行されるため
rescue Ex1, Ex2のように1つのrescueに例外が列挙されている場合には左から順に命令列にコンパイルしてISeqに並ぶ
- 複数の
- ensureに対応するISeq:
- ensureの部分(
c)に対応する命令が入っている - ensureの部分は例外が発生してもしなくても実行するので、例外発生時のensureのためにメインのISeqとは別にensureのISeqを用意している
- メインのISeqとは異なり最後に
throwする
- ensureの部分(
ここでcatch tableについてすこし触れておきましょう。 catch tableには4つの数値が設定されています。
- st(start): そのcatch tableがカバーするprogram counterの最小値
- ed(end): そのcatch tableがカバーするprogram counterの最大値
- sp(stack pointer?): catch tableを抜けたあとのstack pointerの値(だとおもう)
- cont(continue?): catch tableを抜けたあとのprogram counterの値(のはず)
VMはthrow命令が呼ばれたときに対応するcatch tableを探します。
ここで対応するというのは、現在のpc(program counter)がそのcatch tableのstとedの間に収まっているかどうかで判断します。
さきほどのコードでいえばrescueのcatch tableはpc 0000 ~ 0003をカバーしています。
| catch type: rescue st: 0000 ed: 0003 sp: 0000 cont: 0008
本体のISeqでいうとちょうどaメソッドの呼び出しの部分がrescueのcatch tableでカバーされていることがわかります。
-- ここから 0000 putself ( 2)[Li] 0001 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 pop ( 3) -- ここまで 0004 putself ( 8)[Li] 0005 opt_send_without_block <calldata!mid:b, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0007 nop ( 3) 0008 putself ( 10)[Li] 0009 opt_send_without_block <calldata!mid:c, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0011 pop 0012 leave
バイトコードを生成する
これまでは複数の異なるノードを逆順に木構造にすることでbegin ... endを表してきました。
NODE_ENSURENODE_RESCUENODE_RESBODY
書き換えによってBeginNodeのフィールドとしてrescue, else, ensureが並ぶことになります。
書き換え前はそれぞれのノードに対応する関数が用意されています。
case NODE_BEGIN:{ CHECK(COMPILE_(ret, "NODE_BEGIN", RNODE_BEGIN(node)->nd_body, popped)); break; } case NODE_RESCUE: CHECK(compile_rescue(iseq, ret, node, popped)); break; case NODE_RESBODY: CHECK(compile_resbody(iseq, ret, node, popped)); break; case NODE_ENSURE: CHECK(compile_ensure(iseq, ret, node, popped)); break;
まずはrescueから対応していきましょう。
再度書き換え前のノードを確認しておきます。
begin :begin rescue Ex => ex :rescue else :else end # @ NODE_RESCUE (id: 8, line: 1, location: (2,2)-(6,7))* # +- nd_head: # | @ NODE_SYM (id: 0, line: 2, location: (2,2)-(2,8))* # | +- string: :begin # +- nd_resq: # | @ NODE_RESBODY (id: 6, line: 3, location: (3,0)-(4,9)) # | +- nd_args: # | | @ NODE_LIST (id: 2, line: 3, location: (3,7)-(3,9)) # | | +- as.nd_alen: 1 # | | +- nd_head: # | | | @ NODE_CONST (id: 1, line: 3, location: (3,7)-(3,9)) # | | | +- nd_vid: :Ex # | | +- nd_next: # | | (null node) # | +- nd_exc_var: # | | @ NODE_LASGN (id: 3, line: 3, location: (3,10)-(3,15)) # | | +- nd_vid: :ex # | | +- nd_value: # | | @ NODE_ERRINFO (id: 5, line: 3, location: (3,10)-(3,15)) # | +- nd_body: # | | @ NODE_SYM (id: 4, line: 4, location: (4,2)-(4,9))* # | | +- string: :rescue # | +- nd_next: # | (null node) # +- nd_else: # @ NODE_SYM (id: 7, line: 6, location: (6,2)-(6,7))* # +- string: :else
既存のコンパイル処理をおおまかに説明すると以下のようになっています。
compile_rescue関数compile_resbody関数1
static int compile_rescue(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { // `rescue`以下の部分をコンパイルする const rb_iseq_t *rescue = NEW_CHILD_ISEQ(RNODE_RESCUE(node)->nd_resq, rb_str_concat(rb_str_new2("rescue in "), ISEQ_BODY(iseq)->location.label), ISEQ_TYPE_RESCUE, line); bool prev_in_rescue = ISEQ_COMPILE_DATA(iseq)->in_rescue; ISEQ_COMPILE_DATA(iseq)->in_rescue = true; { // `begin`の本体部分をコンパイルする CHECK(COMPILE(ret, "rescue head", RNODE_RESCUE(node)->nd_head)); } ISEQ_COMPILE_DATA(iseq)->in_rescue = prev_in_rescue; // `else`があればコンパイルする if (RNODE_RESCUE(node)->nd_else) { ADD_INSN(ret, line_node, pop); CHECK(COMPILE(ret, "rescue else", RNODE_RESCUE(node)->nd_else)); } ADD_INSN(ret, line_node, nop); ADD_LABEL(ret, lcont); if (popped) { ADD_INSN(ret, line_node, pop); } /* register catch entry */ ADD_CATCH_ENTRY(CATCH_TYPE_RESCUE, lstart, lend, rescue, lcont); ADD_CATCH_ENTRY(CATCH_TYPE_RETRY, lend, lcont, NULL, lstart); return COMPILE_OK; } static int compile_resbody(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { while (resq) { label_miss = NEW_LABEL(line); label_hit = NEW_LABEL(line); // `Ex =>` の部分をコンパイルする narg = RNODE_RESBODY(resq)->nd_args; if (narg) { switch (nd_type(narg)) { case NODE_LIST: while (narg) { ADD_GETLOCAL(ret, line_node, LVAR_ERRINFO, 0); CHECK(COMPILE(ret, "rescue arg", RNODE_LIST(narg)->nd_head)); ADD_INSN1(ret, line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_RESCUE)); ADD_INSNL(ret, line_node, branchif, label_hit); narg = RNODE_LIST(narg)->nd_next; } break; case NODE_SPLAT: case NODE_ARGSCAT: case NODE_ARGSPUSH: ADD_GETLOCAL(ret, line_node, LVAR_ERRINFO, 0); CHECK(COMPILE(ret, "rescue/cond splat", narg)); ADD_INSN1(ret, line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_RESCUE | VM_CHECKMATCH_ARRAY)); ADD_INSNL(ret, line_node, branchif, label_hit); break; default: UNKNOWN_NODE("NODE_RESBODY", narg, COMPILE_NG); } } else { ADD_GETLOCAL(ret, line_node, LVAR_ERRINFO, 0); ADD_INSN1(ret, line_node, putobject, rb_eStandardError); ADD_INSN1(ret, line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_RESCUE)); ADD_INSNL(ret, line_node, branchif, label_hit); } ADD_INSNL(ret, line_node, jump, label_miss); ADD_LABEL(ret, label_hit); ADD_TRACE(ret, RUBY_EVENT_RESCUE); // `=> ex` の部分をコンパイルする if (RNODE_RESBODY(resq)->nd_exc_var) { CHECK(COMPILE_POPPED(ret, "resbody exc_var", RNODE_RESBODY(resq)->nd_exc_var)); } // `rescue`のbodyをコンパイルする if (nd_type(RNODE_RESBODY(resq)->nd_body) == NODE_BEGIN && RNODE_BEGIN(RNODE_RESBODY(resq)->nd_body)->nd_body == NULL && !RNODE_RESBODY(resq)->nd_exc_var) { // empty body ADD_SYNTHETIC_INSN(ret, nd_line(RNODE_RESBODY(resq)->nd_body), -1, putnil); } else { CHECK(COMPILE(ret, "resbody body", RNODE_RESBODY(resq)->nd_body)); } if (ISEQ_COMPILE_DATA(iseq)->option->tailcall_optimization) { ADD_INSN(ret, line_node, nop); } ADD_INSN(ret, line_node, leave); ADD_LABEL(ret, label_miss); resq = RNODE_RESBODY(resq)->nd_next; } return COMPILE_OK; }
ノードの書き換え後はBeginNodeにrescueやensureがぶら下がることになるので、BeginNodeを処理するcompile_begin関数を用意しておきます。
compile_begin関数はrescueの有無を確認して、rescueがあればcompile_begin_rescue関数を、そうでなければiseq_compile_each0関数を呼び出します。
static int compile_begin(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { const int line = nd_line(node); const NODE *line_node = node; LABEL *lstart = NEW_LABEL(line); LABEL *lend = NEW_LABEL(line); LABEL *lcont = NEW_LABEL(line); if (RB_NODE_BEGIN(node)->rescue_clause) { compile_begin_rescue(iseq, ret, RB_NODE_BEGIN(node), popped); } else { CHECK(COMPILE_(ret, "begin body", RB_NODE_BEGIN(node)->statements, popped)); } return COMPILE_OK; }
compile_begin_rescue関数はというと、引数がBeginNodeになったこと以外はcompile_rescue関数と変わりません。
引数が変わったのはbeginのbodyにあたる部分やelseにあたる部分がBeginNodeに移動したためです。
static int compile_begin_rescue(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const rb_begin_node_t *const node, int popped) { const int line = nd_line(node); const NODE *line_node = node; LABEL *lstart = NEW_LABEL(line); LABEL *lend = NEW_LABEL(line); LABEL *lcont = NEW_LABEL(line); const rb_iseq_t *rescue = NEW_CHILD_ISEQ(node->rescue_clause, rb_str_concat(rb_str_new2("rescue in "), ISEQ_BODY(iseq)->location.label), ISEQ_TYPE_RESCUE, line); lstart->rescued = LABEL_RESCUE_BEG; lend->rescued = LABEL_RESCUE_END; ADD_LABEL(ret, lstart); bool prev_in_rescue = ISEQ_COMPILE_DATA(iseq)->in_rescue; ISEQ_COMPILE_DATA(iseq)->in_rescue = true; { CHECK(COMPILE(ret, "rescue head", node->statements)); } ISEQ_COMPILE_DATA(iseq)->in_rescue = prev_in_rescue; ADD_LABEL(ret, lend); if (node->else_clause) { ADD_INSN(ret, line_node, pop); CHECK(COMPILE(ret, "rescue else", node->else_clause)); } ADD_INSN(ret, line_node, nop); ADD_LABEL(ret, lcont); if (popped) { ADD_INSN(ret, line_node, pop); } /* register catch entry */ ADD_CATCH_ENTRY(CATCH_TYPE_RESCUE, lstart, lend, rescue, lcont); ADD_CATCH_ENTRY(CATCH_TYPE_RETRY, lend, lcont, NULL, lstart); return COMPILE_OK; }
rescue EX => ex ...の部分にあたるRescueNodeのコンパイルはcompile_resbody関数とほとんど変わらないので割愛します。
ensure部分のコンパイル
次にensureがある場合のコンパイル処理についてです。
compile_ensure関数NODE_ENSUREのnd_ensr、つまりensureのbodyを別のISeqとしてコンパイルしてensure用のcatch tableのためのバイトコードをつくる。実際のコンパイル処理はcompile_ensure関数が行う- 別のanchorをつくり本体のISeq用に
NODE_ENSUREのnd_ensrをコンパイルする NODE_ENSUREのnd_ensrをpush_ensure_entryするNODE_ENSUREのnd_head、つまりbegin ... rescue ...の部分をコンパイルする- 別のanchorにコンパイルした
nd_ensrの結果を本体のISeqにくっつける - ensure用のcatch tableをつくる
static int compile_ensure(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { // ensureのcatch table用に`ensure ...`の部分をコンパイルする const rb_iseq_t *ensure = NEW_CHILD_ISEQ(RNODE_ENSURE(node)->nd_ensr, rb_str_concat(rb_str_new2 ("ensure in "), ISEQ_BODY(iseq)->location.label), ISEQ_TYPE_ENSURE, line); // 本体用に`ensure ...`の部分をコンパイルする CHECK(COMPILE_POPPED(ensr, "ensure ensr", RNODE_ENSURE(node)->nd_ensr)); er.begin = lstart; er.end = lend; er.next = 0; // これについては今回は触れない push_ensure_entry(iseq, &enl, &er, RNODE_ENSURE(node)->nd_ensr); // `ensure`以外の部分をコンパイルする CHECK(COMPILE_(ret, "ensure head", RNODE_ENSURE(node)->nd_head, (popped | last_leave))); ADD_SEQ(ret, ensr); if (!popped && last_leave) ADD_INSN(ret, line_node, putnil); if (last_leave) ADD_INSN(ret, line_node, pop); // ensureのcatch tableを生成する erange = ISEQ_COMPILE_DATA(iseq)->ensure_node_stack->erange; if (lstart->link.next != &lend->link) { while (erange) { ADD_CATCH_ENTRY(CATCH_TYPE_ENSURE, erange->begin, erange->end, ensure, lcont); erange = erange->next; } } ISEQ_COMPILE_DATA(iseq)->ensure_node_stack = enl.prev; return COMPILE_OK; }
ensureもBeginNodeの配下に移動したので、ensureがある場合のエントリーポイントもcompile_begin関数になります。
compile_begin関数に分岐を追加して
ensureがあるときensureはないけどrescueがあるとき- どちらもないとき
の3パターンで処理を変えます。
static int compile_begin(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { if (RB_NODE_BEGIN(node)->ensure_clause){ compile_begin_ensure(iseq, ret, RB_NODE_BEGIN(node), popped); } else if (RB_NODE_BEGIN(node)->rescue_clause) { compile_begin_rescue(iseq, ret, RB_NODE_BEGIN(node), popped); } else { CHECK(COMPILE_(ret, "begin body", RB_NODE_BEGIN(node)->statements, popped)); } return COMPILE_OK; }
生成されるバイトコードを確認します。
begin p :begin rescue Ex => ex p :rescue else p :else ensure p :ensure end
== disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(9,3)> == catch table | catch type: rescue st: 0000 ed: 0005 sp: 0000 cont: 0012 | == disasm: #<ISeq:rescue in <main>@../../test.rb:3 (3,0)-(4,11)> | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) | [ 1] "$!"@0 | 0000 getlocal_WC_0 "$!"@0 ( 3) | 0002 opt_getconstant_path <ic:0 Ex> | 0004 checkmatch 3 | 0006 branchunless 18 | 0008 getlocal_WC_0 "$!"@0[Rs] | 0010 setlocal_WC_1 ex@0 | 0012 putself ( 4)[Li] | 0013 putobject :rescue | 0015 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> | 0017 leave ( 3) | 0018 getlocal_WC_0 "$!"@0 | 0020 throw 0 | catch type: retry st: 0005 ed: 0012 sp: 0000 cont: 0000 | catch type: ensure st: 0000 ed: 0012 sp: 0001 cont: 0018 | == disasm: #<ISeq:ensure in <main>@../../test.rb:7 (7,0)-(8,11)> | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) | [ 1] "$!"@0 | 0000 putself ( 8)[Li] | 0001 putobject :ensure | 0003 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> | 0005 pop | 0006 getlocal_WC_0 "$!"@0 | 0008 throw 0 |------------------------------------------------------------------------ local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] ex@0 0000 putself ( 2)[Li] 0001 putobject :begin 0003 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> 0005 pop ( 1) 0006 putself ( 6)[Li] 0007 putobject :else 0009 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> 0011 nop ( 1) 0012 putself ( 8)[Li] 0013 putobject :ensure 0015 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> 0017 pop 0018 leave ( 6)
良さそうですね。
後置rescueをコンパイルする
ノードの書き換えによってbegin ... resuceと後置rescueとで違うノードを使うようになりました。
いままではどちらのケースもNODE_RESCUEというノードを使い、そのコンパイル処理はcompile_rescue関数が行っていました。
compile_rescue関数はcompile_begin_rescue関数になったので、後置rescueについてもcompile_begin_rescue関数に処理を任せたいところです。
static int compile_begin_rescue(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { const int line = nd_line(node); const NODE *line_node = node; LABEL *lstart = NEW_LABEL(line); LABEL *lend = NEW_LABEL(line); LABEL *lcont = NEW_LABEL(line); NODE *nd_stmts, *nd_rescue, *nd_else; switch (nd_type(node)) { case RB_BEGIN_NODE: nd_stmts = RB_NODE_BEGIN(node)->statements; nd_rescue = RB_NODE_BEGIN(node)->rescue_clause; nd_else = RB_NODE_BEGIN(node)->else_clause; break; case RB_RESCUE_MODIFIER_NODE: nd_stmts = RB_NODE_RESCUE_MODIFIER(node)->expression; nd_rescue = RB_NODE_RESCUE_MODIFIER(node)->rescue_expression; nd_else = NULL; break; default: } ... } static int iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { ... case RB_RESCUE_MODIFIER_NODE: { CHECK(compile_begin_rescue(iseq, ret, node, popped)); break; } ... }
:begin rescue :rescueをコンパイルしてみます。
== disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(1,21)> == catch table | catch type: rescue st: 0000 ed: 0002 sp: 0000 cont: 0003 | == disasm: #<ISeq:rescue in <main>@../../test.rb:1 (1,7)-(1,21)> | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) | [ 1] "$!"@0 | 0000 getlocal_WC_0 "$!"@0 ( 1) | 0002 putobject StandardError | 0004 checkmatch 3 | 0006 branchunless 11 | 0008 putobject :rescue[Rs] | 0010 leave | 0011 getlocal_WC_0 "$!"@0 | 0013 throw 0 | catch type: retry st: 0002 ed: 0003 sp: 0000 cont: 0000 |------------------------------------------------------------------------ 0000 putobject :begin ( 1)[Li] 0002 nop 0003 leave
良さそうですね。
まとめ
今日の成果です。
次回はretryやnextといった制御構文に取り組みたいと思います。