2日目: 変数と代入
1日目にいくつかのリテラルとif/unlessのノードを移行しました。 数値リテラルや文字列リテラルなどほかのリテラルや、whileなどの他の制御構文に関するノードを置き換えてもいいのですが、2日目はちょっとそれらとは違う方向のノードを置き換えていきたいと思います。 ということで今回は変数と代入をやっていきたいと思います。
どの変数が簡単か
はじめに変数の種類を見ておきましょう。
ノードの一覧を眺めてもいいのですが、今回はparse.yにあるgettableという関数をみましょう。
static NODE* gettable(struct parser_params *p, ID id, const YYLTYPE *loc) { ... switch (id_type(id)) { case ID_LOCAL: if (dyna_in_block(p) && dvar_defined_ref(p, id, &vidp)) { if (NUMPARAM_ID_P(id) && (numparam_nested_p(p) || it_used_p(p))) return 0; if (vidp) *vidp |= LVAR_USED; node = NEW_DVAR(id, loc); return node; } if (local_id_ref(p, id, &vidp)) { if (vidp) *vidp |= LVAR_USED; node = NEW_LVAR(id, loc); return node; } if (dyna_in_block(p) && NUMPARAM_ID_P(id) && parser_numbered_param(p, NUMPARAM_ID_TO_IDX(id))) { if (numparam_nested_p(p) || it_used_p(p)) return 0; node = NEW_DVAR(id, loc); struct local_vars *local = p->lvtbl; if (!local->numparam.current) local->numparam.current = node; return node; } # if WARN_PAST_SCOPE if (!p->ctxt.in_defined && RTEST(ruby_verbose) && past_dvar_p(p, id)) { rb_warning1("possible reference to past scope - %"PRIsWARN, rb_id2str(id)); } # endif /* method call without arguments */ if (dyna_in_block(p) && id == rb_intern("it") && !(DVARS_TERMINAL_P(p->lvtbl->args) || DVARS_TERMINAL_P(p->lvtbl->args->prev))) { if (numparam_used_p(p)) return 0; if (p->max_numparam == ORDINAL_PARAM) { compile_error(p, "ordinary parameter is defined"); return 0; } if (!p->it_id) { p->it_id = internal_id(p); vtable_add(p->lvtbl->args, p->it_id); } NODE *node = NEW_DVAR(p->it_id, loc); if (!p->lvtbl->it) p->lvtbl->it = node; return node; } return NEW_VCALL(id, loc); case ID_GLOBAL: return NEW_GVAR(id, loc); case ID_INSTANCE: return NEW_IVAR(id, loc); case ID_CONST: return NEW_CONST(id, loc); case ID_CLASS: return NEW_CVAR(id, loc); } compile_error(p, "identifier %"PRIsVALUE" is not valid to get", rb_id2str(id)); return 0; }
ざっと以下のような変数があることがわかりますね。
定数も変数といえば変数か...という気持ちになったところで、どの変数が簡単かを考えてみます。
普段一番よく使うしローカル変数が一番簡単なはず!と思ってはいけません。
ちゃんとgettableのなかの分岐の長さをみましたか? という冗談はさておき1、それぞれをコンパイルした結果を観察してみましょう。
$ ruby --parser=parse.y --dump=i -e 'lv = 1; lv' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] lv@0 0000 putobject_INT2FIX_1_ ( 1)[Li] 0001 setlocal_WC_0 lv@0 0003 getlocal_WC_0 lv@0 0005 leave $ ruby --parser=parse.y --dump=i -e '$gv' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,3)> 0000 getglobal :$gv ( 1)[Li] 0002 leave $ ruby --parser=parse.y --dump=i -e '@iv' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,3)> 0000 getinstancevariable :@iv, <is:0> ( 1)[Li] 0003 leave $ ruby --parser=parse.y --dump=i -e 'Const' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,5)> 0000 opt_getconstant_path <ic:0 Const> ( 1)[Li] 0002 leave $ ruby --parser=parse.y --dump=i -e '@@cv' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,4)> 0000 getclassvariable :@@cv, <is:0> ( 1)[Li] 0003 leave
ローカル変数のときだけlocal tableというものが出現しています。 詳しくは「Rubyのしくみ Ruby Under a Microscope」などをみていただくとして、ローカル変数はスコープに付随するlocal tableに格納されます。 ということでローカル変数を取り扱うさいには、iseqのlocal tableのことを考えないといけません。 といってもこの辺の処理はcompile.cにすでにあるはずです2。
では何が面倒かというと、いわゆるdynamic variable (DVAR)というやつが問題になってきます。 ためしに次のスクリプトのバイトコードを見てみましょう3。
sum = 0 10.times do |i| j = i sum += j end
$ ruby --parser=parse.y --dump=i test.rb == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(6,3)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] sum@0 0000 putobject_INT2FIX_0_ ( 1)[Li] 0001 setlocal_WC_0 sum@0 0003 putobject 10 ( 3)[Li] 0005 send <calldata!mid:times, argc:0>, block in <main> 0008 leave == disasm: #<ISeq:block in <main>@test.rb:3 (3,9)-(6,3)> local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] i@0<AmbiguousArg>[ 1] j@1 0000 getlocal_WC_0 i@0 ( 4)[LiBc] 0002 setlocal_WC_0 j@1 0004 getlocal_WC_1 sum@0 ( 5)[Li] 0006 getlocal_WC_0 j@1 0008 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] 0010 dup 0011 setlocal_WC_1 sum@0 0013 leave ( 6)[Br]
注目すべきところは
- ブロックの外側と内側に対応するように、2つのiseqがつくられている
sumはブロックの外側のiseqのlocal tableで管理しているのに対して、iとjはブロックのiseqで管理している- 外側のiseqで
sumを触るときはsetlocal_WC_0という命令をつかっているのに対して、ブロックのiseqで触るときはsetlocal_WC_1という命令をつかっている
このWC_0やWC_1といった数値がどのくらい外側のlocal tableを見に行くかを表しています。
一方のノードはというと、もともとのノードではLVARとDVARというノードを使いわけていますし、書き換え後のLOCAL_VARIABLE_READ_NODEにはuint32_t depthというフィールドがあります。
$ ruby --parser=parse.y --dump=p test.rb
###########################################################
## Do NOT use this node dump for any purpose other than ##
## debug and research. Compatibility is not guaranteed. ##
###########################################################
# @ NODE_SCOPE (id: 19, line: 1, location: (1,0)-(6,3))
# +- nd_tbl: :sum
# +- nd_args:
# | (null node)
# +- nd_body:
# @ NODE_BLOCK (id: 17, line: 1, location: (1,0)-(6,3))
# +- nd_head (1):
# | @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,7))*
# | +- nd_vid: :sum
# | +- nd_value:
# | @ NODE_INTEGER (id: 1, line: 1, location: (1,6)-(1,7))
# | +- val: 0
# +- nd_head (2):
# @ NODE_ITER (id: 16, line: 3, location: (3,0)-(6,3))*
# +- nd_iter:
# | @ NODE_CALL (id: 3, line: 3, location: (3,0)-(3,8))
# | +- nd_mid: :times
# | +- nd_recv:
# | | @ NODE_INTEGER (id: 2, line: 3, location: (3,0)-(3,2))
# | | +- val: 10
# | +- nd_args:
# | (null node)
# +- nd_body:
# @ NODE_SCOPE (id: 15, line: 3, location: (3,9)-(6,3))
# +- nd_tbl: :i,:j
# +- nd_args:
# | @ NODE_ARGS (id: 5, line: 3, location: (3,13)-(3,14))
# | +- nd_ainfo.forwarding: 0 (no forwarding)
# | +- nd_ainfo.pre_args_num: 1
# | +- nd_ainfo.pre_init:
# | | (null node)
# | +- nd_ainfo.post_args_num: 0
# | +- nd_ainfo.post_init:
# | | (null node)
# | +- nd_ainfo.first_post_arg: (null)
# | +- nd_ainfo.rest_arg: (null)
# | +- nd_ainfo.block_arg: (null)
# | +- nd_ainfo.opt_args:
# | | (null node)
# | +- nd_ainfo.kw_args:
# | | (null node)
# | +- nd_ainfo.kw_rest_arg:
# | (null node)
# +- nd_body:
# @ NODE_BLOCK (id: 13, line: 4, location: (4,2)-(5,10))
# +- nd_head (1):
# | @ NODE_DASGN (id: 6, line: 4, location: (4,2)-(4,7))*
# | +- nd_vid: :j
# | +- nd_value:
# | @ NODE_DVAR (id: 7, line: 4, location: (4,6)-(4,7))
# | +- nd_vid: :i
# +- nd_head (2):
# @ NODE_DASGN (id: 8, line: 5, location: (5,2)-(5,10))*
# +- nd_vid: :sum
# +- nd_value:
# @ NODE_CALL (id: 12, line: 5, location: (5,2)-(5,10))
# +- nd_mid: :+
# +- nd_recv:
# | @ NODE_DVAR (id: 10, line: 5, location: (5,2)-(5,5))
# | +- nd_vid: :sum
# +- nd_args:
# @ NODE_LIST (id: 11, line: 5, location: (5,9)-(5,10))
# +- as.nd_alen: 1
# +- nd_head:
# | @ NODE_DVAR (id: 9, line: 5, location: (5,9)-(5,10))
# | +- nd_vid: :j
# +- nd_next:
# (null node)
$ ruby --parser=prism --dump=p test.rb
@ ProgramNode (location: (1,0)-(6,3))
+-- locals: [:sum]
+-- statements:
@ StatementsNode (location: (1,0)-(6,3))
+-- body: (length: 2)
+-- @ LocalVariableWriteNode (location: (1,0)-(1,7))
| +-- name: :sum
| +-- depth: 0
| +-- name_loc: (1,0)-(1,3) = "sum"
| +-- value:
| | @ IntegerNode (location: (1,6)-(1,7))
| | +-- IntegerBaseFlags: decimal
| | +-- value: 0
| +-- operator_loc: (1,4)-(1,5) = "="
+-- @ CallNode (location: (3,0)-(6,3))
+-- CallNodeFlags: nil
+-- receiver:
| @ IntegerNode (location: (3,0)-(3,2))
| +-- IntegerBaseFlags: decimal
| +-- value: 10
+-- call_operator_loc: (3,2)-(3,3) = "."
+-- name: :times
+-- message_loc: (3,3)-(3,8) = "times"
+-- opening_loc: nil
+-- arguments: nil
+-- closing_loc: nil
+-- equal_loc: nil
+-- block:
@ BlockNode (location: (3,9)-(6,3))
+-- locals: [:i, :j]
+-- parameters:
| @ BlockParametersNode (location: (3,12)-(3,15))
| +-- parameters:
| | @ ParametersNode (location: (3,13)-(3,14))
| | +-- requireds: (length: 1)
| | | +-- @ RequiredParameterNode (location: (3,13)-(3,14))
| | | +-- ParameterFlags: nil
| | | +-- name: :i
| | +-- optionals: (length: 0)
| | +-- rest: nil
| | +-- posts: (length: 0)
| | +-- keywords: (length: 0)
| | +-- keyword_rest: nil
| | +-- block: nil
| +-- locals: (length: 0)
| +-- opening_loc: (3,12)-(3,13) = "|"
| +-- closing_loc: (3,14)-(3,15) = "|"
+-- body:
| @ StatementsNode (location: (4,2)-(5,10))
| +-- body: (length: 2)
| +-- @ LocalVariableWriteNode (location: (4,2)-(4,7))
| | +-- name: :j
| | +-- depth: 0
| | +-- name_loc: (4,2)-(4,3) = "j"
| | +-- value:
| | | @ LocalVariableReadNode (location: (4,6)-(4,7))
| | | +-- name: :i
| | | +-- depth: 0
| | +-- operator_loc: (4,4)-(4,5) = "="
| +-- @ LocalVariableOperatorWriteNode (location: (5,2)-(5,10))
| +-- name_loc: (5,2)-(5,5) = "sum"
| +-- binary_operator_loc: (5,6)-(5,8) = "+="
| +-- value:
| | @ LocalVariableReadNode (location: (5,9)-(5,10))
| | +-- name: :j
| | +-- depth: 0
| +-- name: :sum
| +-- binary_operator: :+
| +-- depth: 1
+-- opening_loc: (3,9)-(3,11) = "do"
+-- closing_loc: (6,0)-(6,3) = "end"
話が長くなりましたが、実際はこの辺のことを10秒くらい考えて、ローカル変数は後回しにしようという決意を固めます。 ということでローカル変数を除いた4つの変数のノードを置き換えていきましょう。
parse.yの変更
parse.yの変更箇所はgettable関数に閉じています。
新しいノードを生成するマクロを定義して、gettableの該当箇所を書き換えます。
@@ -13320,11 +13363,11 @@ gettable(struct parser_params *p, ID id, const YYLTYPE *loc) case ID_GLOBAL: return NEW_GVAR(id, loc); case ID_INSTANCE: - return NEW_IVAR(id, loc); + return NEW_RB_INSTANCE_VARIABLE_READ(id, loc); case ID_CONST: - return NEW_CONST(id, loc); + return NEW_RB_CONSTANT_READ(id, loc); case ID_CLASS: - return NEW_CVAR(id, loc); + return NEW_RB_CLASS_VARIABLE_READ(id, loc); } compile_error(p, "identifier %"PRIsVALUE" is not valid to get", rb_id2str(id)); return 0;
compile.cの変更
ノードの置き換えが1対1に対応しているので、compile.cの変更も基本的にはキャストの変更とアクセスするフィールド名の変更ですみます。 インスタンス変数へのアクセスを例にあげてみましたが、ほとんど差分がないことがわかると思います。
// Before case NODE_IVAR:{ debugi("nd_vid", RNODE_IVAR(node)->nd_vid); if (!popped) { ADD_INSN2(ret, node, getinstancevariable, ID2SYM(RNODE_IVAR(node)->nd_vid), get_ivar_ic_value(iseq, RNODE_IVAR(node)->nd_vid)); } break; } // After case RB_INSTANCE_VARIABLE_READ_NODE: { debugi("name", RB_NODE_INSTANCE_VARIABLE_READ(node)->name); if (!popped) { ADD_INSN2(ret, node, getinstancevariable, ID2SYM(RB_NODE_INSTANCE_VARIABLE_READ(node)->name), get_ivar_ic_value(iseq, RB_NODE_INSTANCE_VARIABLE_READ(node)->name)); } break; }
なんか... 代入の種類が多い...
つい本音が漏れましたが、変数の代入って種類が多いんですよ。 先ほどあげた5つの変数への代入以外にもこういった代入があります。
- multiple assignment(
a, b = foo) - attribute assignment(
struct.field = foo) - operator assignment 1(
ary[1] += foo) - operator assignment 2(
struct.field += foo) - operator assignment and(
foo &&= bar) - operator assignment or(
foo ||= bar)
attribute assignmentやoperator assignmentはコンパイル結果が長くなりそうですし、multiple assignmentにいたってはどういうコンパイル結果になるかパッと思いつきません。 使うときはどれも無意識に使うから全く気にならないんですけどね。 これらの複雑な代入は後回しにしましょう。
それから定数への代入も後回しにします。
定数に代入するところはsetconstantを使えばいいのですが、他の変数とちがって定数のパスはネストすることができるので、代入の左辺がやや複雑なのです。
# インスタンス変数はネストできないが @iv = 1 # 定数は `::` を用いることでネストできる A::B::C::D = 1
ということで以下の3つに関してノードを書き換えていくことにします。
parse.yの変更
parse.yにおける代入のノードの扱いは2つのステップからなります。
- 代入を表すノードを生成する
- 1で作ったノードに右辺に相当するノードを設定する
// @iv = 1 // ステップ2: 1で作ったノードに右辺に相当するノードを設定する arg : lhs '=' arg_rhs { $$ = node_assign(p, (NODE *)$lhs, $rhs, $lex_ctxt, &@$); } ; // ステップ1: 代入を表すノードを生成する lhs : user_or_keyword_variable { $$ = assignable(p, $1, 0, &@$); } ; // user_variableは@ivなど // keyword_variableはnilやselfなど user_or_keyword_variable : user_variable | keyword_variable ;
ステップ1ではassignableという関数が呼び出されます。
assignableは渡されたidに応じて適切なノードを生成して返します。
書き換える対象のノードについて、生成するマクロを変更すればよいでしょう。
static NODE* assignable(struct parser_params *p, ID id, NODE *val, const YYLTYPE *loc) { const char *err = 0; int node_type = assignable0(p, id, &err); switch (node_type) { case NODE_DASGN: return NEW_DASGN(id, val, loc); case NODE_LASGN: return NEW_LASGN(id, val, loc); case NODE_GASGN: return NEW_GASGN(id, val, loc); case NODE_IASGN: return NEW_IASGN(id, val, loc); case NODE_CDECL: return NEW_CDECL(id, val, 0, p->ctxt.shareable_constant_value, loc); case NODE_CVASGN: return NEW_CVASGN(id, val, loc); } /* TODO: FIXME */ #ifndef RIPPER if (err) yyerror1(loc, err); #else if (err) set_value(assign_error(p, err, p->s_lvalue)); #endif return NEW_ERROR(loc); }
assignable0はidに応じたノードの種別を判定するための関数で、self = 1のような入力をエラーにする役割も担っています。
こちらも書き換えるノードについて、ノードのタイプを変更すればよいでしょう。
static int assignable0(struct parser_params *p, ID id, const char **err) { if (!id) return -1; switch (id) { case keyword_self: *err = "Can't change the value of self"; return -1; case keyword_nil: *err = "Can't assign to nil"; return -1; ... } switch (id_type(id)) { case ID_LOCAL: if (dyna_in_block(p)) { if (p->max_numparam > NO_PARAM && NUMPARAM_ID_P(id)) { compile_error(p, "Can't assign to numbered parameter _%d", NUMPARAM_ID_TO_IDX(id)); return -1; } if (dvar_curr(p, id)) return NODE_DASGN; if (dvar_defined(p, id)) return NODE_DASGN; if (local_id(p, id)) return NODE_LASGN; dyna_var(p, id); return NODE_DASGN; } else { if (!local_id(p, id)) local_var(p, id); return NODE_LASGN; } break; case ID_GLOBAL: return NODE_GASGN; case ID_INSTANCE: return NODE_IASGN; case ID_CONST: if (!p->ctxt.in_def) return NODE_CDECL; *err = "dynamic constant assignment"; return -1; case ID_CLASS: return NODE_CVASGN; default: compile_error(p, "identifier %"PRIsVALUE" is not valid to set", rb_id2str(id)); } return -1; }
ステップ2ではnode_assignという関数が呼ばれます。
基本的にはset_nd_valueという関数に処理を丸投げしているので、それぞれの関数でノードのタイプとキャスト用のマクロを書き換えればよいでしょう。
NODE_ATTRASGN(struct.field = fooの形をした代入)がチラッと見えましたが、コメントアウトするなどしてお茶を濁します。
static NODE * node_assign(struct parser_params *p, NODE *lhs, NODE *rhs, struct lex_context ctxt, const YYLTYPE *loc) { if (!lhs) return 0; switch (nd_type(lhs)) { case NODE_CDECL: case NODE_GASGN: case NODE_IASGN: case NODE_LASGN: case NODE_DASGN: case NODE_MASGN: case NODE_CVASGN: set_nd_value(p, lhs, rhs); nd_set_loc(lhs, loc); break; case NODE_ATTRASGN: RNODE_ATTRASGN(lhs)->nd_args = arg_append(p, RNODE_ATTRASGN(lhs)->nd_args, rhs, loc); nd_set_loc(lhs, loc); break; default: /* should not happen */ break; } return lhs; } static void set_nd_value(struct parser_params *p, NODE *node, NODE *rhs) { switch (nd_type(node)) { case NODE_CDECL: RNODE_CDECL(node)->nd_value = rhs; break; case NODE_GASGN: RNODE_GASGN(node)->nd_value = rhs; break; case NODE_IASGN: RNODE_IASGN(node)->nd_value = rhs; break; case NODE_LASGN: RNODE_LASGN(node)->nd_value = rhs; break; case NODE_DASGN: RNODE_DASGN(node)->nd_value = rhs; break; case NODE_MASGN: RNODE_MASGN(node)->nd_value = rhs; break; case NODE_CVASGN: RNODE_CVASGN(node)->nd_value = rhs; break; default: compile_error(p, "set_nd_value: unexpected node: %s", parser_node_name(nd_type(node))); break; } }
compile.cの変更
書き換えの前後でノードが1対1に対応しているので、compile.cもキャストするためのマクロやアクセスするフィールド名を変えるだけで済みます。 参考までにインスタンス変数への代入に関して書き換えの前後のコードを並べておきます。
// Before case NODE_IASGN:{ CHECK(COMPILE(ret, "lvalue", RNODE_IASGN(node)->nd_value)); if (!popped) { ADD_INSN(ret, node, dup); } ADD_INSN2(ret, node, setinstancevariable, ID2SYM(RNODE_IASGN(node)->nd_vid), get_ivar_ic_value(iseq,RNODE_IASGN(node)->nd_vid)); break; } // After case RB_INSTANCE_VARIABLE_WRITE_NODE: { CHECK(COMPILE(ret, "lvalue", RB_NODE_INSTANCE_VARIABLE_WRITE(node)->value)); if (!popped) { ADD_INSN(ret, node, dup); } ADD_INSN2(ret, node, setinstancevariable, ID2SYM(RB_NODE_INSTANCE_VARIABLE_WRITE(node)->name), get_ivar_ic_value(iseq, RB_NODE_INSTANCE_VARIABLE_WRITE(node)->name)); break; }
これらの変更によって変数への代入と参照ができるようになりました。 一部で位置情報がおかしくなっていますが、最後にまとめて直せばよいでしょう。
$ ./miniruby --parser=parse.y --dump=p,i -e '@iv = self; @iv'
@ ProgramNode (location: (1,0)-(1,15))
+-- locals: []
+-- statements:
@ StatementsNode (location: (1,0)-(1,15))
+-- body: (length: 2)
+-- @ InstanceVariableWriteNode (location: (1,0)-(1,10))*
| +-- name: :@iv
| +-- name_loc: (0,4294967295)-(0,4294967295) = ""
| +-- value:
| | @ SelfNode (location: (1,6)-(1,10))
| +-- operator_loc: (0,4294967295)-(0,4294967295) = ""
+-- @ InstanceVariableReadNode (location: (1,12)-(1,15))*
+-- name: :@iv
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,15)>
0000 putself ( 1)[Li]
0001 setinstancevariable :@iv, <is:0>
0004 getinstancevariable :@iv, <is:1>
0007 leave
おまけ: iseqの命令を調べる
iseqを眺めていると「これってどういう命令だっけ」とか、「オペランドが複数あるけどそれぞれなにを意味していたっけ」などといった疑問が生じることがあります。 生成されるバイトコードが等しいことをもってノードの置き換えを進めていく今回の作業においては、そのような疑問は完全に邪念でしかないのですが、手元にソースコードがある以上は手が滑ってバイトコードを調べてしまうこともあるでしょう。
というわけでgetlocal_WC_0を調べてみることにしましょう。
バイトコードについて調べるときはまずinsns.defというファイルを眺めます。
getlocal_WC_0という命令はここにはなくて、代わりにgetlocalという命令が見つかります。
/**********************************************************/ /* deal with variables */ /**********************************************************/ /* Get local variable (pointed by `idx' and `level'). 'level' indicates the nesting depth from the current block. */ DEFINE_INSN getlocal (lindex_t idx, rb_num_t level) () (VALUE val) { val = *(vm_get_ep(GET_EP(), level) - idx); RB_DEBUG_COUNTER_INC(lvar_get); (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0); }
簡潔にして十分な説明が書いてあることが多いので、ちゃんとコメントを読みます。
この命令はindex(idx)とブロックの深さ(level)を受け取って、ローカル変数を返すようです。
あるスコープのローカル変数は並べて置いてあるため、indexを用いて指定することができます。
ブロックの深さというのはさきほどDVARでみたように、いまいるブロックの外側にあるブロックに触ることがあるので、そこまでの距離を表しています。
命令のオペランドにlevelがあるということは、ブロックの深さについてはコンパイル時に計算しておく必要があります。
先送りしてよかったですね。
RB_DEBUG_COUNTER_INCとRB_DEBUG_COUNTER_INC_IFはきっとデバッグ用のカウンターを増やす処理でしょうから、この命令のキモは1行目でしょう。
GET_EP()でその時点のEnvironment Pointerを取得して、vm_get_epでlevelの分だけEnvironment Pointerを戻るのでしょう。
そしてEnvironment Pointerはローカル変数の配列の先頭へのポインタになっているから、idx分を調整することでお目当ての変数(のポインタ)が手に入るというようになっていることでしょう。
この辺はあまり詳細に立ち入らず「そういう実装になっていないと動かないはずなので、そうなっているはず」という気持ちで読んでいます。
ちょっと脱線をしている気もしますが、getlocal_WC_0がどこで定義されているか調べましょう。
insns.defはそのままではCのコードとして扱えません。ということはこのファイルを元にしてCのファイルを生成しているはずです。
その手の生成物はビルドを行っているディレクトリに作られることが多いので、そちらを検索してみます。
insns_info.inc, insns.incなどいくつかのファイルがヒットしますが、vm.incというファイルに命令の具体的な内容が書いてあるようです。
デバッグ用のマクロや、どの命令でも共通したセットアップ処理などを読み飛ばすとこんな感じのコードになっています。
getlocalでは2つ目のオペランドでlevelを初期化するのに対して、getlocal_WC_0では0、getlocal_WC_1では1でlevelを初期化しています。
勝手な想像ですが、GET_OPERANDを呼び出すコストを軽減するというより、バイトコードのオペランドを節約するための工夫かなと思います。
// vm.inc /* insn getlocal(idx, level)()(val) */ INSN_ENTRY(getlocal) { /* ### Declare and assign variables. ### */ lindex_t idx = (lindex_t)GET_OPERAND(1); rb_num_t level = (rb_num_t)GET_OPERAND(2); const bool MAYBE_UNUSED(leaf) = INSN_ATTR(leaf); VALUE val; /* ### Here we do the instruction body. ### */ # line 81 "../../insns.def" { val = *(vm_get_ep(GET_EP(), level) - idx); RB_DEBUG_COUNTER_INC(lvar_get); (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0); } # line 95 "vm.inc" } /* insn getlocal_WC_0(idx)()(val) */ INSN_ENTRY(getlocal_WC_0) { /* ### Declare and assign variables. ### */ #line 10 "../../defs/opt_operand.def" const rb_num_t level = 0; #line 4641 "vm.inc" lindex_t idx = (lindex_t)GET_OPERAND(1); const bool MAYBE_UNUSED(leaf) = INSN_ATTR(leaf); VALUE val; /* ### Here we do the instruction body. ### */ # line 81 "../../insns.def" { val = *(vm_get_ep(GET_EP(), level) - idx); RB_DEBUG_COUNTER_INC(lvar_get); (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0); } # line 4661 "vm.inc" } /* insn getlocal_WC_1(idx)()(val) */ INSN_ENTRY(getlocal_WC_1) { /* ### Declare and assign variables. ### */ #line 11 "../../defs/opt_operand.def" const rb_num_t level = 1; #line 4687 "vm.inc" lindex_t idx = (lindex_t)GET_OPERAND(1); const bool MAYBE_UNUSED(leaf) = INSN_ATTR(leaf); VALUE val; /* ### Here we do the instruction body. ### */ # line 81 "../../insns.def" { val = *(vm_get_ep(GET_EP(), level) - idx); RB_DEBUG_COUNTER_INC(lvar_get); (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0); } # line 4707 "vm.inc" }
さて、これらの命令の差分は"defs/opt_operand.def"というファイルで定義されているようです。
getlocal *, 0という定義の*はWildCardということなので、最終的にgetlocal_WC_0という命令が生成されることになるようです。
# # configuration file for operand union optimization # # format: # [insn name] op1, op2 ... # # wildcard: * # getlocal *, 0 getlocal *, 1 setlocal *, 0 setlocal *, 1 putobject INT2FIX(0) putobject INT2FIX(1) __END__ putobject Qtrue putobject Qfalse
おまけ2: vm.incが生成される仕組みを調べる
脱線ついでに"defs/opt_operand.def"からvm.incが生成される仕組みについても見ておきましょう。
defs/opt_operand.defでgrepをすると"tool/ruby_vm/loaders/opt_operand_def.rb"というファイルがヒットします。
そもそも"tool/ruby_vm/"とは...? という気持ちになるので、ディレクトリを見てみましょう。
controllers, helpers, models, viewsといったなにか見覚えのあるディレクトリが並んでいます。 controllersのなかにはapplication_controller.rbというファイルが置かれていて、「なんだRailsか」という気持ちになります。
$ ls tool/ruby_vm controllers helpers loaders models scripts views
真面目な話をすると、loadersに置かれているファイルが定義ファイルを読み込んで、modelでラップして、viewsに置いてあるテンプレートファイルをもとにCのファイルを生成しているようです。 ということはloaderが定義ファイルのparseをしているんだろうなと思ってみてみると、やっぱり文法定義が書かれていました4。
# tool/ruby_vm/loaders/opt_operand_def.rb require_relative '../helpers/scanner' json = [] scanner = RubyVM::Scanner.new '../../../defs/opt_operand.def' path = scanner.__FILE__ grammar = %r/ (?<comment> \# .+? \n ){0} (?<ws> \g<comment> | \s ){0} (?<insn> \w+ ){0} (?<paren> \( (?: \g<paren> | [^()]+)* \) ){0} (?<expr> (?: \g<paren> | [^(),\ \n] )+ ){0} (?<remain> \g<expr> ){0} (?<arg> \g<expr> ){0} (?<extra> , \g<ws>* \g<remain> ){0} (?<args> \g<arg> \g<extra>* ){0} (?<decl> \g<insn> \g<ws>+ \g<args> \n ){0} /mx
まとめ
今日の成果です。
あきらかに寄り道が多いので、次回は寄り道せずに進みたいものです。
-
ID_LOCALのときはitや_1を考慮する必要があるので分岐が複雑になっているだけで、基本的にはLVAR(ローカル変数)、DVAR(ダイナミック変数)、VCALL(メソッド呼び出し)のいずれかになると理解すればOKです。問題はDVARというやつにあって... 続きは本編で。↩ -
具体的には
iseq_set_local_tableという関数があります。↩ -
make runrubyやmake runで実行するデフォルトのファイル名がtest.rbなので、test.rbを使いがち。↩ - 文法の定義はBNFで行いたいけど、parser generatorをもってくるのは大がかりなので正規表現で記述する。わかります...↩