On 6/6/25 9:17 AM, Jakub Jelinek wrote:
On Wed, Jun 04, 2025 at 02:03:17PM +0200, Jakub Jelinek wrote:
You mean also check TREE_NOTHROW (fndecl) which we currently propagate
conservatively (if we can prove something never throws, we set it)
or something else?
If TREE_NOTHROW, that would be twice
+          tree callee_fn = cp_get_fndecl_from_callee (callee, false);
            if (!flag_enforce_eh_specs
                || type_dependent_expression_p (callee)
                || !POINTER_TYPE_P (TREE_TYPE (callee))
-              || !type_noexcept_p (TREE_TYPE (TREE_TYPE (callee))))
+              || (!type_noexcept_p (TREE_TYPE (TREE_TYPE (callee)))
+                  && (callee_fn == NULL_TREE
+                      || !TREE_NOTHROW (callee_fn))))
              *jump_target = void_node;
or so.

I've now added this and

I think that shouldn't be necessary.

Ok, I can remove those then.

and removed these.
Further the g++.dg/cpp0x/constexpr-neg2.C hunk isn't needed anymore because
with the TREE_NOTHROW stuff we diagnose it even for C++26, there is no throw
involved.

+/* For exception object EXC if it has class type and usable what () method
+   which returns cv char * return the xmalloced string literal which it returns
+   if possible, otherwise return NULL.  */
+
+static char *
+exception_what_str (const constexpr_ctx *ctx, tree exc)
+{
+  tree type = strip_array_types (TREE_TYPE (exc));
+  if (!CLASS_TYPE_P (type))
+    return NULL;
+  if (type != TREE_TYPE (exc))
+    exc = build4 (ARRAY_REF, type, exc, size_zero_node, NULL, NULL);
+  tree call
+    = finish_class_member_access_expr (exc, get_identifier ("what"), false,
+                                      tf_none);
+  if (call == error_mark_node)
+    return NULL;

This will work for any class type with a what() method, which is different from vterminate.cc that only calls what() for a type derived from std::exception. I think it makes sense to be consistent with that; a what() from an unrelated class might mean something different.

+  releasing_vec what_args;
+  call = finish_call_expr (call, &what_args, false, false, tf_none);
+  if (call == error_mark_node)
+    return NULL;
+  if (TREE_CODE (TREE_TYPE (call)) != POINTER_TYPE
+      || !INTEGRAL_TYPE_P (TREE_TYPE (TREE_TYPE (call)))
+      || !COMPLETE_TYPE_P (TREE_TYPE (TREE_TYPE (call)))
+      || !tree_int_cst_equal (TYPE_SIZE_UNIT (TREE_TYPE (TREE_TYPE (call))),
+                             TYPE_SIZE_UNIT (char_type_node))
+      || TYPE_PRECISION (TREE_TYPE (TREE_TYPE (call))) != BITS_PER_UNIT)
+    return NULL;
+  if (!potential_constant_expression (call))
+    return NULL;
+  bool non_constant_p = false, overflow_p = false;
+  tree jmp_target = NULL;
+  tree ptr = cxx_eval_constant_expression (ctx, call, vc_prvalue,
+                                          &non_constant_p, &overflow_p,
+                                          &jmp_target);
+  if (!throws (&jmp_target)
+      && !non_constant_p
+      && reduced_constant_expression_p (ptr))
+    if (const char *msg = c_getstr (ptr))
+      return xstrdup (msg);
+  auto_vec <char, 32> v;
+  for (unsigned i = 0; i < INT_MAX; ++i)
+    {
+      tree t = call;
+      if (i)
+       t = build2 (POINTER_PLUS_EXPR, TREE_TYPE (call), call, size_int (i));

It seems wrong to evaluate the call multiple times, in case it changes constexpr state. It should be better to index off 'ptr' rather than 'call'.

+      t = build1 (INDIRECT_REF, TREE_TYPE (TREE_TYPE (t)), t);
+      non_constant_p = false;
+      overflow_p = false;
+      jmp_target = NULL;
+      tree t2 = cxx_eval_constant_expression (ctx, t, vc_prvalue,
+                                             &non_constant_p, &overflow_p,
+                                             &jmp_target);
+      if (throws (&jmp_target)
+         || non_constant_p
+         || !tree_fits_shwi_p (t2))
+       return NULL;
+      char c = tree_to_shwi (t2);
+      v.safe_push (c);
+      if (c == '\0')
+       break;
+    }
+  return xstrdup (v.address ());
+}
+
+/* Diagnose constant expression evaluation encountering call to
+   std::terminate due to exception EXC.  */
+
+static void
+diagnose_std_terminate (location_t loc, const constexpr_ctx *ctx, tree exc)
+{
+  tree type = strip_array_types (TREE_TYPE (exc));
+  if (char *str = exception_what_str (ctx, exc))
+    {
+      error_at (loc, "%qs called after throwing an exception of type %qT; "
+                    "%<what()%>: %qs", "std::terminate", type, str);
+      free (str);
+    }
+  else
+    {
+      if (type != TREE_TYPE (exc))
+       exc = build4 (ARRAY_REF, type, exc, size_zero_node, NULL, NULL);
+      bool non_constant_p = false, overflow_p = false;
+      tree jmp_target = NULL;
+      tree val = cxx_eval_constant_expression (ctx, exc, vc_prvalue,
+                                              &non_constant_p, &overflow_p,
+                                              &jmp_target);
+      if (!throws (&jmp_target)
+         && !non_constant_p
+         && reduced_constant_expression_p (val))
+       error_at (loc, "%qs called after throwing an exception %qE",
+                 "std::terminate", val);
+      else
+       error_at (loc, "%qs called after throwing an exception of type %qT",
+                 "std::terminate", type);
+    }
+}
+
+/* Diagnose constant expression evaluation encountering call to
+   uncaught exception EXC.  */
+
+static void
+diagnose_uncaught_exception (location_t loc, const constexpr_ctx *ctx, tree 
exc)
+{
+  tree type = strip_array_types (TREE_TYPE (exc));
+  if (char *str = exception_what_str (ctx, exc))
+    {
+      error_at (loc, "uncaught exception of type %qT; %<what()%>: %qs", type, 
str);
+      free (str);
+    }
+  else
+    {
+      if (type != TREE_TYPE (exc))
+       exc = build4 (ARRAY_REF, type, exc, size_zero_node, NULL, NULL);
+      bool non_constant_p = false, overflow_p = false;
+      tree jmp_target = NULL;
+      tree val = cxx_eval_constant_expression (ctx, exc, vc_prvalue,
+                                              &non_constant_p, &overflow_p,
+                                              &jmp_target);
+      if (!throws (&jmp_target)
+         && !non_constant_p
+         && reduced_constant_expression_p (val))
+       error_at (loc, "uncaught exception %qE", val);
+      else
+       error_at (loc, "uncaught exception of type %qT", type);
+    }
+}
+
+/* Kinds of __cxa_* functions (and a few other EH related ones) we handle as
+   magic constexpr functions for C++26.  */
+
+enum cxa_builtin {
+  CXA_NONE = 0,
+  CXA_ALLOCATE_EXCEPTION = 1,
+  CXA_FREE_EXCEPTION = 2,
+  CXA_THROW = 3,
+  CXA_BEGIN_CATCH = 4,
+  CXA_END_CATCH = 5,
+  CXA_RETHROW = 6,
+  CXA_GET_EXCEPTION_PTR = 7,
+  CXA_BAD_CAST = 8,
+  CXA_BAD_TYPEID = 9,
+  CXA_THROW_BAD_ARRAY_NEW_LENGTH = 10,
+  STD_UNCAUGHT_EXCEPTIONS = 11,
+  STD_CURRENT_EXCEPTION = 12,
+  STD_RETHROW_EXCEPTION = 13,
+  BUILTIN_EH_PTR_ADJUST_REF = 14
+};
+
+/* Return cxa_builtin if FNDECL is a __cxa_* function handled as
+   magic constexpr function for C++26.  Return CXA_NONE otherwise.  */
+
+static enum cxa_builtin
+cxx_cxa_builtin_fn_p (tree fndecl)
+{
+  if (cxx_dialect < cxx26)
+    return CXA_NONE;
+  if (DECL_LANGUAGE (fndecl) != lang_c)
+    {
+      if (!decl_in_std_namespace_p (fndecl))
+       return CXA_NONE;
+      if (id_equal (DECL_NAME (fndecl), "uncaught_exceptions"))
+       return STD_UNCAUGHT_EXCEPTIONS;
+      if (id_equal (DECL_NAME (fndecl), "current_exception"))
+       return STD_CURRENT_EXCEPTION;
+      if (id_equal (DECL_NAME (fndecl), "rethrow_exception"))
+       return STD_RETHROW_EXCEPTION;
+      return CXA_NONE;
+    }
+  if (!startswith (IDENTIFIER_POINTER (DECL_NAME (fndecl)), "__cxa_"))
+    return CXA_NONE;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_allocate_exception"))
+    return CXA_ALLOCATE_EXCEPTION;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_free_exception"))
+    return CXA_FREE_EXCEPTION;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_throw"))
+    return CXA_THROW;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_begin_catch"))
+    return CXA_BEGIN_CATCH;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_end_catch"))
+    return CXA_END_CATCH;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_rethrow"))
+    return CXA_RETHROW;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_get_exception_ptr"))
+    return CXA_GET_EXCEPTION_PTR;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_bad_cast"))
+    return CXA_BAD_CAST;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_bad_typeid"))
+    return CXA_BAD_TYPEID;
+  if (id_equal (DECL_NAME (fndecl), "__cxa_throw_bad_array_new_length"))
+    return CXA_THROW_BAD_ARRAY_NEW_LENGTH;
+  return CXA_NONE;
+}
+
+/* Evaluate various __cxa_* calls as magic constexpr builtins for
+   C++26 constexpr exception support (P3068R5).  */
+
+static tree
+cxx_eval_cxa_builtin_fn (const constexpr_ctx *ctx, tree call,
+                        enum cxa_builtin kind, tree fndecl,
+                        bool *non_constant_p, bool *overflow_p,
+                        tree *jump_target)
+{
+  int nargs = call_expr_nargs (call);
+  location_t loc = cp_expr_loc_or_input_loc (call);
+  tree args[4], arg;
+  if (nargs > 4)
+    {
+    invalid_nargs:
+      if (!ctx->quiet)
+       error_at (loc, "call to %qD function with incorrect"
+                 "number of arguments", fndecl);
+      *non_constant_p = true;
+      return call;
+    }
+  if ((kind == CXA_BEGIN_CATCH || kind == CXA_GET_EXCEPTION_PTR)
+      && nargs == 1
+      && (arg = CALL_EXPR_ARG (call, 0))
+      && TREE_CODE (arg) == CALL_EXPR
+      && call_expr_nargs (arg) == 1
+      && integer_zerop (CALL_EXPR_ARG (arg, 0)))
+    if (tree fun = get_function_named_in_call (arg))
+      if (fndecl_built_in_p (fun, BUILT_IN_EH_POINTER))
+       {
+         if (ctx->global->caught_exceptions.length () < 2)
+           {
+           no_caught_exceptions:
+             if (!ctx->quiet)
+               error_at (loc, "%qD called with no caught exceptions pending",
+                         fndecl);
+             *non_constant_p = true;
+             return call;
+           }
+         tree handler_type = ctx->global->caught_exceptions.last ();
+         if (handler_type && VAR_P (handler_type))

Let's add a comment to clarify that both functions expect the end of caught_exceptions to be a type and a variable, and begin_catch pops the type, leaving the variable for __cxa_rethrow.

+           goto no_caught_exceptions;
+         unsigned idx = ctx->global->caught_exceptions.length () - 2;
+         arg = ctx->global->caught_exceptions[idx];
+         gcc_assert (VAR_P (arg));
+         if (kind == CXA_BEGIN_CATCH)
+           {
+             ctx->global->caught_exceptions.pop ();
+             --ctx->global->uncaught_exceptions;
+           }
+         if (handler_type == NULL_TREE)
+           /* __cxa_begin_catch used for catch (...).  Just return
+              void.  */
+           return void_node;
+         else if (POINTER_TYPE_P (handler_type))
+           {
+             /* __cxa_begin_catch used for catch of a pointer.  */
+             if (TREE_CODE (TREE_TYPE (arg)) == ARRAY_TYPE)
+               arg = build4 (ARRAY_REF, TREE_TYPE (TREE_TYPE (arg)), arg,
+                             size_zero_node, NULL_TREE, NULL_TREE);
+             arg = cp_convert (handler_type, arg,
+                               ctx->quiet ? tf_none : tf_warning_or_error);
+             if (arg == error_mark_node)
+               {
+                 *non_constant_p = true;
+                 return call;
+               }
+           }
+         else
+           {
+             /* __cxa_begin_catch used for catch of a non-pointer type.  */
+             tree exc_type = strip_array_types (TREE_TYPE (arg));
+             tree exc_ptr_type = build_pointer_type (exc_type);
+             arg = build_fold_addr_expr_with_type (arg, exc_ptr_type);
+             if (CLASS_TYPE_P (handler_type))
+               {
+                 tree ptr_type = build_pointer_type (handler_type);
+                 arg = cp_build_c_cast (loc, ptr_type, arg,
+                                        ctx->quiet ? tf_none
+                                        : tf_warning_or_error);

A C cast seems more than necessary here; why doesn't the same cp_convert work, or perform_implicit_conversion?

+                 if (arg == error_mark_node)
+                   {
+                     *non_constant_p = true;
+                     return call;
+                   }
+               }
+           }
+         return cxx_eval_constant_expression (ctx, arg, vc_prvalue,
+                                              non_constant_p, overflow_p,
+                                              jump_target);
+       }
+  for (int i = 0; i < nargs; ++i)
+    {
+      args[i] = cxx_eval_constant_expression (ctx, CALL_EXPR_ARG (call, i),
+                                             vc_prvalue, non_constant_p,
+                                             overflow_p, jump_target);
+      if (*non_constant_p)
+       return call;
+      if (*jump_target)
+       return NULL_TREE;
+    }
+  switch (kind)
+    {
+    case CXA_ALLOCATE_EXCEPTION:
+      if (nargs != 1)
+       goto invalid_nargs;
+      if (!tree_fits_uhwi_p (args[0]))
+       {
+         if (!ctx->quiet)
+           error_at (loc, "cannot allocate exception: size not constant");
+         *non_constant_p = true;
+         return call;
+       }
+      else
+       {
+         tree type = build_array_type_nelts (char_type_node,
+                                             tree_to_uhwi (args[0]));
+         tree var = build_decl (loc, VAR_DECL, heap_uninit_identifier,
+                                type);
+         DECL_ARTIFICIAL (var) = 1;
+         retrofit_lang_decl (var);
+         DECL_EXCEPTION_REFCOUNT (var) = size_zero_node;
+         ctx->global->heap_vars.safe_push (var);
+         ctx->global->put_value (var, NULL_TREE);
+         return fold_convert (ptr_type_node, build_address (var));
+       }
+    case CXA_FREE_EXCEPTION:
+      if (nargs != 1)
+       goto invalid_nargs;
+      STRIP_NOPS (args[0]);
+      if (TREE_CODE (args[0]) != ADDR_EXPR)
+       {
+       invalid_ptr:
+         if (!ctx->quiet)
+           error_at (loc, "first argument to %qD function not result of "
+                     "%<__cxa_allocate_exception%>", fndecl);
+         *non_constant_p = true;
+         return call;
+       }
+      arg = TREE_OPERAND (args[0], 0);
+      if (!VAR_P (arg)
+         || !DECL_ARTIFICIAL (arg)
+         || (DECL_NAME (arg) != heap_uninit_identifier
+             && DECL_NAME (arg) != heap_identifier)
+         || !DECL_LANG_SPECIFIC (arg))
+       goto invalid_ptr;
+      DECL_NAME (arg) = heap_deleted_identifier;
+      ctx->global->destroy_value (arg);
+      ctx->global->heap_dealloc_count++;
+      return void_node;
+    case CXA_THROW:
+      if (nargs != 3)
+       goto invalid_nargs;
+      STRIP_NOPS (args[0]);
+      if (TREE_CODE (args[0]) != ADDR_EXPR)
+       goto invalid_ptr;
+      arg = TREE_OPERAND (args[0], 0);
+      if (!VAR_P (arg)
+         || !DECL_ARTIFICIAL (arg)
+         || DECL_NAME (arg) != heap_identifier
+         || !DECL_LANG_SPECIFIC (arg))

Let's factor out this test from here and __cxa_free_exception.

+       goto invalid_ptr;
+      DECL_EXCEPTION_REFCOUNT (arg)
+       = size_binop (PLUS_EXPR, DECL_EXCEPTION_REFCOUNT (arg),
+                     size_one_node);
+      ++ctx->global->uncaught_exceptions;
+      *jump_target = arg;
+      return void_node;
+    case CXA_BEGIN_CATCH:
+    case CXA_GET_EXCEPTION_PTR:
+      goto invalid_nargs;
+    case CXA_END_CATCH:
+      if (nargs != 0)
+       goto invalid_nargs;
+      if (ctx->global->caught_exceptions.is_empty ())
+       {
+         if (!ctx->quiet)
+           error_at (loc, "%qD called with no caught exceptions active",
+                     fndecl);
+         *non_constant_p = true;
+         return call;
+       }
+      else
+       {
+         arg = ctx->global->caught_exceptions.pop ();
+         if (arg == NULL_TREE || !VAR_P (arg))
+           {
+             gcc_assert (!ctx->global->caught_exceptions.is_empty ());
+             arg = ctx->global->caught_exceptions.pop ();

How is this be possible? begin_catch should have left us with a variable at the end? Should this be an error rather than a workaround?

+           }
+         gcc_assert (VAR_P (arg));
+       free_except:
+         DECL_EXCEPTION_REFCOUNT (arg)
+           = size_binop (MINUS_EXPR, DECL_EXCEPTION_REFCOUNT (arg),
+                         size_one_node);
+         if (integer_zerop (DECL_EXCEPTION_REFCOUNT (arg)))
+           {
+             tree type = strip_array_types (TREE_TYPE (arg));
+             if (type_build_dtor_call (type))

You don't need to strip_array_types before passing to type_build_dtor_call, it will strip them.

+               {
+                 tree cleanup
+                   = cxx_maybe_build_cleanup (arg, (ctx->quiet ? tf_none
+                                                    : tf_warning_or_error));
+                 if (cleanup == error_mark_node)
+                   *non_constant_p = true;
+                 tree jmp_target = NULL_TREE;
+                 cxx_eval_constant_expression (ctx, cleanup, vc_discard,
+                                               non_constant_p, overflow_p,
+                                               &jmp_target);
+                 if (throws (&jmp_target))
+                   *jump_target = jmp_target;
+               }
+             DECL_NAME (arg) = heap_deleted_identifier;
+             ctx->global->destroy_value (arg);
+             ctx->global->heap_dealloc_count++;
+           }
+       }
+      return void_node;
+    case CXA_RETHROW:
+      if (nargs != 0)
+       goto invalid_nargs;
+      unsigned idx;
+      FOR_EACH_VEC_ELT_REVERSE (ctx->global->caught_exceptions, idx, arg)
+       if (arg == NULL_TREE || !VAR_P (arg))
+         --idx;
+       else
+         break;
+      if (arg == NULL_TREE)
+       {
+         if (!ctx->quiet)
+           error_at (loc, "%qD called with no caught exceptions active",
+                     fndecl);
+         *non_constant_p = true;
+         return call;
+       }
+      DECL_EXCEPTION_REFCOUNT (arg)
+       = size_binop (PLUS_EXPR, DECL_EXCEPTION_REFCOUNT (arg), size_one_node);
+      ++ctx->global->uncaught_exceptions;
+      *jump_target = arg;
+      return void_node;
+    case CXA_BAD_CAST:
+    case CXA_BAD_TYPEID:
+    case CXA_THROW_BAD_ARRAY_NEW_LENGTH:
+      if (nargs != 0)
+       goto invalid_nargs;
+      else
+       {
+         tree name;
+         switch (kind)
+           {
+           case CXA_BAD_CAST:
+             name = get_identifier ("bad_cast");
+             break;
+           case CXA_BAD_TYPEID:
+             name = get_identifier ("bad_typeid");
+             break;
+           case CXA_THROW_BAD_ARRAY_NEW_LENGTH:
+             name = get_identifier ("bad_array_new_length");
+             break;
+           default:
+             gcc_unreachable ();
+           }
+         tree decl = lookup_qualified_name (std_node, name);
+         if (TREE_CODE (decl) != TYPE_DECL
+             || !CLASS_TYPE_P (TREE_TYPE (decl))
+             || !type_build_ctor_call (TREE_TYPE (decl)))
+           {
+             if (!ctx->quiet)
+               error_at (loc, "%qD called without %<std::%D%> being defined",
+                         fndecl, name);
+             *non_constant_p = true;
+             return call;
+           }
+         tree type = TREE_TYPE (decl);
+         tree var = build_decl (loc, VAR_DECL, heap_identifier, type);
+         DECL_ARTIFICIAL (var) = 1;
+         retrofit_lang_decl (var);

Let's factor this out of here and __cxa_allocate_exception.

+         DECL_EXCEPTION_REFCOUNT (var) = size_one_node;
+         tree ctor
+           = build_special_member_call (var, complete_ctor_identifier,
+                                        NULL, type, LOOKUP_NORMAL,
+                                        ctx->quiet ? tf_none
+                                        : tf_warning_or_error);
+         if (ctor == error_mark_node)
+           {
+             *non_constant_p = true;
+             return call;
+           }
+         ctx->global->heap_vars.safe_push (var);
+         if (TREE_CONSTANT (ctor))
+           ctx->global->put_value (var, ctor);
+         else
+           {
+             ctx->global->put_value (var, NULL_TREE);
+             cxx_eval_constant_expression (ctx, ctor, vc_discard,
+                                           non_constant_p, overflow_p,
+                                           jump_target);
+             if (*non_constant_p)
+               return call;
+             if (throws (jump_target))
+               return NULL_TREE;
+           }
+         ++ctx->global->uncaught_exceptions;
+         *jump_target = var;
+       }
+      return void_node;
+    case STD_UNCAUGHT_EXCEPTIONS:
+      if (nargs != 0)
+       goto invalid_nargs;
+      /* Similarly to __builtin_is_constant_evaluated (), we don't
+        want to give a definite answer during mce_unknown evaluation,
+        because that might prevent evaluation later on when some
+        exceptions might be uncaught.  But unlikely to that, we don't

*unlike that

("unlikely" means improbable)

+        want to constant fold it even during cp_fold, because at runtime
+        std::uncaught_exceptions () might still be non-zero.  */
+      if (ctx->manifestly_const_eval != mce_true)
+       {
+         *non_constant_p = true;
+         return call;
+       }
+      return build_int_cst (integer_type_node,
+                           ctx->global->uncaught_exceptions);
+    case STD_CURRENT_EXCEPTION:
+      if (nargs != 0)
+       goto invalid_nargs;
+      else
+       {
+         tree name = get_identifier ("exception_ptr");
+         tree decl = lookup_qualified_name (std_node, name);
+         tree fld;
+         if (TREE_CODE (decl) != TYPE_DECL
+             || !CLASS_TYPE_P (TREE_TYPE (decl))
+             || !COMPLETE_TYPE_P (TREE_TYPE (decl))
+             || !(fld = next_aggregate_field (TYPE_FIELDS (TREE_TYPE (decl))))
+             || DECL_ARTIFICIAL (fld)
+             || TREE_CODE (TREE_TYPE (fld)) != POINTER_TYPE
+             || next_aggregate_field (DECL_CHAIN (fld))
+             || !tree_int_cst_equal (TYPE_SIZE (TREE_TYPE (decl)),
+                                     TYPE_SIZE (TREE_TYPE (fld))))
+           {
+             if (!ctx->quiet)
+               error_at (loc, "%qD called without supportable %qs",
+                         fndecl, "std::exception_ptr");
+             *non_constant_p = true;
+             return call;
+           }
+         FOR_EACH_VEC_ELT_REVERSE (ctx->global->caught_exceptions, idx, arg)
+           if (arg == NULL_TREE || !VAR_P (arg))
+             --idx;
+           else
+             break;
+         /* Similarly to __builtin_is_constant_evaluated (), we don't
+            want to give a definite answer during mce_unknown evaluation,
+            because that might prevent evaluation later on when some
+            exceptions might be current.  But unlikely to that, we don't

*unlike that

+            want to constant fold it even during cp_fold, because at runtime
+            std::current_exception () might still be non-null.  */
+         if (ctx->manifestly_const_eval == mce_unknown
+             || (ctx->manifestly_const_eval == mce_false && arg == NULL_TREE))

Can't we return a non-null arg for mce_unknown as well?

+           {
+             *non_constant_p = true;
+             return call;
+           }
+         if (arg == NULL_TREE)
+           arg = build_zero_cst (TREE_TYPE (fld));
+         else
+           {
+             DECL_EXCEPTION_REFCOUNT (arg)
+               = size_binop (PLUS_EXPR, DECL_EXCEPTION_REFCOUNT (arg),
+                             size_one_node);
+             arg = fold_convert (ptr_type_node, build_address (arg));
+           }
+         return build_constructor_single (TREE_TYPE (decl), fld, arg);
+       }
+    case STD_RETHROW_EXCEPTION:
+      if (nargs != 1)
+       goto invalid_nargs;
+      if (TYPE_REF_P (TREE_TYPE (args[0])))
+       {
+         arg = args[0];
+         STRIP_NOPS (arg);
+         if (TREE_CODE (arg) == ADDR_EXPR)
+           {
+             args[0]
+               = cxx_eval_constant_expression (ctx, TREE_OPERAND (arg, 0),
+                                               vc_prvalue, non_constant_p,
+                                               overflow_p, jump_target);
+             if (*non_constant_p)
+               return call;
+             if (*jump_target)
+               return NULL_TREE;
+           }
+       }
+      if (TREE_CODE (args[0]) != CONSTRUCTOR
+         || CONSTRUCTOR_NELTS (args[0]) != 1)
+       {
+       invalid_std_rethrow:
+         if (!ctx->quiet)
+           error_at (loc, "%qD called with %qs argument other than "
+                     "null pointer", fndecl, "std::exception_ptr");

The rest of this code doesn't seem to be looking for a null pointer?

+         *non_constant_p = true;
+         return void_node;
+       }
+      arg = CONSTRUCTOR_ELT (args[0], 0)->value;
+      STRIP_NOPS (arg);
+      if (TREE_CODE (arg) != ADDR_EXPR)
+       goto invalid_std_rethrow;
+      arg = TREE_OPERAND (arg, 0);
+      if (!VAR_P (arg)
+         || !DECL_ARTIFICIAL (arg)
+         || DECL_NAME (arg) != heap_identifier
+         || !DECL_LANG_SPECIFIC (arg))
+       goto invalid_std_rethrow;
+      DECL_EXCEPTION_REFCOUNT (arg)
+       = size_binop (PLUS_EXPR, DECL_EXCEPTION_REFCOUNT (arg), size_one_node);
+      ++ctx->global->uncaught_exceptions;
+      *jump_target = arg;
+      return void_node;
+    case BUILTIN_EH_PTR_ADJUST_REF:
+      if (nargs != 2)
+       goto invalid_nargs;
+      STRIP_NOPS (args[0]);
+      if (TREE_CODE (args[0]) != ADDR_EXPR)
+       goto invalid_ptr;
+      arg = TREE_OPERAND (args[0], 0);
+      if (!VAR_P (arg)
+         || !DECL_ARTIFICIAL (arg)
+         || DECL_NAME (arg) != heap_identifier
+         || !DECL_LANG_SPECIFIC (arg))
+       goto invalid_ptr;
+      if (integer_onep (args[1]))
+       DECL_EXCEPTION_REFCOUNT (arg)
+         = size_binop (PLUS_EXPR, DECL_EXCEPTION_REFCOUNT (arg),
+                       size_one_node);
+      else if (integer_minus_onep (args[1]))
+       goto free_except;
+      else
+       {
+         if (!ctx->quiet)
+           error_at (loc, "%qD called with second argument "
+                     "other than 1 or -1", fndecl);
+         *non_constant_p = true;
+       }
+      return void_node;
+    default:
+      gcc_unreachable ();
+    }
+}
+
  /* Attempt to evaluate T which represents a call to a builtin function.
     We assume here that all builtin functions evaluate to scalar types
     represented by _CST nodes.  */
@@ -2536,7 +3256,8 @@ get_component_with_type (tree path, tree
static tree
  cxx_eval_dynamic_cast_fn (const constexpr_ctx *ctx, tree call,
-                         bool *non_constant_p, bool *overflow_p)
+                         bool *non_constant_p, bool *overflow_p,
+                         tree *jump_target)
  {
    /* T will be something like
        __dynamic_cast ((B*) b, &_ZTI1B, &_ZTI1D, 8)
@@ -2559,15 +3280,18 @@ cxx_eval_dynamic_cast_fn (const constexp
    bool reference_p = false;
    while (CONVERT_EXPR_P (obj) || TREE_CODE (obj) == SAVE_EXPR)
      {
-      reference_p |= TYPE_REF_P (TREE_TYPE (obj));
+      if (cxx_dialect < cxx26 || !flag_exceptions)
+       reference_p |= TYPE_REF_P (TREE_TYPE (obj));

I assume this is because a failed reference dynamic_cast now throws, but that's a bit confusing, since it is still a reference. Maybe rename reference_p to failure_is_non_constant or something?

        obj = TREE_OPERAND (obj, 0);
      }
/* Evaluate the object so that we know its dynamic type. */
    obj = cxx_eval_constant_expression (ctx, obj, vc_prvalue, non_constant_p,
-                                     overflow_p);
+                                     overflow_p, jump_target);
    if (*non_constant_p)
      return call;
+  if (*jump_target)
+    return NULL_TREE;
/* We expect OBJ to be in form of &d.D.2102 when HINT == 0,
       but when HINT is > 0, it can also be something like
@@ -3066,7 +3810,12 @@ cxx_eval_call_expression (const constexp
          return arg1;
        }
        else if (cxx_dynamic_cast_fn_p (fun))
-       return cxx_eval_dynamic_cast_fn (ctx, t, non_constant_p, overflow_p);
+       return cxx_eval_dynamic_cast_fn (ctx, t, non_constant_p, overflow_p,
+                                        jump_target);
+      else if (enum cxa_builtin kind = cxx_cxa_builtin_fn_p (fun))
+       return cxx_eval_cxa_builtin_fn (ctx, t, kind, fun,
+                                       non_constant_p, overflow_p,
+                                       jump_target);
if (!ctx->quiet)
        {

If evaluating the arguments might throw, we can't give up at this point for a non-constexpr function.

@@ -3111,7 +3860,8 @@ cxx_eval_call_expression (const constexp
    constexpr_call new_call;
    new_call.bindings
      = cxx_bind_parameters_in_call (ctx, t, fun, non_constant_p,
-                                  overflow_p, &non_constant_args);
+                                  overflow_p, &non_constant_args,
+                                  jump_target);
/* We build up the bindings list before we know whether we already have this
       call cached.  If we don't end up saving these bindings, ggc_free them 
when
@@ -7154,6 +8015,9 @@ label_matches (const constexpr_ctx *ctx,
         breaks (jump_target) or continues (jump_target).  */
        break;
+ case VAR_DECL:
+      break;

This could use a comment.

+
      default:
        gcc_unreachable ();
      }
@@ -8017,9 +8877,21 @@ cxx_eval_constant_expression (const cons
        /* Pass vc_prvalue because this indicates
           initialization of a temporary.  */
        r = cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 1), vc_prvalue,
-                                         non_constant_p, overflow_p);
+                                         non_constant_p, overflow_p,
+                                         jump_target);
        if (*non_constant_p)
          break;
+       if (TARGET_EXPR_CLEANUP (t)
+           && (!CLEANUP_EH_ONLY (t) || cxx_dialect >= cxx26))
+         {
+           ctx->global->cleanups->safe_push (TARGET_EXPR_CLEANUP (t));
+           /* Mark CLEANUP_EH_ONLY cleanups by pushing NULL_TREE after
+              them.  */
+           if (CLEANUP_EH_ONLY (t))
+             ctx->global->cleanups->safe_push (NULL_TREE);
+         }
+       if (*jump_target)
+         return NULL_TREE;

I don't think we want to move the cleanup push up here; in particular, if the initialization throws it isn't completely initialized, so there is no cleanup to run.

        /* If the initializer is complex, evaluate it to initialize slot.  */
        bool is_complex = target_expr_needs_replace (t);
        if (!is_complex)
@@ -8029,8 +8901,6 @@ cxx_eval_constant_expression (const cons
            r = adjust_temp_type (type, r);
            ctx->global->put_value (slot, r);
          }
-       if (TARGET_EXPR_CLEANUP (t) && !CLEANUP_EH_ONLY (t))
-         ctx->global->cleanups->safe_push (TARGET_EXPR_CLEANUP (t));
        if (ctx->save_exprs)
          ctx->save_exprs->safe_push (slot);
        if (lval)
@@ -8107,9 +8964,45 @@ cxx_eval_constant_expression (const cons
                                        jump_target);
        break;
+ case TRY_BLOCK:
+      r = cxx_eval_constant_expression (ctx, TRY_STMTS (t), lval,
+                                       non_constant_p, overflow_p,
+                                       jump_target);
+      if (!*non_constant_p && throws (jump_target))
+       if (tree h = TRY_HANDLERS (t))
+         {
+           tree type = strip_array_types (TREE_TYPE (*jump_target));
+           if (TREE_CODE (h) == STATEMENT_LIST)
+             {
+               for (tree stmt : tsi_range (h))
+                 if (TREE_CODE (stmt) == HANDLER
+                     && handler_match_for_exception_type (stmt, type))
+                   {
+                     h = stmt;
+                     break;
+                   }
+               if (TREE_CODE (h) == STATEMENT_LIST)
+                 h = NULL_TREE;
+             }
+           else if (TREE_CODE (h) != HANDLER
+                    || !handler_match_for_exception_type (h, type))
+             h = NULL_TREE;
+           if (h)
+             {
+               gcc_assert (VAR_P (*jump_target));
+               ctx->global->caught_exceptions.safe_push (*jump_target);
+               ctx->global->caught_exceptions.safe_push (HANDLER_TYPE (h));
+               *jump_target = NULL_TREE;
+               r = cxx_eval_constant_expression (ctx, HANDLER_BODY (h),
+                                                 vc_discard, non_constant_p,
+                                                 overflow_p, jump_target);
+             }
+         }
+      break;
+
      case CLEANUP_POINT_EXPR:
        {
-       auto_vec<tree, 2> cleanups;
+       auto_vec<tree, 4> cleanups;

What's the rationale for this increase? It seems costly to increase the size of a local variable in cxx_eval_constant_expression given the deep recursion. I think we need a rationale for using any internal storage at all in this auto_vec; all full-expressions are wrapped in CLEANUP_POINT_EXPR, and only some of them actually have any cleanups.

Though I don't know for sure whether this actually affects the size of the stack frame.

        vec<tree> *prev_cleanups = ctx->global->cleanups;
        ctx->global->cleanups = &cleanups;
@@ -8124,47 +9017,188 @@ cxx_eval_constant_expression (const cons ctx->global->cleanups = prev_cleanups;
        unsigned int i;
-       tree cleanup;
+       tree cleanup, jmp_target = NULL_TREE;
+       bool eh = throws (jump_target);
        /* Evaluate the cleanups.  */
        FOR_EACH_VEC_ELT_REVERSE (cleanups, i, cleanup)
-         cxx_eval_constant_expression (&new_ctx, cleanup, vc_discard,
-                                       non_constant_p, overflow_p);
+         if (cleanup == NULL_TREE)
+           {
+             /* NULL_TREE cleanup is a marker that before it is
+                CLEANUP_EH_ONLY cleanup.  Skip the cleanup before it
+                if the body didn't throw.  */
+             if (!eh)
+               --i;
+           }
+         else
+           cxx_eval_constant_expression (&new_ctx, cleanup, vc_discard,
+                                         non_constant_p, overflow_p,
+                                         &jmp_target);
/* Forget SAVE_EXPRs and TARGET_EXPRs created by this
           full-expression.  */
        for (tree save_expr : save_exprs)
          destroy_value_checked (ctx, save_expr, non_constant_p);
+       if (throws (&jmp_target))
+         *jump_target = jmp_target;
        }
        break;
+ case MUST_NOT_THROW_EXPR:
+      r = cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 0),
+                                       lval,
+                                       non_constant_p, overflow_p,
+                                       jump_target);
+      if (throws (jump_target))
+       {
+         /* [except.handle]/7 - If the search for a handler exits the
+            function body of a function with a non-throwing exception
+            specification, the function std::terminate is invoked.  */
+         if (!ctx->quiet)
+           {
+             auto_diagnostic_group d;
+             diagnose_std_terminate (loc, ctx, *jump_target);
+             if (MUST_NOT_THROW_NOEXCEPT_P (t)
+                 && ctx->call
+                 && ctx->call->fundef)
+               inform (loc, "uncaught exception exited from %<noexcept%> "
+                            "function %qD",
+                       ctx->call->fundef->decl);
+             else if (MUST_NOT_THROW_THROW_P (t))
+               inform (loc, "destructor exited with an exception");

...after initializing the exception object
https://eel.is/c++draft/except#terminate-1.1

+             else if (MUST_NOT_THROW_CATCH_P (t))
+               inform (loc, "constructor exited with another exception while "
+                            "entering handler");
+           }
+         *non_constant_p = true;
+         *jump_target = NULL_TREE;
+         r = NULL_TREE;
+       }
+      break;
+
+    case TRY_CATCH_EXPR:
+      if (TREE_OPERAND (t, 0) == NULL_TREE)
+       {
+         r = void_node;
+         break;
+       }
+      r = cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 0), lval,
+                                       non_constant_p, overflow_p,
+                                       jump_target);
+      if (!*non_constant_p && throws (jump_target))
+       {
+         tree jmp_target = NULL_TREE;
+         cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 1), vc_discard,
+                                       non_constant_p, overflow_p,
+                                       &jmp_target);
+         if (throws (&jmp_target))
+           {
+             if (throws (jump_target))
+               {
+                 /* [except.throw]/9 - If the exception handling mechanism
+                    handling an uncaught exception directly invokes a function
+                    that exits via an exception, the function std::terminate is
+                    invoked.  */
+                 if (!ctx->quiet)
+                   {
+                     auto_diagnostic_group d;
+                     diagnose_std_terminate (loc, ctx, *jump_target);
+                     inform (loc, "destructor exited with an exception");
+                   }
+                 *non_constant_p = true;
+                 *jump_target = NULL_TREE;
+                 r = NULL_TREE;
+               }
+             else
+               *jump_target = jmp_target;
+           }
+       }
+      break;
+
      case TRY_FINALLY_EXPR:
        r = cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 0), lval,
                                        non_constant_p, overflow_p,
                                        jump_target);
        if (!*non_constant_p)
-       /* Also evaluate the cleanup.  */
-       cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 1), vc_discard,
-                                     non_constant_p, overflow_p);
+       {
+         tree jmp_target = NULL_TREE;
+         /* Also evaluate the cleanup.  */
+         if (TREE_CODE (TREE_OPERAND (t, 1)) == EH_ELSE_EXPR
+             && throws (jump_target))
+           cxx_eval_constant_expression (ctx,
+                                         TREE_OPERAND (TREE_OPERAND (t, 1),
+                                                       1), vc_discard,
+                                         non_constant_p, overflow_p,
+                                         &jmp_target);
+         else
+           cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 1), vc_discard,
+                                         non_constant_p, overflow_p,
+                                         &jmp_target);
+         if (throws (&jmp_target))
+           {
+             if (throws (jump_target))
+               {
+                 /* [except.throw]/9 - If the exception handling mechanism
+                    handling an uncaught exception directly invokes a function
+                    that exits via an exception, the function std::terminate is
+                    invoked.  */
+                 if (!ctx->quiet)
+                   {
+                     auto_diagnostic_group d;
+                     diagnose_std_terminate (loc, ctx, *jump_target);
+                     inform (loc, "destructor exited with an exception");
+                   }
+                 *non_constant_p = true;
+                 *jump_target = NULL_TREE;
+                 r = NULL_TREE;
+               }
+             else
+               *jump_target = jmp_target;
+           }
+       }
        break;
case EH_ELSE_EXPR:
        /* Evaluate any cleanup that applies to non-EH exits.  */
        cxx_eval_constant_expression (ctx, TREE_OPERAND (t, 0), vc_discard,
-                                   non_constant_p, overflow_p);
+                                   non_constant_p, overflow_p,
+                                   jump_target);
- /* We do not have constexpr exceptions yet, so skip the EH path. */
+      /* The EH path is handled in TRY_FINALLY_EXPR handling above.  */
        break;
case CLEANUP_STMT:
        r = cxx_eval_constant_expression (ctx, CLEANUP_BODY (t), lval,
                                        non_constant_p, overflow_p,
                                        jump_target);
-      if (!CLEANUP_EH_ONLY (t) && !*non_constant_p)
+      if ((!CLEANUP_EH_ONLY (t) || throws (jump_target)) && !*non_constant_p)
        {
          iloc_sentinel ils (loc);
+         tree jmp_target = NULL_TREE;
          /* Also evaluate the cleanup.  */
          cxx_eval_constant_expression (ctx, CLEANUP_EXPR (t), vc_discard,
-                                       non_constant_p, overflow_p);
+                                       non_constant_p, overflow_p,
+                                       &jmp_target);
+         if (throws (&jmp_target))
+           {
+             if (throws (jump_target))
+               {
+                 /* [except.throw]/9 - If the exception handling mechanism
+                    handling an uncaught exception directly invokes a function
+                    that exits via an exception, the function std::terminate is
+                    invoked.  */
+                 if (!ctx->quiet)
+                   {
+                     auto_diagnostic_group d;
+                     diagnose_std_terminate (loc, ctx, *jump_target);
+                     inform (loc, "destructor exited with an exception");
+                   }
+                 *non_constant_p = true;
+                 *jump_target = NULL_TREE;
+                 r = NULL_TREE;
+               }
+             else
+               *jump_target = jmp_target;
+           }
        }
        break;
@@ -712,6 +716,13 @@ build_throw (location_t loc, tree exp, t
        allocate_expr = do_allocate_exception (temp_type);
        if (allocate_expr == error_mark_node)
        return error_mark_node;
+      tree ptr_copy = NULL_TREE;
+      /* For C++26, copy ptr inside of the CLEANUP_POINT_EXPR
+        added below to a TARGET_EXPR slot added outside of it,
+        otherwise during constant evaluation of throw expression
+        we'd diagnose accessing ptr outside of its lifetime.  */

Ah, because we force the cleanups to run before we pass the ptr to __cxa_throw, and destroy the temporaries. I guess non-constexpr code doesn't have this issue currently because gimplify_target_expr only adds a clobber cleanup if needs_to_live_in_memory, which this pointer doesn't. But it still seems wrong for non-constexpr as well.

So let's do this regardless of cxx_dialect.

+      if (cxx_dialect >= cxx26)
+       ptr_copy = get_internal_target_expr (null_pointer_node);
        allocate_expr = get_internal_target_expr (allocate_expr);
        ptr = TARGET_EXPR_SLOT (allocate_expr);
        TARGET_EXPR_CLEANUP (allocate_expr) = do_free_exception (ptr);
--- gcc/cp/decl.cc.jj   2025-05-31 00:43:13.835238535 +0200
+++ gcc/cp/decl.cc      2025-05-31 00:43:32.174996782 +0200
@@ -5070,6 +5070,18 @@ cxx_init_decl_processing (void)
                            BUILT_IN_FRONTEND, NULL, NULL_TREE);
    set_call_expr_flags (decl, ECF_CONST | ECF_NOTHROW | ECF_LEAF);
+ if (cxx_dialect >= cxx26)
+    {
+      tree void_ptrintftype
+       = build_function_type_list (void_type_node, ptr_type_node,
+                                   integer_type_node, NULL_TREE);
+      decl = add_builtin_function ("__builtin_eh_ptr_adjust_ref",

Instead of this, I wonder about hijacking the exception_ptr constructor and destructor? That doesn't need to be in this patch, just a thought. Do we know if there's a clang/libc++ plan for constexpr exception_ptr yet?

Jason

Reply via email to