---
# System prepended metadata

title: 2020 年 10 月

---

2020 年 10 月
===

# 蒼時

## 翻找原始碼

這塊其實不太容易，需要先在原始碼把跟 `super` 相關的檔案找出來，但裡面混雜不少東西

> CRuby 2.6 的 `super` 是關鍵字，可以從這個方向下手

## Bison

基本上 Ruby 體系的語言都是用 Yacc 的格式處理，這塊沒學過不是那麼容易閱讀，大致上來說我們可以在 `parse.y` 裡面找到關於 `super` 相關的幾段程式碼。

> 這塊我自己也不算熟悉

根據 `parse.y` 裡面產生的程式碼往下追，會在 `node.h` 找到定義，這是用來描述 AST 結構的行為。

```c
// node.h

#define NEW_SUPER(a,loc) NEW_NODE(NODE_SUPER,0,0,a,loc)
#define NEW_ZSUPER(loc) NEW_NODE(NODE_ZSUPER,0,0,0,loc)
```

> Ruby 的巨集其實寫的很直覺，大致上可以看出來目的是製作 AST 的 Node

## Compiler / AST

把目標先鎖定在 `NODE_SUPER` 的處理，可以在 `compile.c` 和 `ast.c` 找到對應的處理。

```c
// ast.c

      case NODE_SUPER:
        return rb_ary_new_from_node_args(ast, 1, node->nd_args);
      case NODE_ZSUPER:
        return rb_ary_new_from_node_args(ast, 0);
```

這塊蠻有趣的，在 AST 裡面產生的是 Ruby 的 Array，不過他是 `from_node_args` 也可能跟 Ruby 的 Array 沒有關係，不過考慮到 Ruby 2.6+ 開始把 VM 的一些資訊開放 API 讓 Ruby 可以存取到，在建構 AST 的時候就製作成 Ruby Array 是蠻有可能的。

```c
// compile.c

      case NODE_SUPER:
      case NODE_ZSUPER:
        ADD_INSN(ret, line, putnil);
        ADD_INSN3(ret, line, defined, INT2FIX(DEFINED_ZSUPER), 0,
		  needstr);
        return;
```

這塊看起來就蠻直觀的，這個 `ADD_INSN` 巨集應該是用來製作 OP Code 的處理，能找到對應的 OP Code 就可以去反推 Ruby VM 的處理。

然後除了第一段之外還有第二段，長得有點懷疑人生⋯⋯

```c
// compile.c

      case NODE_SUPER:
      case NODE_ZSUPER:{
	DECL_ANCHOR(args);
	int argc;
	unsigned int flag = 0;
	struct rb_call_info_kw_arg *keywords = NULL;
	const rb_iseq_t *parent_block = ISEQ_COMPILE_DATA(iseq)->current_block;

	INIT_ANCHOR(args);
	ISEQ_COMPILE_DATA(iseq)->current_block = NULL;
	if (type == NODE_SUPER) {
	    VALUE vargc = setup_args(iseq, args, node->nd_args, &flag, &keywords);
	    CHECK(!NIL_P(vargc));
	    argc = FIX2INT(vargc);
	}
	else {
	    /* NODE_ZSUPER */
	    int i;
	    const rb_iseq_t *liseq = body->local_iseq;
	    const struct rb_iseq_constant_body *const local_body = liseq->body;
	    const struct rb_iseq_param_keyword *const local_kwd = local_body->param.keyword;
	    int lvar_level = get_lvar_level(iseq);

	    argc = local_body->param.lead_num;

	    /* normal arguments */
	    for (i = 0; i < local_body->param.lead_num; i++) {
		int idx = local_body->local_table_size - i;
		ADD_GETLOCAL(args, line, idx, lvar_level);
	    }

	    if (local_body->param.flags.has_opt) {
		/* optional arguments */
		int j;
		for (j = 0; j < local_body->param.opt_num; j++) {
		    int idx = local_body->local_table_size - (i + j);
		    ADD_GETLOCAL(args, line, idx, lvar_level);
		}
		i += j;
		argc = i;
	    }
	    if (local_body->param.flags.has_rest) {
		/* rest argument */
		int idx = local_body->local_table_size - local_body->param.rest_start;

		ADD_GETLOCAL(args, line, idx, lvar_level);
		ADD_INSN1(args, line, splatarray, Qfalse);

		argc = local_body->param.rest_start + 1;
		flag |= VM_CALL_ARGS_SPLAT;
	    }
	    if (local_body->param.flags.has_post) {
		/* post arguments */
		int post_len = local_body->param.post_num;
		int post_start = local_body->param.post_start;

		if (local_body->param.flags.has_rest) {
		    int j;
		    for (j=0; j<post_len; j++) {
			int idx = local_body->local_table_size - (post_start + j);
			ADD_GETLOCAL(args, line, idx, lvar_level);
		    }
		    ADD_INSN1(args, line, newarray, INT2FIX(j));
		    ADD_INSN (args, line, concatarray);
		    /* argc is settled at above */
		}
		else {
		    int j;
		    for (j=0; j<post_len; j++) {
			int idx = local_body->local_table_size - (post_start + j);
			ADD_GETLOCAL(args, line, idx, lvar_level);
		    }
		    argc = post_len + post_start;
		}
	    }

	    if (local_body->param.flags.has_kw) { /* TODO: support keywords */
		int local_size = local_body->local_table_size;
		argc++;

		ADD_INSN1(args, line, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));

		if (local_body->param.flags.has_kwrest) {
		    int idx = local_body->local_table_size - local_kwd->rest_start;
		    ADD_GETLOCAL(args, line, idx, lvar_level);
		    ADD_SEND (args, line, rb_intern("dup"), INT2FIX(0));
		}
		else {
		    ADD_INSN1(args, line, newhash, INT2FIX(0));
		}
		for (i = 0; i < local_kwd->num; ++i) {
		    ID id = local_kwd->table[i];
		    int idx = local_size - get_local_var_idx(liseq, id);
		    ADD_INSN1(args, line, putobject, ID2SYM(id));
		    ADD_GETLOCAL(args, line, idx, lvar_level);
		}
		ADD_SEND(args, line, id_core_hash_merge_ptr, INT2FIX(i * 2 + 1));
		if (local_body->param.flags.has_rest) {
		    ADD_INSN1(args, line, newarray, INT2FIX(1));
		    ADD_INSN (args, line, concatarray);
		    --argc;
		}
		flag |= VM_CALL_KW_SPLAT;
	    }
	    else if (local_body->param.flags.has_kwrest) {
		int idx = local_body->local_table_size - local_kwd->rest_start;
		ADD_GETLOCAL(args, line, idx, lvar_level);

		ADD_SEND (args, line, rb_intern("dup"), INT2FIX(0));
		if (local_body->param.flags.has_rest) {
		    ADD_INSN1(args, line, newarray, INT2FIX(1));
		    ADD_INSN (args, line, concatarray);
		}
		else {
		    argc++;
		}
		flag |= VM_CALL_KW_SPLAT;
	    }
	}

	ADD_INSN(ret, line, putself);
	ADD_SEQ(ret, args);
        ADD_INSN2(ret, line, invokesuper,
                  new_callinfo(iseq, 0, argc, flag | VM_CALL_SUPER | (type == NODE_ZSUPER ? VM_CALL_ZSUPER : 0) | VM_CALL_FCALL, keywords, parent_block != NULL),
		  parent_block);

	if (popped) {
	    ADD_INSN(ret, line, pop);
	}
	break;
      }
```

大致是看起來像是在處理 `super(...)` 的情況，不過我們可以先跳過細節，這邊關鍵應該是 `ret` 是什麼，並且 `ADD_INSN` 如何去判斷 OP Code 是什麼。

```c
/* add an instruction */
#define ADD_INSN(seq, line, insn) \
  ADD_ELEM((seq), (LINK_ELEMENT *) new_insn_body(iseq, (line), BIN(insn), 0))
```

問題應該會是 `LINK_ELEMENT` 或者在前面這幾段 `ret` 對應的 `LINK_ANCHOR` 是什麼東西為關鍵，所以需要繼續往下追。

```c
typedef struct iseq_link_element {
    enum {
	ISEQ_ELEMENT_ANCHOR,
	ISEQ_ELEMENT_LABEL,
	ISEQ_ELEMENT_INSN,
	ISEQ_ELEMENT_ADJUST,
        ISEQ_ELEMENT_TRACE,
    } type;
    struct iseq_link_element *next;
    struct iseq_link_element *prev;
} LINK_ELEMENT;

typedef struct iseq_link_anchor {
    LINK_ELEMENT anchor;
    LINK_ELEMENT *last;
} LINK_ANCHOR;
```

`LINK_ANCHOR` 和 `LINK_ELEMENT` 看起來都是 AST 的一部分，這樣問題又回到 OP Code 是在哪裡被定義的，才能回推 Ruby VM 的實際行為。

回去看 `ADD_INSN` 行為，第三個參數是指令碼，在 `insns.def` 可以找到

```c
/* put nil to stack. */
DEFINE_INSN
putnil
()
()
(VALUE val)
{
    val = Qnil;
}
```

這邊大致上可以確定前面看到的處理的 OP Code 要從哪裡找，仔細看比較大串的地方會找到

```c
ADD_SEQ(ret, args);
        ADD_INSN2(ret, line, invokesuper,
                  new_callinfo(iseq, 0, argc, flag | VM_CALL_SUPER | (type == NODE_ZSUPER ? VM_CALL_ZSUPER : 0) | VM_CALL_FCALL, keywords, parent_block != NULL),
		  parent_block);
```

這邊放入的指令碼 `invokesuper` 應該就是實際呼叫 `super` 處置，後面的 `new_callinfo` 則是 `super` 呼叫傳入的參數，因此我們在 Ruby VM 實作去找 `invokesuper` 對應的行為就能反推相關的處理。

## Ruby VM

尋找了 `invokesuper` 會發現並沒有在 Ruby VM 裡面實作，而是在 `compile.c` 裡面還有優化的處理。

```c
// compile.c

  if (do_tailcallopt &&
	(IS_INSN_ID(iobj, send) ||
	 IS_INSN_ID(iobj, opt_aref_with) ||
	 IS_INSN_ID(iobj, opt_aset_with) ||
	 IS_INSN_ID(iobj, invokesuper))) {
	/*
	 *  send ...
	 *  leave
	 * =>
	 *  send ..., ... | VM_CALL_TAILCALL, ...
	 *  leave # unreachable
	 */
	INSN *piobj = NULL;
	if (iobj->link.next) {
	    LINK_ELEMENT *next = iobj->link.next;
	    do {
		if (!IS_INSN(next)) {
		    next = next->next;
		    continue;
		}
		switch (INSN_OF(next)) {
		  case BIN(nop):
		    next = next->next;
		    break;
		  case BIN(jump):
		    /* if cond
		     *   return tailcall
		     * end
		     */
		    next = get_destination_insn((INSN *)next);
		    break;
		  case BIN(leave):
		    piobj = iobj;
                    /* fall through */
		  default:
		    next = NULL;
		    break;
		}
	    } while (next);
	}

	if (piobj) {
            struct rb_call_info *ci = (struct rb_call_info *)OPERAND_AT(piobj, 0);
	    if (IS_INSN_ID(piobj, send) || IS_INSN_ID(piobj, invokesuper)) {
                if (OPERAND_AT(piobj, 1) == 0) { /* no blockiseq */
		    ci->flag |= VM_CALL_TAILCALL;
		}
	    }
	    else {
		ci->flag |= VM_CALL_TAILCALL;
	    }
	}
    }
```

像是上面這段是 Tail Call 的優化處理。

回到 `insns.def` 找線索，因為除了 `compile.c` 之外沒有其他資訊。

```c
/* super(args) # args.size => num */
DEFINE_INSN
invokesuper
(CALL_DATA cd, ISEQ blockiseq)
(...)
(VALUE val)
// attr rb_snum_t sp_inc = sp_inc_of_sendish(&cd->ci);
// attr rb_snum_t comptime_sp_inc = sp_inc_of_sendish(ci);
{
    VALUE bh = vm_caller_setup_arg_block(ec, GET_CFP(), &cd->ci, blockiseq, true);
    val = vm_sendish(ec, GET_CFP(), cd, bh, vm_search_super_method);

    if (val == Qundef) {
        RESTORE_REGS();
        NEXT_INSN();
    }
}
```

看起來 CRuby 會直接在 `insns.def` 描述實作的內容，實際上的 `super` 處理其實就在上面這段程式碼中。

* 呼叫 `vm_caller_setup_arg_block` 方法
* 呼叫 `vm_sendish` 方法，推測 `val` 會是呼叫後的回傳值

目前問題是 `insns.def` 產生的檔案為何，不過要判斷 `super` 的行為應該是足夠。

## `vm_search_super_method`

上面的 `vm_sendish` 裡面呼叫了 `vm_search_super_method` 應該就會是 `super` 行為的本體。

```c
static void
vm_search_super_method(const rb_control_frame_t *reg_cfp, struct rb_call_data *cd, VALUE recv)
{
    VALUE current_defined_class, klass;
    const rb_callable_method_entry_t *me = rb_vm_frame_method_entry(reg_cfp);
    struct rb_call_info *ci = &cd->ci;
    struct rb_call_cache *cc = &cd->cc;

    if (!me) {
	vm_super_outside();
    }

    current_defined_class = me->defined_class;

    if (!NIL_P(RCLASS_REFINED_CLASS(current_defined_class))) {
	current_defined_class = RCLASS_REFINED_CLASS(current_defined_class);
    }

    if (BUILTIN_TYPE(current_defined_class) != T_MODULE &&
	BUILTIN_TYPE(current_defined_class) != T_ICLASS && /* bound UnboundMethod */
	!FL_TEST(current_defined_class, RMODULE_INCLUDED_INTO_REFINEMENT) &&
        !rb_obj_is_kind_of(recv, current_defined_class)) {
	VALUE m = RB_TYPE_P(current_defined_class, T_ICLASS) ?
	    RBASIC(current_defined_class)->klass : current_defined_class;

	rb_raise(rb_eTypeError,
		 "self has wrong type to call super in this context: "
		 "%"PRIsVALUE" (expected %"PRIsVALUE")",
                 rb_obj_class(recv), m);
    }

    if (me->def->type == VM_METHOD_TYPE_BMETHOD && (ci->flag & VM_CALL_ZSUPER)) {
	rb_raise(rb_eRuntimeError,
		 "implicit argument passing of super from method defined"
		 " by define_method() is not supported."
		 " Specify all arguments explicitly.");
    }

    ci->mid = me->def->original_id;
    klass = vm_search_normal_superclass(me->defined_class);

    if (!klass) {
	/* bound instance method of module */
	cc->aux.method_missing_reason = MISSING_SUPER;
        CC_SET_FASTPATH(cc, vm_call_method_missing, TRUE);
    }
    else {
        /* TODO: use inline cache */
	cc->me = rb_callable_method_entry(klass, ci->mid);
        CC_SET_FASTPATH(cc, vm_call_super_method, TRUE);
    }
}
```

行為上大致上看起來是這樣

* 製作呼叫相關結構
* 檢查 `me` (Method Entry)
  * 如果不存在表示不是在 Method 中呼叫，拋出錯誤
* 檢查目前的 Class 有沒有被 `refine` 修改過
* 檢查 Class 是否正確 Ex. 呼叫者是否是該 Class 實體、是否為 Module 等
* 檢查 `BMETHOD` 情境（跟 `ZSUPER` 處理有關，推測是無法自動推出 `super` 參數的情況）
* 搜尋 `superclass` 的方法
* 設定 `mid` (Method ID) 作為呼叫用的參考

> 這塊沒有看到怎麼持續往上找的處理，有機會可以再繼續往下找

```
MJIT_STATIC const rb_callable_method_entry_t *
rb_vm_frame_method_entry(const rb_control_frame_t *cfp)
{
    const VALUE *ep = cfp->ep;
    rb_callable_method_entry_t *me;

    while (!VM_ENV_LOCAL_P(ep)) {
	if ((me = check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], FALSE)) != NULL) return me;
	ep = VM_ENV_PREV_EP(ep);
    }

    return check_method_entry(ep[VM_ENV_DATA_INDEX_ME_CREF], TRUE);
}
```

推測是在製作相關結構（Method Entry）的時候就已經跑過迴圈往回找到上一層定義的 Method Entry 來當作參考，所以實際呼叫 Super 最後設定 `mid` 只是去套用該數值。