diff options
-rw-r--r-- | ChangeLog | 20 | ||||
-rw-r--r-- | compile.c | 70 | ||||
-rw-r--r-- | iseq.c | 2 | ||||
-rw-r--r-- | test/ripper/dummyparser.rb | 2 | ||||
-rw-r--r-- | test/ruby/test_keyword.rb | 105 | ||||
-rw-r--r-- | vm_core.h | 3 | ||||
-rw-r--r-- | vm_insnhelper.c | 39 |
7 files changed, 238 insertions, 3 deletions
@@ -1,3 +1,23 @@ +Mon Dec 26 22:15:27 2011 Yusuke Endoh <mame@tsg.ne.jp> + + * vm_core.h (struct rb_iseq_struct), compile.c (iseq_set_arguments, + iseq_compile_each), vm_insnhelper.c (vm_callee_setup_arg_complex): + implement keyword arguments. See [ruby-core:40290] + The feature is promised to be included in 2.0, but the detail spec + is still under discussion; this commit is a springboard for further + discussion. Please try it and give us feedback. + This commit includes fixes for some problems reported by Benoit + Daloze <eregontp AT gmail.com> [ruby-core:40518] and Marc-Andre + Lafortune <ruby-core-mailing-list AT marc-andre.ca> + [ruby-core:41772]. + + * iseq.c (iseq_free, prepare_iseq_build): bookkeeping. + + * test/ruby/test_keyword.rb: add tests for keyword arguments. + + * test/ripper/dummyparser.rb (class DummyParser): temporal fix for + ripper test. + Mon Dec 26 22:00:17 2011 Yusuke Endoh <mame@tsg.ne.jp> * node.h, node.c, parse.y: implement a parser part for keyword @@ -1127,6 +1127,36 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *optargs, NODE *node_args) iseq->arg_opts = 0; } + if (args->kw_args) { + NODE *node = args->kw_args; + VALUE keywords = rb_ary_tmp_new(1); + int i = 0, j; + + iseq->arg_keyword = get_dyna_var_idx_at_raw(iseq, args->kw_rest_arg->nd_vid); + COMPILE(optargs, "kwarg", args->kw_rest_arg); + while (node) { + rb_ary_push(keywords, INT2FIX(node->nd_body->nd_vid)); + COMPILE_POPED(optargs, "kwarg", node); /* nd_type(node) == NODE_KW_ARG */ + node = node->nd_next; + i += 1; + } + if ((args->kw_rest_arg->nd_vid & ID_SCOPE_MASK) == ID_JUNK) { + iseq->arg_keywords = i; + iseq->arg_keyword_table = ALLOC_N(ID, i); + for (j = 0; j < i; j++) { + iseq->arg_keyword_table[j] = FIX2INT(RARRAY_PTR(keywords)[j]); + } + } + else { + iseq->arg_keywords = 0; + iseq->arg_keyword_table = 0; + } + ADD_INSN(optargs, nd_line(args->kw_args), pop); + } + else { + iseq->arg_keyword = -1; + } + if (args->pre_init) { /* m_init */ COMPILE_POPED(optargs, "init arguments (m)", args->pre_init); } @@ -1151,11 +1181,15 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *optargs, NODE *node_args) } if (iseq->arg_opts != 0 || iseq->arg_post_len != 0 || - iseq->arg_rest != -1 || iseq->arg_block != -1) { + iseq->arg_rest != -1 || iseq->arg_block != -1 || + iseq->arg_keyword != -1) { iseq->arg_simple = 0; /* set arg_size: size of arguments */ - if (iseq->arg_block != -1) { + if (iseq->arg_keyword != -1) { + iseq->arg_size = iseq->arg_keyword + 1; + } + else if (iseq->arg_block != -1) { iseq->arg_size = iseq->arg_block + 1; } else if (iseq->arg_post_len) { @@ -4919,6 +4953,38 @@ iseq_compile_each(rb_iseq_t *iseq, LINK_ANCHOR *ret, NODE * node, int poped) } break; } + case NODE_KW_ARG:{ + LABEL *default_label = NEW_LABEL(nd_line(node)); + LABEL *end_label = NEW_LABEL(nd_line(node)); + int idx, lv, ls; + ID id = node->nd_body->nd_vid; + + ADD_INSN(ret, nd_line(node), dup); + ADD_INSN1(ret, nd_line(node), putobject, ID2SYM(id)); + ADD_SEND(ret, nd_line(node), ID2SYM(rb_intern("key?")), INT2FIX(1)); + ADD_INSNL(ret, nd_line(node), branchunless, default_label); + ADD_INSN(ret, nd_line(node), dup); + ADD_INSN1(ret, nd_line(node), putobject, ID2SYM(id)); + ADD_SEND(ret, nd_line(node), ID2SYM(rb_intern("delete")), INT2FIX(1)); + switch (nd_type(node->nd_body)) { + case NODE_LASGN: + idx = iseq->local_iseq->local_size - get_local_var_idx(iseq, id); + ADD_INSN1(ret, nd_line(node), setlocal, INT2FIX(idx)); + break; + case NODE_DASGN: + case NODE_DASGN_CURR: + idx = get_dyna_var_idx(iseq, id, &lv, &ls); + ADD_INSN2(ret, nd_line(node), setdynamic, INT2FIX(ls - idx), INT2FIX(lv)); + break; + default: + rb_bug("iseq_compile_each (NODE_KW_ARG): unknown node: %s", ruby_node_name(nd_type(node->nd_body))); + } + ADD_INSNL(ret, nd_line(node), jump, end_label); + ADD_LABEL(ret, default_label); + COMPILE_POPED(ret, "keyword default argument", node->nd_body); + ADD_LABEL(ret, end_label); + break; + } case NODE_DSYM:{ compile_dstr(iseq, ret, node); if (!poped) { @@ -83,6 +83,7 @@ iseq_free(void *ptr) RUBY_FREE_UNLESS_NULL(iseq->ic_entries); RUBY_FREE_UNLESS_NULL(iseq->catch_table); RUBY_FREE_UNLESS_NULL(iseq->arg_opt_table); + RUBY_FREE_UNLESS_NULL(iseq->arg_keyword_table); compile_data_free(iseq->compile_data); } ruby_xfree(ptr); @@ -239,6 +240,7 @@ prepare_iseq_build(rb_iseq_t *iseq, iseq->type = type; iseq->arg_rest = -1; iseq->arg_block = -1; + iseq->arg_keyword = -1; iseq->klass = 0; /* diff --git a/test/ripper/dummyparser.rb b/test/ripper/dummyparser.rb index 8a706b558a..1403203dae 100644 --- a/test/ripper/dummyparser.rb +++ b/test/ripper/dummyparser.rb @@ -151,7 +151,7 @@ class DummyParser < Ripper "&#{var}" end - def on_params(required, optional, rest, more, block) + def on_params(required, optional, rest, more, keyword, keyword_rest, block) args = NodeList.new required.each do |req| diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb new file mode 100644 index 0000000000..27edc6b8fd --- /dev/null +++ b/test/ruby/test_keyword.rb @@ -0,0 +1,105 @@ +require 'test/unit' + +class TestKeywordArguments < Test::Unit::TestCase + def f1(str: "foo", num: 424242) + [str, num] + end + + def test_f1 + assert_equal(["foo", 424242], f1) + assert_equal(["bar", 424242], f1(str: "bar")) + assert_equal(["foo", 111111], f1(num: 111111)) + assert_equal(["bar", 111111], f1(str: "bar", num: 111111)) + assert_raise(ArgumentError) { f1(str: "bar", check: true) } + assert_raise(ArgumentError) { f1("string") } + end + + + def f2(x, str: "foo", num: 424242) + [x, str, num] + end + + def test_f2 + assert_equal([:xyz, "foo", 424242], f2(:xyz)) + end + + + def f3(str: "foo", num: 424242, **h) + [str, num, h] + end + + def test_f3 + assert_equal(["foo", 424242, {}], f3) + assert_equal(["bar", 424242, {}], f3(str: "bar")) + assert_equal(["foo", 111111, {}], f3(num: 111111)) + assert_equal(["bar", 111111, {}], f3(str: "bar", num: 111111)) + assert_equal(["bar", 424242, {:check=>true}], f3(str: "bar", check: true)) + assert_raise(ArgumentError) { f3("string") } + end + + + define_method(:f4) {|str: "foo", num: 424242| [str, num] } + + def test_f4 + assert_equal(["foo", 424242], f4) + assert_equal(["bar", 424242], f4(str: "bar")) + assert_equal(["foo", 111111], f4(num: 111111)) + assert_equal(["bar", 111111], f4(str: "bar", num: 111111)) + assert_raise(ArgumentError) { f4(str: "bar", check: true) } + assert_raise(ArgumentError) { f4("string") } + end + + + define_method(:f5) {|str: "foo", num: 424242, **h| [str, num, h] } + + def test_f5 + assert_equal(["foo", 424242, {}], f5) + assert_equal(["bar", 424242, {}], f5(str: "bar")) + assert_equal(["foo", 111111, {}], f5(num: 111111)) + assert_equal(["bar", 111111, {}], f5(str: "bar", num: 111111)) + assert_equal(["bar", 424242, {:check=>true}], f5(str: "bar", check: true)) + assert_raise(ArgumentError) { f5("string") } + end + + + def f6(str: "foo", num: 424242, **h, &blk) + [str, num, h, blk] + end + + def test_f6 # [ruby-core:40518] + assert_equal(["foo", 424242, {}, nil], f6) + assert_equal(["bar", 424242, {}, nil], f6(str: "bar")) + assert_equal(["foo", 111111, {}, nil], f6(num: 111111)) + assert_equal(["bar", 111111, {}, nil], f6(str: "bar", num: 111111)) + assert_equal(["bar", 424242, {:check=>true}, nil], f6(str: "bar", check: true)) + a = f6 {|x| x + 42 } + assert_equal(["foo", 424242, {}], a[0, 3]) + assert_equal(43, a.last.call(1)) + end + + def f7(*r, str: "foo", num: 424242, **h) + [r, str, num, h] + end + + def test_f7 # [ruby-core:41772] + assert_equal([[], "foo", 424242, {}], f7) + assert_equal([[], "bar", 424242, {}], f7(str: "bar")) + assert_equal([[], "foo", 111111, {}], f7(num: 111111)) + assert_equal([[], "bar", 111111, {}], f7(str: "bar", num: 111111)) + assert_equal([[1], "foo", 424242, {}], f7(1)) + assert_equal([[1, 2], "foo", 424242, {}], f7(1, 2)) + assert_equal([[1, 2, 3], "foo", 424242, {}], f7(1, 2, 3)) + assert_equal([[1], "bar", 424242, {}], f7(1, str: "bar")) + assert_equal([[1, 2], "bar", 424242, {}], f7(1, 2, str: "bar")) + assert_equal([[1, 2, 3], "bar", 424242, {}], f7(1, 2, 3, str: "bar")) + end + + + def test_lambda + f = ->(str: "foo", num: 424242) { [str, num] } + assert_equal(["foo", 424242], f[]) + assert_equal(["bar", 424242], f[str: "bar"]) + assert_equal(["foo", 111111], f[num: 111111]) + assert_equal(["bar", 111111], f[str: "bar", num: 111111]) + end +end @@ -220,6 +220,9 @@ struct rb_iseq_struct { int arg_post_start; int arg_size; VALUE *arg_opt_table; + int arg_keyword; + int arg_keywords; + ID *arg_keyword_table; size_t stack_max; /* for stack overflow check */ diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 85b5e6ee78..9a22741cc3 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -132,6 +132,15 @@ argument_error(const rb_iseq_t *iseq, int miss_argc, int correct_argc) rb_exc_raise(exc); } +NORETURN(static void unknown_keyword_error(const rb_iseq_t *iseq, VALUE hash)); +static void +unknown_keyword_error(const rb_iseq_t *iseq, VALUE hash) +{ + (void) iseq; + (void) hash; + rb_raise(rb_eArgError, "unknown keyword"); +} + #define VM_CALLEE_SETUP_ARG(ret, th, iseq, orig_argc, orig_argv, block) \ if (LIKELY((iseq)->arg_simple & 0x01)) { \ /* simple check */ \ @@ -153,9 +162,30 @@ vm_callee_setup_arg_complex(rb_thread_t *th, const rb_iseq_t * iseq, int argc = orig_argc; VALUE *argv = orig_argv; rb_num_t opt_pc = 0; + VALUE keyword_hash = Qnil; th->mark_stack_len = argc + iseq->arg_size; + if (iseq->arg_keyword != -1) { + int i, j; + if (argc > 0) keyword_hash = rb_check_convert_type(argv[argc-1], T_HASH, "Hash", "to_hash"); + if (!NIL_P(keyword_hash)) { + argc--; + keyword_hash = rb_hash_dup(keyword_hash); + if (iseq->arg_keywords) { + for (i = j = 0; i < iseq->arg_keywords; i++) { + if (st_lookup(RHASH_TBL(keyword_hash), ID2SYM(iseq->arg_keyword_table[i]), 0)) j++; + } + if (RHASH_TBL(keyword_hash)->num_entries > (unsigned int) j) { + unknown_keyword_error(iseq, keyword_hash); + } + } + } + else { + keyword_hash = rb_hash_new(); + } + } + /* mandatory */ if (argc < (m + iseq->arg_post_len)) { /* check with post arg */ argument_error(iseq, argc, m + iseq->arg_post_len); @@ -205,6 +235,11 @@ vm_callee_setup_arg_complex(rb_thread_t *th, const rb_iseq_t * iseq, argc = 0; } + /* keyword argument */ + if (iseq->arg_keyword != -1) { + orig_argv[iseq->arg_keyword] = keyword_hash; + } + /* block arguments */ if (block && iseq->arg_block != -1) { VALUE blockval = Qnil; @@ -230,6 +265,10 @@ vm_callee_setup_arg_complex(rb_thread_t *th, const rb_iseq_t * iseq, orig_argv[iseq->arg_block] = blockval; /* Proc or nil */ } + if (iseq->arg_keyword && argc != 0) { + argument_error(iseq, orig_argc, m + iseq->arg_post_len); + } + th->mark_stack_len = 0; return (int)opt_pc; } |