From 63644016669c173190c71d39fb33ad9502fe2b0f Mon Sep 17 00:00:00 2001
From: Paul Sokolovsky <pfalcon@users.sourceforge.net>
Date: Sat, 21 Oct 2017 12:13:44 +0300
Subject: [PATCH] py/objgenerator: Allow to pend an exception for next
 execution.

This implements .pend_throw(exc) method, which sets up an exception to be
triggered on the next call to generator's .__next__() or .send() method.
This is unlike .throw(), which immediately starts to execute the generator
to process the exception. This effectively adds Future-like capabilities
to generator protocol (exception will be raised in the future).

The need for such a method arised to implement uasyncio wait_for() function
efficiently (its behavior is clearly "Future" like, and normally would
require to introduce an expensive Future wrapper around all native
couroutines, like upstream asyncio does).

py/objgenerator: pend_throw: Return previous pended value.

This effectively allows to store an additional value (not necessary an
exception) in a coroutine while it's not being executed. uasyncio has
exactly this usecase: to mark a coro waiting in I/O queue (and thus
not executed in the normal scheduling queue), for the purpose of
implementing wait_for() function (cancellation of such waiting coro
by a timeout).
---
 py/mpconfig.h                            |  9 +++++++
 py/objgenerator.c                        | 30 ++++++++++++++++++++++--
 tests/basics/generator_pend_throw.py     | 26 ++++++++++++++++++++
 tests/basics/generator_pend_throw.py.exp |  4 ++++
 tests/run-tests                          |  2 +-
 5 files changed, 68 insertions(+), 3 deletions(-)
 create mode 100644 tests/basics/generator_pend_throw.py
 create mode 100644 tests/basics/generator_pend_throw.py.exp

diff --git a/py/mpconfig.h b/py/mpconfig.h
index 19bae4f88..763bb378e 100644
--- a/py/mpconfig.h
+++ b/py/mpconfig.h
@@ -706,6 +706,15 @@ typedef double mp_float_t;
 #define MICROPY_PY_ASYNC_AWAIT (1)
 #endif
 
+// Non-standard .pend_throw() method for generators, allowing for
+// Future-like behavior with respect to exception handling: an
+// exception set with .pend_throw() will activate on the next call
+// to generator's .send() or .__next__(). (This is useful to implement
+// async schedulers.)
+#ifndef MICROPY_PY_GENERATOR_PEND_THROW
+#define MICROPY_PY_GENERATOR_PEND_THROW (1)
+#endif
+
 // Issue a warning when comparing str and bytes objects
 #ifndef MICROPY_PY_STR_BYTES_CMP_WARN
 #define MICROPY_PY_STR_BYTES_CMP_WARN (0)
diff --git a/py/objgenerator.c b/py/objgenerator.c
index 9a294debb..8c1260b60 100644
--- a/py/objgenerator.c
+++ b/py/objgenerator.c
@@ -4,7 +4,7 @@
  * The MIT License (MIT)
  *
  * Copyright (c) 2013, 2014 Damien P. George
- * Copyright (c) 2014 Paul Sokolovsky
+ * Copyright (c) 2014-2017 Paul Sokolovsky
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -104,7 +104,16 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
             mp_raise_TypeError("can't send non-None value to a just-started generator");
         }
     } else {
-        *self->code_state.sp = send_value;
+        #if MICROPY_PY_GENERATOR_PEND_THROW
+        // If exception is pending (set using .pend_throw()), process it now.
+        if (*self->code_state.sp != mp_const_none) {
+            throw_value = *self->code_state.sp;
+            *self->code_state.sp = MP_OBJ_NULL;
+        } else
+        #endif
+        {
+            *self->code_state.sp = send_value;
+        }
     }
     mp_obj_dict_t *old_globals = mp_globals_get();
     mp_globals_set(self->globals);
@@ -125,6 +134,9 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
 
         case MP_VM_RETURN_YIELD:
             *ret_val = *self->code_state.sp;
+            #if MICROPY_PY_GENERATOR_PEND_THROW
+            *self->code_state.sp = mp_const_none;
+            #endif
             break;
 
         case MP_VM_RETURN_EXCEPTION: {
@@ -219,10 +231,24 @@ STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
 
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(gen_instance_close_obj, gen_instance_close);
 
+STATIC mp_obj_t gen_instance_pend_throw(mp_obj_t self_in, mp_obj_t exc_in) {
+    mp_obj_gen_instance_t *self = MP_OBJ_TO_PTR(self_in);
+    if (self->code_state.sp == self->code_state.state - 1) {
+        mp_raise_TypeError("can't pend throw to just-started generator");
+    }
+    mp_obj_t prev = *self->code_state.sp;
+    *self->code_state.sp = exc_in;
+    return prev;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_pend_throw_obj, gen_instance_pend_throw);
+
 STATIC const mp_rom_map_elem_t gen_instance_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&gen_instance_close_obj) },
     { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&gen_instance_send_obj) },
     { MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&gen_instance_throw_obj) },
+    #if MICROPY_PY_GENERATOR_PEND_THROW
+    { MP_ROM_QSTR(MP_QSTR_pend_throw), MP_ROM_PTR(&gen_instance_pend_throw_obj) },
+    #endif
 };
 
 STATIC MP_DEFINE_CONST_DICT(gen_instance_locals_dict, gen_instance_locals_dict_table);
diff --git a/tests/basics/generator_pend_throw.py b/tests/basics/generator_pend_throw.py
new file mode 100644
index 000000000..949655612
--- /dev/null
+++ b/tests/basics/generator_pend_throw.py
@@ -0,0 +1,26 @@
+def gen():
+    i = 0
+    while 1:
+        yield i
+        i += 1
+
+g = gen()
+
+try:
+    g.pend_throw
+except AttributeError:
+    print("SKIP")
+    raise SystemExit
+
+
+print(next(g))
+print(next(g))
+g.pend_throw(ValueError())
+
+v = None
+try:
+    v = next(g)
+except Exception as e:
+    print("raised", repr(e))
+
+print("ret was:", v)
diff --git a/tests/basics/generator_pend_throw.py.exp b/tests/basics/generator_pend_throw.py.exp
new file mode 100644
index 000000000..f9894a089
--- /dev/null
+++ b/tests/basics/generator_pend_throw.py.exp
@@ -0,0 +1,4 @@
+0
+1
+raised ValueError()
+ret was: None
diff --git a/tests/run-tests b/tests/run-tests
index 45cb1a746..8719befbd 100755
--- a/tests/run-tests
+++ b/tests/run-tests
@@ -337,7 +337,7 @@ def run_tests(pyb, tests, args, base_path="."):
     # Some tests are known to fail with native emitter
     # Remove them from the below when they work
     if args.emit == 'native':
-        skip_tests.update({'basics/%s.py' % t for t in 'gen_yield_from gen_yield_from_close gen_yield_from_ducktype gen_yield_from_exc gen_yield_from_iter gen_yield_from_send gen_yield_from_stopped gen_yield_from_throw gen_yield_from_throw2 gen_yield_from_throw3 generator1 generator2 generator_args generator_close generator_closure generator_exc generator_return generator_send'.split()}) # require yield
+        skip_tests.update({'basics/%s.py' % t for t in 'gen_yield_from gen_yield_from_close gen_yield_from_ducktype gen_yield_from_exc gen_yield_from_iter gen_yield_from_send gen_yield_from_stopped gen_yield_from_throw gen_yield_from_throw2 gen_yield_from_throw3 generator1 generator2 generator_args generator_close generator_closure generator_exc generator_pend_throw generator_return generator_send'.split()}) # require yield
         skip_tests.update({'basics/%s.py' % t for t in 'bytes_gen class_store_class globals_del string_join'.split()}) # require yield
         skip_tests.update({'basics/async_%s.py' % t for t in 'def await await2 for for2 with with2'.split()}) # require yield
         skip_tests.update({'basics/%s.py' % t for t in 'try_reraise try_reraise2'.split()}) # require raise_varargs
-- 
GitLab