From: Avi Kivity <[email protected]>

The CWG 2867 implementation introduced conditional cleanup guards for
structured binding base variables, using internal TARGET_EXPRs.  These
guard variables are not part of any BIND_EXPR_VARS list, so the
coroutine frame promotion in register_local_var_uses missed them.
After a suspend/resume cycle, the guard's stack location is dead,
causing the conditional destructor to be skipped.

Fix this by introducing TARGET_EXPR_DECOMP_GUARD_P to label decomposition
cleanup guard TARGET_EXPRs at creation time in cp_finish_decl, then
extending both register_local_var_uses and transform_local_var_uses to
detect them and promote their slots to the coroutine frame.

gcc/cp/ChangeLog:

        PR c++/124584
        * cp-tree.h (TARGET_EXPR_DECOMP_GUARD_P): New macro using
        TREE_LANG_FLAG_5 on TARGET_EXPR.
        * decl.cc (cp_finish_decl): Set TARGET_EXPR_DECOMP_GUARD_P on
        cleanup guard TARGET_EXPRs for structured bindings.
        * coroutines.cc (register_local_var_uses): Also register
        TARGET_EXPR_SLOT of TARGET_EXPR_DECOMP_GUARD_P nodes for frame
        promotion.
        (transform_local_var_uses): Also transform
        TARGET_EXPR_DECOMP_GUARD_P slots by setting DECL_VALUE_EXPR to
        the frame access expression.

gcc/testsuite/ChangeLog:

        PR c++/124584
        * g++.dg/coroutines/pr124584.C: New test.
---

This patch was prepared with extensive AI help (Claude Opus 4.6 medium).
I reviewed it to the best of my ability and I don't think it is AI slop.
Of course, my ability in this regard is limited as I don't deeply
understand gcc code. My contribution was to insist on labeling the
cleanup guard so we don't promote any internal expression to the coroutine
frame, just the structured binding guard.

I hope that in the case the patch is not accepted, it at least inspires
someone to fix the bug, as it makes gcc useless with coroutines (as no
one[1] will agree to avoid structured bindings).

[1] No one who has access to a compiler that does work with structured
    bindings

 gcc/cp/coroutines.cc                       | 58 ++++++++++++++++++++
 gcc/cp/cp-tree.h                           |  8 +++
 gcc/cp/decl.cc                             |  2 +
 gcc/testsuite/g++.dg/coroutines/pr124584.C | 61 ++++++++++++++++++++++
 4 files changed, 129 insertions(+)
 create mode 100644 gcc/testsuite/g++.dg/coroutines/pr124584.C

diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
index 46ec8c0a953..55d9dbfca56 100644
--- a/gcc/cp/coroutines.cc
+++ b/gcc/cp/coroutines.cc
@@ -2489,10 +2489,32 @@ struct local_vars_transform
 static tree
 transform_local_var_uses (tree *stmt, int *do_subtree, void *d)
 {
   local_vars_transform *lvd = (local_vars_transform *) d;
 
+  /* PR c++/124584: Transform internal TARGET_EXPR slots that were promoted
+     to the coroutine frame (e.g. cleanup guard variables for structured
+     bindings).  Set DECL_VALUE_EXPR so all uses resolve to the frame.  */
+  if (TREE_CODE (*stmt) == TARGET_EXPR
+      && TARGET_EXPR_DECOMP_GUARD_P (*stmt))
+    {
+      tree slot = TARGET_EXPR_SLOT (*stmt);
+      local_var_info *local_var = lvd->local_var_uses->get (slot);
+      if (local_var && local_var->field_id)
+       {
+         DECL_CONTEXT (slot) = lvd->context;
+         tree fld_idx
+           = coro_build_frame_access_expr (lvd->actor_frame,
+                                           local_var->field_id, true,
+                                           tf_warning_or_error);
+         local_var->field_idx = fld_idx;
+         SET_DECL_VALUE_EXPR (slot, fld_idx);
+         DECL_HAS_VALUE_EXPR_P (slot) = true;
+       }
+      return NULL_TREE;
+    }
+
   /* For each var in this bind expr (that has a frame id, which means it was
      accessed), build a frame reference and add it as the DECL_VALUE_EXPR.  */
 
   if (TREE_CODE (*stmt) == BIND_EXPR)
     {
@@ -4339,10 +4361,46 @@ coro_make_frame_entry (tree *field_list, const char 
*name, tree fld_type,
    need it, so that they can be 'promoted' across suspension points.  */
 
 static tree
 register_local_var_uses (tree *stmt, int *do_subtree, void *d)
 {
+  /* PR c++/124584: Internal TARGET_EXPRs (such as cleanup guard variables
+     created for structured bindings by CWG 2867) need to be promoted to the
+     coroutine frame so their values survive across suspension points.
+     Without this, the conditional cleanup guard is lost after resume and
+     the destructor of the binding base variable is never called.  */
+  if (TREE_CODE (*stmt) == TARGET_EXPR
+      && TARGET_EXPR_DECOMP_GUARD_P (*stmt))
+    {
+      local_vars_frame_data *lvd = (local_vars_frame_data *) d;
+      tree slot = TARGET_EXPR_SLOT (*stmt);
+      if (TREE_CODE (slot) == VAR_DECL
+         && !TREE_STATIC (slot)
+         && !lvd->local_var_uses->get (slot))
+       {
+         bool existed;
+         local_var_info &local_var
+           = lvd->local_var_uses->get_or_insert (slot, &existed);
+         gcc_checking_assert (!existed);
+         local_var.def_loc = DECL_SOURCE_LOCATION (slot);
+         tree lvtype = TREE_TYPE (slot);
+         local_var.frame_type = lvtype;
+         local_var.field_idx = local_var.field_id = NULL_TREE;
+         local_var.is_static = false;
+         local_var.is_lambda_capture = false;
+         local_var.has_value_expr_p = false;
+
+         lvd->local_var_seen = true;
+         unsigned serial = lvd->bind_indx;
+         char *buf = xasprintf ("_Guard%u_%u", lvd->nest_depth, serial);
+         local_var.field_id
+           = coro_make_frame_entry (lvd->field_list, buf, lvtype, lvd->loc);
+         free (buf);
+       }
+      return NULL_TREE;
+    }
+
   if (TREE_CODE (*stmt) != BIND_EXPR)
     return NULL_TREE;
 
   local_vars_frame_data *lvd = (local_vars_frame_data *) d;
 
diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
index 8807d80a21a..58385e3a577 100644
--- a/gcc/cp/cp-tree.h
+++ b/gcc/cp/cp-tree.h
@@ -540,10 +540,11 @@ extern GTY(()) tree cp_global_trees[CPTI_MAX];
       FUNCTION_RVALUE_QUALIFIED (in FUNCTION_TYPE, METHOD_TYPE)
       CALL_EXPR_REVERSE_ARGS (in CALL_EXPR, AGGR_INIT_EXPR)
       CONSTRUCTOR_PLACEHOLDER_BOUNDARY (in CONSTRUCTOR)
       OVL_EXPORT_P (in OVERLOAD)
       DECL_NTTP_OBJECT_P (in VAR_DECL)
+      TARGET_EXPR_DECOMP_GUARD_P (in TARGET_EXPR)
    6: TYPE_MARKED_P (in _TYPE)
       DECL_NONTRIVIALLY_INITIALIZED_P (in VAR_DECL)
       RANGE_FOR_IVDEP (in RANGE_FOR_STMT)
       CALL_EXPR_OPERATOR_SYNTAX (in CALL_EXPR, AGGR_INIT_EXPR)
       CONSTRUCTOR_IS_DESIGNATED_INIT (in CONSTRUCTOR)
@@ -6031,10 +6032,17 @@ decl_template_parm_check (const_tree t, const char *f, 
int l, const char *fn)
 /* True if this TARGET_EXPR is for holding an implementation detail like a
    cleanup flag or loop index, and should be ignored by extend_all_temps.  */
 #define TARGET_EXPR_INTERNAL_P(NODE) \
   TREE_LANG_FLAG_4 (TARGET_EXPR_CHECK (NODE))
 
+/* True if NODE is an internal TARGET_EXPR used as a cleanup guard variable
+   for structured binding (decomposition) conditional destruction.  Such
+   guards need to be promoted to the coroutine frame so their values survive
+   across suspension points.  */
+#define TARGET_EXPR_DECOMP_GUARD_P(NODE) \
+  TREE_LANG_FLAG_5 (TARGET_EXPR_CHECK (NODE))
+
 /* True if NODE is a TARGET_EXPR that just expresses a copy of its INITIAL; if
    the initializer has void type, it's doing something more complicated.  */
 #define SIMPLE_TARGET_EXPR_P(NODE)                             \
   (TREE_CODE (NODE) == TARGET_EXPR                             \
    && TARGET_EXPR_INITIAL (NODE)                               \
diff --git a/gcc/cp/decl.cc b/gcc/cp/decl.cc
index 84aa9a72fab..03af6d52fb5 100644
--- a/gcc/cp/decl.cc
+++ b/gcc/cp/decl.cc
@@ -10118,10 +10118,11 @@ cp_finish_decl (tree decl, tree init, bool 
init_const_expr_p,
                 emitted code.  */
              tree guard = NULL_TREE;
              if (cleanups || cleanup)
                {
                  guard = get_internal_target_expr (boolean_false_node);
+                 TARGET_EXPR_DECOMP_GUARD_P (guard) = true;
                  add_stmt (guard);
                  guard = TARGET_EXPR_SLOT (guard);
                }
              tree sl = push_stmt_list ();
              initialize_local_var (decl, init, true);
@@ -10147,10 +10148,11 @@ cp_finish_decl (tree decl, tree init, bool 
init_const_expr_p,
                     extend_ref_init_temps created vars, pop_stmt_list
                     popped that all, so push those extra cleanups around
                     the whole sequence with a guard variable.  */
                  gcc_assert (TREE_CODE (sl) == STATEMENT_LIST);
                  guard = get_internal_target_expr (integer_zero_node);
+                 TARGET_EXPR_DECOMP_GUARD_P (guard) = true;
                  add_stmt (guard);
                  guard = TARGET_EXPR_SLOT (guard);
                  for (unsigned i = 0; i < n_extra_cleanups; ++i)
                    {
                      tree_stmt_iterator tsi = tsi_last (sl);
diff --git a/gcc/testsuite/g++.dg/coroutines/pr124584.C 
b/gcc/testsuite/g++.dg/coroutines/pr124584.C
new file mode 100644
index 00000000000..116dc8aed88
--- /dev/null
+++ b/gcc/testsuite/g++.dg/coroutines/pr124584.C
@@ -0,0 +1,61 @@
+// PR c++/124584
+// { dg-do run { target c++20 } }
+// { dg-additional-options "-O1" }
+// Verify that the destructor of a structured binding base variable
+// (using the tuple protocol) is called after a coroutine resumes,
+// when the binding's scope spans a suspension point.
+
+#include <coroutine>
+#include <tuple>
+#include <utility>
+
+static int g_dtor_count = 0;
+
+struct guard {
+  bool live_ = true;
+  ~guard () { if (live_) ++g_dtor_count; }
+  guard () = default;
+  guard (guard&& o) noexcept : live_(std::exchange (o.live_, false)) {}
+};
+
+static std::coroutine_handle<> g_pending;
+
+struct yield_aw {
+  bool await_ready () const noexcept { return false; }
+  void await_suspend (std::coroutine_handle<> h) noexcept { g_pending = h; }
+  void await_resume () const noexcept {}
+};
+
+struct task {
+  struct promise_type {
+    task get_return_object () noexcept
+    { return {std::coroutine_handle<promise_type>::from_promise (*this)}; }
+    std::suspend_never initial_suspend () noexcept { return {}; }
+    std::suspend_always final_suspend () noexcept { return {}; }
+    void return_void () noexcept {}
+    void unhandled_exception () { __builtin_abort (); }
+  };
+  std::coroutine_handle<promise_type> h_;
+  ~task () { if (h_) h_.destroy (); }
+};
+
+std::tuple<guard> make_guarded_tuple () { return {guard{}}; }
+
+task
+test_fn ()
+{
+  {
+    auto [g] = make_guarded_tuple ();
+    co_await yield_aw{};
+  } // guard should be destroyed here
+}
+
+int
+main ()
+{
+  auto t = test_fn ();
+  std::exchange (g_pending, nullptr).resume ();
+
+  if (g_dtor_count != 1)
+    __builtin_abort ();
+}
-- 
2.54.0

Reply via email to