[v2] coroutines: Implement n4849 changes to exception handling.

Message ID 4763058A-F2BF-46D9-BA14-9A45A4211BB7@sandoe.co.uk
State New
Headers show
Series
  • [v2] coroutines: Implement n4849 changes to exception handling.
Related show

Commit Message

Iain Sandoe March 25, 2020, 7:46 p.m.
Hi,

Iain Sandoe <developer@sandoe-acoustics.co.uk> wrote:

> This is the first of two remaining changes needed to bring the GCC

> implementation into line with the standard draft at n4849.


Here is a revised version with the “initial await resume called” variable
renamed to be more consistent, as we discussed off-list.

OK for master?
thanks
Iain

    coroutines: Implement n4849 changes to exception handling.
    
    The standard now calls up a revised mechanism to handle exceptions
    where exceptions thrown by the await_resume () method of the
    initial suspend expression are considered in the same manner as
    exceptions thrown by the user's function body.
    
    This implements [dcl.fct.def.coroutine] / 5.3.
    
    gcc/cp/ChangeLog:
    
    2020-03-25  Iain Sandoe  <iain@sandoe.co.uk>
    
            * coroutines.cc (co_await_expander): If we are expanding the
            initial await expression, set a boolean flag to show that we
            have now reached the initial await_resume() method call.
            (expand_co_awaits): Handle the 'initial await resume called' flag.
            (build_actor_fn): Insert the initial await expression into the
            start of the user's function-body. Handle the 'initial await
            resume called' flag.
            (morph_fn_to_coro): Initialise the 'initial await resume called'
            flag.  Modify the unhandled exception catch clause to recognise
            exceptions that occur before the initial await_resume() and re-
            throw them.
    
    gcc/testsuite/ChangeLog:
    
    2020-03-25  Iain Sandoe  <iain@sandoe.co.uk>
    
            * g++.dg/coroutines/torture/exceptions-test-01-n4849-a.C: New test.

Comments

Nathan Sidwell March 26, 2020, 12:01 p.m. | #1
On 3/25/20 3:46 PM, Iain Sandoe wrote:
> Hi,

> 

> Iain Sandoe <developer@sandoe-acoustics.co.uk> wrote:

> 

>> This is the first of two remaining changes needed to bring the GCC

>> implementation into line with the standard draft at n4849.

> 

> Here is a revised version with the “initial await resume called” variable

> renamed to be more consistent, as we discussed off-list.

> 


> +  /* n4849 adds specific behaviour to treat exceptions thrown by the

> +     await_resume () of the initial suspend expression.  In order to

> +     implement this, we need to treat the initial_suspend expression

> +     as if it were part of the user's authored function body.  This


(channelling Sandra) user-authored, I think?
  only need to exercise the initial

Otherwise fine -- no cognative dissonance on variable name vs meaning :)

ok, thanks

nathan

-- 
Nathan Sidwell

Patch

diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
index ef1ce150680..192d4e7faa1 100644
--- a/gcc/cp/coroutines.cc
+++ b/gcc/cp/coroutines.cc
@@ -1348,6 +1348,7 @@  struct coro_aw_data
   tree actor_fn;   /* Decl for context.  */
   tree coro_fp;    /* Frame pointer var.  */
   tree resume_idx; /* This is the index var in the frame.  */
+  tree i_a_r_c;    /* initial suspend await_resume() was called if true.  */
   tree self_h;     /* This is a handle to the current coro (frame var).  */
   tree cleanup;    /* This is where to go once we complete local destroy.  */
   tree cororet;    /* This is where to go if we suspend.  */
@@ -1445,6 +1446,8 @@  co_await_expander (tree *stmt, int * /*do_subtree*/, void *d)
   tree awaiter_calls = TREE_OPERAND (saved_co_await, 3);
 
   tree source = TREE_OPERAND (saved_co_await, 4);
+  bool is_initial =
+    (source && TREE_INT_CST_LOW (source) == (int) INITIAL_SUSPEND_POINT);
   bool is_final = (source
 		   && TREE_INT_CST_LOW (source) == (int) FINAL_SUSPEND_POINT);
   bool needs_dtor = TYPE_HAS_NONTRIVIAL_DESTRUCTOR (TREE_TYPE (var));
@@ -1586,6 +1589,16 @@  co_await_expander (tree *stmt, int * /*do_subtree*/, void *d)
   resume_label = build_stmt (loc, LABEL_EXPR, resume_label);
   append_to_statement_list (resume_label, &stmt_list);
 
+  if (is_initial)
+    {
+      /* Note that we are about to execute the await_resume() for the initial
+	 await expression.  */
+      r = build2_loc (loc, MODIFY_EXPR, boolean_type_node, data->i_a_r_c,
+		      boolean_true_node);
+      r = coro_build_cvt_void_expr_stmt (r, loc);
+      append_to_statement_list (r, &stmt_list);
+    }
+
   /* This will produce the value (if one is provided) from the co_await
      expression.  */
   tree resume_call = TREE_VEC_ELT (awaiter_calls, 2); /* await_resume().  */
@@ -1634,10 +1647,10 @@  co_await_expander (tree *stmt, int * /*do_subtree*/, void *d)
 
 static tree
 expand_co_awaits (tree fn, tree *fnbody, tree coro_fp, tree resume_idx,
-		  tree cleanup, tree cororet, tree self_h)
+		  tree i_a_r_c, tree cleanup, tree cororet, tree self_h)
 {
   coro_aw_data data
-    = {fn, coro_fp, resume_idx, self_h, cleanup, cororet, 2};
+    = {fn, coro_fp, resume_idx, i_a_r_c, self_h, cleanup, cororet, 2};
   cp_walk_tree (fnbody, co_await_expander, &data, NULL);
   return *fnbody;
 }
@@ -2158,8 +2171,7 @@  build_actor_fn (location_t loc, tree coro_frame_type, tree actor, tree fnbody,
 
   /* Get a reference to the initial suspend var in the frame.  */
   transform_await_expr (initial_await, &xform);
-  r = coro_build_expr_stmt (initial_await, loc);
-  add_stmt (r);
+  tree initial_await_stmt = coro_build_expr_stmt (initial_await, loc);
 
   /* co_return branches to the final_suspend label, so declare that now.  */
   tree fs_label = create_named_label_with_ctx (loc, "final.suspend", actor);
@@ -2167,6 +2179,34 @@  build_actor_fn (location_t loc, tree coro_frame_type, tree actor, tree fnbody,
   /* Expand co_returns in the saved function body  */
   fnbody = expand_co_returns (&fnbody, promise_proxy, ap, fs_label);
 
+  /* n4849 adds specific behaviour to treat exceptions thrown by the
+     await_resume () of the initial suspend expression.  In order to
+     implement this, we need to treat the initial_suspend expression
+     as if it were part of the user's authored function body.  This
+     only applies if exceptions are enabled.  */
+  if (flag_exceptions)
+    {
+      tree outer = fnbody;
+      if (TREE_CODE (outer) == BIND_EXPR)
+	outer = BIND_EXPR_BODY (outer);
+      gcc_checking_assert (TREE_CODE (outer) == TRY_BLOCK);
+      tree sl = TRY_STMTS (outer);
+      if (TREE_CODE (sl) == STATEMENT_LIST)
+	{
+	  tree_stmt_iterator si = tsi_start (sl);
+	  tsi_link_before (&si, initial_await_stmt, TSI_NEW_STMT);
+	}
+      else
+	{
+	  tree new_try = NULL_TREE;
+	  append_to_statement_list (initial_await_stmt, &new_try);
+	  append_to_statement_list (sl, &new_try);
+	  TRY_STMTS (outer) = new_try;
+	}
+    }
+  else
+    add_stmt (initial_await_stmt);
+
   /* Transform the await expressions in the function body.  Only do each
      await tree once!  */
   hash_set<tree> pset;
@@ -2336,10 +2376,20 @@  build_actor_fn (location_t loc, tree coro_frame_type, tree actor, tree fnbody,
     = build_class_member_access_expr (actor_frame, res_idx_m, NULL_TREE, false,
 				      tf_warning_or_error);
 
+  /* Boolean value to flag that the initial suspend expression's
+     await_resume () has been called, and therefore we are in the user's
+     function body for the purposes of handing exceptions.  */
+  tree i_a_r_c_m
+    = lookup_member (coro_frame_type, get_identifier ("__i_a_r_c"),
+		     /*protect=*/1, /*want_type=*/0, tf_warning_or_error);
+  tree i_a_r_c
+    = build_class_member_access_expr (actor_frame, i_a_r_c_m, NULL_TREE,
+				      false, tf_warning_or_error);
+
   /* We've now rewritten the tree and added the initial and final
      co_awaits.  Now pass over the tree and expand the co_awaits.  */
   actor_body = expand_co_awaits (actor, &actor_body, actor_fp, res_idx,
-				 del_promise_label, ret_label, ash);
+				 i_a_r_c, del_promise_label, ret_label, ash);
 
   actor_body = pop_stmt_list (actor_body);
   BIND_EXPR_BODY (actor_bind) = actor_body;
@@ -3040,6 +3090,7 @@  act_des_fn (tree orig, tree fn_type, tree coro_frame_ptr, const char* name)
   void (*__destroy)(_R_frame *);
   coro1::promise_type __p;
   bool frame_needs_free; free the coro frame mem if set.
+  bool i_a_r_c; [dcl.fct.def.coroutine] / 5.3
   short __resume_at;
   handle_type self_handle;
   (maybe) parameter copies.
@@ -3161,6 +3212,8 @@  morph_fn_to_coro (tree orig, tree *resumer, tree *destroyer)
     = coro_make_frame_entry (&field_list, "__p", promise_type, fn_start);
   tree fnf_name = coro_make_frame_entry (&field_list, "__frame_needs_free",
 					 boolean_type_node, fn_start);
+  tree iarc_name = coro_make_frame_entry (&field_list, "__i_a_r_c",
+					 boolean_type_node, fn_start);
   tree resume_idx_name
     = coro_make_frame_entry (&field_list, "__resume_at",
 			     short_unsigned_type_node, fn_start);
@@ -3725,6 +3778,17 @@  morph_fn_to_coro (tree orig, tree *resumer, tree *destroyer)
   r = coro_build_cvt_void_expr_stmt (r, fn_start);
   add_stmt (r);
 
+  /* Initialize 'initial-await-resume-called' as per
+     [dcl.fct.def.coroutine] / 5.3 */
+  tree i_a_r_c_m
+    = lookup_member (coro_frame_type, iarc_name, 1, 0, tf_warning_or_error);
+  tree i_a_r_c = build_class_member_access_expr (deref_fp, i_a_r_c_m,
+						 NULL_TREE, false,
+						 tf_warning_or_error);
+  r = build2 (INIT_EXPR, boolean_type_node, i_a_r_c, boolean_false_node);
+  r = coro_build_cvt_void_expr_stmt (r, fn_start);
+  add_stmt (r);
+
   /* So .. call the actor ..  */
   r = build_call_expr_loc (fn_start, actor, 1, coro_fp);
   r = maybe_cleanup_point_expr_void (r);
@@ -3845,6 +3909,25 @@  morph_fn_to_coro (tree orig, tree *resumer, tree *destroyer)
       /* Mimic what the parser does for the catch.  */
       tree handler = begin_handler ();
       finish_handler_parms (NULL_TREE, handler); /* catch (...) */
+
+      /* Get the initial await resume called value.  */
+      tree i_a_r_c = build_class_member_access_expr (actor_frame, i_a_r_c_m,
+						     NULL_TREE, false,
+						     tf_warning_or_error);
+      tree not_iarc_if = begin_if_stmt ();
+      tree not_iarc = build1_loc (fn_start, TRUTH_NOT_EXPR,
+				  boolean_type_node, i_a_r_c);
+      finish_if_stmt_cond (not_iarc, not_iarc_if);
+      /* If the initial await resume called value is false, rethrow...  */
+      tree rethrow = build_throw (fn_start, NULL_TREE);
+      TREE_NO_WARNING (rethrow) = true;
+      finish_expr_stmt (rethrow);
+      finish_then_clause (not_iarc_if);
+      tree iarc_scope = IF_SCOPE (not_iarc_if);
+      IF_SCOPE (not_iarc_if) = NULL;
+      not_iarc_if = do_poplevel (iarc_scope);
+      add_stmt (not_iarc_if);
+      /* ... else call the promise unhandled exception method.  */
       ueh = maybe_cleanup_point_expr_void (ueh);
       add_stmt (ueh);
       finish_handler (handler);
diff --git a/gcc/testsuite/g++.dg/coroutines/torture/exceptions-test-01-n4849-a.C b/gcc/testsuite/g++.dg/coroutines/torture/exceptions-test-01-n4849-a.C
new file mode 100644
index 00000000000..e96b4ed1a8b
--- /dev/null
+++ b/gcc/testsuite/g++.dg/coroutines/torture/exceptions-test-01-n4849-a.C
@@ -0,0 +1,213 @@ 
+//  { dg-do run }
+
+// Test exceptions in the initial await expression, per n4849.
+
+#include "../coro.h"
+#include <exception>
+
+int gX = 0;
+
+struct coro1 {
+  struct promise_type;
+  using handle_type = coro::coroutine_handle<coro1::promise_type>;
+  handle_type handle;
+
+  coro1 () : handle(0) {}
+  coro1 (handle_type _handle)
+    : handle(_handle) {
+        PRINT("Created coro1 object from handle");
+  }
+  coro1 (const coro1 &) = delete; // no copying
+  coro1 (coro1 &&s) : handle(s.handle) {
+    s.handle = nullptr;
+    PRINT("coro1 mv ctor ");
+  }
+  coro1 &operator = (coro1 &&s) {
+    handle = s.handle;
+    s.handle = nullptr;
+    PRINT("coro1 op=  ");
+    return *this;
+  }
+  ~coro1() {
+    PRINT("Destroyed coro1");
+    if ( handle )
+      handle.destroy();
+  }
+
+  struct suspend_never_prt {
+    bool await_ready() const noexcept { return true; }
+    void await_suspend(handle_type) const noexcept { PRINT ("susp-never-susp"); }
+    void await_resume() const noexcept { PRINT ("susp-never-resume");}
+  };
+
+  struct  suspend_always_prt {
+    bool await_ready() const noexcept { return false; }
+    void await_suspend(handle_type) const noexcept { PRINT ("susp-always-susp"); }
+    void await_resume() const noexcept { PRINT ("susp-always-resume"); }
+    ~suspend_always_prt() { PRINT ("susp-always-DTOR"); }
+  };
+
+  /* Constructing this with:
+      * a value of '1' will cause the initial suspend await_suspend()
+      call to throw.
+      * a value of '2' will cause the await resume to throw.  */
+  struct  suspend_always_susp_throws_prt {
+    int thrower;
+    suspend_always_susp_throws_prt (int _t) : thrower(_t) {}
+    bool await_ready() const noexcept { return false; }
+
+    void await_suspend(handle_type) const
+      { PRINT ("suspend_always_susp_throws_prt:await_suspend");
+        if (thrower == 1)
+          throw (42);
+      }
+    void await_resume() const
+      { PRINT ("suspend_always_susp_throws_prt:await_resume");
+        if (thrower == 2)
+          throw (6174);
+      }
+    ~suspend_always_susp_throws_prt() { PRINT ("suspend_always_susp_throws_prt-DTOR"); }
+  };
+
+  struct promise_type {
+  int throw_control = 0;
+  int value;
+  promise_type(int k)  : throw_control(k), value(-373)
+  {  PRINTF ("Created Promise with %d\n", k);}
+
+  ~promise_type() { PRINT ("Destroyed Promise"); }
+
+  auto get_return_object () {
+    PRINT ("get_return_object: handle from promise");
+    return handle_type::from_promise (*this);
+  }
+  // This provides the tests for what catches exceptions thrown at
+  // different points in the initial await expression.
+  auto initial_suspend () {
+    PRINT ("get initial_suspend (always)");
+    return suspend_always_susp_throws_prt(throw_control);
+  }
+  auto final_suspend () {
+    PRINT ("get final_suspend (always)");
+    return suspend_always_prt{};
+  }
+  void return_value (int v) {
+    PRINTF ("return_value () %d\n",v);
+    value = v;
+  }
+  auto yield_value (int v) {
+    PRINTF ("yield_value () %d and suspend always\n",v);
+    value = v;
+    return suspend_always_prt{};
+  }
+  int get_value (void) { return value; }
+
+  void unhandled_exception() {
+    PRINT ("unhandled_exception: caught one!");
+    gX = -11;
+    // returning from here should end up in final_suspend.
+    }
+  };
+};
+
+// This doesn't have to do much - we only need to exercise the initial
+// await expression.
+
+struct coro1
+n4849_ia_thrower (int k)
+{
+  int caught = 0;
+  PRINT ("f: about to return 22");
+  co_return 22;
+}
+
+int main ()
+{
+  {
+  /* Case 0 - nothing should throw.  */
+  struct coro1 x0;
+  try {
+    x0 = n4849_ia_thrower (0);
+  } catch (...) {
+    PRINT ("main: case 0 ctor threw?");
+    abort ();
+  }
+  /* Resume the initial suspend expression.  */
+  PRINT ("main: got coro, resuming..");
+  x0.handle.resume();
+  int y = x0.handle.promise().get_value();
+  if ( y != 22 )
+    {
+      PRINT ("main: case 0 got the wrong answer.");
+      abort ();
+    }
+  if (!x0.handle.done())
+    {
+      PRINT ("main: case 0 not done.");
+      abort ();
+    }
+  if (gX != 0)
+    {
+      PRINT ("main: case 0 body threw?");
+      abort ();
+    }
+  }
+
+  {
+  /* Case 1 - initial suspend should throw and thus be caught by the 
+     ramp's caller.  */
+  struct coro1 x1;
+  try {
+    x1 = n4849_ia_thrower (1);
+  } catch (int message) {
+    PRINTF ("main: caught an int %d\n", message);
+    if (message != 42)
+      {
+        PRINT ("main: unexpected value?");
+        abort ();
+      }
+  } catch (...) {
+    PRINT ("main: case 1 ctor threw something else?");
+    abort ();
+  }
+  if (gX != 0)
+    {
+      PRINT ("main: case 0 body threw (how?)");
+      abort ();
+    }
+  }
+
+  {
+  /* Case 2 - the await_resume from the initial await expression throws
+     this should be caught by the regular function body wrapper.  */
+  struct coro1 x2;
+  try {
+    x2 = n4849_ia_thrower (2);
+  } catch (...) {
+    PRINT ("main: case 2 ctor threw?");
+    abort ();
+  }
+  // We now resume - and expect the await_resume to throw which should
+  // be caught by unhandled_exception().
+  PRINT ("main: got coro, resuming..");
+  x2.handle.resume();
+  int y = x2.handle.promise().get_value();
+  if ( y != -373 )
+    {
+      PRINT ("main: case 2 got the wrong answer.");
+      abort ();
+    }
+  if (!x2.handle.done())
+    {
+      PRINT ("main: case 2 not done.");
+      abort ();
+    }
+  if (gX != -11)
+    {
+      PRINT ("main: n4849_is_thrower await_resume exception not caught");
+      abort ();
+    }
+  }
+  PRINT ("main: returning");
+  return 0;
+}