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


Ruby Parser開発日誌 (24-18) - parse.yが生成するノードを変える ー クラスとモジュール

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という生成規則はConstantReadNodeConstantPathNodeの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)>をbody
  • VALUE 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_CLASS
  • VM_DEFINECLASS_TYPE_SINGLETON_CLASS
  • VM_DEFINECLASS_TYPE_MODULE

その他にVM_DEFINECLASS_FLAG_SCOPEDVM_DEFINECLASS_FLAG_HAS_SUPERCLASSという情報も保持することができます。

VM_DEFINECLASS_FLAG_SCOPEDclass 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日が経過したのであらためて進捗を確認しておきましょう。

yui-knk.hatenablog.com

前回と比べて対応した機能は以下のとおりです。

  • 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日目: クラス定義 / モジュール定義 / シングルトンクラス定義

まあまあ順調に進んでいるのではないでしょうか。 機能の一覧とその進捗は以下のとおりです。 引き続き頑張っていきましょう。

  • リテラル
    • nil
    • true
    • false
    • self
    • __LINE__
    • __FILE__
    • __ENCODING__
    • array
    • hash
    • string
    • integer
    • float
    • rational
    • imaginary
    • regex
    • range
    • interpolationを含むstring
    • interpolationを含むsymbol
    • interpolationを含むregex
    • lambda
  • 変数の参照と代入
  • 定義
    • メソッド定義
      • 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)
  • 制御構文
    • if
    • unless
    • flipflop
    • case when
    • while / until
    • for
    • retry / rescue / ensure
    • && / || / and / or
    • yield
    • return
    • super
    • break / next / redo
    • BEGIN
    • END
  • パターンマッチング
  • その他
    • alias
    • undef
    • defined?



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

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