diff --git a/NEWS b/NEWS index 97f0e0fcce..4e5abab47c 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,9 @@ sufficient information, see the ChangeLog file or Redmine * Non-Symbol keys in a keyword arguments hash was prohibited at 2.6.0, but now allowed again. [Bug #15658] +* Numbered parameter as the default block parameter is introduced as an + experimental feature. [Feature #4475] + === Core classes updates (outstanding ones only) Enumerable:: diff --git a/bootstraptest/test_syntax.rb b/bootstraptest/test_syntax.rb index a111990a1f..d06ebc9281 100644 --- a/bootstraptest/test_syntax.rb +++ b/bootstraptest/test_syntax.rb @@ -532,7 +532,7 @@ end assert_syntax_error "unterminated string meets end of file", '().."', '[ruby-dev:29732]' assert_equal %q{[]}, %q{$&;[]}, '[ruby-dev:31068]' assert_syntax_error "syntax error, unexpected *, expecting '}'", %q{{*0}}, '[ruby-dev:31072]' -assert_syntax_error "`@0' is not allowed as an instance variable name", %q{@0..0}, '[ruby-dev:31095]' +assert_syntax_error "leading zero is not allowed as a numbered parameter", %q{@0..0}, '[ruby-dev:31095]' assert_syntax_error "identifier $00 is not valid to get", %q{$00..0}, '[ruby-dev:31100]' assert_syntax_error "identifier $00 is not valid to set", %q{0..$00=1} assert_equal %q{0}, %q{[*0];0}, '[ruby-dev:31102]' diff --git a/ext/ripper/eventids2.c b/ext/ripper/eventids2.c index 039034af48..88985accd9 100644 --- a/ext/ripper/eventids2.c +++ b/ext/ripper/eventids2.c @@ -51,6 +51,7 @@ typedef struct { ID ripper_id_label_end; ID ripper_id_tlambda; ID ripper_id_tlambeg; + ID ripper_id_tnumparam; ID ripper_id_ignored_nl; ID ripper_id_comment; @@ -113,6 +114,7 @@ ripper_init_eventids2(void) set_id2(label_end); set_id2(tlambda); set_id2(tlambeg); + set_id2(tnumparam); set_id2(ignored_nl); set_id2(comment); @@ -276,6 +278,7 @@ static const struct token_assoc { {tLABEL_END, O(label_end)}, {tLAMBDA, O(tlambda)}, {tLAMBEG, O(tlambeg)}, + {tNUMPARAM, O(tnumparam)}, /* ripper specific tokens */ {tIGNORED_NL, O(ignored_nl)}, diff --git a/parse.y b/parse.y index b56e33b4f6..eebf137103 100644 --- a/parse.y +++ b/parse.y @@ -167,6 +167,8 @@ struct local_vars { struct local_vars *prev; }; +#define NUMPARAM_MAX 100 /* INT_MAX */ + #define DVARS_INHERIT ((void*)1) #define DVARS_TOPSCOPE NULL #define DVARS_TERMINAL_P(tbl) ((tbl) == DVARS_INHERIT || (tbl) == DVARS_TOPSCOPE) @@ -244,6 +246,8 @@ struct parser_params { rb_ast_t *ast; int node_id; + int max_numparam; + unsigned int command_start:1; unsigned int eofp: 1; unsigned int ruby__end__seen: 1; @@ -413,6 +417,8 @@ static NODE *method_add_block(struct parser_params*p, NODE *m, NODE *b, const YY static NODE *new_args(struct parser_params*,NODE*,NODE*,ID,NODE*,NODE*,const YYLTYPE*); static NODE *new_args_tail(struct parser_params*,NODE*,ID,ID,const YYLTYPE*); static NODE *new_kw_arg(struct parser_params *p, NODE *k, const YYLTYPE *loc); +static NODE *args_with_numbered(struct parser_params*,NODE*,int); +static ID numparam_id(struct parser_params*,int); static VALUE negate_lit(struct parser_params*, VALUE); static NODE *ret_args(struct parser_params*,NODE*); @@ -686,6 +692,12 @@ new_args_tail(struct parser_params *p, VALUE kw_args, VALUE kw_rest_arg, VALUE b return (VALUE)t; } +static inline VALUE +args_with_numbered(struct parser_params *p, VALUE args, int max_numparam) +{ + return args; +} + #define new_defined(p,expr,loc) dispatch1(defined, (expr)) static VALUE heredoc_dedent(struct parser_params*,VALUE); @@ -854,6 +866,7 @@ static void token_info_warn(struct parser_params *p, const char *token, token_in %token tBACK_REF "back reference" %token tSTRING_CONTENT "literal content" %token tREGEXP_END +%token tNUMPARAM "numbered parameter" %type singleton strings string string1 xstring regexp %type string_contents xstring_contents regexp_contents string_content @@ -3118,6 +3131,7 @@ block_param_def : '|' opt_bv_decl '|' } | tOROP { + p->max_numparam = -1; /*%%%*/ $$ = 0; /*% %*/ @@ -3126,6 +3140,7 @@ block_param_def : '|' opt_bv_decl '|' | '|' block_param opt_bv_decl '|' { p->cur_arg = 0; + p->max_numparam = -1; /*%%%*/ $$ = $2; /*% %*/ @@ -3357,20 +3372,34 @@ brace_block : '{' brace_body '}' ; brace_body : {$$ = dyna_push(p);} + { + $$ = p->max_numparam; + p->max_numparam = 0; + } opt_block_param compstmt { + int max_numparam = p->max_numparam; + p->max_numparam = $2; + $3 = args_with_numbered(p, $3, max_numparam); /*%%%*/ - $$ = NEW_ITER($2, $3, &@$); + $$ = NEW_ITER($3, $4, &@$); /*% %*/ - /*% ripper: brace_block!(escape_Qundef($2), $3) %*/ + /*% ripper: brace_block!(escape_Qundef($3), $4) %*/ dyna_pop(p, $1); } ; do_body : {$$ = dyna_push(p);} - {CMDARG_PUSH(0);} + { + $$ = p->max_numparam; + p->max_numparam = 0; + CMDARG_PUSH(0); + } opt_block_param bodystmt { + int max_numparam = p->max_numparam; + p->max_numparam = $2; + $3 = args_with_numbered(p, $3, max_numparam); /*%%%*/ $$ = NEW_ITER($3, $4, &@$); /*% %*/ @@ -3773,6 +3802,13 @@ string_dvar : tGVAR /*% %*/ /*% ripper: var_ref!($1) %*/ } + | tNUMPARAM + { + /*%%%*/ + $$ = NEW_DVAR(numparam_id(p, $1), &@1); + /*% %*/ + /*% ripper: var_ref!(number_arg!($1)) %*/ + } | backref ; @@ -3828,6 +3864,13 @@ user_variable : tIDENTIFIER | tGVAR | tCONSTANT | tCVAR + | tNUMPARAM + { + /*%%%*/ + $$ = numparam_id(p, $1); + /*% %*/ + /*% ripper: number_arg!($1) %*/ + } ; keyword_variable: keyword_nil {$$ = KWD2EID(nil, $1);} @@ -5967,6 +6010,9 @@ parser_peek_variable_name(struct parser_params *p) if (++ptr >= p->lex.pend) return 0; c = *ptr; } + else if (ISDIGIT(c)) { + return tSTRING_DVAR; + } break; case '{': p->lex.pcur = ptr; @@ -7612,12 +7658,34 @@ parse_atmark(struct parser_params *p, const enum lex_state_e last_state) return 0; } else if (ISDIGIT(c)) { - RUBY_SET_YYLLOC(loc); - pushback(p, c); if (result == tIVAR) { - compile_error(p, "`@%c' is not allowed as an instance variable name", c); + const char *ptr = p->lex.pcur - 1; + size_t len = p->lex.pend - ptr; + int overflow; + unsigned long n = ruby_scan_digits(ptr, len, 10, &len, &overflow); + p->lex.pcur = ptr + len; + RUBY_SET_YYLLOC(loc); + if (ptr[0] == '0') { + compile_error(p, "leading zero is not allowed as a numbered parameter"); + } + else if (overflow || n > NUMPARAM_MAX) { + compile_error(p, "too large numbered parameter"); + } + else if (DVARS_TERMINAL_P(p->lvtbl->args) || DVARS_TERMINAL_P(p->lvtbl->args->prev)) { + compile_error(p, "numbered parameter outside block"); + } + else if (p->max_numparam < 0) { + compile_error(p, "ordinary parameter is defined"); + } + else { + set_yylval_num((int)n); + SET_LEX_STATE(EXPR_ARG); + return tNUMPARAM; + } } else { + RUBY_SET_YYLLOC(loc); + pushback(p, c); compile_error(p, "`@@%c' is not allowed as a class variable name", c); } parser_show_error_line(p, &loc); @@ -8876,6 +8944,12 @@ gettable(struct parser_params *p, ID id, const YYLTYPE *loc) return NEW_LIT(add_mark_object(p, rb_enc_from_encoding(p->enc)), loc); } switch (id_type(id)) { + case ID_INTERNAL: + { + int idx = vtable_included(p->lvtbl->args, id); + if (idx) return NEW_DVAR(id, loc); + } + break; case ID_LOCAL: if (dyna_in_block(p) && dvar_defined_ref(p, id, &vidp)) { if (id == p->cur_arg) { @@ -9331,6 +9405,15 @@ assignable0(struct parser_params *p, ID id, const char **err) *err = "dynamic constant assignment"; return -1; case ID_CLASS: return NODE_CVASGN; + case ID_INTERNAL: + { + int idx = vtable_included(p->lvtbl->args, id); + if (idx) { + compile_error(p, "Can't assign to numbered parameter @%d", idx); + break; + } + } + /* fallthru */ default: compile_error(p, "identifier %"PRIsVALUE" is not valid to set", rb_id2str(id)); } @@ -9355,6 +9438,21 @@ assignable(struct parser_params *p, ID id, NODE *val, const YYLTYPE *loc) if (err) yyerror1(loc, err); return NEW_BEGIN(0, loc); } + +static ID +numparam_id(struct parser_params *p, int idx) +{ + struct vtable *args; + if (idx <= 0) return (ID)0; + if (p->max_numparam < idx) { + p->max_numparam = idx; + } + args = p->lvtbl->args; + while (idx > args->pos) { + vtable_add(args, internal_id(p)); + } + return args->tbl[idx-1]; +} #else static VALUE assignable(struct parser_params *p, VALUE lhs) @@ -10227,6 +10325,17 @@ new_args_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, ID block, return node; } +static NODE * +args_with_numbered(struct parser_params *p, NODE *args, int max_numparam) +{ + if (max_numparam > 0) { + if (!args) args = new_args_tail(p, 0, 0, 0, 0); + args->nd_ainfo->pre_args_num = max_numparam; + args->nd_ainfo->rest_arg = excessed_comma; + } + return args; +} + static NODE* dsym_node(struct parser_params *p, NODE *node, const YYLTYPE *loc) { diff --git a/test/ripper/test_parser_events.rb b/test/ripper/test_parser_events.rb index 0591eeb781..e4860ef533 100644 --- a/test/ripper/test_parser_events.rb +++ b/test/ripper/test_parser_events.rb @@ -812,6 +812,12 @@ class TestRipper::ParserEvents < Test::Unit::TestCase assert_equal true, thru_next end + def test_number_arg + thru_number_arg = false + parse('proc {@1}', :on_number_arg) {thru_number_arg = true} + assert_equal true, thru_number_arg + end + def test_opassign thru_opassign = false tree = parse('a += b', :on_opassign) {thru_opassign = true} @@ -1477,8 +1483,11 @@ class TestRipper::ParserEvents < Test::Unit::TestCase assert_equal("unterminated regexp meets end of file", compile_error('/')) end + def test_invalid_numbered_parameter_name + assert_equal("leading zero is not allowed as a numbered parameter", compile_error('proc{@0}')) + end + def test_invalid_instance_variable_name - assert_equal("`@1' is not allowed as an instance variable name", compile_error('@1')) assert_equal("`@' without identifiers is not allowed as an instance variable name", compile_error('@%')) assert_equal("`@' without identifiers is not allowed as an instance variable name", compile_error('@')) end diff --git a/test/ripper/test_scanner_events.rb b/test/ripper/test_scanner_events.rb index 1184b23435..bb98e5d3d9 100644 --- a/test/ripper/test_scanner_events.rb +++ b/test/ripper/test_scanner_events.rb @@ -259,6 +259,8 @@ class TestRipper::ScannerEvents < Test::Unit::TestCase scan('embvar', '"#@ivar"') assert_equal ['#'], scan('embvar', '"#@@cvar"') + assert_equal ['#'], + scan('embvar', '"#@1"') assert_equal [], scan('embvar', '"#lvar"') assert_equal [], @@ -348,6 +350,13 @@ class TestRipper::ScannerEvents < Test::Unit::TestCase scan('ivar', 'm(lvar, @ivar, @@cvar, $gvar)') end + def test_tnumparam + assert_equal [], + scan('tnumparam', '') + assert_equal ['@1'], + scan('tnumparam', 'proc {@1}') + end + def test_kw assert_equal [], scan('kw', '') @@ -742,6 +751,7 @@ class TestRipper::ScannerEvents < Test::Unit::TestCase assert_equal [" E\n\n"], scan('tstring_content', "<<""'E'\n E\n\n"), bug10392 + scan('tstring_content', "tap{<<""EOS}\n""there\n""heredoc\#@1xxx\nEOS") end def test_heredoc_end diff --git a/test/ruby/test_parse.rb b/test/ruby/test_parse.rb index d21484f4b6..dc4c143241 100644 --- a/test/ruby/test_parse.rb +++ b/test/ruby/test_parse.rb @@ -363,7 +363,7 @@ class TestParse < Test::Unit::TestCase def test_dstr_disallowed_variable bug8375 = '[ruby-core:54885] [Bug #8375]' - %w[@ @1 @. @@ @@1 @@. $ $%].each do |src| + %w[@ @. @@ @@1 @@. $ $%].each do |src| src = '#'+src+' ' str = assert_nothing_raised(SyntaxError, "#{bug8375} #{src.dump}") do break eval('"'+src+'"') @@ -378,15 +378,15 @@ class TestParse < Test::Unit::TestCase def assert_disallowed_variable(type, noname, invalid) noname.each do |name| - assert_syntax_error("a = #{name}", "`#{noname[0]}' without identifiers is not allowed as #{type} variable name") + assert_syntax_error("proc{a = #{name} }", "`#{noname[0]}' without identifiers is not allowed as #{type} variable name") end invalid.each do |name| - assert_syntax_error("a = #{name}", "`#{name}' is not allowed as #{type} variable name") + assert_syntax_error("proc {a = #{name} }", "`#{name}' is not allowed as #{type} variable name") end end def test_disallowed_instance_variable - assert_disallowed_variable("an instance", %w[@ @.], %w[@1]) + assert_disallowed_variable("an instance", %w[@ @.], %w[]) end def test_disallowed_class_variable diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index c28bc80bc0..42ad002b8e 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -1291,6 +1291,19 @@ eom assert_valid_syntax('obj::foo (1) {}') end + def test_numbered_parameter + assert_valid_syntax('proc {@1}') + assert_equal(3, eval('[1,2].then {@1+@2}')) + assert_equal("12", eval('[1,2].then {"#@1#@2"}')) + assert_syntax_error('proc {|| @1}', /ordinary parameter is defined/) + assert_syntax_error('proc {|x| @1}', /ordinary parameter is defined/) + assert_syntax_error('proc {@1 = nil}', /Can't assign to numbered parameter @1/) + assert_syntax_error('proc {@01}', /leading zero/) + assert_syntax_error('proc {@1_}', /unexpected/) + assert_syntax_error('proc {@9999999999999999}', /too large/) + assert_syntax_error('@1', /outside block/) + end + private def not_label(x) @result = x; @not_label ||= nil end