14日目: multiple assignmentのコンパイルに関する細かな話
multiple assignmentのノードの変更とコンパイルは前回までで大体できました。 今日はノードの書き換えのときには触れなかったmultiple assignmentのコンパイルに関する小話を2つしたいと思います。 ということで本日は進捗はでません。
小話1: struct masgn_stateとはなにか
multiple assignmentをコンパイルする際にはstruct masgn_stateという構造体がついてまわります。
この構造体はcompile_massign関数のスタックに確保され、compile_massign0関数, compile_massign_lhs関数へと受け渡されます。
小話の1つ目としてstruct masgn_stateについて詳しくみていきます。
2つの構造体
この構造体については導入時のコミットメッセージおよびコード内のコメントが詳細に書かれていますので、興味がある場合にはそちらもあわせて見るとよいでしょう。
まずはstruct masgn_stateおよび、そこで使われているstruct masgn_lhs_nodeについてみてみます。
struct masgn_lhs_node { INSN *before_insn; struct masgn_lhs_node *next; const NODE *line_node; int argn; int num_args; int lhs_pos; }; struct masgn_state { struct masgn_lhs_node *first_memo; struct masgn_lhs_node *last_memo; int lhs_level; int num_args; bool nested; };
struct masgn_lhs_nodeはnextというフィールドに自身の次の要素をもつリスト構造になっていることがわかります。
またstruct masgn_stateはfirst_memoとlast_memoというフィールドをもっていることから、struct masgn_lhs_nodeのリスト構造の先頭と末尾を保持していることがわかります。
図にするとざっくりこんな感じです。

struct masgn_lhs_nodeを操作する
次にstruct masgn_lhs_nodeとstruct masgn_stateを操作する関数をみてみましょう。
add_masgn_lhs_nodeはstruct masgn_lhs_nodeを生成するための関数です。
static int add_masgn_lhs_node(struct masgn_state *state, int lhs_pos, const NODE *line_node, int argc, INSN *before_insn) { if (!state) { rb_bug("no masgn_state"); } struct masgn_lhs_node *memo; // メモリの確保 memo = malloc(sizeof(struct masgn_lhs_node)); if (!memo) { return COMPILE_NG; } // struct masgn_lhs_nodeのセットアップ memo->before_insn = before_insn; memo->line_node = line_node; memo->argn = state->num_args + 1; memo->num_args = argc; state->num_args += argc; memo->lhs_pos = lhs_pos; memo->next = NULL; // struct masgn_state *stateのfirstとlastを更新する if (!state->first_memo) { state->first_memo = memo; } else { state->last_memo->next = memo; } state->last_memo = memo; return COMPILE_OK; }
特に難しいところはないでしょう。
注意が必要なのはmemo->argn = state->num_args + 1;とstate->num_args += argc;でしょうか。
memoの他のフィールドは関数の引数を代入するだけですが、これら2つのフィールドはadd_masgn_lhs_node関数が呼ばれる度に累積していくようです。
add_masgn_lhs_node関数はcompile_massign_lhs関数でCallTargetNodeやIndexTargetNodeを処理するときに呼ばれます1。
ということはstruct masgn_lhs_nodeの1つ1つはmultiple assignmentの左辺におけるメソッド呼び出しの項に対応していることがわかります。
struct masgn_stateを操作する
struct masgn_stateはcompile_massign関数でスタックにallocateされ、初期化されます。
static int compile_massign(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const rb_multi_write_node_t *const node, int popped) { if (!popped || node->rest || !compile_massign_opt(iseq, ret, node->value, &node->lefts)) { struct masgn_state state; state.lhs_level = popped ? 0 : 1; state.nested = 0; state.num_args = 0; state.first_memo = NULL; state.last_memo = NULL; ... }
また一部のフィールドはcompile_massign_lhs関数がMultiTargetNodeを処理するときに更新されます。
static int compile_massign_lhs(rb_iseq_t *iseq, LINK_ANCHOR *const pre, LINK_ANCHOR *const rhs, LINK_ANCHOR *const lhs, LINK_ANCHOR *const post, const NODE *const node, struct masgn_state *state, int lhs_pos) { switch (nd_type(node)) { case RB_MULTI_TARGET_NODE: { DECL_ANCHOR(nest_rhs); INIT_ANCHOR(nest_rhs); DECL_ANCHOR(nest_lhs); INIT_ANCHOR(nest_lhs); int prev_level = state->lhs_level; bool prev_nested = state->nested; state->nested = 1; state->lhs_level = lhs_pos - 1; CHECK(compile_massign0(iseq, pre, nest_rhs, nest_lhs, post, node, state, 1)); state->lhs_level = prev_level; state->nested = prev_nested; ADD_SEQ(lhs, nest_rhs); ADD_SEQ(lhs, nest_lhs); break; } ... }
ということでstruct masgn_stateはmultiple assignmentのネストに関連する情報を管理していることがわかります。
バイトコード生成時のことを考える
一度compile.cの実装から離れてバイトコードを生成するときのことを考えてみましょう。
a[0], s.f = [1, 2, 3, 4]に対応するバイトコードを生成するとしましょう。
# ここは普通にコンパイルすればよい 0000 putself ( 1)[Li] 0001 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 putobject_INT2FIX_0_ 0004 putself 0005 opt_send_without_block <calldata!mid:s, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0007 duparray [1, 2, 3, 4] 0009 dup 0010 expandarray 2, 0 # ここでは以下のことを判断しないといけない # * topnの回数 # * topnのオペランド # * popの回数 0013 topn 5 0015 topn 5 0017 topn 2 0019 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] 0021 pop 0022 pop # ここでは以下のことを判断しないといけない # * topnの回数 # * topnのオペランド # * popの回数 0023 topn 2 0025 swap 0026 opt_send_without_block <calldata!mid:f=, argc:1, ARGS_SIMPLE> 0028 pop # ここでは以下のことを判断しないといけない # * setnのオペランド # * popの回数 0029 setn 3 0031 pop 0032 pop 0033 pop 0034 leave
このようにメソッドを呼び出すときと、最後のclean upのときに左辺の要素に依存して変わる部分があります。
メソッド呼び出しのためのバイトコード
a[0] = 1に対応するバイトコードを考えてみます。
0013 topn 5 0015 topn 5 0017 topn 2 0019 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] 0021 pop 0022 pop
topnの回数はレシーバーの数(常に1)、左辺に書かれた引数の数(可変)、=の右辺の数(常に1)の合計回数と一致します。
topnのオペランドの計算はそれなりに複雑です。
# expandarray 2, 0をした直後 1 2 # a[0]よりあとにあるメソッド呼び出しの数に依存する [1, 2, 3, 4] # dupをするかで数が変わる s # a[0]よりあとにあるメソッド呼び出しの数とそれらの引数の数に依存する 0 # a[0]の引数の数に依存する a # これをスタックトップに持ってきたい
popの回数はswap命令を使わないときは2、使っているときは1で固定です。
というのはメソッドを呼び出すことでレシーバーと引数が全てスタックから取り除かれ、メソッド呼び出しの戻り値がスタックトップに置かれます。
swapを使わないときはexpandarrayで展開した結果がスタックに残っているので、メソッド呼び出しの戻り値とあわせて2つ分popする必要があります。
# a[0] = 1を呼び出した直後 a[0] = 1 # の戻り値 1 # ここまで不要になった 2 [1, 2, 3, 4] s 0 a
一方でswapを使った時はexpandarrayで展開した結果がスタックに残らないので、1回popをすれば済みます。
# swapでスタックトップの2つの要素を入れ替える # こうすることで上から引数、レシーバーの順にオブジェクトが並ぶ 2 s [1, 2, 3, 4] s 0 a # s.f = 2を呼び出した直後 s.f = 2 # の戻り値 [1, 2, 3, 4] # これはpopしてはいけない s 0 a
最後のclean upのためのバイトコード
s.f = 2まで実行したら、最後にスタックに[1, 2, 3, 4]だけが残るようにします。
対応するバイトコードは以下の通りです。
0029 setn 3 0031 pop 0032 pop 0033 pop 0034 leave
0028 popまで実行したときのスタックは以下のようになっています。
# `0028 pop`を実行した直後 [1, 2, 3, 4] s 0 a # ここに[1, 2, 3, 4]をコピーしたい
式全体の戻り値になる[1, 2, 3, 4]はこの時点でスタックトップにきているので、あとはスタックの一番下に値をコピーすればよいはずです。
というわけでsetnのオペランドを計算する必要がありますが、この値は左辺のメソッド呼び出し全部のレシーバーと引数の数の合計です。
またsetnをしたあとに複数回popをしてスタックを短くする必要がありますが、この回数はsetnのオペランドと一致します。
以上より、メソッドの呼び出しとclean up処理をするときに次のような情報が必要だとわかります。
- メソッド呼び出し
- clean up
setnのオペランドの計算- 左辺のメソッド呼び出し全部のレシーバーと引数の数の合計
popの回数の計算- 左辺のメソッド呼び出し全部のレシーバーと引数の数の合計
いくつか補足をしておくと、topnの回数の計算についてはレシーバーの数と=の右辺の数はどちらも1で固定なので個別の情報としては必要ありません。
topnのオペランドの計算については"自身よりあとにあるメソッド呼び出しに関して、それらの総数と引数の総数"は別々で必要になります。
setnのオペランドの計算とpopの回数の計算では同じ値を使い回すことができます。
再度struct masgn_lhs_nodeの定義をみてみると、int argn, int num_argsあたりでtopnの計算に必要な情報を管理しているように思えます。
struct masgn_lhs_node { INSN *before_insn; struct masgn_lhs_node *next; const NODE *line_node; int argn; int num_args; int lhs_pos; };
念のためtopn命令を生成しているcompile_massign関数もみておくと、int argn, int num_argsに加えてint lhs_posも使ってオペランドの計算をしていることがわかります。
struct masgn_lhs_node *memo = state.first_memo, *tmp_memo; while (memo) { VALUE topn_arg = INT2FIX((state.num_args - memo->argn) + memo->lhs_pos); for (int i = 0; i < memo->num_args; i++) { INSERT_BEFORE_INSN1(memo->before_insn, nd_line(memo->line_node), nd_node_id(memo->line_node), topn, topn_arg); } tmp_memo = memo->next; free(memo); memo = tmp_memo; }
topnを何回生成するかは自身の引数の数に依存しています。
compile_massign関数ではmemo->num_argsの回数だけtopnを生成しているのでnum_argsは自身の引数の数を保持していると予想できます。
topnのオペランドを計算するには3つの情報が必要でした。
- 自身の引数の数
- 自身よりあとにあるメソッド呼び出しに関して、それらの総数と引数の総数
- 左辺の値をdupするかどうか
compile_massign関数では(state.num_args - memo->argn) + memo->lhs_pos)で計算をしています。
VMのスタック上、右辺の値を起点にして、その上側の深さ(右辺の値を含む)を管理するのがmemo->lhs_posで、下側の深さ(右辺の値を含まない)を管理するのが(state.num_args - memo->argn)です。

上側の深さはさらに2つに分解することができます。
- 右辺の値をdupするかどうか
- 左辺のメソッド呼び出しのなかで何番目のメソッド呼び出しか

1つめの右辺のdupの有無はmultiple assignment全体の値をその後で使うことがあるかで変わります。
def m1 a[0], s.f = foo end def m2 a[0], s.f = foo nil end
m1の場合はmultiple assignmentの値が戻り値になるので右辺の値をdupする必要があります。
一方でm2の場合はmultiple assignmentの値は使われないのでdupは不要です。
dupの有無によってスタックの深さが一つ変わります。
dupによるスタックの深さの変化は左辺のメソッド呼び出し全体で共通なので、struct masgn_stateのlhs_levelで管理しています。
static int compile_massign(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const rb_multi_write_node_t *const node, int popped) { if (!popped || node->rest || !compile_massign_opt(iseq, ret, node->value, &node->lefts)) { struct masgn_state state; state.lhs_level = popped ? 0 : 1;
2つめの何番目のメソッド呼び出しかはexpandarrayで展開した要素がいくつスタックに残っているかに関係します。
expandarrayでは複数の代入に関する値を一度に展開するため、先に呼び出されるメソッドほどスタックが深くなります2。
この値は左辺のメソッド呼び出しごとに異なり、compile_massign0関数がcompile_massign_lhsを呼び出すときに計算して最後の引数として渡します。
static int compile_massign0(rb_iseq_t *iseq, LINK_ANCHOR *const pre, LINK_ANCHOR *const rhs, LINK_ANCHOR *const lhs, LINK_ANCHOR *const post, const NODE *const node, struct masgn_state *state, int popped) { const NODE *rhsn = RB_NODE_MULTI_WRITE(node)->value; const rb_node_list2_t *prel = &RB_NODE_MULTI_WRITE(node)->lefts; const NODE *restn = RB_NODE_MULTI_WRITE(node)->rest; const rb_node_list2_t *postl = &RB_NODE_MULTI_WRITE(node)->rights; int lhs_splat = (restn && NODE_NAMED_REST_P2(restn)) ? 1 : 0; int llen = (int)RB_NODE_LIST_LEN(prel); int lpos = 0; for (size_t i = 0; i < RB_NODE_LIST_LEN(prel); i++) { const NODE *lhsn = prel->nodes[i]; CHECK(compile_massign_lhs(iseq, pre, rhs, lhs, post, lhsn, state, (llen - lpos) + lhs_splat + state->lhs_level)); lpos++; }
下側の深さは最後のメソッド呼び出しの最後の引数を0とするindexで表現したいところです。 しかし左辺のノードをコンパイルしている時点では、スタック全体の深さはわかりません。
a[0], s.f = fooでいえば、a[0]のコンパイルをしている最中にs.fというメソッド呼び出しの有無やその引数の数はまだわかりません。
そこで左辺のノードのコンパイル時にはスタックの底からのindexをmemo->argnに保存しておいて、スタック全体のサイズがわかった時点でスタックトップからのindexを計算しなおします3。

state.num_args |
memo->argn |
memo->lhs_pos |
topn_arg |
|
|---|---|---|---|---|
a[0] |
2 + 1 = 3 | 1 | 1 + 2 = 3 | (3 - 1) + 3 = 5 |
s.f |
2 + 1 = 3 | 3 | 1 + 1 = 2 | (3 - 3) + 2 = 2 |
ネストした場合
次にmultiple assignmentがネストしたときについて(a0, (ary[0], s.f, a1), a2) = [1, [2, 3, 4], 5]というコードを例に考えてみましょう。
生成されるバイトコードは以下のとおりです。
# 左辺のメソッド呼び出しに関するセットアップ # 0000 putself ( 1)[Li] # 0001 opt_send_without_block <calldata!mid:ary, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0003 putobject_INT2FIX_0_ # 0004 putself # 0005 opt_send_without_block <calldata!mid:s, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 右辺を評価してdupする # 0007 putobject_INT2FIX_1_ # 0008 duparray [2, 3, 4] # 0010 putobject 5 # 0012 newarray 3 # 0014 dup # 外側の代入 # a0 = 1 # 0015 expandarray 3, 0 # 0018 setlocal_WC_0 a0@0 # 内側の代入 # ary[0] = 2 # 0020 expandarray 3, 0 # 0023 topn 7 # 0025 topn 7 # 0027 topn 2 # 0029 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] # 0031 pop # 0032 pop # s.f = 3 # 0033 topn 4 # 0035 swap # 0036 opt_send_without_block <calldata!mid:f=, argc:1, ARGS_SIMPLE> # 0038 pop # a1 = 4 # 0039 setlocal_WC_0 a1@1 # 再び外側の代入 # a2 = 5 # 0041 setlocal_WC_0 a2@2 # スタックの調整 # 0043 setn 3 # 0045 pop # 0046 pop # 0047 pop # 0048 leave
ネストしたmultiple assignmentの処理に移る前後、命令でいうと0020 expandarray 3, 0の前後におけるスタックの様子は次のようになります。

dupの有無を管理するのに使っていたstate->lhs_levelは最も内側のmultiple assignmentにおいて不変なスタックの深さを管理するのに使うことができます。
ネストしたmultiple assignmentの処理に移る瞬間にはexpandarray命令によってスタックトップのオブジェクトがスタックから取り除かれ、代わりにそのオブジェクトを展開した結果がスタックに積まれます4。
ネストしたmultiple assignmentを処理している間は、外側のmultiple assignmentの残りの部分に関してはスタックの要素が変化しません。
今回の例でいえば(ary[0], s.f, a1)を処理している間はスタックの5が操作されることはありません。
ということでmultiple assignmentのコンパイルがネストするときは、その時点のlhs_posを基準にstate->lhs_levelを更新することでtopn命令のオペランドを適切に計算することができます。
compile_massign_lhs関数のMultiTargetNodeのときにstate->lhs_levelを更新するのはそのためです。
static int compile_massign_lhs(rb_iseq_t *iseq, LINK_ANCHOR *const pre, LINK_ANCHOR *const rhs, LINK_ANCHOR *const lhs, LINK_ANCHOR *const post, const NODE *const node, struct masgn_state *state, int lhs_pos) { switch (nd_type(node)) { case RB_MULTI_TARGET_NODE: { DECL_ANCHOR(nest_rhs); INIT_ANCHOR(nest_rhs); DECL_ANCHOR(nest_lhs); INIT_ANCHOR(nest_lhs); int prev_level = state->lhs_level; bool prev_nested = state->nested; state->nested = 1; state->lhs_level = lhs_pos - 1; CHECK(compile_massign0(iseq, pre, nest_rhs, nest_lhs, post, node, state, 1)); state->lhs_level = prev_level; state->nested = prev_nested; ADD_SEQ(lhs, nest_rhs); ADD_SEQ(lhs, nest_lhs); break; }
小話2: バイトコードの生成とLINK_ANCHOR
小話の2つめとしてcompile.cにたびたびあらわれるANCHORについてみていきます。
ノードを扱う順番とバイトコードの順番が一致しないとき
compile.cは構文木をたどりながらバイトコードを生成していきます。 これらの順番が常に一致していればいいのですが、ノードの種類によっては一致しないことがあります。
multiple assignmentなどはそれらの順番が一致しない例です。 multiple assignmentのバイトコードをざっくりと順をおって分類すると以下のようになります。
これはノードを辿る順番とは一致しません。
- 左辺のノードを辿る
- 右辺のノードを辿る

これらの差を埋めるための仕組みがLINK_ANCHORです。
LINK_ANCHORの構造と操作
LINK_ELEMENTは前後のLINK_ELEMENTに対するポインターをもつリスト構造をしており、LINK_ANCHORはLINK_ELEMENTの実態と末尾のLINK_ELEMENTに対するポインターを持っています。
typedef struct iseq_link_element { enum { ISEQ_ELEMENT_ANCHOR, ISEQ_ELEMENT_LABEL, ISEQ_ELEMENT_INSN, ISEQ_ELEMENT_ADJUST, ISEQ_ELEMENT_TRACE, } type; struct iseq_link_element *next; struct iseq_link_element *prev; } LINK_ELEMENT; typedef struct iseq_link_anchor { LINK_ELEMENT anchor; LINK_ELEMENT *last; } LINK_ANCHOR;
ANCHORを初期化するためのマクロがDECL_ANCHOR(name)です。
このマクロはnameというLINK_ANCHOR型の変数を定義します。
この変数はtypeがISEQ_ELEMENT_ANCHORのLINK_ELEMENTで初期化されます。
#define DECL_ANCHOR(name) \ LINK_ANCHOR name[1] = {{{ISEQ_ELEMENT_ANCHOR,},&name[0].anchor}}
LINK_ELEMENTはINSNやLABELなど5つの種類があります。
ANCHORを除き、4種類それぞれに対応する構造体が定義されています。
typedef struct iseq_label_data { LINK_ELEMENT link; int label_no; int position; int sc_state; int sp; int refcnt; unsigned int set: 1; unsigned int rescued: 2; unsigned int unremovable: 1; } LABEL; typedef struct iseq_insn_data { LINK_ELEMENT link; enum ruby_vminsn_type insn_id; int operand_size; int sc_state; VALUE *operands; struct { int line_no; int node_id; rb_event_flag_t events; } insn_info; } INSN; typedef struct iseq_adjust_data { LINK_ELEMENT link; LABEL *label; int line_no; } ADJUST; typedef struct iseq_trace_data { LINK_ELEMENT link; rb_event_flag_t event; long data; } TRACE;
いずれの構造体も先頭にLINK_ELEMENT linkという構造体が定義されているためリストを作ることができます。

LINK_ANCHORの宣言と初期化はDECL_ANCHORとINIT_ANCHORというマクロで行います。
ANCHOR同士はADD_SEQというマクロで結合することができます。
compile_massign関数とANCHOR
compile_massign関数ではpre, rhs, lhs, postと4つのANCHORを宣言しています。
compile_massign0関数などではこれら4つのANCHORに適宜バイトコードを追加していきます。
最終的にはこれら4つのANCHORはpre, rhs, lhs, postの順番にretというANCHORに結合されます。
static int compile_massign(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const rb_multi_write_node_t *const node, int popped) { if (!popped || node->rest || !compile_massign_opt(iseq, ret, node->value, &node->lefts)) { struct masgn_state state; state.lhs_level = popped ? 0 : 1; state.nested = 0; state.num_args = 0; state.first_memo = NULL; state.last_memo = NULL; DECL_ANCHOR(pre); INIT_ANCHOR(pre); DECL_ANCHOR(rhs); INIT_ANCHOR(rhs); DECL_ANCHOR(lhs); INIT_ANCHOR(lhs); DECL_ANCHOR(post); INIT_ANCHOR(post); int ok = compile_massign0(iseq, pre, rhs, lhs, post, node, &state, popped); struct masgn_lhs_node *memo = state.first_memo, *tmp_memo; while (memo) { VALUE topn_arg = INT2FIX((state.num_args - memo->argn) + memo->lhs_pos); for (int i = 0; i < memo->num_args; i++) { INSERT_BEFORE_INSN1(memo->before_insn, nd_line(memo->line_node), nd_node_id(memo->line_node), topn, topn_arg); } tmp_memo = memo->next; free(memo); memo = tmp_memo; } CHECK(ok); ADD_SEQ(ret, pre); ADD_SEQ(ret, rhs); ADD_SEQ(ret, lhs); if (!popped && state.num_args >= 1) { /* make sure rhs array is returned before popping */ ADD_INSN1(ret, node, setn, INT2FIX(state.num_args)); } ADD_SEQ(ret, post); } return COMPILE_OK; }
a[0], b = [1, 2]を例にどのようにバイトコードが生成されていくかを確認して終わりにしましょう。
MultWriteNodeとcompile_massign関数
最初に呼び出されるのはcompile_massign関数で、対象のノードは代入全体を表すMultWriteNodeです。
compile_massign関数ではpre, rhs, lhs, postの4つのANCHORをつくり、compile_massign0関数を呼び出します。

MultWriteNodeとcompile_massign0関数
compile_massign0関数の対象ノードはcompile_massign関数と同じでMultWriteNodeです。
ここでは左辺のノードを一つずつcompile_massign_lhs関数に渡します。

a[0]とcompile_massign_lhs関数
左辺の最初のノードはa[0]に相当するIndexTargetNodeです。
以下の手順でコンパイルしていきます。
preに対してIndexTargetNodeをコンパイルするpreの末尾のsend命令を削除するlhsにtopnもしくはswapを追加する- 削除した
send命令をlhsに追加する - メソッド呼び出しの戻り値と右辺の要素をスタックから取り除くために
lhsにpop命令を追加する - レシーバーと引数をスタックから取り除くために
postにpop命令を追加する

メソッド呼び出しに関連するノードをコンパイルしたのでstruct masgn_lhs_nodeが新規に作られ、before_insnには0014 topn 2の命令がセットされます(図ではmemoという名前で書かれています)。
最終的なバイトコードでは0014 topn 2の前に2つのtopnが存在しています。
0010 topn 4 0012 topn 4 0014 topn 2
しかしこのtopnのオペランドの計算にはstate.num_argsが必要で、この値は左辺のすべてのノードを辿ってからでないと決定しません。
そこでa[0]を処理するタイミングでは0014 topn 2の直前に命令をあとで入れないといけないという情報をmemo->before_insnに書き込むようにし、あとで命令を追加するというアプローチをとっています。
bとcompile_massign_lhs関数
続いて左辺のbを対象にcompile_massign_lhs関数が呼びだされます。
LocalVariableTargetNodeをコンパイルして、setlocal命令のみをlhsに追記します。

compile_massign0関数に戻ってきたあと
左辺がコンパイルできたら右辺のコンパイルに移ります。
compile_massign0ではrhsに命令を追加していきます。
rhsに対して右辺のノードをコンパイルするrhsにdupを追加するrhsにexpandarrayを追加する

compile_massign関数に戻ってきたあと
左辺と右辺の両方を辿ったのちにcompile_massign関数に戻ってきました。
ここでは先ほど後回しにしたtopn命令の追加をして、4つのANCHORを結合します。
topn命令を追加する処理は以下のようになっており、INSERT_BEFORE_INSN1マクロと先ほど記録したmemo->before_insnを利用してtopn命令を追加していきます。
struct masgn_lhs_node *memo = state.first_memo, *tmp_memo; while (memo) { VALUE topn_arg = INT2FIX((state.num_args - memo->argn) + memo->lhs_pos); for (int i = 0; i < memo->num_args; i++) { INSERT_BEFORE_INSN1(memo->before_insn, nd_line(memo->line_node), nd_node_id(memo->line_node), topn, topn_arg); } tmp_memo = memo->next; free(memo); memo = tmp_memo; }
最後にANCHORをretに結合して完了です。
ADD_SEQ(ret, pre); ADD_SEQ(ret, rhs); ADD_SEQ(ret, lhs); if (!popped && state.num_args >= 1) { /* make sure rhs array is returned before popping */ ADD_INSN1(ret, node, setn, INT2FIX(state.num_args)); } ADD_SEQ(ret, post); } return COMPILE_OK;

まとめ
今日の成果です。
struct masgn_stateの役割と使われ方を理解した- ANCHORの役割と使われ方を理解した
次回は実装にもどります。