Skip to content

Commit

Permalink
Arguments forwarding [Feature #16253]
Browse files Browse the repository at this point in the history
  • Loading branch information
nobu committed Oct 21, 2019
1 parent 35f90bf commit 62d4382
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 3 deletions.
9 changes: 9 additions & 0 deletions NEWS
Expand Up @@ -186,6 +186,15 @@ sufficient information, see the ChangeLog file or Redmine

* +yield+ in singleton class syntax is warned and will be deprecated later [Feature #15575].

* Argument forwarding by <code>...</code> is introduced. [Feature #16253]

def foo(...)
bar(...)
end

All arguments to +foo+ are forwarded to +bar+, including keyword and
block arguments.

=== Core classes updates (outstanding ones only)

Array::
Expand Down
47 changes: 44 additions & 3 deletions parse.y
Expand Up @@ -597,6 +597,10 @@ static void numparam_pop(struct parser_params *p, NODE *prev_inner);
# define METHOD_NOT '!'
#endif

#define idFWD_REST '*'
#define idFWD_KWREST idPow /* Use simple "**", as tDSTAR is "**arg" */
#define idFWD_BLOCK '&'

#define RE_OPTION_ONCE (1<<16)
#define RE_OPTION_ENCODING_SHIFT 8
#define RE_OPTION_ENCODING(e) (((e)&0xff)<<RE_OPTION_ENCODING_SHIFT)
Expand Down Expand Up @@ -1063,7 +1067,7 @@ static void token_info_warn(struct parser_params *p, const char *token, token_in
%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 call_op call_op2 reswords relop dot_or_colon
%type <id> p_kwrest p_kwnorest
%type <id> f_no_kwarg
%type <id> f_no_kwarg args_forward
%token END_OF_INPUT 0 "end-of-input"
%token <id> '.'
/* escaped chars, should be ignored otherwise */
Expand Down Expand Up @@ -2406,6 +2410,23 @@ paren_args : '(' opt_call_args rparen
/*% %*/
/*% ripper: arg_paren!(escape_Qundef($2)) %*/
}
| '(' args_forward rparen
{
if (!local_id(p, idFWD_REST) || !local_id(p, idFWD_KWREST) || !local_id(p, idFWD_BLOCK)) {
compile_error(p, "unexpected ...");
$$ = Qnone;
}
else {
/*%%%*/
NODE *splat = NEW_SPLAT(NEW_LVAR(idFWD_REST, &@2), &@2);
NODE *kwrest = list_append(p, NEW_LIST(0, &@2), NEW_LVAR(idFWD_KWREST, &@2));
NODE *block = NEW_BLOCK_PASS(NEW_LVAR(idFWD_BLOCK, &@2), &@2);
$$ = arg_append(p, splat, new_hash(p, kwrest, &@2), &@2);
$$ = arg_blk_pass($$, block);
/*% %*/
/*% ripper: arg_paren!($2) %*/
}
}
;

opt_paren_args : none
Expand Down Expand Up @@ -3396,15 +3417,15 @@ block_param_def : '|' opt_bv_decl '|'
/*%%%*/
$$ = 0;
/*% %*/
/*% ripper: block_var!(params_new(Qnil,Qnil,Qnil,Qnil,Qnil,Qnil,Qnil), escape_Qundef($2)) %*/
/*% ripper: block_var!(params!(Qnil,Qnil,Qnil,Qnil,Qnil,Qnil,Qnil), escape_Qundef($2)) %*/
}
| tOROP
{
p->max_numparam = ORDINAL_PARAM;
/*%%%*/
$$ = 0;
/*% %*/
/*% ripper: block_var!(params_new(Qnil,Qnil,Qnil,Qnil,Qnil,Qnil,Qnil), Qnil) %*/
/*% ripper: block_var!(params!(Qnil,Qnil,Qnil,Qnil,Qnil,Qnil,Qnil), Qnil) %*/
}
| '|' block_param opt_bv_decl '|'
{
Expand Down Expand Up @@ -4862,13 +4883,33 @@ f_args : f_arg ',' f_optarg ',' f_rest_arg opt_args_tail
{
$$ = new_args(p, Qnone, Qnone, Qnone, Qnone, $1, &@$);
}
| args_forward
{
arg_var(p, idFWD_REST);
arg_var(p, idFWD_KWREST);
arg_var(p, idFWD_BLOCK);
/*%%%*/
$$ = new_args_tail(p, Qnone, idFWD_KWREST, idFWD_BLOCK, &@1);
$$ = new_args(p, Qnone, Qnone, idFWD_REST, Qnone, $$, &@$);
/*% %*/
/*% ripper: params_new(Qnone, Qnone, $1, Qnone, Qnone, Qnone, Qnone) %*/
}
| /* none */
{
$$ = new_args_tail(p, Qnone, Qnone, Qnone, &@0);
$$ = new_args(p, Qnone, Qnone, Qnone, Qnone, $$, &@0);
}
;

args_forward : tBDOT3
{
/*%%%*/
$$ = idDot3;
/*% %*/
/*% ripper: args_forward! %*/
}
;

f_bad_arg : tCONSTANT
{
/*%%%*/
Expand Down
6 changes: 6 additions & 0 deletions test/ripper/test_parser_events.rb
Expand Up @@ -131,6 +131,12 @@ def test_args_new
assert_equal true, thru_args_new
end

def test_args_forward
thru_args_forward = false
parse('def m(...) n(...) end', :on_args_forward) {thru_args_forward = true}
assert_equal true, thru_args_forward
end

def test_arg_paren
# FIXME
end
Expand Down
40 changes: 40 additions & 0 deletions test/ruby/test_syntax.rb
Expand Up @@ -1471,6 +1471,46 @@ def test_value_expr_in_condition
assert_valid_syntax("tap {a = (break unless true)}")
end

def test_argument_forwarding
assert_valid_syntax('def foo(...) bar(...) end')
assert_valid_syntax('def foo(...) end')
assert_syntax_error('iter do |...| end', /unexpected/)
assert_syntax_error('iter {|...|}', /unexpected/)
assert_syntax_error('def foo(x, y, z) bar(...); end', /unexpected/)
assert_syntax_error('def foo(x, y, z) super(...); end', /unexpected/)
assert_syntax_error('def foo(...) yield(...); end', /unexpected/)
assert_syntax_error('def foo(...) return(...); end', /unexpected/)
assert_syntax_error('def foo(...) a = (...); end', /unexpected/)
assert_syntax_error('def foo(...) [...]; end', /unexpected/)
assert_syntax_error('def foo(...) foo[...]; end', /unexpected/)
assert_syntax_error('def foo(...) foo[...] = x; end', /unexpected/)
assert_syntax_error('def foo(...) foo(...) { }; end', /both block arg and actual block given/)
assert_syntax_error('def foo(...) defined?(...); end', /unexpected/)

obj1 = Object.new
def obj1.bar(*args, **kws, &block)
block.call(args, kws)
end
obj1.instance_eval('def foo(...) bar(...) end')

klass = Class.new {
def foo(*args, **kws, &block)
block.call(args, kws)
end
}
obj2 = klass.new
obj2.instance_eval('def foo(...) super(...) end')

[obj1, obj2].each do |obj|
assert_equal([[1, 2, 3], {k1: 4, k2: 5}], obj.foo(1, 2, 3, k1: 4, k2: 5) {|*x| x})
assert_equal(-1, obj.:foo.arity)
parameters = obj.:foo.parameters
assert_equal(:rest, parameters.dig(0, 0))
assert_equal(:keyrest, parameters.dig(1, 0))
assert_equal(:block, parameters.dig(2, 0))
end
end

private

def not_label(x) @result = x; @not_label ||= nil end
Expand Down

0 comments on commit 62d4382

Please sign in to comment.