From a356fe1c3550892902103f66928426ac8279e072 Mon Sep 17 00:00:00 2001 From: nobu Date: Thu, 22 Oct 2015 06:30:12 +0000 Subject: Safe navigation operator * compile.c (iseq_peephole_optimize): peephole optimization for branchnil jumps. * compile.c (iseq_compile_each): generate save navigation operator code. * insns.def (branchnil): new opcode to pop the tos and branch if it is nil. * parse.y (NEW_QCALL, call_op, parser_yylex): parse token '.?'. [Feature #11537] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52214 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 14 +++++++ NEWS | 11 ++++++ compile.c | 53 +++++++++++++++++++++++++-- doc/syntax/calling_methods.rdoc | 4 ++ insns.def | 17 +++++++++ node.c | 5 ++- node.h | 6 ++- parse.y | 81 ++++++++++++++++++++++++----------------- template/id.h.tmpl | 2 +- test/ruby/test_call.rb | 12 ++++++ 10 files changed, 164 insertions(+), 41 deletions(-) diff --git a/ChangeLog b/ChangeLog index b6f093c239..12b006c28a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,17 @@ +Thu Oct 22 15:30:08 2015 Nobuyoshi Nakada + + * compile.c (iseq_peephole_optimize): peephole optimization for + branchnil jumps. + + * compile.c (iseq_compile_each): generate save navigation operator + code. + + * insns.def (branchnil): new opcode to pop the tos and branch if + it is nil. + + * parse.y (NEW_QCALL, call_op, parser_yylex): parse token '.?'. + [Feature #11537] + Thu Oct 22 13:16:19 2015 Guilherme Reis Campos * dir.c (ruby_brace_expand): glob brace expansion edge case fix. diff --git a/NEWS b/NEWS index 800974eadd..8a2c7f5846 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,17 @@ with all sufficient information, see the ChangeLog file. * besides, --enable/--disable=frozen-string-literal options also have been introduced. +* safe navigation operator: + + * new method call syntax, `object.?foo', method #foo is called on + `object' if it is not nil. + this is similar to `try!' in ActiveSupport, except for: + * method name is syntactically required + obj.try! {} # valid + obj.? {} # syntax error + * attribute assignment is valid + obj.?attr += 1 + === Core classes updates (outstanding ones only) * ARGF diff --git a/compile.c b/compile.c index 2527977163..b28861e2b8 100644 --- a/compile.c +++ b/compile.c @@ -1942,6 +1942,7 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal } if (iobj->insn_id == BIN(branchif) || + iobj->insn_id == BIN(branchnil) || iobj->insn_id == BIN(branchunless)) { /* * if L1 @@ -1955,6 +1956,31 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal if (nobj->insn_id == BIN(jump)) { OPERAND_AT(iobj, 0) = OPERAND_AT(nobj, 0); } + + if (nobj->insn_id == BIN(dup)) { + /* + * dup + * if L1 + * ... + * L1: + * dup + * if L2 + * => + * dup + * if L2 + * ... + * L1: + * dup + * if L2 + */ + INSN *pobj = (INSN *)iobj->link.prev; + nobj = (INSN *)nobj->link.next; + /* basic blocks, with no labels in the middle */ + if ((pobj && pobj->insn_id == BIN(dup)) && + (nobj && nobj->insn_id == iobj->insn_id)) { + OPERAND_AT(iobj, 0) = OPERAND_AT(nobj, 0); + } + } } if (do_tailcallopt && iobj->insn_id == BIN(leave)) { @@ -4319,6 +4345,7 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) VALUE asgnflag; LABEL *lfin = NEW_LABEL(line); LABEL *lcfin = NEW_LABEL(line); + LABEL *lskip = 0; /* class C; attr_accessor :c; end r = C.new @@ -4362,6 +4389,11 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) */ asgnflag = COMPILE_RECV(ret, "NODE_OP_ASGN2#recv", node); + if (node->nd_next->nd_aid) { + lskip = NEW_LABEL(line); + ADD_INSN(ret, line, dup); + ADD_INSNL(ret, line, branchnil, lskip); + } ADD_INSN(ret, line, dup); ADD_SEND(ret, line, vid, INT2FIX(0)); @@ -4385,6 +4417,9 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) ADD_LABEL(ret, lfin); ADD_INSN(ret, line, pop); + if (lskip) { + ADD_LABEL(ret, lskip); + } if (poped) { /* we can apply more optimize */ ADD_INSN(ret, line, pop); @@ -4392,14 +4427,16 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) } else { COMPILE(ret, "NODE_OP_ASGN2 val", node->nd_value); - ADD_SEND(ret, line, node->nd_next->nd_mid, - INT2FIX(1)); + ADD_SEND(ret, line, atype, INT2FIX(1)); if (!poped) { ADD_INSN(ret, line, swap); ADD_INSN1(ret, line, topn, INT2FIX(1)); } ADD_SEND_WITH_FLAG(ret, line, aid, INT2FIX(1), INT2FIX(asgnflag)); ADD_INSN(ret, line, pop); + if (lskip) { + ADD_LABEL(ret, lskip); + } } break; } @@ -4548,6 +4585,7 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) } break; } + case NODE_QCALL: case NODE_FCALL: case NODE_VCALL:{ /* VCALL: variable or call */ /* @@ -4557,6 +4595,7 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) */ DECL_ANCHOR(recv); DECL_ANCHOR(args); + LABEL *lskip = 0; ID mid = node->nd_mid; VALUE argc; unsigned int flag = 0; @@ -4631,8 +4670,13 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) } #endif /* receiver */ - if (type == NODE_CALL) { + if (type == NODE_CALL || type == NODE_QCALL) { COMPILE(recv, "recv", node->nd_recv); + if (type == NODE_QCALL) { + lskip = NEW_LABEL(line); + ADD_INSN(recv, line, dup); + ADD_INSNL(recv, line, branchnil, lskip); + } } else if (type == NODE_FCALL || type == NODE_VCALL) { ADD_CALL_RECEIVER(recv, line); @@ -4662,6 +4706,9 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) ADD_SEND_R(ret, line, mid, argc, parent_block, INT2FIX(flag), keywords); + if (lskip) { + ADD_LABEL(ret, lskip); + } if (poped) { ADD_INSN(ret, line, pop); } diff --git a/doc/syntax/calling_methods.rdoc b/doc/syntax/calling_methods.rdoc index 9cec4526b8..db0217cfef 100644 --- a/doc/syntax/calling_methods.rdoc +++ b/doc/syntax/calling_methods.rdoc @@ -27,6 +27,10 @@ This sends the +my_method+ message to +my_object+. Any object can be a receiver but depending on the method's visibility sending a message may raise a NoMethodError. +You may use .? to designate a receiver, then +my_method+ is not +invoked and the result is +nil+ when the receiver is +nil+. In that case, the +argument of +my_method+ are not evaluated. + You may also use :: to designate a receiver, but this is rarely used due to the potential for confusion with :: for namespaces. diff --git a/insns.def b/insns.def index a923d3a4d6..9aa2dd0ef7 100644 --- a/insns.def +++ b/insns.def @@ -1132,6 +1132,23 @@ branchunless } } +/** + @c jump + @e if val is nil, set PC to (PC + dst). + @j もし val が nil ならば、PC を (PC + dst) にする。 + */ +DEFINE_INSN +branchnil +(OFFSET dst) +(VALUE val) +() +{ + if (NIL_P(val)) { + RUBY_VM_CHECK_INTS(th); + JUMP(dst); + } +} + /**********************************************************/ /* for optimize */ diff --git a/node.c b/node.c index db9aa775f3..320e5d3bd4 100644 --- a/node.c +++ b/node.c @@ -357,7 +357,10 @@ dump_node(VALUE buf, VALUE indent, int comment, NODE *node) ANN(" where [attr]: [nd_next->nd_vid]"); ANN("example: struct.field += foo"); F_NODE(nd_recv, "receiver"); - F_ID(nd_next->nd_vid, "attr"); + F_CUSTOM1(nd_next->nd_vid, "attr") { + if (node->nd_next->nd_aid) A("? "); + A_ID(node->nd_next->nd_vid); + } F_CUSTOM1(nd_next->nd_mid, "operator") { switch (node->nd_next->nd_mid) { case 0: A("0 (||)"); break; diff --git a/node.h b/node.h index 791fb14a63..61ac466d93 100644 --- a/node.h +++ b/node.h @@ -96,6 +96,8 @@ enum node_type { #define NODE_FCALL NODE_FCALL NODE_VCALL, #define NODE_VCALL NODE_VCALL + NODE_QCALL, +#define NODE_QCALL NODE_QCALL NODE_SUPER, #define NODE_SUPER NODE_SUPER NODE_ZSUPER, @@ -394,8 +396,8 @@ typedef struct RNode { #define NEW_CVASGN(v,val) NEW_NODE(NODE_CVASGN,v,val,0) #define NEW_CVDECL(v,val) NEW_NODE(NODE_CVDECL,v,val,0) #define NEW_OP_ASGN1(p,id,a) NEW_NODE(NODE_OP_ASGN1,p,id,a) -#define NEW_OP_ASGN2(r,i,o,val) NEW_NODE(NODE_OP_ASGN2,r,val,NEW_OP_ASGN22(i,o)) -#define NEW_OP_ASGN22(i,o) NEW_NODE(NODE_OP_ASGN2,i,o,0) +#define NEW_OP_ASGN2(r,t,i,o,val) NEW_NODE(NODE_OP_ASGN2,r,val,NEW_OP_ASGN22(i,o,t)) +#define NEW_OP_ASGN22(i,o,t) NEW_NODE(NODE_OP_ASGN2,i,o,t) #define NEW_OP_ASGN_OR(i,val) NEW_NODE(NODE_OP_ASGN_OR,i,val,0) #define NEW_OP_ASGN_AND(i,val) NEW_NODE(NODE_OP_ASGN_AND,i,val,0) #define NEW_OP_CDECL(v,op,val) NEW_NODE(NODE_OP_CDECL,v,val,op) diff --git a/parse.y b/parse.y index 21d6131bcf..243ec51a48 100644 --- a/parse.y +++ b/parse.y @@ -371,6 +371,9 @@ static int parser_yyerror(struct parser_params*, const char*); #define ruby_coverage (parser->coverage) #endif +#define NODE_CALL_Q(q) (((q) == tDOTQ) ? NODE_QCALL : NODE_CALL) +#define NEW_QCALL(q,r,m,a) NEW_NODE(NODE_CALL_Q(q),r,m,a) + static int yylex(YYSTYPE*, struct parser_params*); #ifndef RIPPER @@ -457,8 +460,8 @@ static NODE *node_assign_gen(struct parser_params*,NODE*,NODE*); #define node_assign(node1, node2) node_assign_gen(parser, (node1), (node2)) static NODE *new_op_assign_gen(struct parser_params *parser, NODE *lhs, ID op, NODE *rhs); -static NODE *new_attr_op_assign_gen(struct parser_params *parser, NODE *lhs, ID attr, ID op, NODE *rhs); -#define new_attr_op_assign(lhs, type, attr, op, rhs) new_attr_op_assign_gen(parser, (lhs), (attr), (op), (rhs)) +static NODE *new_attr_op_assign_gen(struct parser_params *parser, NODE *lhs, ID atype, ID attr, ID op, NODE *rhs); +#define new_attr_op_assign(lhs, type, attr, op, rhs) new_attr_op_assign_gen(parser, (lhs), (type), (attr), (op), (rhs)) static NODE *new_const_op_assign_gen(struct parser_params *parser, NODE *lhs, ID op, NODE *rhs); #define new_const_op_assign(lhs, op, rhs) new_const_op_assign_gen(parser, (lhs), (op), (rhs)) @@ -844,7 +847,7 @@ static void token_info_pop(struct parser_params*, const char *token, size_t len) %type mlhs mlhs_head mlhs_basic mlhs_item mlhs_node mlhs_post mlhs_inner %type fsym keyword_variable user_variable sym symbol operation operation2 operation3 %type cname fname op f_rest_arg f_block_arg opt_f_block_arg f_norm_arg f_bad_arg -%type f_kwrest f_label f_arg_asgn +%type f_kwrest f_label f_arg_asgn call_op /*%%%*/ /*% %type program reswords then do dot_or_colon @@ -869,6 +872,7 @@ static void token_info_pop(struct parser_params*, const char *token, size_t len) %token tASET RUBY_TOKEN(ASET) "[]=" %token tLSHFT RUBY_TOKEN(LSHFT) "<<" %token tRSHFT RUBY_TOKEN(RSHFT) ">>" +%token tDOTQ RUBY_TOKEN(DOTQ) ".?" %token tCOLON2 "::" %token tCOLON3 ":: at EXPR_BEG" %token tOP_ASGN /* +=, -= etc. */ @@ -1260,15 +1264,15 @@ stmt : keyword_alias fitem {lex_state = EXPR_FNAME;} fitem $$ = dispatch3(opassign, $$, $5, $6); %*/ } - | primary_value '.' tIDENTIFIER tOP_ASGN command_call + | primary_value call_op tIDENTIFIER tOP_ASGN command_call { value_expr($5); - $$ = new_attr_op_assign($1, '.', $3, $4, $5); + $$ = new_attr_op_assign($1, $2, $3, $4, $5); } - | primary_value '.' tCONSTANT tOP_ASGN command_call + | primary_value call_op tCONSTANT tOP_ASGN command_call { value_expr($5); - $$ = new_attr_op_assign($1, '.', $3, $4, $5); + $$ = new_attr_op_assign($1, $2, $3, $4, $5); } | primary_value tCOLON2 tCONSTANT tOP_ASGN command_call { @@ -1456,24 +1460,24 @@ command : fcall command_args %prec tLOWEST $$ = method_add_block($$, $3); %*/ } - | primary_value '.' operation2 command_args %prec tLOWEST + | primary_value call_op operation2 command_args %prec tLOWEST { /*%%%*/ - $$ = NEW_CALL($1, $3, $4); + $$ = NEW_QCALL($2, $1, $3, $4); fixpos($$, $1); /*% - $$ = dispatch4(command_call, $1, ripper_id2sym('.'), $3, $4); + $$ = dispatch4(command_call, $1, ripper_id2sym($2), $3, $4); %*/ } - | primary_value '.' operation2 command_args cmd_brace_block + | primary_value call_op operation2 command_args cmd_brace_block { /*%%%*/ block_dup_check($4,$5); - $5->nd_iter = NEW_CALL($1, $3, $4); + $5->nd_iter = NEW_QCALL($2, $1, $3, $4); $$ = $5; fixpos($$, $1); /*% - $$ = dispatch4(command_call, $1, ripper_id2sym('.'), $3, $4); + $$ = dispatch4(command_call, $1, ripper_id2sym($2), $3, $4); $$ = method_add_block($$, $5); %*/ } @@ -1713,12 +1717,12 @@ mlhs_node : user_variable $$ = dispatch2(aref_field, $1, escape_Qundef($3)); %*/ } - | primary_value '.' tIDENTIFIER + | primary_value call_op tIDENTIFIER { /*%%%*/ $$ = attrset($1, $3); /*% - $$ = dispatch3(field, $1, ripper_id2sym('.'), $3); + $$ = dispatch3(field, $1, ripper_id2sym($2), $3); %*/ } | primary_value tCOLON2 tIDENTIFIER @@ -1729,12 +1733,12 @@ mlhs_node : user_variable $$ = dispatch2(const_path_field, $1, $3); %*/ } - | primary_value '.' tCONSTANT + | primary_value call_op tCONSTANT { /*%%%*/ $$ = attrset($1, $3); /*% - $$ = dispatch3(field, $1, ripper_id2sym('.'), $3); + $$ = dispatch3(field, $1, ripper_id2sym($2), $3); %*/ } | primary_value tCOLON2 tCONSTANT @@ -1804,12 +1808,12 @@ lhs : user_variable $$ = dispatch2(aref_field, $1, escape_Qundef($3)); %*/ } - | primary_value '.' tIDENTIFIER + | primary_value call_op tIDENTIFIER { /*%%%*/ $$ = attrset($1, $3); /*% - $$ = dispatch3(field, $1, ripper_id2sym('.'), $3); + $$ = dispatch3(field, $1, ripper_id2sym($2), $3); %*/ } | primary_value tCOLON2 tIDENTIFIER @@ -1820,12 +1824,12 @@ lhs : user_variable $$ = dispatch3(field, $1, ID2SYM(idCOLON2), $3); %*/ } - | primary_value '.' tCONSTANT + | primary_value call_op tCONSTANT { /*%%%*/ $$ = attrset($1, $3); /*% - $$ = dispatch3(field, $1, ripper_id2sym('.'), $3); + $$ = dispatch3(field, $1, ripper_id2sym($2), $3); %*/ } | primary_value tCOLON2 tCONSTANT @@ -2064,15 +2068,15 @@ arg : lhs '=' arg $$ = dispatch3(opassign, $1, $5, $6); %*/ } - | primary_value '.' tIDENTIFIER tOP_ASGN arg + | primary_value call_op tIDENTIFIER tOP_ASGN arg { value_expr($5); - $$ = new_attr_op_assign($1, '.', $3, $4, $5); + $$ = new_attr_op_assign($1, $2, $3, $4, $5); } - | primary_value '.' tCONSTANT tOP_ASGN arg + | primary_value call_op tCONSTANT tOP_ASGN arg { value_expr($5); - $$ = new_attr_op_assign($1, '.', $3, $4, $5); + $$ = new_attr_op_assign($1, $2, $3, $4, $5); } | primary_value tCOLON2 tIDENTIFIER tOP_ASGN arg { @@ -3648,7 +3652,7 @@ method_call : fcall paren_args $$ = method_arg(dispatch1(fcall, $1), $2); %*/ } - | primary_value '.' operation2 + | primary_value call_op operation2 { /*%%%*/ $$ = ruby_sourceline; @@ -3657,10 +3661,10 @@ method_call : fcall paren_args opt_paren_args { /*%%%*/ - $$ = NEW_CALL($1, $3, $5); + $$ = NEW_QCALL($2, $1, $3, $5); nd_set_line($$, $4); /*% - $$ = dispatch3(call, $1, ripper_id2sym('.'), $3); + $$ = dispatch3(call, $1, ripper_id2sym($2), $3); $$ = method_optarg($$, $5); %*/ } @@ -3688,7 +3692,7 @@ method_call : fcall paren_args $$ = dispatch3(call, $1, ID2SYM(idCOLON2), $3); %*/ } - | primary_value '.' + | primary_value call_op { /*%%%*/ $$ = ruby_sourceline; @@ -3697,10 +3701,10 @@ method_call : fcall paren_args paren_args { /*%%%*/ - $$ = NEW_CALL($1, idCall, $4); + $$ = NEW_QCALL($2, $1, idCall, $4); nd_set_line($$, $3); /*% - $$ = dispatch3(call, $1, ripper_id2sym('.'), + $$ = dispatch3(call, $1, ripper_id2sym($2), ID2SYM(idCall)); $$ = method_optarg($$, $4); %*/ @@ -5103,7 +5107,7 @@ operation3 : tIDENTIFIER | op ; -dot_or_colon : '.' +dot_or_colon : call_op /*%c%*/ /*%c { $$ = $1; } @@ -5115,6 +5119,10 @@ dot_or_colon : '.' %*/ ; +call_op : '.' {$$ = '.';} + | tDOTQ {$$ = tDOTQ;} + ; + opt_terms : /* none */ | terms ; @@ -8356,6 +8364,10 @@ parser_yylex(struct parser_params *parser) pushback(c); return tDOT2; } + if (c == '?') { + lex_state = EXPR_DOT; + return tDOTQ; + } pushback(c); if (c != -1 && ISDIGIT(c)) { yyerror("no . floating literal anymore; put 0 before dot"); @@ -10036,7 +10048,8 @@ new_op_assign_gen(struct parser_params *parser, NODE *lhs, ID op, NODE *rhs) } static NODE * -new_attr_op_assign_gen(struct parser_params *parser, NODE *lhs, ID attr, ID op, NODE *rhs) +new_attr_op_assign_gen(struct parser_params *parser, NODE *lhs, + ID atype, ID attr, ID op, NODE *rhs) { NODE *asgn; @@ -10046,7 +10059,7 @@ new_attr_op_assign_gen(struct parser_params *parser, NODE *lhs, ID attr, ID op, else if (op == tANDOP) { op = 1; } - asgn = NEW_OP_ASGN2(lhs, attr, op, rhs); + asgn = NEW_OP_ASGN2(lhs, (atype == tDOTQ), attr, op, rhs); fixpos(asgn, lhs); return asgn; } diff --git a/template/id.h.tmpl b/template/id.h.tmpl index ff2cb6b771..ab09556f2b 100644 --- a/template/id.h.tmpl +++ b/template/id.h.tmpl @@ -18,7 +18,7 @@ op_id_offset = 128 token_op_ids = %w[ tDOT2 tDOT3 tUPLUS tUMINUS tPOW tDSTAR tCMP tLSHFT tRSHFT tLEQ tGEQ tEQ tEQQ tNEQ tMATCH tNMATCH tAREF tASET - tCOLON2 tCOLON3 tANDOP tOROP + tCOLON2 tCOLON3 tANDOP tOROP tDOTQ ] defs = File.join(File.dirname(File.dirname(erb.filename)), "defs/id.def") diff --git a/test/ruby/test_call.rb b/test/ruby/test_call.rb index 5b81eb187a..14b6a6431b 100644 --- a/test/ruby/test_call.rb +++ b/test/ruby/test_call.rb @@ -31,4 +31,16 @@ class TestCall < Test::Unit::TestCase assert_nothing_raised(ArgumentError) {o.foo} assert_raise_with_message(ArgumentError, e.message, bug9622) {o.foo(100)} end + + def test_safe_call + s = Struct.new(:x, :y) + o = s.new("x") + assert_equal("X", o.x.?upcase) + assert_nil(o.y.?upcase) + assert_equal("x", o.x) + o.?x = 6 + assert_equal(6, o.x) + o.?x *= 7 + assert_equal(42, o.x) + end end -- cgit v1.2.3