On Thu, Dec 04, 2025 at 02:09:21PM +0100, Jakub Jelinek wrote:
> For those we only clear the cv_cache and fold_cache.  We never clear
> constexpr_call_table.
> So, do you want to clear the whole constexpr_call_cache if (flag_reflection)
> in the various cases that can change the metafunction behavior
> 1) defining enum
> 2) defining a class
> 3) during duplicate_decls
> and wherever else we find a problem?  Currently with
> -     ctx->global->metafns_called = true;
> +//   ctx->global->metafns_called = true;
> only
> FAIL: g++.dg/reflect/enumerators_of1.C  -std=c++26 (test for excess errors)
> FAIL: g++.dg/reflect/p2996-17.C  -std=c++26 (test for excess errors)
> failures appear, but that is mostly because in most tests (with the main
> exception of the various
> consteval bool
> whatever (info r)
> {
>   try { whatever_metafn (r); }
>   catch (std::meta::exception &) { return false; }
>   return true;
> }
> functions) call metafunctions directly rather than through a chain of other
> constexpr functions.  enumerators_of1.C fails exactly because of the missing
> clearing of the constexpr_call_cache when defining an enum.  I will try to
> construct a testcase where it will fail because of the duplicate_decls case
> (has_identifier changing one way or the other on redeclarations of the same
> decl; we have tests for it but likely all direct and we don't cache calls
> to metafns in constexpr_call_cache).  The p2996-17.C case is about the class
> type completions, but it is using define_aggregate, so another question is
> if it is safe to clear the constexpr_call_cache from within a constexpr
> call evaluation, but maybe it is as it is GC allocated and so we could
> in the callers just store something that nothing else will find.
> 
> Or, shall we record in constexpr_call_cache whether a call called any
> metafunctions (so keep the metafns_called flag and remember it, just don't
> use it for cacheable = false) and only flush on 1)/2)/3) etc. just the
> calls that called any metafns?

I've played with this some more.
The following patch keeps recording metafns_called and also records that
flag in constexpr_call_cache.
In an event which can change behavior of some metafunctions, it then
traverses constexpr_call_cache and purges cached values of functions
which (ever) called directly or indirectly metafunctions.
Plus has special handling when such event happens during constexpr
evaluation, because calls with constexpr evaluation in progress could
change behavior too.
This fixes everything in the testsuite, unfortunately in the 1)/2)/3)
list above I've missed the most important spot, anything that pushes new
decls into a namespace can affect std::meta::members_of, and I'm afraid
both that and the finish_struct_1 cases can be pretty expensive.

We can have hundreds of thousands of constexpr_call_cache entries in large
sources and walking them all each time could consume a lot of compile time.

So, I wonder if we couldn't help that with some global variables that would
cause maybe_clear_constexpr_call_cache to do nothing if there is no point
for that.  We would need to traverse it either if there is any new
cached result (i.e. entry->result (???)
= something_other_than_NULL_error_unknown_type_node_global_namespace;
for entry->metafns_called) since last maybe_clear_constexpr_call_cache
traversal (that can be handled by a simple bool flag set by
cxx_eval_call_expression when recording such case and clearing it in
maybe_clear_constexpr_call_cache) and for the in progress constexpr
evaluation a flag whether any of those called any metafns (we can have
nexted cxx_eval_outermost* calls, so probably a counter of how many
cxx_eval_outermost* calls are there currently increased at the
start of that function, decreased later and again another bool flag
set when setting metafns_called = true; and cleared when the count
is decreased to zero (i.e. in the outermost cxx_eval_outermost* call).

Another thing is what to do with cv_cache/fold_cache.  If the 1)/2)/3)/4)
event happens not from inside of some cxx_eval_outermost* call, then
I guess we could clear_cv_and_fold_caches if we've cleared any
entry->metafns_called remembered value.  But for the in-progress calls,
we don't know yet if anything ought to be actually not cached (could
be solved by the above mentioned changes though), and more importantly,
I think we should arrange for the outcome of the cxx_eval_outermost*
not to be cached either in such case in cv_cache somehow.

Oh, and just now realized I likely miss flushing somewhere in
duplicate_decls also for the non-FUNCTION_DECLs (maybe just VAR_DECLs),
because a VAR_DECL redeclaration could be adding new annotations.

Thoughts on this?

diff --git a/gcc/cp/class.cc b/gcc/cp/class.cc
index b56cc518a11..45204debbe2 100644
--- a/gcc/cp/class.cc
+++ b/gcc/cp/class.cc
@@ -8153,6 +8153,11 @@ finish_struct_1 (tree t)
          TYPE_TRANSPARENT_AGGR (t) = 0;
        }
     }
+
+  /* Clear caches of constexpr calls that called metafunctions,
+     e.g. std::meta::is_complete_type can change behavior at this point.  */
+  if (flag_reflection)
+    maybe_clear_constexpr_call_cache ();
 }
 
 /* When T was built up, the member declarations were added in reverse
diff --git a/gcc/cp/constexpr.cc b/gcc/cp/constexpr.cc
index de9be778a13..18c8529dc07 100644
--- a/gcc/cp/constexpr.cc
+++ b/gcc/cp/constexpr.cc
@@ -1144,6 +1144,8 @@ struct GTY((for_user)) constexpr_call {
   /* Result of the call, indexed by the value of
      constexpr_ctx::manifestly_const_eval.
        unknown_type_node means the call is being evaluated.
+       global_namespace ditto, but additionally says that calls
+       which called metafunctions shouldn't be cached.
        error_mark_node means that the evaluation was erroneous or otherwise
        uncacheable (e.g. because it depends on the caller).
        Otherwise, the actual value of the call.  */
@@ -1151,6 +1153,8 @@ struct GTY((for_user)) constexpr_call {
   /* The hash of this call; we remember it here to avoid having to
      recalculate it when expanding the hash table.  */
   hashval_t hash = 0;
+  /* Whether the call ever called metafunctions.  */
+  bool metafns_called = 0;
 
   /* The result slot corresponding to the given mce_value.  */
   tree& result (mce_value mce) { return results[1 + int(mce)]; }
@@ -4244,7 +4248,9 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree 
t,
         so that we can detect circular dependencies.  Now that we only cache
         up to constexpr_cache_depth this won't catch circular dependencies that
         start deeper, but they'll hit the recursion or ops limit.  */
-      else if (entry->result (ctx->manifestly_const_eval) == unknown_type_node)
+      else if (entry->result (ctx->manifestly_const_eval) == unknown_type_node
+              || (entry->result (ctx->manifestly_const_eval)
+                  == global_namespace))
        {
          if (!ctx->quiet)
            error ("call has circular dependency");
@@ -4395,8 +4401,13 @@ cxx_eval_call_expression (const constexpr_ctx *ctx, tree 
t,
                }
            }
 
-         if (ctx->global->metafns_called)
-           cacheable = false;
+         if (ctx->global->metafns_called && entry)
+           {
+             entry->metafns_called = true;
+             if (entry->result (ctx->manifestly_const_eval)
+                 == global_namespace)
+               cacheable = false;
+           }
          ctx->global->metafns_called |= save_metafns_called;
 
          /* At this point, the object's constructor will have run, so
@@ -11185,6 +11196,43 @@ clear_cv_and_fold_caches ()
   clear_fold_cache ();
 }
 
+/* Callback for maybe_clear_constexpr_call_cache cache traversal.  */
+
+static int
+clear_constexpr_call_cache_entry (constexpr_call **slot, bool)
+{
+  constexpr_call *v = *slot;
+  if (!v)
+    return 1;
+  for (int i = 0; i < 3; ++i)
+    {
+      /* Change unknown_type_node values to global_namespace,
+        which tells cxx_eval_call_expression that if the call
+        called any metafunctions, it shouldn't be cached.  */
+      if (v->results[i] == unknown_type_node
+         || v->results[i] == global_namespace)
+       v->results[i] = global_namespace;
+      /* And purge cache of any previously called functions
+        which called any metafunctions.  */
+      else if (v->metafns_called && v->results[i])
+       v->results[i] = error_mark_node;
+    }
+  return 1;
+}
+
+/* For -freflection purge cached return values of constexpr calls
+   which called any metafunctions and for currently in progress
+   calls arrange for those which called any metafunctions not to be
+   cached.  */
+
+void
+maybe_clear_constexpr_call_cache ()
+{
+  if (flag_reflection && constexpr_call_table)
+    constexpr_call_table->traverse <bool,
+                                   clear_constexpr_call_cache_entry> (false);
+}
+
 /* Internal function handling expressions in templates for
    fold_non_dependent_expr and fold_non_dependent_init.
 
diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
index 9e671926c50..4b2ce7eee48 100644
--- a/gcc/cp/cp-tree.h
+++ b/gcc/cp/cp-tree.h
@@ -9190,6 +9190,7 @@ extern void explain_invalid_constexpr_fn        (tree);
 extern vec<tree> cx_error_context               (void);
 extern tree fold_sizeof_expr                   (tree);
 extern void clear_cv_and_fold_caches           (void);
+extern void maybe_clear_constexpr_call_cache   (void);
 extern tree unshare_constructor                        (tree 
CXX_MEM_STAT_INFO);
 extern bool decl_implicit_constexpr_p          (tree);
 struct constexpr_ctx;
diff --git a/gcc/cp/decl.cc b/gcc/cp/decl.cc
index e1bc2714192..906d825c945 100644
--- a/gcc/cp/decl.cc
+++ b/gcc/cp/decl.cc
@@ -2477,6 +2477,12 @@ duplicate_decls (tree newdecl, tree olddecl, bool 
hiding, bool was_hidden)
 
   if (TREE_CODE (newdecl) == FUNCTION_DECL)
     {
+      /* Clear caches of constexpr calls that called metafunctions,
+        e.g. std::meta::has_identifier or std::meta::identifier_of can change
+        behavior at this point for function parameter reflections.  */
+      if (flag_reflection)
+       maybe_clear_constexpr_call_cache ();
+
       if (merge_attr)
        {
          {
@@ -19187,6 +19193,12 @@ finish_enum_value_list (tree enumtype)
   /* Each enumerator now has the type of its enumeration.  Clear the cache
      so that this change in types doesn't confuse us later on.  */
   clear_cv_and_fold_caches ();
+
+  /* Similarly clear caches of constexpr calls that called metafunctions,
+     e.g. std::meta::enumerators_of or std::meta::is_complete_type can
+     change behavior at this point.  */
+  if (flag_reflection)
+    maybe_clear_constexpr_call_cache ();
 }
 
 /* Finishes the enum type. This is called only the first time an
diff --git a/gcc/cp/name-lookup.cc b/gcc/cp/name-lookup.cc
index 68f6b1944ed..a9b2d14b883 100644
--- a/gcc/cp/name-lookup.cc
+++ b/gcc/cp/name-lookup.cc
@@ -3980,6 +3980,12 @@ pushdecl (tree decl, bool hiding)
 
       if (level->kind == sk_namespace)
        {
+         /* Clear caches of constexpr calls that called metafunctions,
+            e.g. std::meta::members_of can change behavior at this
+            point.  */
+         if (flag_reflection)
+           maybe_clear_constexpr_call_cache ();
+
          /* We look in the decl's namespace for an existing
             declaration, even though we push into the current
             namespace.  */
diff --git a/gcc/testsuite/g++.dg/reflect/members_of7.C 
b/gcc/testsuite/g++.dg/reflect/members_of7.C
new file mode 100644
index 00000000000..54024055f2d
--- /dev/null
+++ b/gcc/testsuite/g++.dg/reflect/members_of7.C
@@ -0,0 +1,32 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-freflection" }
+// Test std::meta::members_of.
+
+#include <meta>
+
+using namespace std::meta;
+
+constexpr access_context uctx = access_context::unchecked ();
+
+namespace N
+{
+}
+
+consteval std::size_t
+foo ()
+{
+  return members_of (^^N, uctx).size ();
+}
+
+namespace N
+{
+  static_assert (foo () == 0);
+  int a;
+  static_assert (foo () == 1);
+  void bar (int);
+  static_assert (foo () == 2);
+  void baz (long);
+  static_assert (foo () == 3);
+  void baz (short, float);
+  static_assert (foo () == 4);
+}


        Jakub

Reply via email to