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


Ruby Parser開発日誌 (24-35) - parse.yが生成するノードを変える ー assignment with operator

35日目: a += 1

前回はary[1] += fooおよびs.f += 1といった形式の代入に対応しました。 今回はa += 1の形式の代入に取り組みたいとおもいます。

前回と同様に以下の3種類の形式を分けて考える必要があります。

  1. a += foo
  2. a ||= foo
  3. a &&= foo

ここで1つ面白いのは、ノードの書き換え前はa += fooがローカル変数への代入を表すNODE_LASGNを用いて表現されていることです。

# @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,6))*
# +- nd_vid: :a
# +- nd_value:
#     @ NODE_CALL (id: 4, line: 1, location: (1,0)-(1,6))
#     +- nd_mid: :+
#     +- nd_recv:
#     |   @ NODE_LVAR (id: 2, line: 1, location: (1,0)-(1,1))
#     |   +- nd_vid: :a
#     +- nd_args:
#         @ NODE_LIST (id: 3, line: 1, location: (1,5)-(1,6))
#         +- as.nd_alen: 1
#         +- nd_head:
#         |   @ NODE_INTEGER (id: 1, line: 1, location: (1,5)-(1,6))
#         |   +- val: 1
#         +- nd_next:
#             (null node)
a += 1

# @ NODE_LASGN (id: 5, line: 2, location: (2,0)-(2,5))*
# +- nd_vid: :b
# +- nd_value:
#     @ NODE_INTEGER (id: 6, line: 2, location: (2,4)-(2,5))
#     +- val: 2
b = 2

a += 1からa = a + 1を表すノードを生成しているので、通常のローカル変数への代入とおなじようにコンパイルすれば、期待したバイトコードが生成されるというわけです。

書き換え前後におけるノードをまとめると以下のようになります。

a += foo NODE_LASGN LocalVariableOperatorWriteNode
a ||= foo NODE_OP_ASGN_OR LocalVariableOrWriteNode
a &&= foo NODE_OP_ASGN_AND LocalVariableAndWriteNode

もう1つ注意することとして、LocalVariableOperatorWriteNodeという名前から想像できるように変数の種類に応じて、対応するOperatorWriteNode, OrWriteNode, AndWriteNodeが存在します。

  • a += foo: LocalVariableOperatorWriteNode
  • @a += foo: InstanceVariableOperatorWriteNode
  • @@a += foo: ClassVariableOperatorWriteNode
  • $a += foo: GlobalVariableOperatorWriteNode

parserを修正する

以上を意識しながらparse.yを変更していきます。 parse.yでは左辺の変数の種類によらず以下の生成規則が対応しています。 そのためnew_op_assign関数のなかで分岐して、適切なノードを生成するようにしましょう。

%rule op_asgn(rhs) <node>
                : var_lhs tOP_ASGN lex_ctxt rhs
                    {
                        $$ = new_op_assign(p, $var_lhs, $tOP_ASGN, $rhs, $lex_ctxt, &@$);
                    /*% ripper: opassign!($:1, $:2, $:4) %*/
                    }

new_op_assign関数ではまず&&=, ||=, それ以外で分岐をして、その中でさらに左辺の変数の種類に応じて分岐をすればよいでしょう。

static rb_node_t *
new_op_assign(struct parser_params *p, rb_node_t *lhs, ID op, rb_node_t *rhs, struct lex_context ctxt, const YYLTYPE *loc)
{
    rb_node_t *asgn;

    if (lhs) {
        if (op == tOROP) {
            switch (nd_type(lhs)) {
              case RB_LOCAL_VARIABLE_WRITE_NODE: {
                rb_local_variable_write_node_t *cast = (rb_local_variable_write_node_t *)lhs;
                asgn = NEW_RB_LOCAL_VARIABLE_OR_WRITE(cast->name, rhs, cast->depth, loc);
                break;
              }
              case RB_INSTANCE_VARIABLE_WRITE_NODE: {
                rb_instance_variable_write_node_t *cast = (rb_instance_variable_write_node_t *)lhs;
                asgn = NEW_RB_INSTANCE_VARIABLE_OR_WRITE(cast->name, rhs, loc);
                break;
              }
              ...
        }
        else if (op == tANDOP) {
            switch (nd_type(lhs)) {
              ...
            }
        }
        else {
            switch (nd_type(lhs)) {
              ...
            }
        }
    }
    else {
        asgn = NEW_ERROR(loc);
    }
    return asgn;
}

compilerを修正する

operatorに応じて3種類のノードがあるので、それぞれ見ていきましょう。

まずはOperatorWriteNode、つまりa += fooのときです。 このときa = a + foo相当のバイトコードが生成されます。

# `a + foo`に相当する部分
#
# 0000 getlocal_WC_0                          a@0                       (   1)[Li]
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]

# `a = ...`に相当する部分
#
# 0007 dup
# 0008 setlocal_WC_0                          a@0
# 0010 leave
a += foo

コンパイラでいうと変数の読み書きをする部分は変数の種類によって命令が変わり、+ fooの部分は変数の種類によらず共通です。 ここでは特に関数に切り出さず愚直にそれぞれのノードをコンパイルするようにします。

      case RB_LOCAL_VARIABLE_OPERATOR_WRITE_NODE: {
        rb_local_variable_operator_write_node_t *cast = (rb_local_variable_operator_write_node_t *)node;
        CHECK(compile_lvar(iseq, ret, node, cast->name));
        CHECK(COMPILE(ret, "op asgn value", cast->value));
        ADD_SEND_R(ret, node, cast->binary_operator, INT2FIX(1), NULL, INT2FIX(0), NULL);
        if (!popped) {
            ADD_INSN(ret, node, dup);
        }
        CHECK(compile_lasgn_lhs(iseq, ret, node, cast->name));
        break;
      }
      case RB_INSTANCE_VARIABLE_OPERATOR_WRITE_NODE: {
        rb_instance_variable_operator_write_node_t *cast = (rb_instance_variable_operator_write_node_t *)node;
        ADD_INSN2(ret, node, getinstancevariable, ID2SYM(cast->name), get_ivar_ic_value(iseq, cast->name));
        CHECK(COMPILE(ret, "op asgn value", cast->value));
        ADD_SEND_R(ret, node, cast->binary_operator, INT2FIX(1), NULL, INT2FIX(0), NULL);
        if (!popped) {
            ADD_INSN(ret, node, dup);
        }
        ADD_INSN2(ret, node, setinstancevariable, ID2SYM(cast->name), get_ivar_ic_value(iseq, cast->name));
        break;
      }

つぎにAndWriteNode、つまりa &&= fooの場合です。 このときはa = a && foo相当の命令が生成されます。

# 0000 getlocal_WC_0                          a@0                       (   1)[Li]
# 0002 dup
#
# `&&`なので`a`がfalsyのときは最後の命令へjumpする
#
# 0003 branchunless                           12
# 0005 pop
# 0006 putself
# 0007 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0009 dup
# 0010 setlocal_WC_0                          a@0
# 0012 leave
a &&= foo

いままではcompile_op_log関数を呼んでいたので、引数を調整しつつ引き続きcompile_op_log関数を呼ぶようにします。

      case RB_LOCAL_VARIABLE_AND_WRITE_NODE: {
        CHECK(compile_op_log(iseq, ret, node, RB_NODE_LOCAL_VARIABLE_AND_WRITE(node)->name, RB_NODE_LOCAL_VARIABLE_AND_WRITE(node)->value, popped, TRUE));
        break;
      }
      case RB_INSTANCE_VARIABLE_AND_WRITE_NODE: {
        CHECK(compile_op_log(iseq, ret, node, RB_NODE_INSTANCE_VARIABLE_AND_WRITE(node)->name, RB_NODE_INSTANCE_VARIABLE_AND_WRITE(node)->value, popped, TRUE));
        break;
      }

compile_op_log関数はおおまかに以下の4つのステップからなります。

  1. a(変数へのアクセス)をコンパイルする
  2. &&なのでbranchunlessを追加する
  3. fooコンパイルする
  4. a =(変数への代入)をコンパイルする
static int
compile_op_log(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, ID name, const NODE *const nd_value, int popped, bool op_and)
{
    switch (nd_type(node)) {
      case RB_LOCAL_VARIABLE_AND_WRITE_NODE:
        CHECK(compile_lvar(iseq, ret, node, name));
        break;
      case RB_INSTANCE_VARIABLE_AND_WRITE_NODE:
        ADD_INSN2(ret, node, getinstancevariable, ID2SYM(name), get_ivar_ic_value(iseq, name));
        break;
      ...
    }

    if (!popped) {
        ADD_INSN(ret, node, dup);
    }

    if (op_and) {
        ADD_INSNL(ret, node, branchunless, lfin);
    }
    else {
        ADD_INSNL(ret, node, branchif, lfin);
    }

    if (!popped) {
        ADD_INSN(ret, node, pop);
    }

    ADD_LABEL(ret, lassign);
    CHECK(COMPILE(ret, "NODE_OP_ASGN_AND/OR#nd_value", nd_value));
    if (!popped) {
        ADD_INSN(ret, node, dup);
    }

    switch (nd_type(node)) {
      case RB_LOCAL_VARIABLE_AND_WRITE_NODE:
        CHECK(compile_lasgn_lhs(iseq, ret, node, name));
        break;
      case RB_INSTANCE_VARIABLE_AND_WRITE_NODE:
        ADD_INSN2(ret, node, setinstancevariable, ID2SYM(name), get_ivar_ic_value(iseq, name));
        break;
      ...
    }

    ADD_LABEL(ret, lfin);
    return COMPILE_OK;
}

最後にOrWriteNode、つまりa ||= fooの場合です。 このときはa = a || foo相当の命令が生成されます。

# 0000 getlocal_WC_0                          a@0                       (   1)[Li]
# 0002 dup
#
# `||`なので`a`がtruthyのときは最後の命令へjumpする
#
# 0003 branchif                               12
# 0005 pop
# 0006 putself
# 0007 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0009 dup
# 0010 setlocal_WC_0                          a@0
# 0012 leave
a ||= foo

この場合もcompile_op_log関数に処理を任せるようにします。 &&の場合も||の場合も変数を読み書きする部分は変わらないので、大枠での処理は変わりません。

static int
compile_op_log(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, ID name, const NODE *const nd_value, int popped, bool op_and)
{
    switch (nd_type(node)) {
      case RB_LOCAL_VARIABLE_AND_WRITE_NODE:
      case RB_LOCAL_VARIABLE_OR_WRITE_NODE:
        CHECK(compile_lvar(iseq, ret, node, name));
        break;
      case RB_INSTANCE_VARIABLE_AND_WRITE_NODE:
      case RB_INSTANCE_VARIABLE_OR_WRITE_NODE:
        ADD_INSN2(ret, node, getinstancevariable, ID2SYM(name), get_ivar_ic_value(iseq, name));
        break;
      ...
    }

defined? ?

これでおしまいと言いたいところですが、実は||=のケースはすこしだけ特殊です。 左辺の変数の種類を変えながらバイトコードを見てみると、クラス変数とグローバル変数のケースではdefinedという命令をつかって変数の有無を確認しています。

# 0000 getlocal_WC_0                          a@0                       (   1)[Li]
# 0002 branchif                               9
# 0004 putself
# 0005 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0007 setlocal_WC_0                          a@0
a ||= foo

# 0009 getinstancevariable                    :@a, <is:0>               (   2)[Li]
# 0012 branchif                               20
# 0014 putself
# 0015 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0017 setinstancevariable                    :@a, <is:1>
@a ||= foo

# 0020 putnil                                                           (   3)[Li]
# 0021 defined                                class variable, :@@a, true
# 0025 branchunless                           32
# 0027 getclassvariable                       :@@a, <is:2>
# 0030 branchif                               38
# 0032 putself
# 0033 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0035 setclassvariable                       :@@a, <is:2>
@@a ||= foo

# 0038 putnil                                                           (   4)[Li]
# 0039 defined                                global-variable, :$a, true
# 0043 branchunless                           51
# 0045 getglobal                              :$a
# 0047 dup
# 0048 branchif                               57
# 0050 pop
# 0051 putself
# 0052 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0054 dup
# 0055 setglobal                              :$a
# 0057 leave
$a ||= foo

もともとのcompile_op_log関数の実装でもORかつインスタンス変数以外のときにdefined_expr関数を呼び出しています。

static int
compile_op_log(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped, const enum node_type type)
{
    const int line = nd_line(node);
    LABEL *lfin = NEW_LABEL(line);
    LABEL *lassign;

    if (type == NODE_OP_ASGN_OR && !nd_type_p(RNODE_OP_ASGN_OR(node)->nd_head, NODE_IVAR)) {
        LABEL *lfinish[2];
        lfinish[0] = lfin;
        lfinish[1] = 0;
        defined_expr(iseq, ret, RNODE_OP_ASGN_OR(node)->nd_head, lfinish, Qfalse, false);
        lassign = lfinish[1];
        if (!lassign) {
            lassign = NEW_LABEL(line);
        }
        ADD_INSNL(ret, node, branchunless, lassign);
    }
    else {
        lassign = NEW_LABEL(line);
    }

ちなみにローカル変数のケースではdefined命令が生成されていないように見えますが、これはバイトコードの最適化により消えているだけです。 debugモードをonにしてバイトコードを生成してみるとわかります。

# -- raw disasm--------
#   trace: 1
#   0000 putobject            true                                        (   8)
#   0002 branchunless         <L001>                                      (   8)
#   0004 getlocal             3, 0                                        (   8)
#   0007 dup                                                              (   8)
#   0008 branchif             <L000>                                      (   8)
#   0010 pop                                                              (   8)
# <L001> [sp: -1, unremovable: 0, refcnt: 1]
#   0011 putself                                                          (   8)
#   0012 send                 <calldata:foo, 0>, nil                      (   8)
#   0015 dup                                                              (   8)
#   0016 setlocal             3, 0                                        (   8)
# <L000> [sp: -1, unremovable: 0, refcnt: 1]
#   0019 leave                                                            (   8)
# ---------------------
# [compile step 3.1 (iseq_optimize)]
# -- raw disasm--------
#   trace: 1
#   0000 getlocal_WC_0        3                                           (   8)
#   0002 dup                                                              (   8)
#   0003 branchif             <L000>                                      (   8)
#   0005 pop                                                              (   8)
#   0006 putself                                                          (   8)
#   0007 opt_send_without_block <calldata:foo, 0>                         (   8)
#   0009 dup                                                              (   8)
#   0010 setlocal_WC_0        3                                           (   8)
# <L000> [sp: -1, unremovable: 0, refcnt: 1]
#   0012 leave                                                            (   8)
# ---------------------
a ||= foo

defined_exprによってputobject truebranchunlessが生成されますが、この場合は絶対にjumpしないので、2つまとめて削除されています。

defined命令の部分についていくつかの実装案が思い浮かびます。

  1. defined_expr関数でうまくClassVariableOrWriteNodeGlobalVariableOrWriteNodeをハンドリングする
  2. 呼び出し元のcompile_op_log関数でClassVariableWriteNodeGlobalVariableWriteNodeといった代入を表すノードを生成してdefined_expr関数に渡すようにする
  3. defined_expr関数を使わずにcompile_op_log関数内部で直接バイトコードを生成する

1番目の方法はdefined? @@a ||= fooのための実装とぶつかるので現実的ではありません。 2番目の方法はノードの作成にはノードのメモリ空間を管理している構造体を引き摺り回してくるなどの手間があります。

生成されるバイトコードがそこまで多くないことと、ノードの種類以外の要素で分岐しなくていいことを踏まえて3番目の実装案にします。

static int
compile_op_log(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, ID name, const NODE *const nd_value, int popped, bool op_and)
{
    const int line = nd_line(node);
    LABEL *lfin = NEW_LABEL(line);
    LABEL *lassign = NEW_LABEL(line);

    switch (nd_type(node)) {
      case RB_CLASS_VARIABLE_OR_WRITE_NODE:
        ADD_INSN(ret, node, putnil);
        ADD_INSN3(ret, node, defined, INT2FIX(DEFINED_CVAR), ID2SYM(name), Qtrue);
        ADD_INSNL(ret, node, branchunless, lassign);
        break;
      case RB_GLOBAL_VARIABLE_OR_WRITE_NODE:
        ADD_INSN(ret, node, putnil);
        ADD_INSN3(ret, node, defined, INT2FIX(DEFINED_GVAR), ID2SYM(name), Qtrue);
        ADD_INSNL(ret, node, branchunless, lassign);
        break;
    }
    ...

ここまでを踏まえて、簡単なコード例で動作確認してみます。

a = 0

a += 1
p a
#=> 1

a ||= 2
p a
#=> 1

a &&= 3
p a
#=> 3

$a = 0

$a += 1
p $a
#=> 1

$a ||= 2
p $a
#=> 1

$a &&= 3
p $a
#=> 3

良さそうですね。

まとめ

今日の成果です。

  • assignment with operator (a += foo)に対応した



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

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