19日目: rescueとensureを対応する
今回はrescueとensureに取り組みます。 難しい部分はおそらくコンパイルするときにcatch tableを用意したりする部分だと思うのですが、その辺は既存の仕組みに乗っかれるはずなのでノードの書き換え自体はそこまで難しくないんじゃないかなぁと淡い期待を胸に進めていきましょう。
ノードを眺める
書き換え前後のノードを眺めておきましょう。
def m1 a rescue b else c ensure d end # Before # @ NODE_ENSURE (id: 9, line: 1, location: (2,2)-(8,3)) # +- nd_head: # | @ NODE_RESCUE (id: 8, line: 3, location: (2,2)-(6,3)) # | +- nd_head: # | | @ NODE_VCALL (id: 3, line: 2, location: (2,2)-(2,3))* # | | +- nd_mid: :a # | +- nd_resq: # | | @ NODE_RESBODY (id: 5, line: 4, location: (3,0)-(4,3)) # | | +- nd_args: # | | | (null node) # | | +- nd_exc_var: # | | | (null node) # | | +- nd_body: # | | | @ NODE_VCALL (id: 4, line: 4, location: (4,2)-(4,3))* # | | | +- nd_mid: :b # | | +- nd_next: # | | (null node) # | +- nd_else: # | @ NODE_VCALL (id: 6, line: 6, location: (6,2)-(6,3))* # | +- nd_mid: :c # +- nd_ensr: # @ NODE_VCALL (id: 7, line: 8, location: (8,2)-(8,3))* # +- nd_mid: :d # After # @ BeginNode (location: (1,0)-(9,3)) # +-- begin_keyword_loc: nil # +-- statements: # | @ StatementsNode (location: (2,2)-(2,3)) # | +-- body: (length: 1) # | +-- @ CallNode (location: (2,2)-(2,3)) # | +-- CallNodeFlags: variable_call, ignore_visibility # | +-- receiver: nil # | +-- call_operator_loc: nil # | +-- name: :a # | +-- message_loc: (2,2)-(2,3) = "a" # | +-- opening_loc: nil # | +-- arguments: nil # | +-- closing_loc: nil # | +-- equal_loc: nil # | +-- block: nil # +-- rescue_clause: # | @ RescueNode (location: (3,0)-(4,3)) # | +-- keyword_loc: (3,0)-(3,6) = "rescue" # | +-- exceptions: (length: 0) # | +-- operator_loc: nil # | +-- reference: nil # | +-- then_keyword_loc: nil # | +-- statements: # | | @ StatementsNode (location: (4,2)-(4,3)) # | | +-- body: (length: 1) # | | +-- @ CallNode (location: (4,2)-(4,3)) # | | +-- CallNodeFlags: variable_call, ignore_visibility # | | +-- receiver: nil # | | +-- call_operator_loc: nil # | | +-- name: :b # | | +-- message_loc: (4,2)-(4,3) = "b" # | | +-- opening_loc: nil # | | +-- arguments: nil # | | +-- closing_loc: nil # | | +-- equal_loc: nil # | | +-- block: nil # | +-- subsequent: nil # +-- else_clause: # | @ ElseNode (location: (5,0)-(7,6)) # | +-- else_keyword_loc: (5,0)-(5,4) = "else" # | +-- statements: # | | @ StatementsNode (location: (6,2)-(6,3)) # | | +-- body: (length: 1) # | | +-- @ CallNode (location: (6,2)-(6,3)) # | | +-- CallNodeFlags: variable_call, ignore_visibility # | | +-- receiver: nil # | | +-- call_operator_loc: nil # | | +-- name: :c # | | +-- message_loc: (6,2)-(6,3) = "c" # | | +-- opening_loc: nil # | | +-- arguments: nil # | | +-- closing_loc: nil # | | +-- equal_loc: nil # | | +-- block: nil # | +-- end_keyword_loc: (7,0)-(7,6) = "ensure" # +-- ensure_clause: # | @ EnsureNode (location: (7,0)-(9,3)) # | +-- ensure_keyword_loc: (7,0)-(7,6) = "ensure" # | +-- statements: # | | @ StatementsNode (location: (8,2)-(8,3)) # | | +-- body: (length: 1) # | | +-- @ CallNode (location: (8,2)-(8,3)) # | | +-- CallNodeFlags: variable_call, ignore_visibility # | | +-- receiver: nil # | | +-- call_operator_loc: nil # | | +-- name: :d # | | +-- message_loc: (8,2)-(8,3) = "d" # | | +-- opening_loc: nil # | | +-- arguments: nil # | | +-- closing_loc: nil # | | +-- equal_loc: nil # | | +-- block: nil # | +-- end_keyword_loc: (9,0)-(9,3) = "end" # +-- end_keyword_loc: (9,0)-(9,3) = "end"
図にするとこんな感じで、今までNODE_RESCUEとNODE_RESBODYで表現していたものをRescueNodeで、NODE_ENSUREとNODE_RESCUEをネストして表現していたものをBeginNodeで、それぞれ表現するようになります。

beginがないのにBeginNodeなのかと思うかもしれませんが、以下のコードのbegin ~ endもBeginNodeで表現していることを踏まえると、さきほどのコードはbeginを省略したBeginNodeだと捉えているのだと思います。
def m2 begin a rescue b else c ensure d end end
1つ注意が必要な点としてm1とm2でDefNodeのbodyに設定されるノードの種別が異なります。
+-- @ DefNode (location: (1,0)-(9,3))
+-- name: :m1
+-- name_loc: (1,4)-(1,6) = "m1"
+-- receiver: nil
+-- parameters: nil
+-- body:
| @ BeginNode (location: (1,0)-(9,3))
+-- @ DefNode (location: (113,0)-(123,3))
+-- name: :m2
+-- name_loc: (113,4)-(113,6) = "m2"
+-- receiver: nil
+-- parameters: nil
+-- body:
| @ StatementsNode (location: (114,2)-(122,5))
parse.yを変更する
parse.yの生成規則ではbegin ~ rescue ~ else ~ ensure ~ endのうちbeginとendを除いた部分をbodystmtと呼んでいます。
bodystmt : compstmt(stmts)[body] lex_ctxt[ctxt] opt_rescue k_else compstmt(stmts)[elsebody] opt_ensure | compstmt(stmts)[body] lex_ctxt[ctxt] opt_rescue opt_ensure ;
こうしておくことでbeginが書かれているケースとそうでないケースでbodystmt生成規則を共有することができます。
// def m # primary // rescue // end primary | defn_head[head] f_arglist[args] bodystmt k_end // def m // begin # primary // rescue // end // end primary | k_begin bodystmt k_end
再度bodystmt生成規則をみるとopt_rescue, k_else compstmt, opt_ensureが揃ってからbodystmt全体を表すノードをセットアップするようになっているので、書き換えもその順番に行っていきましょう。
bodystmt : compstmt(stmts)[body] lex_ctxt[ctxt] opt_rescue k_else { if (!$opt_rescue) yyerror1(&@k_else, "else without rescue is useless"); next_rescue_context(&p->ctxt, &$ctxt, after_else); } compstmt(stmts)[elsebody] { next_rescue_context(&p->ctxt, &$ctxt, after_ensure); } opt_ensure { $$ = new_bodystmt(p, $body, $opt_rescue, $elsebody, $opt_ensure, &@$); /*% ripper: bodystmt!($:body, $:opt_rescue, $:elsebody, $:opt_ensure) %*/ }
opt_rescue生成規則でRescueNodeをつくる
rescue周りの構文は今回書き換えるノードのなかでは最も複雑です。 構文の要素は以下のとおりです。
E1, expr1のようにrescueする対象の例外を複数書くことができる=> ex1で発生した例外を変数に代入することができるex1のように例外をrescueしたあとで実行するコードを記述することができるrescueは複数書くことができる
書き換え前後のノードを掲載しておきます。
def m a rescue E1, expr1 => ex1 ex1 rescue E2, expr2 => ex2 ex2 end #+- nd_body: # @ NODE_RESCUE (id: 20, line: 1, location: (2,2)-(6,5)) # +- nd_head: # | @ NODE_VCALL (id: 3, line: 2, location: (2,2)-(2,3))* # | +- nd_mid: :a # +- nd_resq: # | @ NODE_RESBODY (id: 19, line: 3, location: (3,0)-(6,5)) # | +- nd_args: # | | @ NODE_LIST (id: 5, line: 3, location: (3,7)-(3,16)) # | | +- as.nd_alen: 2 # | | +- nd_head: # | | | @ NODE_CONST (id: 4, line: 3, location: (3,7)-(3,9)) # | | | +- nd_vid: :E1 # | | +- nd_head: # | | | @ NODE_VCALL (id: 6, line: 3, location: (3,11)-(3,16)) # | | | +- nd_mid: :expr1 # | | +- nd_next: # | | (null node) # | +- nd_exc_var: # | | @ NODE_LASGN (id: 8, line: 3, location: (3,17)-(3,23)) # | | +- nd_vid: :ex1 # | | +- nd_value: # | | @ NODE_ERRINFO (id: 18, line: 3, location: (3,17)-(3,23)) # | +- nd_body: # | | @ NODE_LVAR (id: 9, line: 4, location: (4,2)-(4,5))* # | | +- nd_vid: :ex1 # | +- nd_next: # | @ NODE_RESBODY (id: 17, line: 5, location: (5,0)-(6,5)) # | +- nd_args: # | | @ NODE_LIST (id: 11, line: 5, location: (5,7)-(5,16)) # | | +- as.nd_alen: 2 # | | +- nd_head: # | | | @ NODE_CONST (id: 10, line: 5, location: (5,7)-(5,9)) # | | | +- nd_vid: :E2 # | | +- nd_head: # | | | @ NODE_VCALL (id: 12, line: 5, location: (5,11)-(5,16)) # | | | +- nd_mid: :expr2 # | | +- nd_next: # | | (null node) # | +- nd_exc_var: # | | @ NODE_LASGN (id: 14, line: 5, location: (5,17)-(5,23)) # | | +- nd_vid: :ex2 # | | +- nd_value: # | | @ NODE_ERRINFO (id: 16, line: 5, location: (5,17)-(5,23)) # | +- nd_body: # | | @ NODE_LVAR (id: 15, line: 6, location: (6,2)-(6,5))* # | | +- nd_vid: :ex2 # | +- nd_next: # | (null node) # | +-- rescue_clause: # | | @ RescueNode (location: (3,0)-(6,5)) # | | +-- exceptions: (length: 2) # | | | +-- @ ConstantReadNode (location: (3,7)-(3,9)) # | | | | +-- name: :E1 # | | | +-- @ CallNode (location: (3,11)-(3,16)) # | | | +-- name: :expr1 # | | +-- reference: # | | | @ LocalVariableTargetNode (location: (3,20)-(3,23)) # | | | +-- name: :ex1 # | | | +-- depth: 0 # | | +-- statements: # | | | @ StatementsNode (location: (4,2)-(4,5)) # | | | +-- body: (length: 1) # | | | +-- @ LocalVariableReadNode (location: (4,2)-(4,5)) # | | | +-- name: :ex1 # | | | +-- depth: 0 # | | +-- subsequent: # | | @ RescueNode (location: (5,0)-(6,5)) # | | +-- exceptions: (length: 2) # | | | +-- @ ConstantReadNode (location: (5,7)-(5,9)) # | | | | +-- name: :E2 # | | | +-- @ CallNode (location: (5,11)-(5,16)) # | | | +-- name: :expr2 # | | +-- reference: # | | | @ LocalVariableTargetNode (location: (5,20)-(5,23)) # | | | +-- name: :ex2 # | | | +-- depth: 0 # | | +-- then_keyword_loc: nil # | | +-- statements: # | | | @ StatementsNode (location: (6,2)-(6,5)) # | | | +-- body: (length: 1) # | | | +-- @ LocalVariableReadNode (location: (6,2)-(6,5)) # | | | +-- name: :ex2 # | | | +-- depth: 0 # | | +-- subsequent: nil
書き換え前後でどのように変わるかをまとめておきます。
E1, expr1の部分はノードを列挙する構造で、書き換え前後で大きくは変わらない=> ex1の部分は右辺がNODE_ERRINFOなローカル変数代入のノードから、右辺を持たないLocalVariableTargetNodeに変わる- 例外をrescueしたあとで実行するコードの部分は書き換え前後で大きくは変わらない
- 複数
rescueがあるケースはどちらもリスト構造で表現するので書き換え前後で大きくは変わらない
rescue(がネストしている部分)の生成規則は以下のとおりです。
opt_rescue : k_rescue exc_list exc_var then compstmt(stmts) opt_rescue
1. 例外の列挙
1の例外を列挙する箇所はexc_listという生成規則になっています。
exc_list : arg_value { $$ = NEW_LIST($1, &@$); /*% ripper: rb_ary_new3(1, $:1) %*/ } | mrhs { if (!($$ = splat_array($1))) $$ = $1; } | none ; mrhs : args ',' arg_value { $$ = last_arg_append(p, $args, $arg_value, &@$); /*% ripper: mrhs_add!(mrhs_new_from_args!($:args), $:arg_value) %*/ } | args ',' tSTAR arg_value { $$ = rest_arg_append(p, $args, $arg_value, &@$); /*% ripper: mrhs_add_star!(mrhs_new_from_args!($:args), $:arg_value) %*/ } | tSTAR arg_value { $$ = NEW_SPLAT($arg_value, &@$, &@tSTAR); /*% ripper: mrhs_add_star!(mrhs_new!, $:arg_value) %*/ } ;
exc_list | mrhsのときの生成規則ではsplat_array関数を呼んで、rescue *[expr] => exの形のときにrescue expr => exに相当するノードを生成する最適化が入っています。
ノードの書き換え後はこの最適化をしなくなるので生成規則も修正します。
exc_list : arg_value { $$ = NEW_RB_ARRAY($1, &@$); /*% ripper: rb_ary_new3(1, $:1) %*/ } | mrhs | none ;
2. 変数の宣言
=> ex1の部分は右辺を持たないLocalVariableTargetNodeにする必要があります。
exc_var : tASSOC lhs { $$ = $2; /*% ripper: $:2 %*/ } | none ;
生成規則をみてみると該当する箇所はlhsになっています。
lhsというのはa = 1などの左辺を表す非終端機号で、これはTargetNodeではなくWriteNodeを生成します。
TargetNodeを生成するのはa, b = fooなどの左辺を表すmlhs_nodeです。
ではexc_var : tASSOC mlhs_nodeにすればいいかというと、そうもいかない理由があります。
lhsとmlhs_nodeを比較してみると生成規則の定義は全くおなじで、ごく一部のアクションだけが異なることがわかります。
mlhs_node : user_or_keyword_variable | primary_value '[' opt_call_args rbracket | primary_value call_op ident_or_const { anddot_multiple_assignment_check(p, &@2, $2); $$ = attrset(p, $1, $2, $3, &@$); /*% ripper: field!($:1, $:2, $:3) %*/ } | primary_value tCOLON2 tIDENTIFIER | primary_value tCOLON2 tCONSTANT | tCOLON3 tCONSTANT | backref ; lhs : user_or_keyword_variable | primary_value '[' opt_call_args rbracket | primary_value call_op ident_or_const { $$ = attrset(p, $1, $2, $3, &@$); /*% ripper: field!($:1, $:2, $:3) %*/ } | primary_value tCOLON2 tIDENTIFIER | primary_value tCOLON2 tCONSTANT | tCOLON3 tCONSTANT | backref ;
anddot_multiple_assignment_check関数は&.を弾くための関数です。
確認をしてみるとたしかにmultiple assignmentでは&.を左辺に書くことができませんが、rescue Ex => varのvarには&.を書くことができます。
a, b&.c = foo # -e:1: &. inside multiple assignment destination # ruby: compile error (SyntaxError)
def m rescue E1 => expr1.a ex1 rescue E2 => expr2&.a ex2 end # valid code
mlhs_nodeを使うことはできないので、非終端機号としてはlhsを使いつつアクションではwrite2target関数を用いてノードを変換することにします。
exc_var : tASSOC lhs { $$ = write2target(p, $2); /*% ripper: $:2 %*/ } | none ;
write2targetの実装で注意が必要なのはメソッド呼び出しと定数に関するノードの変換です。
メソッド呼び出しの場合はa[i]とs.fでTargetNodeが異なる点に注意します。
| WriteNode | TargetNode | |
|---|---|---|
a[i] |
CallNode |
IndexTargetNode |
s.f |
CallNode |
CallTargetNode |
定数の場合はConstantWriteとConstantPathWriteに分けて考えます。
ConstantWriteはConstantTargetに変換します。
ConstantPathWriteの場合は直下のConstantPathを1つ潰してConstantPathTargetを生成します。
| WriteNode | TargetNode | |
|---|---|---|
::A |
ConstantPathWrite(ConstantPath(:A)) |
ConstantPathTarget(:A) |
A |
ConstantWrite(:A) |
ConstantTarget(:A) |
::A::B |
ConstantPathWrite(ConstantPath(:B, ConstantPath(:A))) |
ConstantPathTarget(:B, ConstantPath(:A)) |
A::B |
ConstantPathWrite(ConstantPath(:B, ConstantRead(:A))) |
ConstantPathTarget(:B, ConstantRead(:A)) |
a::B |
ConstantPathWrite(ConstantPath(:B, Call)) |
ConstantPathTarget(:B, Call) |
実装はざっくりこんな感じです。
static rb_node_t * write2target(struct parser_params *p, rb_node_t *node) { switch (RB_NODE_TYPE(node)) { case RB_LOCAL_VARIABLE_WRITE_NODE: { rb_local_variable_write_node_t *cast = RB_NODE_LOCAL_VARIABLE_WRITE(node); return NEW_LTARGET(cast->name, nd_code_loc(cast)); } ... case RB_CALL_NODE: { rb_call_node_t *cast = RB_NODE_CALL(node); if (rb_node_get_fl(node) & RB_CALL_NODE_FLAGS_ATTRIBUTE_WRITE) { // `struct.field =` return NEW_RB_CALL_TARGET(cast->receiver, cast->name, rb_node_get_fl(cast), nd_code_loc(cast)); } else { // `a[0] =` return NEW_RB_INDEX_TARGET(cast->receiver, cast->arguments, rb_node_get_fl(cast), nd_code_loc(cast)); } } default: rb_bug("unexpected node: %s", rb_node_type_to_str(RB_NODE_TYPE(node))); UNREACHABLE_RETURN(0); } }
k_else compstmtからElseNodeをつくる
ElseNodeを生成する必要があるのでbodystmtのアクションで生成してnew_bodystmt関数に渡すようにします。
位置情報の修正などはあとでやりましょう。
bodystmt : compstmt(stmts)[body] lex_ctxt[ctxt] opt_rescue k_else compstmt(stmts)[elsebody] opt_ensure { $elsebody = NEW_RB_ELSE($elsebody, &NULL_LOC, &@k_else, &NULL_LOC); $$ = new_bodystmt(p, $body, $opt_rescue, $elsebody, $opt_ensure, &@$); /*% ripper: bodystmt!($:body, $:opt_rescue, $:elsebody, $:opt_ensure) %*/ }
opt_ensure生成規則でEnsureNodeをつくる
ensure ...の部分はopt_ensure生成規則に切り出されているので、そのアクションでEnsureNodeを作ればいいでしょう。
stmtsのチェックをしてからEnsureNodeを作るようにしたいので少し順番を入れ替えています1。
opt_ensure : k_ensure stmts terms?
{
p->ctxt.in_rescue = $1.in_rescue;
- $$ = $2;
- void_expr(p, void_stmts(p, $$));
+ void_expr(p, void_stmts(p, $2));
+ $$ = NEW_RB_ENSURE($2, &@$);
/*% ripper: ensure!($:2) %*/
}
rescue, ensure, elseに対応したので生成されるノードを確認しておきましょう。
class C a rescue E => ex ex else c ensure d end # ./miniruby --parser=parse.y --dump=p ../../test.rb # @ ProgramNode (location: (1,0)-(9,3)) # +-- locals: [] # +-- statements: # @ StatementsNode (location: (1,0)-(9,3)) # +-- body: (length: 1) # +-- @ ClassNode (location: (1,0)-(9,3))* # +-- locals: [:ex] # +-- class_keyword_loc: (1,0)-(1,5) = "" # +-- constant_path: # | @ ConstantReadNode (location: (1,6)-(1,7)) # | +-- name: :C # +-- inheritance_operator_loc: nil # +-- superclass: nil # +-- body: # | @ BeginNode (location: (1,7)-(8,3)) # | +-- begin_keyword_loc: nil # | +-- statements: # | | @ StatementsNode (location: (2,2)-(2,3)) # | | +-- body: (length: 1) # | | +-- @ CallNode (location: (2,2)-(2,3))* # | | +-- CallNodeFlags: variable_call # | | +-- receiver: nil # | | +-- call_operator_loc: nil # | | +-- name: :a # | | +-- message_loc: nil # | | +-- opening_loc: nil # | | +-- arguments: nil # | | +-- closing_loc: nil # | | +-- equal_loc: nil # | | +-- block: nil # | +-- rescue_clause: # | | @ RescueNode (location: (3,0)-(4,4)) # | | +-- keyword_loc: (0,-1)-(0,-1) = "" # | | +-- exceptions: (length: 1) # | | | +-- @ ConstantReadNode (location: (3,7)-(3,8)) # | | | +-- name: :E # | | +-- operator_loc: nil # | | +-- reference: # | | | @ LocalVariableTargetNode (location: (3,12)-(3,14)) # | | | +-- name: :ex # | | | +-- depth: 0 # | | +-- then_keyword_loc: nil # | | +-- statements: # | | | @ StatementsNode (location: (4,2)-(4,4)) # | | | +-- body: (length: 1) # | | | +-- @ LocalVariableReadNode (location: (4,2)-(4,4))* # | | | +-- name: :ex # | | | +-- depth: 0 # | | +-- subsequent: nil # | +-- else_clause: # | | @ ElseNode (location: (0,-1)-(0,-1)) # | | +-- else_keyword_loc: (5,0)-(5,4) = "" # | | +-- statements: # | | | @ StatementsNode (location: (6,2)-(6,3)) # | | | +-- body: (length: 1) # | | | +-- @ CallNode (location: (6,2)-(6,3))* # | | | +-- CallNodeFlags: variable_call # | | | +-- receiver: nil # | | | +-- call_operator_loc: nil # | | | +-- name: :c # | | | +-- message_loc: nil # | | | +-- opening_loc: nil # | | | +-- arguments: nil # | | | +-- closing_loc: nil # | | | +-- equal_loc: nil # | | | +-- block: nil # | | +-- end_keyword_loc: nil # | +-- ensure_clause: # | | @ EnsureNode (location: (7,0)-(8,3)) # | | +-- ensure_keyword_loc: (0,-1)-(0,-1) = "" # | | +-- statements: # | | | @ StatementsNode (location: (8,2)-(8,3)) # | | | +-- body: (length: 1) # | | | +-- @ CallNode (location: (8,2)-(8,3))* # | | | +-- CallNodeFlags: variable_call # | | | +-- receiver: nil # | | | +-- call_operator_loc: nil # | | | +-- name: :d # | | | +-- message_loc: nil # | | | +-- opening_loc: nil # | | | +-- arguments: nil # | | | +-- closing_loc: nil # | | | +-- equal_loc: nil # | | | +-- block: nil # | | +-- end_keyword_loc: (0,-1)-(0,-1) = "" # | +-- end_keyword_loc: nil # +-- end_keyword_loc: (9,0)-(9,3) = "" # +-- name: :C
よさそうですね。
begin ... endとmodifier rescue
関連する構文にbegin ... endと後置rescueがあるので、それらも書き換えておきましょう。
まずはbegin ... endから。
これはNEW_BEGINでノードを生成していたのをやめるだけです。
primary | k_begin
{
CMDARG_PUSH(0);
}
bodystmt
k_end
{
CMDARG_POP();
set_line_body($3, @1.end_pos.lineno);
// $$ = NEW_BEGIN($3, &@$);
$$ = $3;
nd_set_line($$, @1.end_pos.lineno);
/*% ripper: begin!($:3) %*/
}
続いて後置rescueですが、こちらはRescueModifierNodeという専用のノードが追加されました。
a rescue bと書いた場合には、左のaがexpressionフィールドに、右のbがrescue_expressionにセットされます。
RescueModifierNodeを生成するマクロを定義して、それを用いてノードを生成するようにすればよいでしょう。
| stmt modifier_rescue after_rescue stmt
{
p->ctxt.in_rescue = $3.in_rescue;
NODE *resq;
YYLTYPE loc = code_loc_gen(&@2, &@4);
- resq = NEW_RESBODY(0, 0, remove_begin($4), 0, &loc);
- $$ = NEW_RESCUE(remove_begin($1), resq, 0, &@$);
+ $$ = NEW_RB_RESCUE_MODIFIER(remove_begin($1), remove_begin($4), &@$);
/*% ripper: rescue_mod!($:1, $:4) %*/
}
ちょっと長くなったので今日はここまで。
まとめ
今日の成果です。
begin ... rescue ... ensure ... else ... endのノードを書き換えた- 後置rescueのノードを書き換えた