16日目: 定数の参照と代入
しばらくmultiple assignmentの実装が続いたので、今回はちょっと目先を変えて定数の参照と代入をやっていきます。
ノードを眺める
いつものように書き換え前後のノードの確認からはじめましょう。
| 書き換え前 | 書き換え後 | |
|---|---|---|
A, A::BのA |
NODE_CONST | ConstantReadNode |
::A, ::A::Bの::A |
NODE_COLON3 | ConstantPathNode |
A::B, ::A::Bの::B |
NODE_COLON2 | ConstantPathNode |
一番左の定数は::の有無でノードが変わりますが、それ以降の定数は常にNODE_COLON2(書き換え後はConstantPathNode)になるという構造のようです。
図にすると以下のようになっており、書き換えの前後を問わず最も右の定数が頂点にきて、そのnd_head(もしくはparent)フィールドに左側のノードが設定されます。

NODE_COLON3とNODE_COLON2はConstantPathNodeに統合されるので、parentフィールドの有無を考慮してマクロを書き直せばよいでしょう。
-#define NEW_COLON2(c,i,loc,d_loc,n_loc) (NODE *)rb_node_colon2_new(p,c,i,loc,d_loc,n_loc) -#define NEW_COLON3(i,loc,d_loc,n_loc) (NODE *)rb_node_colon3_new(p,i,loc,d_loc,n_loc) +// #define NEW_COLON2(c,i,loc,d_loc,n_loc) (NODE *)rb_node_colon2_new(p,c,i,loc,d_loc,n_loc) +// #define NEW_COLON3(i,loc,d_loc,n_loc) (NODE *)rb_node_colon3_new(p,i,loc,d_loc,n_loc) +#define NEW_CONSTANT_PATH(c,i,loc,d_loc,n_loc) rb_new_constant_path(p,c,i,loc,d_loc,n_loc) +#define NEW_COLON2(c,i,loc,d_loc,n_loc) NEW_CONSTANT_PATH(c,i,loc,d_loc,n_loc) +#define NEW_COLON3(i,loc,d_loc,n_loc) NEW_CONSTANT_PATH(0,i,loc,d_loc,n_loc)
これだけでうまく書き換えができているようです。
$ ./miniruby --parser=parse.y --dump=p -e '::A::B'
+-- @ ConstantPathNode (location: (1,0)-(1,6))*
+-- parent:
| @ ConstantPathNode (location: (1,0)-(1,3))
| +-- parent: nil
| +-- name: :A
+-- name: :B
$ ./miniruby --parser=parse.y --dump=p -e 'A::B'
+-- @ ConstantPathNode (location: (1,0)-(1,4))*
+-- parent:
| @ ConstantReadNode (location: (1,0)-(1,1))
| +-- name: :A
+-- name: :B
ほんと...?
定数の参照をコンパイルする
一発で通ったので気をよくしてコンパイルをしていきましょう。
まずはバイトコードの確認です。
# 0000 opt_getconstant_path <ic:0 A> ( 1)[Li] # 0002 leave A # 0000 opt_getconstant_path <ic:0 ::A> ( 1)[Li] # 0002 leave ::A # 0000 opt_getconstant_path <ic:0 A::B> ( 1)[Li] # 0002 leave A::B # 0000 opt_getconstant_path <ic:0 ::A::B> ( 1)[Li] # 0002 leave ::A::B
どれも最適化されている...
opt_getconstant_pathはoptimizedな命令のことでしょう。
最適化されていないものを見たいときは変数やメソッド呼び出しを挟んでみましょう。
# 0000 putself ( 1)[Li] # 0001 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0003 putobject false # 0005 getconstant :B # 0007 opt_send_without_block <calldata!mid:c, argc:0, ARGS_SIMPLE> # 0009 putobject false # 0011 getconstant :D # 0013 leave a::B::c::D
よし(よしとは?)。
getconstant命令はスタックの上から2つの値と自身のオペランド1つを使って定数を取得する命令です。
a::Bの部分でいえば、aメソッドの戻り値とフラグ(false)と:Bを使ってaの戻り値から:Bを検索して返します。
一方でopt_getconstant_path命令はオペランドのICを使って定数を取得する命令です。
ICは以下のように定義された構造体に対するポインタです。
const ID *segmentsとしてidの配列を持っています。
opt_getconstant_path命令を生成するときにcompile.cでsegmentsを計算します。
/* inline cache */ typedef struct iseq_inline_constant_cache *IC; struct iseq_inline_constant_cache { struct iseq_inline_constant_cache_entry *entry; /** * A null-terminated list of ids, used to represent a constant's path * idNULL is used to represent the :: prefix, and 0 is used to donate the end * of the list. * * For example * FOO {rb_intern("FOO"), 0} * FOO::BAR {rb_intern("FOO"), rb_intern("BAR"), 0} * ::FOO {idNULL, rb_intern("FOO"), 0} * ::FOO::BAR {idNULL, rb_intern("FOO"), rb_intern("BAR"), 0} */ const ID *segments; };
バイトコードを確認したのでcompile.cを書き換えていきましょう。 書き換える前は3つのノードがあったので、compile.cでもそれら3つのノードに対してコンパイル処理が書かれているはずです。
case NODE_CONST:{ debugi("nd_vid", RNODE_CONST(node)->nd_vid); if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) { body->ic_size++; VALUE segments = rb_ary_new_from_args(1, ID2SYM(RNODE_CONST(node)->nd_vid)); RB_OBJ_SET_FROZEN_SHAREABLE(segments); ADD_INSN1(ret, node, opt_getconstant_path, segments); RB_OBJ_WRITTEN(iseq, Qundef, segments); } else { ADD_INSN(ret, node, putnil); ADD_INSN1(ret, node, putobject, Qtrue); ADD_INSN1(ret, node, getconstant, ID2SYM(RNODE_CONST(node)->nd_vid)); } if (popped) { ADD_INSN(ret, node, pop); } break; } case NODE_COLON2: CHECK(compile_colon2(iseq, ret, node, popped)); break; case NODE_COLON3: CHECK(compile_colon3(iseq, ret, node, popped)); break;
NODE_COLON2とNODE_COLON3についてはそれぞれ専用の関数を呼び出しているので、そちらをみます。
static int compile_colon2(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { if (rb_is_const_id(RNODE_COLON2(node)->nd_mid)) { /* constant */ VALUE segments; if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache && (segments = collect_const_segments(iseq, node))) { ISEQ_BODY(iseq)->ic_size++; ADD_INSN1(ret, node, opt_getconstant_path, segments); RB_OBJ_WRITTEN(iseq, Qundef, segments); } else { /* constant */ DECL_ANCHOR(pref); DECL_ANCHOR(body); INIT_ANCHOR(pref); INIT_ANCHOR(body); CHECK(compile_const_prefix(iseq, node, pref, body)); if (LIST_INSN_SIZE_ZERO(pref)) { ADD_INSN(ret, node, putnil); ADD_SEQ(ret, body); } else { ADD_SEQ(ret, pref); ADD_SEQ(ret, body); } } } else { /* function call */ ADD_CALL_RECEIVER(ret, node); CHECK(COMPILE(ret, "colon2#nd_head", RNODE_COLON2(node)->nd_head)); ADD_CALL(ret, node, RNODE_COLON2(node)->nd_mid, INT2FIX(1)); } if (popped) { ADD_INSN(ret, node, pop); } return COMPILE_OK; } static int compile_colon3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { debugi("colon3#nd_mid", RNODE_COLON3(node)->nd_mid); /* add cache insn */ if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) { ISEQ_BODY(iseq)->ic_size++; VALUE segments = rb_ary_new_from_args(2, ID2SYM(idNULL), ID2SYM(RNODE_COLON3(node)->nd_mid)); RB_OBJ_SET_FROZEN_SHAREABLE(segments); ADD_INSN1(ret, node, opt_getconstant_path, segments); RB_OBJ_WRITTEN(iseq, Qundef, segments); } else { ADD_INSN1(ret, node, putobject, rb_cObject); ADD_INSN1(ret, node, putobject, Qtrue); ADD_INSN1(ret, node, getconstant, ID2SYM(RNODE_COLON3(node)->nd_mid)); } if (popped) { ADD_INSN(ret, node, pop); } return COMPILE_OK; }
3種類のノードの全てのケースでISEQ_COMPILE_DATA(iseq)->option->inline_const_cacheをチェックして分岐しています。
inline_const_cacheが無効になっている場合にinline cacheを使うわけにもいかないので、それはそう。
NODE_CONSTとNODE_COLON3ではinline cacheが有効かどうかだけをチェックするのに対して、NODE_COLON2ではcollect_const_segments関数の戻り値もチェックしています。
collect_const_segments関数はidの配列であるsegmentsを構築しますが、途中でNODE_CONST, NODE_COLON2, NODE_COLON3以外のノードを見つけた場合はfalseを返して最適化ではないほうのパスに分岐させます1。
static VALUE collect_const_segments(rb_iseq_t *iseq, const NODE *node) { VALUE arr = rb_ary_new(); for (;;) { switch (nd_type(node)) { case NODE_CONST: rb_ary_unshift(arr, ID2SYM(RNODE_CONST(node)->nd_vid)); RB_OBJ_SET_SHAREABLE(arr); return arr; case NODE_COLON3: rb_ary_unshift(arr, ID2SYM(RNODE_COLON3(node)->nd_mid)); rb_ary_unshift(arr, ID2SYM(idNULL)); RB_OBJ_SET_SHAREABLE(arr); return arr; case NODE_COLON2: rb_ary_unshift(arr, ID2SYM(RNODE_COLON2(node)->nd_mid)); node = RNODE_COLON2(node)->nd_head; break; default: return Qfalse; } } }
NODE_CONSTとNODE_COLON3の場合はそれ以上左側に要素がないことが文法上決定しているため、collect_const_segments関数で行っているようなチェックはとくに必要ありません。
余談ですが、compile_colon2関数の最初にnd_midの内容で定数かメソッド呼び出しか分岐している箇所がありますが、これってメソッド呼び出しになるケースあるんですかね。
まぁ今回は気にせず進みましょう。
基本的には構造体の変更に合わせてマクロなどを修正するいつものやつをやっていきます。
NODE_COLON2とNODE_COLON3は1つのノードに統合されましたが、parentフィールドの有無で判別できるので既存の処理の流れなどには手をつけないようにparentの有無で分岐するようにしました。
+ case RB_CONSTANT_PATH_NODE: { + if (RB_NODE_CONSTANT_PATH(node)->parent) { + CHECK(compile_colon2(iseq, ret, node, popped)); + } + else { + CHECK(compile_colon3(iseq, ret, node, popped)); + } + break; + } +
いい感じにコンパイルできていそうです。
$ ./miniruby --parser=parse.y --dump=i -e 'A; B::C; ::D; ::D::E; f::G' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,26)> 0000 opt_getconstant_path <ic:0 A> ( 1)[Li] 0002 pop 0003 opt_getconstant_path <ic:1 B::C> 0005 pop 0006 opt_getconstant_path <ic:2 ::D> 0008 pop 0009 opt_getconstant_path <ic:3 ::D::E> 0011 pop 0012 putself 0013 opt_send_without_block <calldata!mid:f, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0015 putobject false 0017 getconstant :G 0019 leave
定数の代入を書き換える
順調なので定数に対する代入もやってしまいましょう。
書き換え前のノードは以下のようになっており、定数の代入にはNODE_CDECLというノードを使っています。
A = 0のように左辺の定数がネストしていないもので、かつ::が使われていないときはnd_vidフィールドに、それ以外のケースではnd_elseというフィールドに左辺のノード(もしくは定数を示すid)が入っています。
A = 0 # | @ NODE_CDECL (id: 0, line: 1, location: (1,0)-(1,5))* # | +- nd_vid: :A # | +- nd_else: not used # | +- shareability: none # | +- nd_value: # | @ NODE_INTEGER (id: 1, line: 1, location: (1,4)-(1,5)) # | +- val: 0 B::C = 1 # | @ NODE_CDECL (id: 4, line: 1, location: (1,7)-(1,15))* # | +- nd_vid: 0 (see extension field) # | +- nd_else: # | | @ NODE_COLON2 (id: 3, line: 1, location: (1,7)-(1,11)) # | | +- nd_mid: :C # | | +- nd_head: # | | | @ NODE_CONST (id: 2, line: 1, location: (1,7)-(1,8)) # | | | +- nd_vid: :B # | +- nd_value: # | @ NODE_INTEGER (id: 5, line: 1, location: (1,14)-(1,15)) # | +- val: 1 ::D = 2 # | @ NODE_CDECL (id: 9, line: 1, location: (1,17)-(1,24))* # | +- nd_vid: 0 (see extension field) # | +- nd_else: # | | @ NODE_COLON3 (id: 8, line: 1, location: (1,17)-(1,20)) # | | +- nd_mid: :D # | +- nd_value: # | @ NODE_INTEGER (id: 10, line: 1, location: (1,23)-(1,24)) # | +- val: 2 E::F = 3 # | @ NODE_CDECL (id: 14, line: 1, location: (1,26)-(1,34))* # | +- nd_vid: 0 (see extension field) # | +- nd_else: # | | @ NODE_COLON2 (id: 13, line: 1, location: (1,26)-(1,30)) # | | +- nd_mid: :F # | | +- nd_head: # | | | @ NODE_CONST (id: 12, line: 1, location: (1,26)-(1,27)) # | | | +- nd_vid: :E # | +- nd_value: # | @ NODE_INTEGER (id: 15, line: 1, location: (1,33)-(1,34)) # | +- val: 3 g::H = 4 # @ NODE_CDECL (id: 19, line: 1, location: (1,36)-(1,44))* # +- nd_vid: 0 (see extension field) # +- nd_else: # | @ NODE_COLON2 (id: 18, line: 1, location: (1,36)-(1,40)) # | +- nd_mid: :H # | +- nd_head: # | | @ NODE_VCALL (id: 17, line: 1, location: (1,36)-(1,37)) # | | +- nd_mid: :g # +- nd_value: # @ NODE_INTEGER (id: 20, line: 1, location: (1,43)-(1,44)) # +- val: 4
書き換え後はA = 0のケースはConstantWriteNodeで、それ以外はConstantPathWriteNodeで表現します。
NODE_CDECLのnd_vidフィールドとnd_elseフィールドを使い分けて表現していたものが、それぞれ異なるノードで表現するようになるということです。
A = 0 # +-- @ ConstantWriteNode (location: (1,0)-(1,5)) # | +-- name: :A # | +-- name_loc: (1,0)-(1,1) = "A" # | +-- value: # | | @ IntegerNode (location: (1,4)-(1,5)) # | | +-- IntegerBaseFlags: decimal # | | +-- value: 0 B::C = 1 # +-- @ ConstantPathWriteNode (location: (1,7)-(1,15)) # | +-- target: # | | @ ConstantPathNode (location: (1,7)-(1,11)) # | | +-- parent: # | | | @ ConstantReadNode (location: (1,7)-(1,8)) # | | | +-- name: :B # | | +-- name: :C # | +-- value: # | @ IntegerNode (location: (1,14)-(1,15)) # | +-- IntegerBaseFlags: decimal # | +-- value: 1 ::D = 2 # +-- @ ConstantPathWriteNode (location: (1,17)-(1,24)) # | +-- target: # | | @ ConstantPathNode (location: (1,17)-(1,20)) # | | +-- parent: nil # | | +-- name: :D # | +-- value: # | @ IntegerNode (location: (1,23)-(1,24)) # | +-- IntegerBaseFlags: decimal # | +-- value: 2 E::F = 3 # +-- @ ConstantPathWriteNode (location: (1,26)-(1,34)) # | +-- target: # | | @ ConstantPathNode (location: (1,26)-(1,30)) # | | +-- parent: # | | | @ ConstantReadNode (location: (1,26)-(1,27)) # | | | +-- name: :E # | | +-- name: :F # | +-- value: # | @ IntegerNode (location: (1,33)-(1,34)) # | +-- IntegerBaseFlags: decimal # | +-- value: 3 g::H = 4 # +-- @ ConstantPathWriteNode (location: (1,36)-(1,44)) # +-- target: # | @ ConstantPathNode (location: (1,36)-(1,40)) # | +-- parent: # | | @ CallNode (location: (1,36)-(1,37)) # | | +-- CallNodeFlags: variable_call, ignore_visibility # | | +-- receiver: nil # | | +-- call_operator_loc: nil # | | +-- name: :g # | | +-- arguments: nil # | | +-- block: nil # | +-- name: :H # +-- value: # @ IntegerNode (location: (1,43)-(1,44)) # +-- IntegerBaseFlags: decimal # +-- value: 4
parserのほうはとくに目新しいこともないのでサクッと実装して、コンパイラをみていきます。 参照のときはいままで2つのノードで表現したていたものを1つのノードで表現するようになったのに対して、代入はいままで1つのノードで表現していたものが文法に応じて2つの異なるノードで表現されるようになります。
ということでノードのフィールドの値による分岐から、ノードの種類による分岐に書き換えればいいでしょう。
// Before case NODE_CDECL:{ // nd_vidの有無で大きく分岐していた if (RNODE_CDECL(node)->nd_vid) { CHECK(compile_shareable_constant_value(iseq, ret, RNODE_CDECL(node)->shareability, node, RNODE_CDECL(node)->nd_value)); if (!popped) { ADD_INSN(ret, node, dup); } ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_CONST_BASE)); ADD_INSN1(ret, node, setconstant, ID2SYM(RNODE_CDECL(node)->nd_vid)); } else { compile_cpath(ret, iseq, RNODE_CDECL(node)->nd_else); CHECK(compile_shareable_constant_value(iseq, ret, RNODE_CDECL(node)->shareability, node, RNODE_CDECL(node)->nd_value)); ADD_INSN(ret, node, swap); if (!popped) { ADD_INSN1(ret, node, topn, INT2FIX(1)); ADD_INSN(ret, node, swap); } ADD_INSN1(ret, node, setconstant, ID2SYM(get_node_colon_nd_mid(RNODE_CDECL(node)->nd_else))); } break; } // After // ノードのタイプで分岐するようになる case RB_CONSTANT_WRITE_NODE: { CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_WRITE(node)->value)); if (!popped) { ADD_INSN(ret, node, dup); } ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_CONST_BASE)); ADD_INSN1(ret, node, setconstant, ID2SYM(RB_NODE_CONSTANT_WRITE(node)->name)); break; } case RB_CONSTANT_PATH_WRITE_NODE: { compile_cpath(ret, iseq, RB_NODE_CONSTANT_PATH_WRITE(node)->target); CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_PATH_WRITE(node)->value)); ADD_INSN(ret, node, swap); if (!popped) { ADD_INSN1(ret, node, topn, INT2FIX(1)); ADD_INSN(ret, node, swap); } ADD_INSN1(ret, node, setconstant, ID2SYM(RB_NODE_CONSTANT_PATH_WRITE(node)->target->name)); break; }
いろいろなパターンの左辺をコンパイルしてみましょう。 よさそうですね。
$ ./miniruby --parser=parse.y --dump=i -e 'A = :a; B::C = :b; ::D = :c; E::F = :d; g::H = :e' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,49)> 0000 putobject :a ( 1)[Li] 0002 putspecialobject 3 0004 setconstant :A 0006 opt_getconstant_path <ic:0 B> 0008 putobject :b 0010 swap 0011 setconstant :C 0013 putobject Object 0015 putobject :c 0017 swap 0018 setconstant :D 0020 opt_getconstant_path <ic:1 E> 0022 putobject :d 0024 swap 0025 setconstant :F 0027 putself 0028 opt_send_without_block <calldata!mid:g, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0030 putobject :e 0032 swap 0033 topn 1 0035 swap 0036 setconstant :H 0038 leave
実は1つ問題がありまして...
さてここで一つ問題が発生しました。
compile_shareable_constant_value関数の第三引数に注目してください。
// Before CHECK(compile_shareable_constant_value(iseq, ret, RNODE_CDECL(node)->shareability, node, RNODE_CDECL(node)->nd_value)); // After CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_WRITE(node)->value)); CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_PATH_WRITE(node)->value));
おわかりいただけたでしょうか。
ConstantWriteNodeもConstantPathWriteNodeもshareable_constant_valueマジックコメントに関する情報を持っていないのです。
ではどうしているかというとConstantWriteNodeをラップするShareableConstantNodeというのがマジックコメントの値によっては出現します。
# shareable_constant_value: experimental_everything C1 = r1 +-- @ ShareableConstantNode (location: (2,0)-(2,7)) # +-- ShareableConstantNodeFlags: experimental_everything # +-- write: # @ ConstantWriteNode (location: (2,0)-(2,7)) # +-- name: :C1 # +-- value: # | @ CallNode (location: (2,5)-(2,7)) # | +-- CallNodeFlags: variable_call, ignore_visibility # | +-- receiver: nil # | +-- call_operator_loc: nil # | +-- name: :r1 # | +-- arguments: nil # | +-- block: nil
どうしてこうなった...
この辺の対応は次回にしましょう。 ということで今日はここまで。
まとめ
今日の成果です。
- 定数の参照と代入に対応した
次回はShareableConstantNodeの対応と、可能ならmultiple assignmentの左辺に定数があるケースの対応をやっていきます。