aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authornobu <nobu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2015-10-22 06:30:12 +0000
committernobu <nobu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2015-10-22 06:30:12 +0000
commite2ac7b3d8b58816813ad1d05ed86fec8c80973d6 (patch)
treebee931e36267315487d45d5776ae8f4a568feddf
parentc17cfbea9a4cf6e12d7feae2fdda50457590af6a (diff)
downloadruby-e2ac7b3d8b58816813ad1d05ed86fec8c80973d6.tar.gz
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
-rw-r--r--ChangeLog14
-rw-r--r--NEWS11
-rw-r--r--compile.c53
-rw-r--r--doc/syntax/calling_methods.rdoc4
-rw-r--r--insns.def17
-rw-r--r--node.c5
-rw-r--r--node.h6
-rw-r--r--parse.y81
-rw-r--r--template/id.h.tmpl2
-rw-r--r--test/ruby/test_call.rb12
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 <nobu@ruby-lang.org>
+
+ * 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 <guilhermekbsa@gmail.com>
* 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 <code>.?</code> 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 <code>::</code> to designate a receiver, but this is rarely
used due to the potential for confusion with <code>::</code> 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 <node> mlhs mlhs_head mlhs_basic mlhs_item mlhs_node mlhs_post mlhs_inner
%type <id> fsym keyword_variable user_variable sym symbol operation operation2 operation3
%type <id> cname fname op f_rest_arg f_block_arg opt_f_block_arg f_norm_arg f_bad_arg
-%type <id> f_kwrest f_label f_arg_asgn
+%type <id> f_kwrest f_label f_arg_asgn call_op
/*%%%*/
/*%
%type <val> 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 <id> 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
{
/*%%%*/
$<num>$ = 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($$, $<num>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
{
/*%%%*/
$<num>$ = 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($$, $<num>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
{ $$ = $<val>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 .<digit> 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