18日目: class, singleton class, moduleの定義
今回はclass定義やmodule定義をやっていきます。
なんでこれまでやってこなかったんだっけとしばらく考えていたのですが、class C; endのように定数の参照が関係するので定数の対応をしてからにしようと思っていたのでした。
parse.yを変更する
クラス定義のノード
早速ですが書き換え前後のノードをみていきましょう。
class A::B < C; a = 1; end
# Before
@ NODE_CLASS (id: 6, line: 1, location: (1,0)-(1,26))*
+- nd_cpath:
| @ NODE_COLON2 (id: 1, line: 1, location: (1,6)-(1,10))
| +- nd_mid: :B
| +- nd_head:
| | @ NODE_CONST (id: 0, line: 1, location: (1,6)-(1,7))
| | +- nd_vid: :A
+- nd_super:
| @ NODE_CONST (id: 2, line: 1, location: (1,13)-(1,14))
| +- nd_vid: :C
+- nd_body:
| @ NODE_SCOPE (id: 5, line: 1, location: (1,0)-(1,26))
| +- nd_tbl: :a
| +- nd_args:
| | (null node)
| +- nd_body:
| @ NODE_LASGN (id: 3, line: 1, location: (1,16)-(1,21))*
| +- nd_vid: :a
| +- nd_value:
| @ NODE_INTEGER (id: 4, line: 1, location: (1,20)-(1,21))
| +- val: 1
# After
+-- @ ClassNode (location: (1,0)-(1,26))
+-- locals: [:a]
+-- constant_path:
| @ ConstantPathNode (location: (1,6)-(1,10))
| +-- parent:
| | @ ConstantReadNode (location: (1,6)-(1,7))
| | +-- name: :A
| +-- name: :B
+-- superclass:
| @ ConstantReadNode (location: (1,13)-(1,14))
| +-- name: :C
+-- body:
| @ StatementsNode (location: (1,16)-(1,21))
| +-- body: (length: 1)
| +-- @ LocalVariableWriteNode (location: (1,16)-(1,21))
| +-- name: :a
| +-- depth: 0
| +-- value:
| | @ IntegerNode (location: (1,20)-(1,21))
| | +-- IntegerBaseFlags: decimal
| | +-- value: 1
+-- name: :B
表にするとこんな感じです。
| 書き換え前 | 書き換え後 | |
|---|---|---|
| 全体 | NODE_CLASS | ClassNode |
| C | nd_cpath (NODE_COLON2) | constant_path (ConstantReadNode) |
| D | nd_super (NODE_CONST) | superclass (ConstantReadNode) |
| body | nd_body (NODE_SCOPE) | body (StatementsNode) |
| 変数 | nd_bodyのnd_tbl | locals |
| クラス名 | N/A | name |
constant_pathとは別にクラス名をもつフィールド(name)が追加されていること以外はおおよそ同じ構造です。
特に難しい部分はないでしょう。
一つポイントとしてはクラス(A::Bの部分)を表すcpathという生成規則はConstantReadNodeとConstantPathNodeの2つのノードの可能性があります。
どちらのノードからもnameを取得できる補助的な関数を用意しておきましょう。
static ID get_const_name(struct parser_params *p, NODE *node) { switch (nd_type(node)) { case RB_CONSTANT_READ_NODE: return RB_NODE_CONSTANT_READ(node)->name; case RB_CONSTANT_PATH_NODE: return RB_NODE_CONSTANT_PATH(node)->name; default: compile_error(p, "get_const_name: unexpected node: %s", parser_node_name(nd_type(node))); return 0; } } static rb_class_node_t * rb_new_node_class_new(struct parser_params *p, rb_node_t *nd_cpath, rb_node_t *nd_body, rb_node_t *nd_super, const YYLTYPE *loc, const YYLTYPE *class_keyword_loc, const YYLTYPE *inheritance_operator_loc, const YYLTYPE *end_keyword_loc) { rb_class_node_t *n = RB_NEW_NODE_NEWNODE((enum rb_node_type)RB_CLASS_NODE, rb_class_node_t, loc); n->constant_path = nd_cpath; n->superclass = nd_super; n->body = nd_body; n->locals = local_tbl(p); n->name = get_const_name(p, nd_cpath); n->class_keyword_loc = *class_keyword_loc; n->inheritance_operator_loc = *inheritance_operator_loc; n->end_keyword_loc = *end_keyword_loc; return n; }
生成されるノードを確認します。 よさそうですね。
$ ./miniruby --parser=parse.y --dump=p -e 'class A::B < C; a = nil; end'
@ ProgramNode (location: (1,0)-(1,28))
+-- locals: []
+-- statements:
@ StatementsNode (location: (1,0)-(1,28))
+-- body: (length: 1)
+-- @ ClassNode (location: (1,0)-(1,28))*
+-- locals: [:a]
+-- class_keyword_loc: (1,0)-(1,5) = ""
+-- constant_path:
| @ ConstantPathNode (location: (1,6)-(1,10))
| +-- parent:
| | @ ConstantReadNode (location: (1,6)-(1,7))
| | +-- name: :A
| +-- name: :B
| +-- delimiter_loc: (0,0)-(0,0) = ""
| +-- name_loc: (0,0)-(0,0) = ""
+-- inheritance_operator_loc: (1,11)-(1,12) = ""
+-- superclass:
| @ ConstantReadNode (location: (1,13)-(1,14))
| +-- name: :C
+-- body:
| @ StatementsNode (location: (1,16)-(1,23))
| +-- body: (length: 1)
| +-- @ LocalVariableWriteNode (location: (1,16)-(1,23))*
| +-- name: :a
| +-- depth: 0
| +-- name_loc: (0,-1)-(0,-1) = ""
| +-- value:
| | @ NilNode (location: (1,20)-(1,23))
| +-- operator_loc: (0,-1)-(0,-1) = ""
+-- end_keyword_loc: (1,25)-(1,28) = ""
+-- name: :B
シングルトンクラス定義のノード
書き換え前後のノードを確認します。
class << obj; a = 1; end
# Before
@ NODE_SCLASS (id: 4, line: 1, location: (1,0)-(1,24))*
+- nd_recv:
| @ NODE_VCALL (id: 0, line: 1, location: (1,9)-(1,12))
| +- nd_mid: :obj
+- nd_body:
| @ NODE_SCOPE (id: 3, line: 1, location: (1,0)-(1,24))
| +- nd_tbl: :a
| +- nd_args:
| | (null node)
| +- nd_body:
| @ NODE_LASGN (id: 1, line: 1, location: (1,14)-(1,19))*
| +- nd_vid: :a
| +- nd_value:
| @ NODE_INTEGER (id: 2, line: 1, location: (1,18)-(1,19))
| +- val: 1
# After
+-- @ SingletonClassNode (location: (1,0)-(1,24))
+-- locals: [:a]
+-- expression:
| @ CallNode (location: (1,9)-(1,12))
| +-- CallNodeFlags: variable_call, ignore_visibility
| +-- receiver: nil
| +-- call_operator_loc: nil
| +-- name: :obj
| +-- arguments: nil
| +-- block: nil
+-- body:
| @ StatementsNode (location: (1,14)-(1,19))
| +-- body: (length: 1)
| +-- @ LocalVariableWriteNode (location: (1,14)-(1,19))
| +-- name: :a
| +-- depth: 0
| +-- value:
| | @ IntegerNode (location: (1,18)-(1,19))
| | +-- IntegerBaseFlags: decimal
| | +-- value: 1
基本的には同じ構造ですね。
class定義と異なりnameフィールドがありません。
とくに変わったことをしているわけでもないので実装は割愛します。
$ ./miniruby --parser=parse.y --dump=p -e 'class << obj; a = nil; end'
@ ProgramNode (location: (1,0)-(1,26))
+-- locals: []
+-- statements:
@ StatementsNode (location: (1,0)-(1,26))
+-- body: (length: 1)
+-- @ SingletonClassNode (location: (1,0)-(1,26))*
+-- locals: [:a]
+-- class_keyword_loc: (1,0)-(1,5) = ""
+-- operator_loc: (1,6)-(1,8) = ""
+-- expression:
| @ CallNode (location: (1,9)-(1,12))
| +-- CallNodeFlags: variable_call
| +-- receiver: nil
| +-- call_operator_loc: nil
| +-- name: :obj
| +-- message_loc: nil
| +-- opening_loc: nil
| +-- arguments: nil
| +-- closing_loc: nil
| +-- equal_loc: nil
| +-- block: nil
+-- body:
| @ StatementsNode (location: (1,14)-(1,21))
| +-- body: (length: 1)
| +-- @ LocalVariableWriteNode (location: (1,14)-(1,21))*
| +-- name: :a
| +-- depth: 0
| +-- name_loc: (0,-1)-(0,-1) = ""
| +-- value:
| | @ NilNode (location: (1,18)-(1,21))
| +-- operator_loc: (0,-1)-(0,-1) = ""
+-- end_keyword_loc: (1,23)-(1,26) = ""
モジュール定義のノード
毎度のことですが書き換え前後のノードを確認します。
module M; a = nil; end
# Before
@ NODE_MODULE (id: 7, line: 1, location: (1,0)-(1,22))*
+- nd_cpath:
| @ NODE_COLON2 (id: 0, line: 1, location: (1,7)-(1,8))
| +- nd_mid: :M
| +- nd_head:
| | (null node)
+- nd_body:
| @ NODE_SCOPE (id: 6, line: 1, location: (1,0)-(1,22))
| +- nd_tbl: :a
| +- nd_args:
| | (null node)
| +- nd_body:
| @ NODE_BLOCK (id: 4, line: 1, location: (1,8)-(1,17))
| +- nd_head (1):
| | @ NODE_BEGIN (id: 1, line: 1, location: (1,8)-(1,8))
| | +- nd_body:
| | (null node)
| +- nd_head (2):
| @ NODE_LASGN (id: 2, line: 1, location: (1,10)-(1,17))*
| +- nd_vid: :a
| +- nd_value:
| @ NODE_NIL (id: 3, line: 1, location: (1,14)-(1,17))
# After
+-- @ ModuleNode (location: (1,0)-(1,22))
+-- locals: [:a]
+-- constant_path:
| @ ConstantReadNode (location: (1,7)-(1,8))
| +-- name: :M
+-- body:
| @ StatementsNode (location: (1,10)-(1,17))
| +-- body: (length: 1)
| +-- @ LocalVariableWriteNode (location: (1,10)-(1,17))
| +-- name: :a
| +-- depth: 0
| +-- name_loc: (1,10)-(1,11) = "a"
| +-- value:
| | @ NilNode (location: (1,14)-(1,17))
+-- name: :M
クラス定義同様にnameがあるので、get_const_name関数で定数のノードからnameを取得します。
static rb_module_node_t * rb_new_node_module_new(struct parser_params *p, rb_node_t *nd_cpath, rb_node_t *nd_body, const YYLTYPE *loc, const YYLTYPE *module_keyword_loc, const YYLTYPE *end_keyword_loc) { rb_module_node_t *n = RB_NEW_NODE_NEWNODE((enum rb_node_type)RB_MODULE_NODE, rb_module_node_t, loc); n->constant_path = nd_cpath; n->body = nd_body; n->locals = local_tbl(p); n->name = get_const_name(p, nd_cpath); n->module_keyword_loc = *module_keyword_loc; n->end_keyword_loc = *end_keyword_loc; return n; }
生成されるノードも問題なさそうです。
$ ./miniruby --parser=parse.y --dump=p -e 'module M; a = nil; end'
@ ProgramNode (location: (1,0)-(1,22))
+-- locals: []
+-- statements:
@ StatementsNode (location: (1,0)-(1,22))
+-- body: (length: 1)
+-- @ ModuleNode (location: (1,0)-(1,22))*
+-- locals: [:a]
+-- module_keyword_loc: (1,0)-(1,6) = ""
+-- constant_path:
| @ ConstantReadNode (location: (1,7)-(1,8))
| +-- name: :M
+-- body:
| @ StatementsNode (location: (1,10)-(1,17))
| +-- body: (length: 1)
| +-- @ LocalVariableWriteNode (location: (1,10)-(1,17))*
| +-- name: :a
| +-- depth: 0
| +-- name_loc: (0,-1)-(0,-1) = ""
| +-- value:
| | @ NilNode (location: (1,14)-(1,17))
| +-- operator_loc: (0,-1)-(0,-1) = ""
+-- end_keyword_loc: (1,19)-(1,22) = ""
+-- name: :M
バイトコードを眺める
クラス定義の場合は2つのISeqが作られます。 クラスやモジュール定義のbodyでは変数のスコープが新しく作られるので、それに対応するようにISeqも作られるということです。
class C a = 0 end # == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,3)> # 0000 putspecialobject 3 ( 1)[Li] # 0002 putnil # 0003 defineclass :C, <class:C>, 0 # 0007 leave # クラスのbodyに相当するバイトコード # == disasm: #<ISeq:<class:C>@test.rb:1 (1,0)-(3,3)> # local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) # [ 1] a@0 # 0000 putobject_INT2FIX_0_ ( 2)[LiCl] # 0001 dup # 0002 setlocal_WC_0 a@0 # 0004 leave
クラスやモジュールを定義するさいのコアとなる命令はdefineclassです。
/* enter class definition scope. if super is Qfalse, and class "klass" is defined, it's redefined. Otherwise, define "klass" class. */ DEFINE_INSN defineclass (ID id, ISEQ class_iseq, rb_num_t flags) (VALUE cbase, VALUE super) (VALUE val) { VALUE klass = vm_find_or_create_class_by_id(id, flags, cbase, super); const rb_box_t *box = rb_current_box(); rb_iseq_check(class_iseq); /* enter scope */ vm_push_frame(ec, class_iseq, VM_FRAME_MAGIC_CLASS | VM_ENV_FLAG_LOCAL, klass, GC_GUARDED_PTR(box), (VALUE)vm_cref_push(ec, klass, NULL, FALSE, FALSE), ISEQ_BODY(class_iseq)->iseq_encoded, GET_SP(), ISEQ_BODY(class_iseq)->local_table_size, ISEQ_BODY(class_iseq)->stack_max); RESTORE_REGS(); NEXT_INSN(); }
この命令は3つのオペランド(ID id, ISEQ class_iseq, rb_num_t flags)をとり、スタックの上から2つの要素(VALUE cbase, VALUE super)を使ってクラスやモジュールを定義します。
class C; a = 0; endでいえば
ISEQ class_iseq:#<ISeq:<class:C>@test.rb:1 (1,0)-(3,3)>をbodyVALUE super: superクラスはなし(putnil)ID id: クラス名は:C
というクラスを
VALUE cbase:putspecialobject 3の配下
に定義するといった感じになります。
rb_num_t flagsはいくつかの役割があります。
typedef enum { VM_DEFINECLASS_TYPE_CLASS = 0x00, VM_DEFINECLASS_TYPE_SINGLETON_CLASS = 0x01, VM_DEFINECLASS_TYPE_MODULE = 0x02, /* 0x03..0x06 is reserved */ VM_DEFINECLASS_TYPE_MASK = 0x07 } rb_vm_defineclass_type_t; #define VM_DEFINECLASS_TYPE(x) ((rb_vm_defineclass_type_t)(x) & VM_DEFINECLASS_TYPE_MASK) #define VM_DEFINECLASS_FLAG_SCOPED 0x08 #define VM_DEFINECLASS_FLAG_HAS_SUPERCLASS 0x10 #define VM_DEFINECLASS_SCOPED_P(x) ((x) & VM_DEFINECLASS_FLAG_SCOPED) #define VM_DEFINECLASS_HAS_SUPERCLASS_P(x) \ ((x) & VM_DEFINECLASS_FLAG_HAS_SUPERCLASS)
1つはクラス、シングルトンクラス、モジュールのどれを定義しようとしているのかを表します。 具体的な値は以下の3種類です。
VM_DEFINECLASS_TYPE_CLASSVM_DEFINECLASS_TYPE_SINGLETON_CLASSVM_DEFINECLASS_TYPE_MODULE
その他にVM_DEFINECLASS_FLAG_SCOPEDとVM_DEFINECLASS_FLAG_HAS_SUPERCLASSという情報も保持することができます。
VM_DEFINECLASS_FLAG_SCOPEDはclass A::B; endのように定義しようとしているクラスやモジュールがトップレベルかどうかを表します。
VM_DEFINECLASS_FLAG_HAS_SUPERCLASSはその名の通りでクラス定義のさいにsuperクラスを指定しているかを表します。
コンパイルする
ノードの構造が大きく変わったわけではなのでcompile.cへの変更は割と自明です。
通常のノードと異なるのはclass定義にしろmodule定義にしろ、メソッド定義などと同様に新しくISeqを作る点です。
そのためrb_iseq_compile_node関数にも修正が必要です。
@@ -962,6 +962,9 @@ rb_iseq_compile_node(rb_iseq_t *iseq, const NODE *node) } /* assume node is T_NODE */ else if (nd_type_p(node, RB_PROGRAM_NODE) || + nd_type_p(node, RB_CLASS_NODE) || + nd_type_p(node, RB_SINGLETON_CLASS_NODE) || + nd_type_p(node, RB_MODULE_NODE) || nd_type_p(node, RB_BLOCK_NODE) || nd_type_p(node, RB_DEF_NODE)) { const rb_ast_id_table_t *locals = NULL; @@ -983,22 +986,31 @@ rb_iseq_compile_node(rb_iseq_t *iseq, const NODE *node) // } case RB_CLASS_NODE: { + const rb_class_node_t *cast = (const rb_class_node_t *)node; + locals = cast->locals; + body = (NODE *)cast->body; break; } case RB_SINGLETON_CLASS_NODE: { + const rb_singleton_class_node_t *cast = (const rb_singleton_class_node_t *)node; + locals = cast->locals; + body = (NODE *)cast->body; break; } case RB_MODULE_NODE: { + const rb_module_node_t *cast = (const rb_module_node_t *)node; + locals = cast->locals; + body = (NODE *)cast->body; break; }
参考までにiseq_compile_each0関数におけるClassNodeをコンパイルする部分のコードをのせておきます(が、本当に既存のロジックと違いがない...)。
// Before case NODE_CLASS:{ const rb_iseq_t *class_iseq = NEW_CHILD_ISEQ(RNODE_CLASS(node)->nd_body, rb_str_freeze(rb_sprintf("<class:%"PRIsVALUE">", rb_id2str(get_node_colon_nd_mid(RNODE_CLASS(node)->nd_cpath)))), ISEQ_TYPE_CLASS, line); const int flags = VM_DEFINECLASS_TYPE_CLASS | (RNODE_CLASS(node)->nd_super ? VM_DEFINECLASS_FLAG_HAS_SUPERCLASS : 0) | compile_cpath(ret, iseq, RNODE_CLASS(node)->nd_cpath); CHECK(COMPILE(ret, "super", RNODE_CLASS(node)->nd_super)); ADD_INSN3(ret, node, defineclass, ID2SYM(get_node_colon_nd_mid(RNODE_CLASS(node)->nd_cpath)), class_iseq, INT2FIX(flags)); RB_OBJ_WRITTEN(iseq, Qundef, (VALUE)class_iseq); if (popped) { ADD_INSN(ret, node, pop); } break; } // After case RB_CLASS_NODE: { const rb_iseq_t *class_iseq = NEW_CHILD_ISEQ(node, rb_str_freeze(rb_sprintf("<class:%"PRIsVALUE">", rb_id2str(RB_NODE_CLASS(node)->name))), ISEQ_TYPE_CLASS, line); const int flags = VM_DEFINECLASS_TYPE_CLASS | (RB_NODE_CLASS(node)->superclass ? VM_DEFINECLASS_FLAG_HAS_SUPERCLASS : 0) | compile_cpath(ret, iseq, RB_NODE_CLASS(node)->constant_path); CHECK(COMPILE(ret, "super", RB_NODE_CLASS(node)->superclass)); ADD_INSN3(ret, node, defineclass, ID2SYM(RB_NODE_CLASS(node)->name), class_iseq, INT2FIX(flags)); RB_OBJ_WRITTEN(iseq, Qundef, (VALUE)class_iseq); if (popped) { ADD_INSN(ret, node, pop); } break; }
生成されるバイトコードも問題なさそうです。
$ ./miniruby --parser=parse.y --dump=i -e 'class C; a = nil; end' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,21)> 0000 putspecialobject 3 ( 1)[Li] 0002 putnil 0003 defineclass :C, <class:C>, 0 0007 leave == disasm: #<ISeq:<class:C>@-e:1 (1,0)-(1,21)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] a@0 0000 putnil ( 1)[LiCl] 0001 dup 0002 setlocal_WC_0 a@0 0004 leave [En]
まとめ
今日の成果です。
- クラス定義を対応した
- シングルトンクラス定義を対応した
- モジュール定義を対応した
ここまでの進捗
前回の進捗確認から7日が経過したのであらためて進捗を確認しておきましょう。
前回と比べて対応した機能は以下のとおりです。
- 11日目: attr assignment (
struct.field = foo) - 12-14日目: multiple assignment (
a, b = foo) - 15日目: メソッド定義のmultiple assignment (
def m((a, b))) - 16日目: 定数
- 17日目: 左辺に定数があるmultiple assignment (
A, B = foo) - 18日目: クラス定義 / モジュール定義 / シングルトンクラス定義
まあまあ順調に進んでいるのではないでしょうか。 機能の一覧とその進捗は以下のとおりです。 引き続き頑張っていきましょう。
- リテラル
- 変数の参照と代入
- 定義
- メソッド定義
multiple assignment (def m((a, b)))が未対応- forwarding (
...)が未対応
クラス定義 / モジュール定義 / シングルトンクラス定義
- メソッド定義
- メソッド呼び出し
いわゆる通常のメソッド呼び出しattr assignment (struct.field = foo)- array assignment with operator (
ary[1] += foo) - attr assignment with operator (
struct.field += foo) - assignment with && / || operator (
foo &&= bar) - constant declaration with operator (
A::B ||= 1)
- 制御構文
ifunless- flipflop
- case when
- while / until
- for
- retry / rescue / ensure
- && / || / and / or
- yield
- return
- super
- break / next / redo
- BEGIN
- END
- パターンマッチング
- その他
- alias
- undef
- defined?