Ruby Parser開発日誌 (24-46) - parse.yが生成するノードを変える ー パターンマッチング その9 (1行 pattern matching)
46日目: 1行 pattern matchingに対応する
前回はAs patternに対応をしました。 今回は1行 pattern matchingに取り組みます。
1行 pattern matching
1行 pattern matchingには2つの異なる書き方があります。
inによるpattern matchingの場合、patternにマッチすればtrueを、マッチしなければfalseを返します。
一方で=>によるpattern matchingの場合、マッチしないときはNoMatchingPatternError例外が投げられます。
a in [0, 1, 2] b => [0, 1, 2] #=> []: [] length mismatch (given 0, expected 3) (NoMatchingPatternError)
対応するノードについてみておきましょう。
まずはa in [0, 1, 2]からです。
# @ NODE_CASE3 (id: 11, line: 1, location: (1,0)-(1,14))* # +- nd_head: # | @ NODE_VCALL (id: 0, line: 1, location: (1,0)-(1,1)) # | +- nd_mid: :a # +- nd_body: # | @ NODE_IN (id: 10, line: 1, location: (1,5)-(1,14)) # | +- nd_head: # | | @ NODE_ARYPTN (id: 7, line: 1, location: (1,6)-(1,13)) # | | +- nd_pconst: # | | | (null node) # | | +- pre_args: # | | | @ NODE_LIST (id: 2, line: 1, location: (1,6)-(1,13)) # | | | +- as.nd_alen: 3 # | | | +- nd_head: # | | | | @ NODE_INTEGER (id: 1, line: 1, location: (1,6)-(1,7)) # | | | | +- val: 0 # | | | +- nd_head: # | | | | @ NODE_INTEGER (id: 3, line: 1, location: (1,9)-(1,10)) # | | | | +- val: 1 # | | | +- nd_head: # | | | | @ NODE_INTEGER (id: 5, line: 1, location: (1,12)-(1,13)) # | | | | +- val: 2 # | | | +- nd_next: # | | | (null node) # | | +- rest_arg: # | | | (null node) # | | +- post_args: # | | (null node) # | +- nd_body: # | | @ NODE_TRUE (id: 8, line: 1, location: (1,5)-(1,14)) # | +- nd_next: # | | @ NODE_FALSE (id: 9, line: 1, location: (1,5)-(1,14))
変更前はNODE_CASE3とNODE_INで表現しています。
そしてNODE_INのnd_bodyとnd_nextがそれぞれNODE_TRUEとNODE_FALSEになっています。
これは以下のコードと同じノードになっています。
case a in [0, 1, 2] true else false end
書き換え後はというとMatchPredicateNodeという専用のノードを使って表現するようになります。
# @ MatchPredicateNode (location: (1,0)-(1,14)) # +-- value: # | @ CallNode (location: (1,0)-(1,1)) # | +-- name: :a # +-- pattern: # | @ ArrayPatternNode (location: (1,5)-(1,14)) # | +-- constant: nil # | +-- requireds: (length: 3) # | | +-- @ IntegerNode (location: (1,6)-(1,7)) # | | | +-- IntegerBaseFlags: decimal # | | | +-- value: 0 # | | +-- @ IntegerNode (location: (1,9)-(1,10)) # | | | +-- IntegerBaseFlags: decimal # | | | +-- value: 1 # | | +-- @ IntegerNode (location: (1,12)-(1,13)) # | | +-- IntegerBaseFlags: decimal # | | +-- value: 2 # | +-- rest: nil # | +-- posts: (length: 0)
つぎにa => [0, 1, 2]の場合です。
# @ NODE_CASE3 (id: 9, line: 1, location: (1,0)-(1,14))* # +- nd_head: # | @ NODE_VCALL (id: 0, line: 1, location: (1,0)-(1,1)) # | +- nd_mid: :b # +- nd_body: # | @ NODE_IN (id: 8, line: 1, location: (1,5)-(1,14)) # | +- nd_head: # | | @ NODE_ARYPTN (id: 7, line: 1, location: (1,6)-(1,13)) # | | +- nd_pconst: # | | | (null node) # | | +- pre_args: # | | | @ NODE_LIST (id: 2, line: 1, location: (1,6)-(1,13)) # | | | +- as.nd_alen: 3 # | | | +- nd_head: # | | | | @ NODE_INTEGER (id: 1, line: 1, location: (1,6)-(1,7)) # | | | | +- val: 0 # | | | +- nd_head: # | | | | @ NODE_INTEGER (id: 3, line: 1, location: (1,9)-(1,10)) # | | | | +- val: 1 # | | | +- nd_head: # | | | | @ NODE_INTEGER (id: 5, line: 1, location: (1,12)-(1,13)) # | | | | +- val: 2 # | | | +- nd_next: # | | | (null node) # | | +- rest_arg: # | | | (null node) # | | +- post_args: # | | (null node) # | +- nd_body: # | | (null node) # | +- nd_next: # | | (null node)
書き換え前はinの場合と同様にNODE_CASE3とNODE_INで表現しています。
inと異なりnd_bodyとnd_nextがnullになっています。
これは以下のコードとおおよそ同じノードになっています。
case a in [0, 1, 2] end
書き換え後はというとMatchRequiredNodeという専用のノードを使って表現するようになります。
# @ MatchRequiredNode (location: (1,0)-(1,14)) # +-- value: # | @ CallNode (location: (1,0)-(1,1)) # | +-- name: :b # +-- pattern: # | @ ArrayPatternNode (location: (1,5)-(1,14)) # | +-- constant: nil # | +-- requireds: (length: 3) # | | +-- @ IntegerNode (location: (1,6)-(1,7)) # | | | +-- IntegerBaseFlags: decimal # | | | +-- value: 0 # | | +-- @ IntegerNode (location: (1,9)-(1,10)) # | | | +-- IntegerBaseFlags: decimal # | | | +-- value: 1 # | | +-- @ IntegerNode (location: (1,12)-(1,13)) # | | +-- IntegerBaseFlags: decimal # | | +-- value: 2 # | +-- rest: nil # | +-- posts: (length: 0)
parse.yを書き換える
parse.yの書き換えはシンプルで、対応する生成規則のアクションが生成するノードの種類を変更するだけです。
@@ -4001,7 +4005,7 @@ expr : command_call p->ctxt.in_kwarg = $ctxt.in_kwarg; p->ctxt.in_alt_pattern = $ctxt.in_alt_pattern; p->ctxt.capture_in_pattern = $ctxt.capture_in_pattern; - $$ = NEW_CASE3($arg, NEW_IN($body, 0, 0, &@body, &NULL_LOC, &NULL_LOC, &@2), &@$, &NULL_LOC, &NULL_LOC); + $$ = NEW_RB_MATCH_REQUIRED($arg, $body, &@$, &@tASSOC); /*% ripper: case!($:arg, in!($:body, Qnil, Qnil)) %*/ } | arg keyword_in @@ -4016,7 +4020,7 @@ expr : command_call p->ctxt.in_kwarg = $ctxt.in_kwarg; p->ctxt.in_alt_pattern = $ctxt.in_alt_pattern; p->ctxt.capture_in_pattern = $ctxt.capture_in_pattern; - $$ = NEW_CASE3($arg, NEW_IN($body, NEW_RB_TRUE(&@body), NEW_RB_FALSE(&@body), &@body, &@keyword_in, &NULL_LOC, &NULL_LOC), &@$, &NULL_LOC, &NULL_LOC); + $$ = NEW_RB_MATCH_PREDICATE($arg, $body, &@$, &@keyword_in); /*% ripper: case!($:arg, in!($:body, Qnil, Qnil)) %*/ }
compile.cを書き換える
いままではcase v in ...もa in [0, 1, 2]もb => [0, 1, 2]も同じ種類のノードで表現していました。
そのためcompile.cのcompile_case3という関数に任せることができました。
今回ノードを分割したため、case v in ...とその他ではpatternが単数か複数かという差異が生じるようになりました。
a in [0, 1, 2]やb => [0, 1, 2]はcase v in ...の特殊形なのですが、ここでは下手に関数を共通化せずにMatchPredicateNodeとMatchRequiredNode用にそれぞれ専用の関数を用意することにします。
それぞれのケースのバイトコードを確認するところから始めましょう。
まずはMatchPredicateNodeのケースです。
# 0000 putnil ( 1)[Li] # 0001 putself # 0002 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0004 dup # 0005 topn 2 # 0007 branchnil 18 # 0009 topn 2 # 0011 branchunless 86 # 0013 pop # 0014 topn 1 # 0016 jump 36 # 0018 dup # 0019 putobject :deconstruct # 0021 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0023 setn 3 # 0025 branchunless 86 # 0027 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0029 setn 2 # 0031 dup # 0032 checktype T_ARRAY # 0034 branchunless 77 # 0036 dup # 0037 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0039 putobject 3 # 0041 opt_eq <calldata!mid:==, argc:1, ARGS_SIMPLE>[CcCr] # 0043 branchunless 86 # 0045 dup # 0046 putobject_INT2FIX_0_ # 0047 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0049 putobject_INT2FIX_0_ # 0050 checkmatch 2 # 0052 branchunless 86 # 0054 dup # 0055 putobject_INT2FIX_1_ # 0056 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0058 putobject_INT2FIX_1_ # 0059 checkmatch 2 # 0061 branchunless 86 # 0063 dup # 0064 putobject 2 # 0066 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0068 putobject 2 # 0070 checkmatch 2 # 0072 branchunless 86 # 0074 pop # 0075 jump 92 # 0077 putspecialobject 1 # 0079 putobject TypeError # 0081 putobject "deconstruct must return Array" # 0083 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0085 pop # 0086 pop # 0087 pop # 0088 pop # 0089 putobject false # 0091 leave # 0092 adjuststack 2 # 0094 putobject true # 0096 leave a in [0, 1, 2]
さきほど触れたように以下のコードと同様のバイトコードになります。
そのためマッチに成功したときの値として0094 putobject trueが、失敗したときの値として0089 putobject falseのバイトコードが生成されています。
case a in [0, 1, 2] true else false end
compile_match_predicate関数の実装はcompile_case3関数の実装から必要な部分を切り出したものになります。
static int compile_match_predicate(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const rb_match_predicate_node_t *const node, int popped) { const NODE *line_node = node->pattern; LABEL *endlabel, *elselabel; DECL_ANCHOR(head); DECL_ANCHOR(body_seq); DECL_ANCHOR(cond_seq); int line; VALUE branches = 0; int branch_id = 0; INIT_ANCHOR(head); INIT_ANCHOR(body_seq); INIT_ANCHOR(cond_seq); branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), "case"); line = nd_line(line_node); endlabel = NEW_LABEL(line); elselabel = NEW_LABEL(line); ADD_INSN(head, line_node, putnil); /* allocate stack for cached #deconstruct value */ CHECK(COMPILE(head, "case base", node->value)); ADD_SEQ(ret, head); /* case VAL */ { const NODE *pattern = node->pattern; line = nd_line(pattern); LABEL *l1 = NEW_LABEL(line); ADD_LABEL(body_seq, l1); ADD_INSN1(body_seq, line_node, adjuststack, INT2FIX(2)); const NODE *const coverage_node = pattern; add_trace_branch_coverage( iseq, body_seq, nd_code_loc(coverage_node), nd_node_id(coverage_node), branch_id++, "in", branches); if (!popped) ADD_INSN1(body_seq, line_node, putobject, Qtrue); ADD_INSNL(body_seq, line_node, jump, endlabel); int pat_line = nd_line(pattern); LABEL *next_pat = NEW_LABEL(pat_line); ADD_INSN (cond_seq, pattern, dup); /* dup case VAL */ // NOTE: set base_index (it's "under" the matchee value, so it's position is 2) CHECK(iseq_compile_pattern_each(iseq, cond_seq, pattern, l1, next_pat, false, false, 2, true)); ADD_LABEL(cond_seq, next_pat); LABEL_UNREMOVABLE(next_pat); } { ADD_LABEL(cond_seq, elselabel); ADD_INSN(cond_seq, line_node, pop); ADD_INSN(cond_seq, line_node, pop); /* discard cached #deconstruct value */ add_trace_branch_coverage(iseq, cond_seq, nd_code_loc(node), nd_node_id(node), branch_id, "else", branches); if (!popped) ADD_INSN1(cond_seq, line_node, putobject, Qfalse); ADD_INSNL(cond_seq, line_node, jump, endlabel); ADD_INSN(cond_seq, line_node, putnil); if (popped) { ADD_INSN(cond_seq, line_node, putnil); } } ADD_SEQ(ret, cond_seq); ADD_SEQ(ret, body_seq); ADD_LABEL(ret, endlabel); return COMPILE_OK; }
buildして実行してみると期待した通りの結果が得られます。
def m(a) a in [0, 1, 2] end p m([]) #=> false p m([0, 1, 2]) #=> true
次にMatchRequiredNodeのケースです。
# 0000 putnil ( 1)[Li] # 0001 putnil # 0002 putobject false # 0004 putnil # 0005 putnil # 0006 putself # 0007 opt_send_without_block <calldata!mid:b, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0009 dup # 0010 topn 2 # 0012 branchnil 23 # 0014 topn 2 # 0016 branchunless 215 # 0018 pop # 0019 topn 1 # 0021 jump 60 # 0023 dup # 0024 putobject :deconstruct # 0026 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0028 setn 3 # 0030 dup # 0031 branchif 49 # 0033 putspecialobject 1 # 0035 putobject "%p does not respond to #deconstruct" # 0037 topn 3 # 0039 opt_send_without_block <calldata!mid:core#sprintf, argc:2, ARGS_SIMPLE> # 0041 setn 5 # 0043 putobject false # 0045 setn 7 # 0047 pop # 0048 pop # 0049 branchunless 215 # 0051 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0053 setn 2 # 0055 dup # 0056 checktype T_ARRAY # 0058 branchunless 206 # 0060 dup # 0061 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0063 putobject 3 # 0065 opt_eq <calldata!mid:==, argc:1, ARGS_SIMPLE>[CcCr] # 0067 dup # 0068 branchif 91 # 0070 putspecialobject 1 # 0072 putobject "%p length mismatch (given %p, expected %p)" # 0074 topn 3 # 0076 dup # 0077 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0079 putobject 3 # 0081 opt_send_without_block <calldata!mid:core#sprintf, argc:4, ARGS_SIMPLE> # 0083 setn 5 # 0085 putobject false # 0087 setn 7 # 0089 pop # 0090 pop # 0091 branchunless 215 # 0093 dup # 0094 putobject_INT2FIX_0_ # 0095 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0097 putobject_INT2FIX_0_ # 0098 dupn 2 # 0100 checkmatch 2 # 0102 dup # 0103 branchif 123 # 0105 putspecialobject 1 # 0107 putobject "%p === %p does not return true" # 0109 topn 3 # 0111 topn 5 # 0113 opt_send_without_block <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE> # 0115 setn 7 # 0117 putobject false # 0119 setn 9 # 0121 pop # 0122 pop # 0123 setn 2 # 0125 pop # 0126 pop # 0127 branchunless 215 # 0129 dup # 0130 putobject_INT2FIX_1_ # 0131 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0133 putobject_INT2FIX_1_ # 0134 dupn 2 # 0136 checkmatch 2 # 0138 dup # 0139 branchif 159 # 0141 putspecialobject 1 # 0143 putobject "%p === %p does not return true" # 0145 topn 3 # 0147 topn 5 # 0149 opt_send_without_block <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE> # 0151 setn 7 # 0153 putobject false # 0155 setn 9 # 0157 pop # 0158 pop # 0159 setn 2 # 0161 pop # 0162 pop # 0163 branchunless 215 # 0165 dup # 0166 putobject 2 # 0168 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0170 putobject 2 # 0172 dupn 2 # 0174 checkmatch 2 # 0176 dup # 0177 branchif 197 # 0179 putspecialobject 1 # 0181 putobject "%p === %p does not return true" # 0183 topn 3 # 0185 topn 5 # 0187 opt_send_without_block <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE> # 0189 setn 7 # 0191 putobject false # 0193 setn 9 # 0195 pop # 0196 pop # 0197 setn 2 # 0199 pop # 0200 pop # 0201 branchunless 215 # 0203 pop # 0204 jump 262 # 0206 putspecialobject 1 # 0208 putobject TypeError # 0210 putobject "deconstruct must return Array" # 0212 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0214 pop # 0215 pop # 0216 putspecialobject 1 # 0218 topn 4 # 0220 branchif 238 # 0222 putobject NoMatchingPatternError # 0224 putspecialobject 1 # 0226 putobject "%p: %s" # 0228 topn 4 # 0230 topn 7 # 0232 opt_send_without_block <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE> # 0234 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0236 jump 258 # 0238 putobject NoMatchingPatternKeyError # 0240 putspecialobject 1 # 0242 putobject "%p: %s" # 0244 topn 4 # 0246 topn 7 # 0248 opt_send_without_block <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE> # 0250 topn 7 # 0252 topn 9 # 0254 opt_send_without_block <calldata!mid:new, argc:3, kw:[#<Symbol:0x000000000023410c>,#<Symbol:0x000000000022010c>], KWARG> # 0256 opt_send_without_block <calldata!mid:core#raise, argc:1, ARGS_SIMPLE> # 0258 adjuststack 7 # 0260 putnil # 0261 leave # 0262 adjuststack 6 # 0264 putnil # 0265 leave b => [0, 1, 2]
0216 putspecialobject 1から0256 opt_send_without_block <calldata!mid:core#raise, argc:1, ARGS_SIMPLE>がmatchしなかったときにNoMatchingPatternErrorを投げるためのバイトコードです。
matchに成功した場合にはnilになるというのは、以下のバイトコードが対応しています。
# 0262 adjuststack 6 # 0264 putnil # 0265 leave
compile_match_required関数の実装もまたcompile_case3関数の実装から必要な部分を抜き出したような実装になりますが、ここでは割愛します。
buildして実行してみると期待した通りの結果が得られます。
def m(a) a => [0, 1, 2] end p m([0, 1, 2]) #=> nil p m([]) #=> []: [] length mismatch (given 0, expected 3) (NoMatchingPatternError)
まとめ
- 1行 pattern matchingに対応した
パターンマッチング全体の進捗は以下の通りです。
Value patternp_primitive ("str",1,:symなど)range_expr (1...3など)p_var_ref (^varなど)p_expr_ref (^(cmd 1, 2)など)p_const (A,::A,A::Bなど)
Variable patternArray patternHash patternFind patternAlternative patternAs pattern後置ifと後置unless1行 pattern matching
パターンマッチングが終わったので次回はforに取り組む予定です。
Ruby Parser開発日誌 (24-45) - parse.yが生成するノードを変える ー パターンマッチング その8 (As pattern)
45日目: As patternに対応する
前回はAlternative patternに対応をしました。 今回はAs patternに取り組みます。
As pattern
As patternはpattern matchingをして、matchした部分を変数へ束縛します。
def m(v) case v in Integer => a, Integer p "matched: #{a}" else p "not matched" end end m([1]) #=> "not matched" m([1, 2]) #=> "matched: 1" m([1, 2, 3]) #=> "not matched"
ノードについて言えばNODE_HASHで表現していたInteger => aの部分をCapturePatternNodeで表現するようになります。
# Before # # @ NODE_ARYPTN (id: 12, line: 3, location: (3,5)-(3,26)) # +- nd_pconst: # | (null node) # +- pre_args: # | @ NODE_LIST (id: 13, line: 3, location: (3,5)-(3,26)) # | +- as.nd_alen: 2 # | +- nd_head: # | | @ NODE_HASH (id: 9, line: 3, location: (3,5)-(3,17)) # | | +- nd_brace: 0 (keyword argument) # | | +- nd_head: # | | @ NODE_LIST (id: 7, line: 3, location: (3,5)-(3,17)) # | | +- as.nd_alen: 2 # | | +- nd_head: # | | | @ NODE_CONST (id: 5, line: 3, location: (3,5)-(3,12)) # | | | +- nd_vid: :Integer # | | +- nd_head: # | | | @ NODE_LASGN (id: 6, line: 3, location: (3,16)-(3,17)) # | | | +- nd_vid: :a # | | | +- nd_value: # | | | (null node) # | | +- nd_next: # | | (null node) # | +- nd_head: # | | @ NODE_CONST (id: 10, line: 3, location: (3,19)-(3,26)) # | | +- nd_vid: :Integer # After # # @ ArrayPatternNode (location: (3,5)-(3,26)) # +-- constant: nil # +-- requireds: (length: 2) # | +-- @ CapturePatternNode (location: (3,5)-(3,17)) # | | +-- value: # | | | @ ConstantReadNode (location: (3,5)-(3,12)) # | | | +-- name: :Integer # | | +-- target: # | | | @ LocalVariableTargetNode (location: (3,16)-(3,17)) # | | | +-- name: :a # | | | +-- depth: 0 # | | +-- operator_loc: (3,13)-(3,15) = "=>" # | +-- @ ConstantReadNode (location: (3,19)-(3,26)) # | +-- name: :Integer # +-- rest: nil # +-- posts: (length: 0) def m(v) case v in Integer => a, Integer p "matched: #{a}" else p "not matched" end end
parse.yを書き換える
parse.yの書き換えは単純で該当する生成規則のアクションで生成するノードを変更するだけです。
p_as : p_expr tASSOC p_variable
{
- NODE *n = NEW_LIST($1, &@$);
- n = list_append(p, n, $3);
- $$ = new_hash(p, n, &@$);
+ $$ = NEW_RB_CAPTURE_PATTERN($1, $3, &@$, &@2);
/*% ripper: binary!($:1, ID2VAL((id_assoc)), $:3) %*/
}
| p_alt
変更前はnew_hash関数を呼び出していました。
new_hash関数は内部でwarn_duplicate_keys関数を呼び出し、keyに重複がある場合に警告を出します。
static NODE * new_hash(struct parser_params *p, NODE *hash, const YYLTYPE *loc) { if (hash) warn_duplicate_keys(p, hash); return NEW_HASH(hash, loc); }
しかしp_as : p_expr tASSOC p_variableで生成されていたNODE_HASHはkeyとvalueがちょうど1つずつのノードを生成しています。
これはiseq_compile_pattern_each関数のassertionにも表れています。
ということはas patternに関して言えばwarn_duplicate_keys関数が警告をだすことはありません。
そのためアクションでCapturePatternNodeを直接生成するように書き換えても問題ないはずです。
case NODE_HASH: { NODE *n; LABEL *match_failed; match_failed = NEW_LABEL(line); n = RNODE_HASH(node)->nd_head; if (! (nd_type_p(n, NODE_LIST) && RNODE_LIST(n)->as.nd_alen == 2)) { COMPILE_ERROR(ERROR_ARGS "unexpected node"); return COMPILE_NG; } ADD_INSN(ret, line_node, dup); // (1) CHECK(iseq_compile_pattern_match(iseq, ret, RNODE_LIST(n)->nd_head, match_failed, in_single_pattern, in_alt_pattern, base_index + 1 /* (1) */, use_deconstructed_cache)); CHECK(iseq_compile_pattern_each(iseq, ret, RNODE_LIST(RNODE_LIST(n)->nd_next)->nd_head, matched, match_failed, in_single_pattern, in_alt_pattern, base_index, false)); ADD_INSN(ret, line_node, putnil); ADD_LABEL(ret, match_failed); ADD_INSN(ret, line_node, pop); ADD_INSNL(ret, line_node, jump, unmatched); break; }
compile.cを書き換える
compile.cを書き換える前に生成されるべきバイトコードを確認しておきましょう。
# 0000 putnil ( 3)[LiCa] # 0001 getlocal_WC_0 v@0 ( 2) # 0003 dup ( 3) # 0004 topn 2 # 0006 branchnil 17 # 0008 topn 2 # 0010 branchunless 84 # 0012 pop # 0013 topn 1 # 0015 jump 35 # 0017 dup # 0018 putobject :deconstruct # 0020 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0022 setn 3 # 0024 branchunless 84 # 0026 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0028 setn 2 # 0030 dup # 0031 checktype T_ARRAY # 0033 branchunless 75 # 0035 dup # 0036 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0038 putobject 2 # 0040 opt_eq <calldata!mid:==, argc:1, ARGS_SIMPLE>[CcCr] # 0042 branchunless 84 # 0044 dup # 0045 putobject_INT2FIX_0_ # 0046 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0048 dup # 0049 opt_getconstant_path <ic:0 Integer> # 0051 checkmatch 2 # 0053 branchunless 59 # 0055 setlocal_WC_0 a@1 # 0057 jump 62 # 0059 pop # 0060 jump 84 # 0062 dup # 0063 putobject_INT2FIX_1_ # 0064 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0066 opt_getconstant_path <ic:1 Integer> # 0068 checkmatch 2 # 0070 branchunless 84 # 0072 pop # 0073 jump 93 # 0075 putspecialobject 1 # 0077 putobject TypeError # 0079 putobject "deconstruct must return Array" # 0081 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0083 pop # 0084 pop # 0085 pop ( 6) # 0086 pop # 0087 putself [Li] # 0088 putchilledstring "not matched" # 0090 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0092 leave ( 8)[Re] # 0093 adjuststack 2 ( 3) # 0095 putself ( 4)[Li] # 0096 putobject "matched: " # 0098 getlocal_WC_0 a@1 # 0100 dup # 0101 objtostring <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE> # 0103 anytostring # 0104 concatstrings 2 # 0106 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0108 leave ( 8)[Re] def m(v) case v in Integer => a, Integer p "matched: #{a}" else p "not matched" end end
このスクリプトの場合、生成されるバイトコードの基本的な構造はArray patternと同様のものになります。
Array patternとの差異は最初の要素のmatchingに成功した場合に、0055 setlocal_WC_0 a@1を実行してローカル変数へ値を束縛することです。
# 0044 dup # 0045 putobject_INT2FIX_0_ # 0046 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0048 dup # 0049 opt_getconstant_path <ic:0 Integer> # 0051 checkmatch 2 # 0053 branchunless 59 # 0055 setlocal_WC_0 a@1 <- ここ # 0057 jump 62 # 0059 pop # 0060 jump 84 # 0062 dup # 0063 putobject_INT2FIX_1_ # 0064 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0066 opt_getconstant_path <ic:1 Integer> # 0068 checkmatch 2 # 0070 branchunless 84 # 0072 pop # 0073 jump 93
compile.cの書き換えは構造体の変更に合わせた修正だけなので、ここでは割愛します。 buildして実行してみると期待した通りの結果が得られます。
def m(v) case v in Integer => a, Integer p "matched: #{a}" else p "not matched" end end m([1]) # #=> "not matched" m([1, 2]) # #=> "matched: 1" m([1, 2, 3]) # #=> "not matched"
まとめ
- As patternに対応した
パターンマッチング全体の進捗は以下の通りです。
ここまできて、a in [0, 1, 2]やa => [0, 1, 2]といった1行 pattern matchingの対応をしていないことに気がついたので、次回取り組む予定です。
Value patternp_primitive ("str",1,:symなど)range_expr (1...3など)p_var_ref (^varなど)p_expr_ref (^(cmd 1, 2)など)p_const (A,::A,A::Bなど)
Variable patternArray patternHash patternFind patternAlternative patternAs pattern後置ifと後置unless- 1行 pattern matching
Ruby Parser開発日誌 (24-44) - parse.yが生成するノードを変える ー パターンマッチング その7 (Alternative pattern)
44日目: Alternative patternに対応する
前回はFind patternに対応をしました。 今回はAlternative patternに取り組みます。
Alternative pattern
Alternative patternは|で複数のpatternを列挙したものです。
def m(v) case v in Array | String p "matched" else p "not matched" end end m([]) #=> "matched" m("str") #=> "matched" m(1) #=> "not matched"
ORノードを使って表現したいたところがAlternationPatternNodeになります。
# Before # # @ NODE_IN (id: 14, line: 3, location: (3,2)-(6,19)) # +- nd_head: # | @ NODE_OR (id: 7, line: 3, location: (3,5)-(3,19)) # | +- nd_1st: # | | @ NODE_CONST (id: 5, line: 3, location: (3,5)-(3,10)) # | | +- nd_vid: :Array # | +- nd_2nd: # | | @ NODE_CONST (id: 6, line: 3, location: (3,13)-(3,19)) # | | +- nd_vid: :String # | +- operator_loc: (3,11)-(3,12) # After # # @ InNode (location: (3,2)-(4,15)) # +-- pattern: # | @ AlternationPatternNode (location: (3,5)-(3,19)) # | +-- left: # | | @ ConstantReadNode (location: (3,5)-(3,10)) # | | +-- name: :Array # | +-- right: # | | @ ConstantReadNode (location: (3,13)-(3,19)) # | | +-- name: :String # | +-- operator_loc: (3,11)-(3,12) = "|" def m(v) case v in Array | String p "matched" else p "not matched" end end
以下のようにそれぞれのmatchを試してjumpするというバイトコードが生成されます。
# 0000 putnil ( 3)[LiCa] # 0001 getlocal_WC_0 v@0 ( 2) # 0003 dup ( 3) # # Arrayとのmatching # # 0004 dup # 0005 opt_getconstant_path <ic:0 Array> # 0007 checkmatch 2 # 0009 branchunless 14 # 0011 pop # 0012 jump 28 # # Stringとのmatching # # 0014 opt_getconstant_path <ic:1 String> # 0016 checkmatch 2 # 0018 branchif 28 # 0020 pop ( 6) # 0021 pop # # どちらもmatchしなかったとき # # 0022 putself [Li] # 0023 putchilledstring "not matched" # 0025 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0027 leave ( 8)[Re] # # どちらかにmatchしたとき # # 0028 adjuststack 2 ( 3) # 0030 putself ( 4)[Li] # 0031 putchilledstring "matched" # 0033 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0035 leave ( 8)[Re] def m(v) case v in Array | String p "matched" else p "not matched" end end
parse.yでは生成するノードの種類を変更するだけです。
@@ -6069,7 +6070,7 @@ p_alt : p_alt[left] '|'[alt] yyerror1(&@alt, "alternative pattern after variable capture"); } p->ctxt.in_alt_pattern = 0; - $$ = NEW_OR($left, $right, &@$, &@alt); + $$ = NEW_RB_ALTERNATION_PATTERN($left, $right, &@$, &@alt); /*% ripper: binary!($:left, ID2VAL(idOr), $:right) %*/ } | p_expr_basic
compile.cも合わせてマクロなどを修正します。
@@ -8467,21 +8467,21 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *n // ADD_INSNL(ret, line_node, jump, unmatched); // break; // } - // case NODE_OR: { - // LABEL *match_succeeded, *fin; - // match_succeeded = NEW_LABEL(line); - // fin = NEW_LABEL(line); + case RB_ALTERNATION_PATTERN_NODE: { + LABEL *match_succeeded, *fin; + match_succeeded = NEW_LABEL(line); + fin = NEW_LABEL(line); - // ADD_INSN(ret, line_node, dup); // (1) - // CHECK(iseq_compile_pattern_each(iseq, ret, RNODE_OR(node)->nd_1st, match_succeeded, fin, in_single_pattern, true, base_index + 1 /* (1) */, use_deconstructed_cache)); - // ADD_LABEL(ret, match_succeeded); - // ADD_INSN(ret, line_node, pop); - // ADD_INSNL(ret, line_node, jump, matched); - // ADD_INSN(ret, line_node, putnil); - // ADD_LABEL(ret, fin); - // CHECK(iseq_compile_pattern_each(iseq, ret, RNODE_OR(node)->nd_2nd, matched, unmatched, in_single_pattern, true, base_index, use_deconstructed_cache)); - // break; - // } + ADD_INSN(ret, line_node, dup); // (1) + CHECK(iseq_compile_pattern_each(iseq, ret, RB_NODE_ALTERNATION_PATTERN(node)->left, match_succeeded, fin, in_single_pattern, true, base_index + 1 /* (1) */, use_deconstructed_cache)); + ADD_LABEL(ret, match_succeeded); + ADD_INSN(ret, line_node, pop); + ADD_INSNL(ret, line_node, jump, matched); + ADD_INSN(ret, line_node, putnil); + ADD_LABEL(ret, fin); + CHECK(iseq_compile_pattern_each(iseq, ret, RB_NODE_ALTERNATION_PATTERN(node)->right, matched, unmatched, in_single_pattern, true, base_index, use_deconstructed_cache)); + break; + }
buildして実行してみると期待した通りの結果が得られます。
def m(v) case v in Array | String p "matched" else p "not matched" end end m([1]) #=> "matched" m("str") #=> "matched" m(1) #=> "not matched"
まとめ
- Alternative patternに対応した
パターンマッチング全体の進捗は以下の通りです。
Value patternp_primitive ("str",1,:symなど)range_expr (1...3など)p_var_ref (^varなど)p_expr_ref (^(cmd 1, 2)など)p_const (A,::A,A::Bなど)
Variable patternArray patternHash patternFind patternAlternative pattern- As pattern
後置ifと後置unless
次回はAs patternの対応をする予定です。
Ruby Parser開発日誌 (24-43) - parse.yが生成するノードを変える ー パターンマッチング その6 (Find pattern)
43日目: Find patternに対応する
前回はHash patternに対応をしました。 今回はFind patternに取り組みます。
Find pattern
Find patternはArray patternによく似た文法構造をしています。 Array patternと異なるのは最初の要素と最後の要素がともにsplatになっているという点です。
case ["a", 1, "b", "c", 2] in [*, String, String, *] p "matched" else p "not matched" end #=> "matched"
書き換え前後のノードを比較するとフィールド名は異なるものの、4つのフィールドが存在することがわかります。 これまでと同様にフィールド名に気をつけて書き直せば良さそうです。
# Before # # @ NODE_FNDPTN (id: 14, line: 2, location: (2,4)-(2,24)) # +- nd_pconst: # | (null node) # +- pre_rest_arg: NODE_SPECIAL_NO_NAME_REST (rest argument without name) # +- args: # | @ NODE_LIST (id: 11, line: 2, location: (2,7)-(2,21)) # | +- as.nd_alen: 2 # | +- nd_head: # | | @ NODE_CONST (id: 10, line: 2, location: (2,7)-(2,13)) # | | +- nd_vid: :String # | +- nd_head: # | | @ NODE_CONST (id: 12, line: 2, location: (2,15)-(2,21)) # | | +- nd_vid: :String # | +- nd_next: # | (null node) # +- post_rest_arg: NODE_SPECIAL_NO_NAME_REST (rest argument without name) # After # # @ FindPatternNode (location: (2,3)-(2,25)) # +-- constant: nil # +-- left: # | @ SplatNode (location: (2,4)-(2,5)) # | +-- operator_loc: (2,4)-(2,5) = "*" # | +-- expression: nil # +-- requireds: (length: 2) # | +-- @ ConstantReadNode (location: (2,7)-(2,13)) # | | +-- name: :String # | +-- @ ConstantReadNode (location: (2,15)-(2,21)) # | +-- name: :String # +-- right: # | @ SplatNode (location: (2,23)-(2,24)) # | +-- operator_loc: (2,23)-(2,24) = "*" # | +-- expression: nil case ["a", 1, "b", "c", 2] in [*, String, String, *] p "matched" else p "not matched" end
それぞれのフィールドの意味は以下のとおりです。
| Before | After | |
|---|---|---|
| nd_pconst | constant | in H[*, String, String, *] の H |
| pre_rest_arg | left | in [*pre, String, String, *post] の *pre |
| args | requireds | in [*pre, String, String, *post] の String, String |
| post_rest_arg | right | in [*pre, String, String, *post] の *post |
parse.yを書き換える
parse.yの書き換えは基本的に生成する構造体を変更するだけです。
特筆すべき点としては*のときのp_restという生成規則の値が0からSplatNodeに変わっているため、new_find_pattern_tail関数内部での分岐が消えるという点です。
p_find : p_rest ',' p_args_post ',' p_rest { $$ = new_find_pattern_tail(p, $1, $3, $5, &@$); /*% ripper: [$:1, $:3, $:5] %*/ } ; // Before p_rest : tSTAR tIDENTIFIER { error_duplicate_pattern_variable(p, $2, &@2); /*% ripper: var_field!($:2) %*/ $$ = assignable(p, $2, 0, &@$); } | tSTAR { $$ = 0; /*% ripper: var_field!(Qnil) %*/ } ; // After p_rest : tSTAR tIDENTIFIER { error_duplicate_pattern_variable(p, $2, &@2); /*% ripper: var_field!($:2) %*/ $$ = NEW_RB_SPLAT(assignable_target(p, $2, &@$), &@$, &@tSTAR); } | tSTAR { $$ = NEW_RB_SPLAT(0, &@tSTAR, &@tSTAR); /*% ripper: var_field!(Qnil) %*/ } ;
-static NODE* +static NODE * new_find_pattern_tail(struct parser_params *p, NODE *pre_rest_arg, NODE *args, NODE *post_rest_arg, const YYLTYPE *loc) { - pre_rest_arg = pre_rest_arg ? pre_rest_arg : NODE_SPECIAL_NO_NAME_REST; - post_rest_arg = post_rest_arg ? post_rest_arg : NODE_SPECIAL_NO_NAME_REST; - NODE *node = NEW_FNDPTN(pre_rest_arg, args, post_rest_arg, loc); - - return node; + return NEW_RB_FIND_PATTERN(pre_rest_arg, args, post_rest_arg, loc);; }
compile.cを書き換える
compile.cを書き換える前に生成されるべきバイトコードを確認しておきましょう。
# 0000 putnil ( 3)[LiCa] # 0001 getlocal_WC_0 v@0 ( 2) # 0003 dup ( 3) # 0004 topn 2 # 0006 branchnil 17 # 0008 topn 2 # 0010 branchunless 151 # 0012 pop # 0013 topn 1 # 0015 jump 35 # 0017 dup # 0018 putobject :deconstruct # 0020 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0022 setn 3 # 0024 branchunless 151 # 0026 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0028 setn 2 # 0030 dup # 0031 checktype T_ARRAY # 0033 branchunless 142 # 0035 dup # 0036 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0038 putobject 3 # 0040 opt_ge <calldata!mid:>=, argc:1, ARGS_SIMPLE>[CcCr] # 0042 branchunless 151 # 0044 dup # 0045 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0047 dup # 0048 putobject 3 # 0050 opt_minus <calldata!mid:-, argc:1, ARGS_SIMPLE>[CcCr] # 0052 putobject_INT2FIX_0_ # 0053 dup # 0054 topn 2 # 0056 opt_le <calldata!mid:<=, argc:1, ARGS_SIMPLE>[CcCr] # 0058 branchunless 133 # 0060 topn 3 # 0062 topn 1 # 0064 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0066 putobject :s1 # 0068 checkmatch 2 # 0070 branchunless 128 # 0072 topn 3 # 0074 topn 1 # 0076 putobject_INT2FIX_1_ # 0077 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0079 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0081 putobject :s2 # 0083 checkmatch 2 # 0085 branchunless 128 # 0087 topn 3 # 0089 topn 1 # 0091 putobject 2 # 0093 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0095 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0097 putobject :s3 # 0099 checkmatch 2 # 0101 branchunless 128 # 0103 topn 3 # 0105 putobject_INT2FIX_0_ # 0106 topn 2 # 0108 opt_send_without_block <calldata!mid:[], argc:2, ARGS_SIMPLE> # 0110 setlocal_WC_0 r1@1 # 0112 topn 3 # 0114 topn 1 # 0116 putobject 3 # 0118 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0120 topn 3 # 0122 opt_send_without_block <calldata!mid:[], argc:2, ARGS_SIMPLE> # 0124 setlocal_WC_0 r2@2 # 0126 jump 137 # 0128 putobject_INT2FIX_1_ # 0129 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0131 jump 53 # 0133 adjuststack 3 # 0135 jump 151 # 0137 adjuststack 3 # 0139 pop # 0140 jump 160 # 0142 putspecialobject 1 # 0144 putobject TypeError # 0146 putobject "deconstruct must return Array" # 0148 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0150 pop # 0151 pop # 0152 pop ( 6) # 0153 pop # 0154 putself [Li] # 0155 putobject :b # 0157 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0159 leave ( 8)[Re] # 0160 adjuststack 2 ( 3) # 0162 putself ( 4)[Li] # 0163 putobject :a # 0165 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0167 leave ( 8)[Re] def m(v) case v in [*r1, :s1, :s2, :s3, *r2] p :a else p :b end end
これまでと同様にはじめにスタックの調整をしてcase vの部分を評価します。
# `compile_case3` function # # 0000 putnil ( 3)[LiCa] # 0001 getlocal_WC_0 v@0 ( 2) # 0003 dup ( 3)
Array patternと同様に#deconstructの結果をキャッシュしている場合のチェックと、キャッシュがない場合に#deconstructを呼び出すためのバイトコードが続きます。
# キャッシュのチェック # # 0004 topn 2 # 0006 branchnil 17 # 0008 topn 2 # 0010 branchunless 151 # 0012 pop # 0013 topn 1 # 0015 jump 35 # 0017 dup # `#deconstruct`の呼び出し # # 0018 putobject :deconstruct # 0020 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0022 setn 3 # 0024 branchunless 151 # 0026 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0028 setn 2 # 0030 dup # 0031 checktype T_ARRAY # 0033 branchunless 142
0033の命令までを実行し終わったときのスタックの様子は以下のとおりです。
# 0017まで v v v nil # 0020まで v.respond_to?(:deconstruct) v v nil # 0022まで v.respond_to?(:deconstruct) v v v.respond_to?(:deconstruct) # 0026まで v.deconstruct v v.respond_to?(:deconstruct) # 0033まで v.deconstruct v v.deconstruct
ここまでで
vが#deconstructを実装していることを確認するv.deconstructをキャッシュ領域(スタックの底)に記録するv.deconstructがArrayオブジェクトであることを確認する
の3つが完了しています。
[*r1, :s1, :s2, :s3, *r2]にマッチするためにはv.deconstructが少なくとも3以上の要素をもつ配列でなければいけません。
0035から0042では配列の長さをチェックします。
# 0035 dup # 0036 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0038 putobject 3 # 0040 opt_ge <calldata!mid:>=, argc:1, ARGS_SIMPLE>[CcCr] # 0042 branchunless 151 # 0042まで v.deconstruct v v.deconstruct
ここからマッチングを行なっていきます。
[:s1, :s2, :s3, ...]以外にも[0, :s1, :s2, :s3, ...]や[0, 0, :s1, :s2, :s3, ...]など様々な配列が[*r1, :s1, :s2, :s3, *r2]にマッチします。
そこでFind patternのマッチングでは:s1, :s2, :s3という並びが見つかるまでindexを1つずつずらしながらチェックを行います。
0052までの命令でスタックに必要な情報を載せておきます。
# 0044 dup # 0045 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0047 dup # 0048 putobject 3 # 0050 opt_minus <calldata!mid:-, argc:1, ARGS_SIMPLE>[CcCr] # 0052 putobject_INT2FIX_0_ # 0052まで 0 # index v.deconstruct.length - 3 v.deconstruct.length v.deconstruct v v.deconstruct
もしもindexより後ろの要素数がパターンの数(この場合は3)よりも少なくなってしまったときには、その時点でマッチングは失敗します。
[0, 1, :s1, :s2, :s4] ^---- index
そのためのチェックを行なっているのが0053から0058の命令です。
# 0053 dup # 0054 topn 2 # 0056 opt_le <calldata!mid:<=, argc:1, ARGS_SIMPLE>[CcCr] # 0058 branchunless 133 # 0056まで 0 <= v.deconstruct.length - 3 0 # index v.deconstruct.length - 3 v.deconstruct.length v.deconstruct v v.deconstruct
実際に:s1, :s2, :s3という並びがあるかチェックするための命令が続きます。
途中でマッチしなくなった場合には0128の命令に飛びます。
0128から0131ではindexを1増やして0053の命令に戻ります。
0053はさきほど説明したとおり、indexの後ろにマッチングに必要な数の要素が残っているかをチェックするための命令です。
# 0060 topn 3 # 0062 topn 1 # 0064 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0066 putobject :s1 # 0068 checkmatch 2 # 0070 branchunless 128 # 0072 topn 3 # 0074 topn 1 # 0076 putobject_INT2FIX_1_ # 0077 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0079 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0081 putobject :s2 # 0083 checkmatch 2 # 0085 branchunless 128 # 0087 topn 3 # 0089 topn 1 # 0091 putobject 2 # 0093 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0095 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0097 putobject :s3 # 0099 checkmatch 2 # 0101 branchunless 128 # 0128 putobject_INT2FIX_1_ # 0129 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0131 jump 53
マッチした場合には0103以降の命令を実行します。
ここではindexを基準にしてr1とr2に代入を行います。
# 0103 topn 3 # 0105 putobject_INT2FIX_0_ # 0106 topn 2 # 0108 opt_send_without_block <calldata!mid:[], argc:2, ARGS_SIMPLE> # 0110 setlocal_WC_0 r1@1 # 0112 topn 3 # 0114 topn 1 # 0116 putobject 3 # 0118 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0120 topn 3 # 0122 opt_send_without_block <calldata!mid:[], argc:2, ARGS_SIMPLE> # 0124 setlocal_WC_0 r2@2 # 0126 jump 137
compile.cは構造体の変更に合わせて修正するだけなので割愛します。 buildして実行してみると期待した通りの結果が得られます。
case ["a", 1, "b", "c", 2] in [*r1, String, String, *r2] p r1 p "matched" p r2 else p "not matched" end #=> ["a", 1] #=> "matched" #=> [2]
まとめ
- Find patternに対応した
パターンマッチング全体の進捗は以下の通りです。
Value patternp_primitive ("str",1,:symなど)range_expr (1...3など)p_var_ref (^varなど)p_expr_ref (^(cmd 1, 2)など)p_const (A,::A,A::Bなど)
Variable patternArray patternHash patternFind pattern- Alternative pattern
- As pattern
後置ifと後置unless
次回はAlternative patternの対応をする予定です。
Ruby Parser開発日誌 (24-42) - parse.yが生成するノードを変える ー パターンマッチング その5 (Hash pattern)
42日目: Hash patternに対応する
前回はArray patternに対応をしました。 今回はHash patternに取り組みます。
Hash pattern
Hash patternもまた読んで字の如くハッシュを表したパターンです。
def m(v) case v in {k: :v, k2: :v2} p :a in {k: :v} p :b else p :c end end m({k: :v}) #=> :b m({k: :v, k2: :v2}) #=> :a m({k: :v, k3: :v3}) #=> :b m({k: :v0}) #=> :c
書き換え前後のノードを比較するとフィールド名は異なるものの、3つのフィールドが存在することがわかります。 Array patternと同様にフィールド名に気をつけて書き直せば良さそうです。
def m(v) case v # Before # # @ NODE_HSHPTN (id: 10, line: 3, location: (3,6)-(3,11)) # +- nd_pconst: # | (null node) # +- nd_pkwargs: # | @ NODE_HASH (id: 9, line: 3, location: (3,6)-(3,11)) # | +- nd_brace: 0 (keyword argument) # | +- nd_head: # | @ NODE_LIST (id: 7, line: 3, location: (3,6)-(3,11)) # +- nd_pkwrestarg: # (null node) # After # # @ HashPatternNode (location: (3,5)-(3,12)) # +-- constant: nil # +-- elements: (length: 1) # | +-- @ AssocNode (location: (3,6)-(3,11)) # +-- rest: nil in {k: :v} p :a end end
それぞれのフィールドの意味は以下のとおりです。
| Before | After | |
|---|---|---|
| nd_pconst | constant | in H(k: :v) の H |
| nd_pkwargs | elements | in {k: :v} の {k: :v} |
| nd_pkwrestarg | rest | in {k: :v, **rest} の **rest |
ところでrestの部分には4つの形式があります。
| Before | After | |
|---|---|---|
in {k: :v} |
nil | nil |
in {k: :v, **rest} |
NODE_LASGN |
AssocSplatNode |
in {k: :v, **} |
nil | AssocSplatNode (valueがnil) |
in {k: :v, **nil} |
NODE_SPECIAL_NO_REST_KEYWORD |
NoKeywordsParameterNode |
ちなみにin {k: :v}とin k: :vではHashPatternNodeのopening_locとclosing_locを除き差異はありません。
parse.yを書き換える
parse.yの書き換えは基本的に生成する構造体を変更するだけです。
特筆すべき点をあえてあげるなら、**restや**、**nilを表す生成規則がIDではなくNODE *を生成するようになり、その結果new_hash_pattern_tail関数内部での分岐が消えることくらいかと思います。
p_kwrest : kwrest_mark tIDENTIFIER
{
- $$ = $2;
+ $$ = NEW_RB_ASSOC_SPLAT(assignable_target(p, $2, &@2), &@$, &@1);
/*% ripper: var_field!($:2) %*/
}
| kwrest_mark
{
- $$ = 0;
+ $$ = NEW_RB_ASSOC_SPLAT(0, &@$, &@1);
/*% ripper: Qnil %*/
}
;
p_kwnorest : kwrest_mark keyword_nil
{
- $$ = 0;
+ $$ = NEW_RB_NO_KEYWORDS_PARAMETER(&@$, &@1, &@2);
}
;
p_any_kwrest : p_kwrest
| p_kwnorest
{
- $$ = idNil;
/*% ripper: var_field!(ID2VAL(idNil)) %*/
}
;
-static NODE*
-new_hash_pattern_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, const YYLTYPE *loc)
+static rb_node_t *
+new_hash_pattern_tail(struct parser_params *p, rb_node_t *kw_args, rb_node_t *kw_rest_arg, const YYLTYPE *loc)
{
- NODE *node, *kw_rest_arg_node;
-
- if (kw_rest_arg == idNil) {
- kw_rest_arg_node = NODE_SPECIAL_NO_REST_KEYWORD;
- }
- else if (kw_rest_arg) {
- kw_rest_arg_node = assignable(p, kw_rest_arg, 0, loc);
- }
- else {
- kw_rest_arg_node = NULL;
- }
-
- node = NEW_HSHPTN(0, kw_args, kw_rest_arg_node, loc);
-
- return node;
+ return NEW_RB_HASH_PATTERN(0, kw_args, kw_rest_arg, loc);
}
compile.cを書き換える
compile.cを書き換える前に生成されるべきバイトコードを確認しておきましょう。
# 0000 putnil ( 2)[LiCa] # 0001 getlocal_WC_0 v@0 # 0003 dup ( 3) # 0004 dup # 0005 putobject :deconstruct_keys # 0007 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0009 branchunless 50 # 0011 duparray [:k] # 0013 opt_send_without_block <calldata!mid:deconstruct_keys, argc:1, ARGS_SIMPLE> # 0015 dup # 0016 checktype T_HASH # 0018 branchunless 41 # 0020 dup # 0021 putobject :k # 0023 opt_send_without_block <calldata!mid:key?, argc:1, ARGS_SIMPLE> # 0025 branchunless 50 # 0027 dup # 0028 putobject :k # 0030 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0032 putobject :v # 0034 checkmatch 2 # 0036 branchunless 50 # 0038 pop # 0039 jump 107 # 0041 putspecialobject 1 # 0043 putobject TypeError # 0045 putobject "deconstruct_keys must return Hash" # 0047 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0049 pop # 0050 pop # 0051 dup ( 5) # 0052 dup # 0053 putobject :deconstruct_keys # 0055 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0057 branchunless 98 # 0059 duparray [:k2] # 0061 opt_send_without_block <calldata!mid:deconstruct_keys, argc:1, ARGS_SIMPLE> # 0063 dup # 0064 checktype T_HASH # 0066 branchunless 89 # 0068 dup # 0069 putobject :k2 # 0071 opt_send_without_block <calldata!mid:key?, argc:1, ARGS_SIMPLE> # 0073 branchunless 98 # 0075 dup # 0076 putobject :k2 # 0078 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0080 putobject :v2 # 0082 checkmatch 2 # 0084 branchunless 98 # 0086 pop # 0087 jump 115 # 0089 putspecialobject 1 # 0091 putobject TypeError # 0093 putobject "deconstruct_keys must return Hash" # 0095 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0097 pop # 0098 pop # 0099 pop ( 2) # 0100 pop # 0101 putself ( 8)[Li] # 0102 putobject :c # 0104 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0106 leave ( 10)[Re] # 0107 adjuststack 2 ( 3) # 0109 putself ( 4)[Li] # 0110 putobject :a # 0112 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0114 leave ( 10)[Re] # 0115 adjuststack 2 ( 5) # 0117 putself ( 6)[Li] # 0118 putobject :b # 0120 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0122 leave ( 10)[Re] def m(v) case v in {k: :v} p :a in {k2: :v2} p :b else p :c end end
Array patternと同様にはじめにスタックの調整をしてcase vの部分を評価します。
# `compile_case3` function # # 0000 putnil ( 2)[LiCa] # 0001 getlocal_WC_0 v@0 # 0003 dup ( 3)
つぎにvが#deconstruct_keysメソッドを実装しているか確認して、実装しているなら#deconstruct_keysを呼び出し、結果がhashのオブジェクトかどうかを確認します。
# 0004 dup # 0005 putobject :deconstruct_keys # 0007 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0009 branchunless 50 # 0011 duparray [:k] # 0013 opt_send_without_block <calldata!mid:deconstruct_keys, argc:1, ARGS_SIMPLE> # 0015 dup # 0016 checktype T_HASH # 0018 branchunless 41
hashであることを確認したら:kをキーにもつか、キーにもつ場合は:vと一致するかをチェックします。
チェックが通ったらbodyの部分を実行します。
# 0020 dup # 0021 putobject :k # 0023 opt_send_without_block <calldata!mid:key?, argc:1, ARGS_SIMPLE> # 0025 branchunless 50 # 0027 dup # 0028 putobject :k # 0030 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0032 putobject :v # 0034 checkmatch 2 # 0036 branchunless 50 # 0038 pop # 0039 jump 107 # ... # 0107 adjuststack 2 ( 3) # 0109 putself ( 4)[Li] # 0110 putobject :a # 0112 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0114 leave ( 10)[Re]
Array patternと同様にノードの構造が大きく変わるわけではないので、基本的には構造体名やフィールド名の変更にあわせてcompile.cを変更していきます。
一点注意が必要なのがin {k: :v, **}のケースです。
変更前はin {k: :v}と同じノードが生成されていましたが、今回の変更でAssocSplatNode(ただしvalueがnil)というノードが付与されます。
Array patternではin [1, 2]と書いた場合、それは長さが2の配列を意味します。
一方Hash patternでin {k: :v}と書いた場合、それは少なくとも:kをkey、:vをvalueとして持つハッシュを意味します。
def m(v) case v in {k: :v} p :a else p :b end end m({k: :v}) #=> a m({k: :v, k2: :v2}) #=> a
そのためin {k: :v}とin {k: :v, **}でノードを同じにしておくとコンパイル時に都合がよかったのです。
今回の変更でin {k: :v, **}がAssocSplatNodeを持つようになったので、restの有無で分岐するコードは少し修正が必要になります。
// Before if (RNODE_HSHPTN(node)->nd_pkwrestarg) { ADD_SEND(ret, line_node, rb_intern("dup"), INT2FIX(0)); } // After const NODE *nd_rest = RB_NODE_HASH_PATTERN(node)->rest; bool has_rest = nd_rest && !(nd_type_p(nd_rest, RB_ASSOC_SPLAT_NODE) && RB_NODE_ASSOC_SPLAT(nd_rest)->value == NULL); if (has_rest) { ADD_SEND(ret, line_node, rb_intern("dup"), INT2FIX(0)); }
余談ですがin {k: :v, **rest}の場合はキーのチェックの際に#[]ではなく#deleteを使うようになったり、restに対してsetlocalをしたりします。
またin {k: :v, **nil}の場合はキーのチェックの際に#deleteを使い、最後に#empty?でハッシュが空になっているかチェックします。
以上に注意してcompile.cを修正し、挙動を確認します。
def m(v) case v in {k: :v, k2: :v2} p :a in {k: :v} p :b else p :c end end m({k: :v}) #=> :b m({k: :v, k2: :v2}) #=> :a m({k: :v, k3: :v3}) #=> :b m({k: :v0}) #=> :c
よさそうですね。
まとめ
- Hash patternに対応した
パターンマッチング全体の進捗は以下の通りです。
Value patternp_primitive ("str",1,:symなど)range_expr (1...3など)p_var_ref (^varなど)p_expr_ref (^(cmd 1, 2)など)p_const (A,::A,A::Bなど)
Variable patternArray patternHash pattern- Find pattern
- Alternative pattern
- As pattern
後置ifと後置unless
次回はFind patternの対応をする予定です。
Ruby Parser開発日誌 (24-41) - parse.yが生成するノードを変える ー パターンマッチング その4 (Array pattern)
41日目: Array patternに対応する
前回はVariable patternと後置if, unlessの対応をしました。 今回はArray patternに取り組みます。
Array pattern
Array patternとは読んで字の如く配列を表したパターンです。
def m(v) case v in [1, 2] p :a in [1, 2, 3] p :b else p :c end end m([1, 2]) #=> :a m([1, 2, 3]) #=> :b m([1]) #=> :c
書き換え前後のノードを比較するとフィールド名は異なるものの、4つのフィールドが存在することがわかります。
def m(v) case v # Before # # @ NODE_IN (id: 27, line: 3, location: (3,2)-(8,8)) # +- nd_head: # | @ NODE_ARYPTN (id: 9, line: 3, location: (3,6)-(3,10)) # | +- nd_pconst: # | | (null node) # | +- pre_args: # | | @ NODE_LIST (id: 6, line: 3, location: (3,6)-(3,10)) # | | +- as.nd_alen: 2 # | | +- nd_head: # | | | @ NODE_INTEGER (id: 5, line: 3, location: (3,6)-(3,7)) # | | | +- val: 1 # | | +- nd_head: # | | | @ NODE_INTEGER (id: 7, line: 3, location: (3,9)-(3,10)) # | | | +- val: 2 # | | +- nd_next: # | | (null node) # | +- rest_arg: # | | (null node) # | +- post_args: # | (null node) # +- nd_body: # | ... # After # # @ InNode (location: (3,2)-(4,8)) # +-- pattern: # | @ ArrayPatternNode (location: (3,5)-(3,11)) # | +-- constant: nil # | +-- requireds: (length: 2) # | | +-- @ IntegerNode (location: (3,6)-(3,7)) # | | | +-- IntegerBaseFlags: decimal # | | | +-- value: 1 # | | +-- @ IntegerNode (location: (3,9)-(3,10)) # | | +-- IntegerBaseFlags: decimal # | | +-- value: 2 # | +-- rest: nil # | +-- posts: (length: 0) # +-- statements: # | ... in [1, 2] p :a in [1, 2, 3] p :b else p :c end end
それぞれのフィールドの意味は以下のとおりです。
| Before | After | |
|---|---|---|
| nd_pconst | constant | in A[1, 2] の A |
| pre_args | requireds | in [1, *a, 3] の 1 |
| rest_arg | rest | in [1, *a, 3] の *a |
| post_args | posts | in [1, *a, 3] の 3 |
parse.yを書き換える
parse.yの書き換えは基本的に生成する構造体を変更するだけです。
一点注意が必要なのが、in [1, 2, ]のように,のあとに要素がないケースで、これは先頭の2要素だけを指定して残りの要素は任意の要素であることを示しています。
new_array_pattern_tail関数ではint has_restとrb_node_t *rest_argの2つの引数をみて判断しています。
static rb_node_t * new_array_pattern_tail(struct parser_params *p, rb_array_node_t *pre_args, int has_rest, rb_node_t *rest_arg, rb_array_node_t *post_args, const YYLTYPE *loc, const YYLTYPE *implicit_rest_loc) { if (has_rest) { rest_arg = rest_arg ? rest_arg : NEW_RB_IMPLICIT_REST(implicit_rest_loc); } else { rest_arg = NULL; } rb_node_t *node = NEW_RB_ARRAY_PATTERN(pre_args, rest_arg, post_args, loc); return node; }
compile.cを書き換える
compile.cを書き換える前に生成されるべきバイトコードを確認しておきましょう。
# 0000 putnil ( 141)[LiCa] # 0001 getlocal_WC_0 v@0 ( 140) # 0003 dup ( 141) # 0004 topn 2 # 0006 branchnil 17 # 0008 topn 2 # 0010 branchunless 75 # 0012 pop # 0013 topn 1 # 0015 jump 35 # 0017 dup # 0018 putobject :deconstruct # 0020 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0022 setn 3 # 0024 branchunless 75 # 0026 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0028 setn 2 # 0030 dup # 0031 checktype T_ARRAY # 0033 branchunless 66 # 0035 dup # 0036 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0038 putobject 2 # 0040 opt_eq <calldata!mid:==, argc:1, ARGS_SIMPLE>[CcCr] # 0042 branchunless 75 # 0044 dup # 0045 putobject_INT2FIX_0_ # 0046 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0048 putobject_INT2FIX_1_ # 0049 checkmatch 2 # 0051 branchunless 75 # 0053 dup # 0054 putobject_INT2FIX_1_ # 0055 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0057 putobject 2 # 0059 checkmatch 2 # 0061 branchunless 75 # 0063 pop # 0064 jump 168 # 0066 putspecialobject 1 # 0068 putobject TypeError # 0070 putobject "deconstruct must return Array" # 0072 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0074 pop # 0075 pop # 0076 dup ( 144) # 0077 topn 2 # 0079 branchnil 90 # 0081 topn 2 # 0083 branchunless 159 # 0085 pop # 0086 topn 1 # 0088 jump 108 # 0090 dup # 0091 putobject :deconstruct # 0093 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0095 setn 3 # 0097 branchunless 159 # 0099 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0101 setn 2 # 0103 dup # 0104 checktype T_ARRAY # 0106 branchunless 150 # 0108 dup # 0109 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0111 putobject 3 # 0113 opt_eq <calldata!mid:==, argc:1, ARGS_SIMPLE>[CcCr] # 0115 branchunless 159 # 0117 dup # 0118 putobject_INT2FIX_0_ # 0119 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0121 putobject_INT2FIX_1_ # 0122 checkmatch 2 # 0124 branchunless 159 # 0126 dup # 0127 putobject_INT2FIX_1_ # 0128 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0130 putobject 2 # 0132 checkmatch 2 # 0134 branchunless 159 # 0136 dup # 0137 putobject 2 # 0139 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0141 putobject 3 # 0143 checkmatch 2 # 0145 branchunless 159 # 0147 pop # 0148 jump 182 # 0150 putspecialobject 1 # 0152 putobject TypeError # 0154 putobject "deconstruct must return Array" # 0156 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0158 pop # 0159 pop # 0160 pop ( 147) # 0161 pop # 0162 putself [Li] # 0163 putobject :c # 0165 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0167 leave ( 149)[Re] # 0168 adjuststack 2 ( 141) # 0170 putself ( 142)[Li] # 0171 putobject :a # 0173 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0175 pop # 0176 putself ( 143)[Li] # 0177 putobject :a # 0179 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0181 leave ( 149)[Re] # 0182 adjuststack 2 ( 144) # 0184 putself ( 145)[Li] # 0185 putobject :b # 0187 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0189 leave ( 149)[Re] def m(v) case v in [1, 2] p :a p :a in [1, 2, 3] p :b else p :c end end
はじめにスタックの調整をしてcase vの部分を評価します。
# `compile_case3` function # # 0000 putnil ( 117)[LiCa] # 0001 getlocal_WC_0 v@0 ( 116) # 0003 dup ( 117)
ここまででスタックには3つの値が積まれます。
スタックの底は#deconstructした結果をキャッシュするための領域です。
v v nil # cached #deconstruct value
つぎにvを#deconstructします。
ここでキャッシュがnil, false, それ以外のときで実行するバイトコードが変わります。
# `iseq_compile_array_deconstruct` function # # 0004 topn 2 # 0006 branchnil 17 # 0008 topn 2 # 0010 branchunless 75 # 0012 pop # 0013 topn 1 # 0015 jump 35 # 0017 dup # 0018 putobject :deconstruct # 0020 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0022 setn 3 # 0024 branchunless 75 # 0026 opt_send_without_block <calldata!mid:deconstruct, argc:0, ARGS_SIMPLE> # 0028 setn 2 # 0030 dup # 0031 checktype T_ARRAY # 0033 branchunless 66
nilのときはまだ一度も#deconstructをしていないので、v#deconstructを呼び出します。
バイトコードでいうと0006 branchnil 17でジャンプをして0017 dupからバイトコードを実行します。
0020でv#respond_to? :deconstructをしてその結果を0022 setn 3でキャッシュ領域に書き込みます。
もしvに#deconstructメソッドがない場合には、次のin(0075の命令)までジャンプします。
v#deconstructがある場合にはメソッドを呼び出して、その結果をキャッシュ領域に書き込みます。
そしてv#deconstructの戻り値が配列であるかチェックします(0031)。
もし配列でない場合は例外を発生させます。
# 0066 putspecialobject 1 # 0068 putobject TypeError # 0070 putobject "deconstruct must return Array" # 0072 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0074 pop
v#deconstructの戻り値が配列だった場合には、具体的なマッチングに進みます。
v#deconstructとパターンの配列の長さを比較してから各要素が一致するかを確認します。
# `iseq_compile_pattern_each` function # # パターンの配列との長さを比較する # # 0035 dup # 0036 opt_length <calldata!mid:length, argc:0, ARGS_SIMPLE>[CcCr] # 0038 putobject 2 # 0040 opt_eq <calldata!mid:==, argc:1, ARGS_SIMPLE>[CcCr] # 0042 branchunless 75 # # それぞれの要素を比較する # # 0044 dup # 0045 putobject_INT2FIX_0_ # 0046 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0048 putobject_INT2FIX_1_ # 0049 checkmatch 2 # 0051 branchunless 75 # 0053 dup # 0054 putobject_INT2FIX_1_ # 0055 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0057 putobject 2 # 0059 checkmatch 2 # 0061 branchunless 75 # 0063 pop # # パターンにマッチしたのでbodyのコードにジャンプする # # 0064 jump 168 # ... # 0168 adjuststack 2 ( 117) # 0170 putself ( 118)[Li] # 0171 putobject :a # 0173 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0175 leave ( 124)[Re]
バイトコードの構造は複雑ですが、書き換え前後でノードの構造は大きくは変わりません。 そのためcompile.cでは構造体名やフィールド名の書き換えをするだけで済みます。 驚くほど修正箇所が少ないのでdiffは割愛して、実際にコードを実行して挙動を確認します。
def m(v) case v in [1, 2] p :a in [1, 2, 3] p :b else p :c end end m([1, 2]) #=> :a m([1, 2, 3]) #=> :b m([1]) #=> :c
よさそうです。
まとめ
- Array patternに対応した
パターンマッチング全体の進捗は以下の通りです。
Value patternp_primitive ("str",1,:symなど)range_expr (1...3など)p_var_ref (^varなど)p_expr_ref (^(cmd 1, 2)など)p_const (A,::A,A::Bなど)
Variable patternArray pattern- Hash pattern
- Find pattern
- Alternative pattern
- As pattern
後置ifと後置unless
次回はHash patternの対応をする予定です。
Ruby Parser開発日誌 (25) - RubyKaigi 2026で発表してきた ~ この一瞬が最高のパーティーなんだ ~
4/22から4/24の3日間にわたって開催されたRubyKaigi 2026に参加するために北海道へ行ってきました。 今回はコミッター/登壇者として参加しました。そのため初日の自分の発表と、3日目の"Ruby Committers and the World"の計2回登壇することになりました。
Kingdom of the Machine: The Tale of Operators and Commands
まずは自分のセッションから。
今年は"Kingdom of the Machine: The Tale of Operators and Commands"というタイトルで話しました。 とくにRubyKaigiで発表するときは、いくつもの切り口で解釈できるようなトークをすることを心がけています1。 Abstructに書いたように、"そもそも構文解析ではどのような問題を解く必要があるのか"という点を明らかにするために、特に(中置)演算子とコマンド(括弧なしのメソッド呼び出し)を中心にparserについて深掘りをしていくという話をしました2。
また昨年のブログにはこう書きました。
実は抽象度がそれなりに高い話だったと思うのですが、例が良かったのか、はたまたここ数年の僕の発表を聞いてみなさんの理解度が高くなっているのか、"例年に比べてよくわかりました"という感想をもらうことが多かったです。 これなら来年はもう少し難しくしても大丈夫かな。
ということで昨年よりも難易度をだいぶあげたつもりです。 この辺の企みがうまくいっているかどうかは今後出てくるであろうみなさんの感想ブログを待ちたいと思います。
セッションについて
今年は準備が大変だったこともあり、例年よりも聞くことができたセッションが少なくなってしまいましたが、自分がみたセッションからいくつか感想を書いておこうと思います。
Liberating Ruby's Parser from Lexer Hacks
"Most of lex_state Was Unnecessary!" です。以上!
Most of lex_state Was Unnecessary!#rubykaigi #rubykaigiA pic.twitter.com/D2YCEsofSe
— kaneko.y (@spikeolaf) 2026年4月22日
ちゃんと感想をいうと、lex_stateは消えるはずだとずっと思い続けていたのですが、思っているのはタダ。 実際に手を動かして検証したというところにすごい価値があると思っています。 自分よりlex_stateに詳しい人間が誕生するとは思っていませんでした3。
いつかlex_stateを腑分けするLTをしたいと思っていたのですが、これでお蔵入りかなぁ...
Digits, Digits, and Digits
ここ最近ぺんさんがruby/bigdecimalをずっといじっていたのは知っていたので、今年はそのあたりを題材に話すのだろうと思っていました。 一方で数学という題材がそれなりに前提知識を必要とすると思っていたので、RubyKaigiでの登壇にあわせてどんなふうにまとめてくるのだろうという点が気になっていました4。
そんなこんなを思いながら聞いていたのですが、まず掴みがよかった! RubyKaigiなんだから役に立たない話をしますと言い切ってもいいのに、あんなに丁寧にいろいろと背景を説明していて、しかもその説明がすごくぺんさんっぽいのが最高でした。
おそらく事前知識なしで30分聞くことができるギリギリの内容量と構成だったのではないでしょうか。
僕? 僕は数学の素養がないので、ニコニコして聞いていましたよ。
(Re)make Regexp in Ruby: Democratizing internals for the JIT
構文解析を主戦場にしている自分からすると、近いようで遠い領域であるRegexpに関する発表。 Fujinamiさんの発表は毎回聞きにいくようにしているのと、今回は登壇前夜にWhite Seedで事前にスライドを見ていたので、現地確認の意味もあってセッションを見にいきました。
正規表現エンジンについてはFujinamiさんが良いと思うように改良していっていただければと思っていますが、一点今回の発表で学んだのはRubyのsyntaxとOnigumoが受け付けるpatterのsyntaxには差異があって、その差分をparserで吸収しているということです5。 ちゃんと勉強して出直してきます...
これは正規表現エンジンに限らずですが、個人的には安易な書き直しは良くないことが多いと思っています6。 ですが今回の発表ではNarakuによって書き直す理由がちゃんと検討・説明されていたと思うので、真摯な取り組みだなと思いました。
Road to RubyKaigi: Play Hard(ware)
観客の期待を裏切らない、とてもいいデモでした。 ここで多くは語らないのでもし見逃していたら後日アップロードされるであろう動画をみるのがよいと思います。 コンセプトといい、うまいというか"ずるい"というか。
Is Ruby's Multi-Encoding Overhead Heavy?
本編でやりましょうよーという感想は現地でお伝えしたのでいいとして、そのほかの感想としてはLTモードでしゃべるima1zumiさんが見れたのがよかったです。 本編とLTは別の競技と僕は理解しているので。
RubyKaigiへの道
人はバグを倒し締切から逃げながらRubyKaigiへの参加を目指すと言われています7。 ということで今年のRoad to RubyKaigiの様子を振り返っておきましょう。
4/22から4/24の3日間にわたって函館で開催されたRubyKaigi 2026に参加するために、4/18から4/26まで北海道へ行ってきました。 8泊9日の旅になりました。
松山で開催されたRubyKaigi 2025のときが10泊11日(うち1泊は車中泊@サンライズ瀬戸)で、那覇で開催されたRubyKaigi 2024のときが8泊9日だったので、おおよそ例年通りの期間RubyKaigiをしていたようです。
毎度のことながら1週間以上ふらふらしているのを受け入れてくれてる各位には感謝しています。
Day -3 (4/18) 初めの北海道
普通に羽田空港から飛行機にのって北海道へ向かいました。
北海道初上陸です pic.twitter.com/oJZz11LPoN
— kaneko.y (@spikeolaf) 2026年4月18日
人生初の北海道ということで、せっかくなら行けるところまで行っておくか。ということで稚内空港に降り立ちました。 ここまで来れば締切も簡単には追ってこないだろうと。
着陸態勢に入ったあたりで山肌が白くなっていることに気がつき、これまさか雪か? と思ったのですが、そのとおり雪でした。 地元の人に聞いたところ今年は春が早いのでそろそろ冬のもろもろを片付けることができるとおっしゃっていて、これが北海道かと...
このあたりLocal Organizerのいかるがさんが事前に"冬だからな"(意訳)ということを言ってくれていたので、とても助かりました。 いうて大丈夫だろと思いつつ、冬用のコートとヒートテックの上下という(東京の)冬仕様で行って本当によかった... それらがなかったら途中で風邪をひいていたと思います。
函館多分朝晩寒いので夜歩くつもりがある人はコートなりダウンなりあった方がいい気がしている
— いかるが (@UVB_76) 2026年4月16日
このツイートに限らず会期中は幾度となくいかるがさんのツイートを眺めていました。ありがとう、ありがとう。
この日は稚内市樺太記念館や旧瀬戸邸などを見ながらJR稚内駅のほうに移動してホテルにチェックインしました。 そのあとは稚内港北防波堤ドームをみつつ、駅に戻ってスライドを作ったりしました。
Day -2 (4/19) 最北端への旅
せっかく稚内まで来たので北海道最北端の地、宗谷岬に行きたくなるのが人情。 ということで稚内2日目はバスでゆられること1時間、宗谷岬に行ってきました。 とにかく風が強い...
— kaneko.y (@spikeolaf) 2026年4月19日
稚内駅にもどったあとはせっかくなのでバスでノシャップ岬へ。 ノシャップ寒流水族館や南極越冬隊資料展示コーナーなどを見たかったのですが4/1から4/28までは休館しているようで、岬にあるイルカの像を見て再び稚内駅へ戻りました。 そのあとは再び駅でスライドを作ったりしました。
稚内駅にはキタカラという複合施設があって、そこのアトリウムやロビーで作業させてもらっていました。
稚内駅の発着本数は少ないのですが、始発が6時台、終電の到着が23時台ということで駅自体は朝早くから夜遅くまで開いています。 またそれに合わせてキタカラのセイコーマートも6時から23時まで営業しており、なかなか便利です。 今回のスライドの40 ~ 50%はキタカラで作成したので、稚内に行ってなかったら登壇も危ぶまれていたことでしょう。
ありがとう稚内、ありがとうキタカラ。
Day -1 (4/20) 南下!南下!
さすがにそろそろ函館に行かないといけないことに気がつき、この日は移動日となりました。
行くぞ函館!!! pic.twitter.com/jaHhOYvPBy
— kaneko.y (@spikeolaf) 2026年4月20日
途中で利尻富士を眺めつつ、スライド作成を進めます8。
利尻富士!!! pic.twitter.com/hbEQXjg2KO
— kaneko.y (@spikeolaf) 2026年4月20日
ちょうどラベンダーラウンジがついている編成だったので、そこで作業をすることに。

夕方に旭川に着き、駅前のイオン(イオンシネマ)に入ったところまでは記憶があるのですが、そこから2時間半くらい記憶が飛んでいます。
— kaneko.y (@spikeolaf) 2026年4月20日
その後は特急ライラックにのって札幌を目指します。
行くぞ函館!! pic.twitter.com/6YTjgaNUw2
— kaneko.y (@spikeolaf) 2026年4月20日
この日は札幌で一泊。
Day 0 (4/21) 函館到着!
この日は朝から移動だったので、高速で札幌観光をすませて特急に乗車しました。
これをもって札幌観光とします pic.twitter.com/cPcqPFZnwV
— kaneko.y (@spikeolaf) 2026年4月21日
今日はみれません pic.twitter.com/0ivsWPI899
— kaneko.y (@spikeolaf) 2026年4月20日
行くぞ函館!!! pic.twitter.com/dkcICDjj2V
— kaneko.y (@spikeolaf) 2026年4月21日
道中とくにトラブルもなくほぼほぼ定刻どおりに函館に到着です9。
到着ー pic.twitter.com/cxkytf2xUD
— kaneko.y (@spikeolaf) 2026年4月21日
今年はここまで一度もRubyistに会ってないのですが、さすがはDay 0の函館。 駅を出た瞬間にRubyistに遭遇10。
函館駅前で北斗の旅を終えた @spikeolaf とエンカウント#rubykaigi
— iberianpig(いべりこ) (@nukumaro22) 2026年4月21日
今回の宿は函館アリーナ近辺だったので市電にのって東へゴトゴト。 途中思い立って五稜郭のあたりでおりてラッキーピエロへいくことに。
— kaneko.y (@spikeolaf) 2026年4月21日
五稜郭公園前店をわざと外してほかの店舗に行ったのですがそこでも知り合いのRubyistに遭遇することになりました。 完全に油断していたので、なんか間抜けな声を上げた記憶が...
やんちゃハウス到着後は部屋に荷物を置いて夜中まで資料の作成をすることに。 さて、正直なことをいうとこの辺の記憶があんまりないんですが、今回の担当編集であるところの炬燵(敬称略)の記録によると、ちゃんとスライドを作っていたようです。 ちなみにこの黒グラデの背景をつくるとき僕が相当ポンコツだったようで、「そこをxポイントにして、あーそうじゃない」とものすごく具体的な指示をもらっていた記憶が朧げにあります。 その節はありがとうございました。
ちなみに今年はこの時点でまだ通し練習ができていません。 昨年はDay 0 松山駅到着時点でリハーサルができていたことを考えると、今年は松本のときくらいには進捗が厳しかったようです。
発表のリハーサルを何度かしつつ資料を微調整しました。 海際を走るので要所要所で造船所をみることができて良かったです。 Ruby Parser開発日誌 (21) - RubyKaigi 2025で発表してきた ~ 四国漫遊記 ~ - かねこにっき
その後資料が80%くらい完成して、リハーサルもしたので夜中は許可を得てビールを飲むなどしていました。 リハーサルをしたところ27分くらいだったので、まあなんとかなるだろうと一安心。 資料完成後の酒はうまい。
昨日の記憶あんまりないんだよな... https://t.co/OCipNx6e13
— kaneko.y (@spikeolaf) 2026年4月22日
Day 1 (4/22) 登壇
資料作成、リハーサル、通訳打ち合わせなどがあるのでydahさんの発表だけを聞いて、あとは自分の登壇の準備をしていました。
Most of lex_state Was Unnecessary!#rubykaigi #rubykaigiA pic.twitter.com/D2YCEsofSe
— kaneko.y (@spikeolaf) 2026年4月22日
Day 1についても自分の登壇までの記憶があんまりないのですが、その際の様子もちゃんと炬燵の記録に書かれていました。 その節はありがとうございました。
お昼ご飯を受け取ったあとにアリーナの2階の廊下でリハーサルをしたあたりでようやく登壇する実感が湧いてきました。
通訳打ち合わせといえば、どうも名前か顔が割れているような感じがしており、"やたら早口で話す人間"で"いつも当日資料に変更がある人間"(NEW!)という認識をされている雰囲気を感じます。 気のせいだといいのですが... 通訳の方にはいつも大変お世話になっております。 ありがとうございます。
登壇後はフラフラしていたのでメインアリーナにいたような気がしますが、気のせいかもしれません。 あんまり記憶がない。
LTを見たあとは一度宿に戻って市電にのってOfficial Partyへ。 Official Partyはいろいろな人と話すことができて良いものですね。
疲れか緊張からか頭痛が酷かったので、Official Partyのあとはまっすぐ宿に戻ることにしました。 RubyKaigiは長期戦なので仕方ない11。
2026/5/3 追記
前職ですごくお世話になったエンジニアの方が今年RubyKaigiに初参加してくれていて、僕の登壇も見にきてくれていたのがすごく嬉しかったです。 僕の発表もRubyKaigiも楽しんでいただけたようで、大満足。
Day 2 (4/23) メインアリーナにて
まとまった時間眠れたのと登壇が終わったことから、起床したときには頭痛もひいていました。 朝は向かいのホテルの温泉にはいってから会場へ。
この日は1日中メインアリーナにいたような気がします。 例年に比べてスポンサーブースの空間が広かったこと、Hack Space(SmartBankさん、ありがとうございます)やホールの前方に座れる場所が多かったこと、メインアリーナ以外に大きな廊下がなかったことなどが理由かなと。
体調は回復していたものの、糖分とカロリーが足らない... となっていたので1時間おきにANDPADさんのブースにいってガラナやソフトカツゲンをいただいてました。 ANDPADさんといえばラッキーピエロ、いかめし、やきとり弁当と会期中毎日異なる函館名物をお昼ご飯として提供してくださっていて、おかげさまでいかめし、やきとり弁当を食べることができました12。
夜はESMさんのDrink upにお邪魔しました。 うまくいけないけどESMさんのイベントはいつも練度が高くて実家のような安心感がある...
その後はDay 1にいきそびれたWhite Seedにいきました。 いきなりparserバトルを仕掛けられるなど、RubyKaigiの夜はこうじゃないとね!13
突然parserバトルを仕掛けられた... #rubykaigi
— kaneko.y (@spikeolaf) 2026年4月23日
翌日は決まった時間には会場にいる必要があるので2時くらいにはnobuとタクシーで帰宅しました14。
2026/5/3 追記
この日はるびま Hotlinks のインタビューをお昼に開催してもらいました15。 記事は後日公開されるので、お楽しみにー
また午後のAfternoon Breakの時間には本屋さんのゲストコーナーに参加していました。 今回も前回同様にオートマトンに関する書籍を並べてもらいました。
- 計算理論の基礎 [原著第3版] 1.オートマトンと言語
- 白と黒のとびら: オートマトンと形式言語をめぐる冒険
Day 3 (4/24) 光・音・映像
Ruby Committers and the Worldがあるのでちゃんと起床。 Day 2と同様に向かいのホテルの温泉にはいってから会場へ。
And the Worldのあとは流石にこのタイミングで行かないと行きそびれそうということで、一瞬だけ五稜郭にいくなどしつつ、メインアリーナにいたり、セッションを聞いたりしていました。
Matz Keynote、とてもよかったですね。 まつもとさんがとっても楽しそうにプログラミング言語を作っている姿から得られる栄養ってやっぱりあると思うんですよ。 今年はその要素がすごく強くてワクワクしました。
RubyKaigiに来て生でセッションを聞く一番の理由は登壇者の気持ちを直に感じることなのかもしれません。と10数年目にして一周したような感想を持ちました。
その後はkoicさんに誘われてご飯にいく予定が、いい時間になってしまったので途中でお別れしてpixiv presents RubyIlluminations 2026へ。 koicさんごめんなさい...
pixiv presents RubyIlluminations 2026
まずは簡潔に感想を。
最高のイベントでした!!!
例年は音がメインのイベントを主催しているpixivさんが、今年はそのコンセプトを"光・音・映像"に広げてRubyIlluminationsが開催されました。 正直なことをいうと当日までプレゼンテーションパートってちゃんと盛り上がるのかな... と勝手に心配していたのですが、そこはpixivさんです。 完全に御見それしました!!!
プレゼンテーションパート
プレゼンテーションパートはogomさんの静かな光のセッションからはじまりました。 この時点ではまだ全体の方向性ははっきりとはわかっていませんでした。
そのあとに出てきたのがasonas(敬称略)!! なーんかガチャガチャやってんなー(褒めている) 実際問題としてasonasさんは僕と登壇の時間が同じだったので絶対に聞きに行けないセッションだったので、ここで見ることができて嬉しかったです16。
そして音のasonasから一転して映像のfusgikoさん。 RubyMusicMixinに初めて行って以来、VJというか映像にも興味があったので興味深く聞かせていただきました。
Intervalを挟んでbashさん。 手作りRuby楽器があなたを演奏する!! じゃあないんですよと爆笑しながら聞いていました。 楽器というものも自分で発明していいんだというのは、"なければ(あっても)作ればいい"というRubyKaigiの根底に流れると僕が思っている価値観と一致していて、あぁこの会場もまたRubyKaigiなんだなと思った記憶があります。 後半は映像も混ざって状況は混沌と... (褒めている)
MakiさんはMIDIってなんでもできるんですよ(超訳)という話で、RubyIlluminationsで一番のテックパートだったのではないでしょうか。 この辺でもうだいぶ語彙力が怪しくなっている...
プレゼンテーションパートの最後はyuchiさん。 ちゃんとわかってないので曖昧な書き方になるんですが、国土交通省の3Dデータ?をギュンギュンしていたような記憶があります。 映像(画像?)がその場で作られていく様子は本当に面白くてワクワク・ドキドキしました。
記憶違いじゃなければ後半のライブイベントでyuchiさんの映像を使っていたはずで、プレゼンテーションパートとライブイベントがちゃんと関係している構成になっていて、よく考えられているなぁとこの記事を書きながら思っています。
箱の具合なのか、"光・音・映像"というコンセプトがいいのか、すこしお酒がはいっているのがいいのか、RubyKaigiの本編では再現できない空間になっていたと思います。
来年はparse.yを光らせるもしくは奏でるか?
ライブイベント
にっこにこのima1zumiさんがDJをしていました。 現場からは以上です。
pastakさんの異常な量のコンテキストを浴びるというイベントは今年も健在でした。
ikaruga & sylph01(敬称略)は本当にありがとうございました。 たしかRubyKaigiの数ヶ月前にどこかで会った時にどちらかに「... 超かぐや姫 ...」とお願いした記憶があり、その節は大変感謝しています。
もうこれで終わってもいいっておもってたのに
babaさん & kiridarumaさんが... Replyの2番を... そして「満ちる月のセレナーデ ~八千年の旅~」と「超かぐや姫!」を...
追撃が入ったwwwwwwwwwwww #rubyilluminations
— sylph01 / G4きゅーぶ (@s01) 2026年4月24日
というわけで例年以上に楽しませていただきました。
僕が登壇で言った「今は昔……」が伏線になっていたとは。というコメントをいただいたのですが、僕としてはikarugaさん & sylph01さんが回収してくれるかなーと淡い期待をしていたところに、まさか最後にあんな伏線回収があるとは。ですよ。
Replyの2番まで聞けたことで僕のなかでRubyKaigi 2026を終わらせることができました。
その後なんやかんやあって、このお話にはまだ続きがある。という気持ちになったので来年こそは頑張ります。
だからやっぱりまだReplyの1番なわけで、そうすると某人の指摘である、本当にやりたいことをぶつけてほしいという感想はすごく的をえているなと、やっと理解できた。持つべきものは親友。
— kaneko.y (@spikeolaf) 2026年4月26日
というわけで来年のRubyKaigiはReplyの2番になる可能性があります。
— kaneko.y (@spikeolaf) 2026年4月26日
このあと赤い風車(レストラン)で食事をして、まっすぐ宿に帰りました。
Day 4 (4/25) 函館観光
市電があるとLT大会が発生するのはよくあることなので、LTをしてきました。
そのあとは函館どつくを起点に十字街までウロウロし、大量のRubyistと会合しました。 まぁそうなるよね。
その後戻ってきたぽっけさん & sylph01さんと合流してジンギスカンを食べてから解散。
Day 5 (4/26) 帰路
今回は飛行機以外で帰る予定だったのでいつ帰ってもよかったのですが、体力面などを考慮してこの日に帰ることにしました。 北海道新幹線で青函トンネルというのも考えたのですが、まだ船に乗っていなかったので青函フェリーで青森に渡ることにしました。
朝早いフェリーだったので流石に知り合いはおらんやろと思っていたのですが、そんなはずもなく...
最終的に青森県立美術館を見て帰路へ。
楽しかった #rubykaigi pic.twitter.com/UsX7Q6bgxl
— kaneko.y (@spikeolaf) 2026年4月26日
到着ー。お疲れ様でした!! ありがとうございました!!! #rubykaigi pic.twitter.com/fWwgQ0EBSa
— kaneko.y (@spikeolaf) 2026年4月26日
ということで長きにわたったRubyKaigi 2026もこれでおしまいです。 お疲れ様でした!
- この辺の話は緞帳のうらがわ by 東京Ruby会議13で話すかもしれません↩
- という感想はいまのところいただけてないようですが...↩
- モンスターはモンスターなので...↩
- 毎年parserについて話をしていますが、parserの場合も専門的な話をどこまで盛り込むか、どう盛り込むかは毎年悩んでいます↩
- 単にいままで見て見ぬふりをしていたともいいますね↩
- Bisonというparser generatorを書き直した人が言うのだから間違いない↩
- Road to RubyKaigi: Making Tinny Chiptunes with Ruby - Speaker Deck↩
- 利尻富士が見えるあたりに差し掛かると、特急がめっちゃ速度を落として運行するのがよいですね↩
- 北海道の列車は鹿と衝突することがあると聞いていたのですが、今回乗った列車は特にそういうトラブルもなかったので一安心。実際札幌についたときに函館からやってきた特急が鹿との衝突で遅延していたり、サロベツにしろ北斗にしろ鹿が出やすい区間では急ブレーキの可能性を車内アナウンスするなど実際に起こりうるんだなと思ったりしていました↩
- ニアミスで1つ後ろの北斗にRubyistが乗っていた雰囲気はありました↩
- 去年のブログにも同じことを書いている気がします↩
- 会場の近辺にあまり繁華街がないので、今年は基本的に会場でお昼を食べるというスタイルでした。お弁当のほかにキッチンカーなども手配されており楽しいかったですね↩
-
puts%|4|がなぜsyntax errorになるのかという話だった気がする↩ - 京都のRubyKaigi以来な気がする↩
- Hotlinksではインタビューの最後に次のインタビュイーを決めるという運用になっていて、前回のインタビューで僕にお鉢が回ってきていたのでした。当日インタビューに参加してくださったみなさん、ありがとうございました↩
- 発表資料の準備中、登壇だからと言って本当に生身の僕が壇上にいる必要があるのか?としばらく考えたことはあります↩