Skip to content

Commit

Permalink
Do not restart enumerator if it prematurely terminates due to exception
Browse files Browse the repository at this point in the history
Previously, if an exception occurred during enumeration, the next
call to Enumerator#next and similar methods would restart the
Enumerator from the beginning, which is unlikely to be the desired
behavior.

This changes the behavior so that after the enumerator has raised
an exception, future calls to Enumerator#next and similar methods
will raise StopIteration, similar to when calling Enumerator#next
after the enumerator has finished.

Fixes [Bug #16816]
  • Loading branch information
jeremyevans committed Apr 9, 2021
1 parent 5c4ff3f commit 851534b
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 6 deletions.
45 changes: 39 additions & 6 deletions enumerator.c
Expand Up @@ -758,6 +758,37 @@ next_init(VALUE obj, struct enumerator *e)
e->lookahead = Qundef;
}

struct get_next_values_args {
struct enumerator *e;
VALUE curr;
};

NORETURN(static void get_next_values_error(struct enumerator *, VALUE));

static void
get_next_values_error(struct enumerator *e, VALUE exc) {
e->fib = 0;
e->dst = Qnil;
e->lookahead = Qundef;
e->feedvalue = Qundef;
rb_exc_raise(exc);
}

static VALUE
get_next_values_fiber_resume(VALUE vargs) {
struct get_next_values_args *args = (struct get_next_values_args*)vargs;
return rb_fiber_resume(args->e->fib, 1, &args->curr);
}

NORETURN(static VALUE get_next_values_fiber_resume_rescue(VALUE, VALUE));

static VALUE
get_next_values_fiber_resume_rescue(VALUE vargs, VALUE exc) {
struct enumerator *e = ((struct get_next_values_args*)vargs)->e;
e->stop_exc = rb_exc_new2(rb_eStopIteration, "iteration raised an error");
get_next_values_error(e, exc);
}

static VALUE
get_next_values(VALUE obj, struct enumerator *e)
{
Expand All @@ -772,13 +803,15 @@ get_next_values(VALUE obj, struct enumerator *e)
next_init(obj, e);
}

vs = rb_fiber_resume(e->fib, 1, &curr);
struct get_next_values_args args;
args.e = e;
args.curr = curr;
vs = rb_rescue2(get_next_values_fiber_resume, (VALUE)&args,
get_next_values_fiber_resume_rescue, (VALUE)&args,
rb_eException, 0);

if (e->stop_exc) {
e->fib = 0;
e->dst = Qnil;
e->lookahead = Qundef;
e->feedvalue = Qundef;
rb_exc_raise(e->stop_exc);
get_next_values_error(e, e->stop_exc);
}
return vs;
}
Expand Down
10 changes: 10 additions & 0 deletions test/ruby/test_enumerator.rb
Expand Up @@ -38,6 +38,16 @@ def test_next
assert_raise(StopIteration){e.next}
end

def test_next_raise
f = Enumerator.new { |y|
3.times {|i| i == 2 ? raise(i.to_s) : (y.yield i) }
}

result = 4.times.map { f.next rescue $!.message}
expected = [0, 1, "2", "iteration raised an error"]
assert_equal(expected, result)
end

def test_loop
e = 3.times
i = 0
Expand Down

0 comments on commit 851534b

Please sign in to comment.