以下の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/04/163105より取得しました。


Ruby Parser開発日誌 (24-14) - parse.yが生成するノードを変える ー multiple assignment (おまけ)

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つの構造体

この構造体については導入時のコミットメッセージおよびコード内のコメントが詳細に書かれていますので、興味がある場合にはそちらもあわせて見るとよいでしょう。

github.com

まずは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_nodenextというフィールドに自身の次の要素をもつリスト構造になっていることがわかります。 またstruct masgn_statefirst_memolast_memoというフィールドをもっていることから、struct masgn_lhs_nodeのリスト構造の先頭と末尾を保持していることがわかります。

図にするとざっくりこんな感じです。

struct masgn_lhs_nodeを操作する

次にstruct masgn_lhs_nodestruct masgn_stateを操作する関数をみてみましょう。

add_masgn_lhs_nodestruct 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関数でCallTargetNodeIndexTargetNodeを処理するときに呼ばれます1。 ということはstruct masgn_lhs_nodeの1つ1つはmultiple assignmentの左辺におけるメソッド呼び出しの項に対応していることがわかります。

struct masgn_stateを操作する

struct masgn_statecompile_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処理をするときに次のような情報が必要だとわかります。

  • メソッド呼び出し
    1. topnの回数の計算
      1. 左辺に書かれた引数の数(a[0]であれば1)
    2. topnオペランドの計算
      1. 自身の引数の数
      2. 自身よりあとにあるメソッド呼び出しに関して、それらの総数と引数の総数
      3. 左辺の値をdupするかどうか
      4. 自身よりあとにあるメソッド呼び出しに関して、それらの総数
  • clean up
    1. setnオペランドの計算
      1. 左辺のメソッド呼び出し全部のレシーバーと引数の数の合計
    2. popの回数の計算
      1. 左辺のメソッド呼び出し全部のレシーバーと引数の数の合計

いくつか補足をしておくと、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つの情報が必要でした。

  1. 自身の引数の数
  2. 自身よりあとにあるメソッド呼び出しに関して、それらの総数と引数の総数
  3. 左辺の値をdupするかどうか

compile_massign関数では(state.num_args - memo->argn) + memo->lhs_pos)で計算をしています。

VMのスタック上、右辺の値を起点にして、その上側の深さ(右辺の値を含む)を管理するのがmemo->lhs_posで、下側の深さ(右辺の値を含まない)を管理するのが(state.num_args - memo->argn)です。

上側の深さはさらに2つに分解することができます。

  1. 右辺の値をdupするかどうか
  2. 左辺のメソッド呼び出しのなかで何番目のメソッド呼び出しか

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_statelhs_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つめとしてcompile.cにたびたびあらわれるANCHORについてみていきます。

ノードを扱う順番とバイトコードの順番が一致しないとき

compile.cは構文木をたどりながらバイトコードを生成していきます。 これらの順番が常に一致していればいいのですが、ノードの種類によっては一致しないことがあります。

multiple assignmentなどはそれらの順番が一致しない例です。 multiple assignmentのバイトコードをざっくりと順をおって分類すると以下のようになります。

  1. 左辺のうちメソッド呼び出しのレシーバーと引数に関する部分のバイトコード
  2. 右辺の評価をするバイトコード
  3. 左辺のメソッド呼び出しをするバイトコード

これはノードを辿る順番とは一致しません。

  1. 左辺のノードを辿る
  2. 右辺のノードを辿る

これらの差を埋めるための仕組みがLINK_ANCHORです。

LINK_ELEMENTは前後のLINK_ELEMENTに対するポインターをもつリスト構造をしており、LINK_ANCHORLINK_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型の変数を定義します。 この変数はtypeISEQ_ELEMENT_ANCHORLINK_ELEMENTで初期化されます。

#define DECL_ANCHOR(name) \
    LINK_ANCHOR name[1] = {{{ISEQ_ELEMENT_ANCHOR,},&name[0].anchor}}

LINK_ELEMENTINSNLABELなど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_ANCHORINIT_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]を例にどのようにバイトコードが生成されていくかを確認して終わりにしましょう。

MultWriteNodecompile_massign関数

最初に呼び出されるのはcompile_massign関数で、対象のノードは代入全体を表すMultWriteNodeです。 compile_massign関数ではpre, rhs, lhs, postの4つのANCHORをつくり、compile_massign0関数を呼び出します。

MultWriteNodecompile_massign0関数

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

a[0]compile_massign_lhs関数

左辺の最初のノードはa[0]に相当するIndexTargetNodeです。

以下の手順でコンパイルしていきます。

  1. preに対してIndexTargetNodeコンパイルする
  2. preの末尾のsend命令を削除する
  3. lhstopnもしくはswapを追加する
  4. 削除したsend命令をlhsに追加する
  5. メソッド呼び出しの戻り値と右辺の要素をスタックから取り除くためにlhspop命令を追加する
  6. レシーバーと引数をスタックから取り除くためにpostpop命令を追加する

メソッド呼び出しに関連するノードをコンパイルしたので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に書き込むようにし、あとで命令を追加するというアプローチをとっています。

bcompile_massign_lhs関数

続いて左辺のbを対象にcompile_massign_lhs関数が呼びだされます。 LocalVariableTargetNodeコンパイルして、setlocal命令のみをlhsに追記します。

compile_massign0関数に戻ってきたあと

左辺がコンパイルできたら右辺のコンパイルに移ります。 compile_massign0ではrhsに命令を追加していきます。

  1. rhsに対して右辺のノードをコンパイルする
  2. rhsdupを追加する
  3. rhsexpandarrayを追加する

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の役割と使われ方を理解した

次回は実装にもどります。


  1. 実際には定数の代入のときにもadd_masgn_lhs_node関数が呼び出されますが、まだ対応していないので割愛します。
  2. expandarray命令によってスタックにつまれるオブジェクトの数は、メソッド呼び出しか変数への代入かを問わず一定です。
  3. スタックのサイズを基準とした補数表現とでもいいましょうか。
  4. 対象が配列出ない場合や、展開した要素が足らない場合にはその分だけスタックにnilが積まれます。そのためスタックの深さはexpandarrayする対象によらず一定になります。



以上の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/04/163105より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14