On 1/8/26 4:57 AM, Marek Polacek wrote:
On Tue, Dec 16, 2025 at 09:27:25AM +0700, Jason Merrill wrote:
On 12/16/25 7:35 AM, Marek Polacek wrote:
On Tue, Dec 09, 2025 at 08:38:26PM +0800, Jason Merrill wrote:
On 12/6/25 4:19 AM, Marek Polacek wrote:
On Fri, Nov 28, 2025 at 08:25:05AM +0530, Jason Merrill wrote:
On 11/15/25 6:04 AM, Marek Polacek wrote:
@@ -5514,6 +5582,15 @@ cxx_init_decl_processing (void)
      /* Create the `std' namespace.  */
      push_namespace (get_identifier ("std"));
      std_node = current_namespace;
+  if (flag_reflection)
+    {
+      /* Note that we haven't initialized void_type_node yet, so
+        std_meta_node will be initially typeless; its type will be
+        set a little later in init_reflection.  */
+      push_namespace (get_identifier ("meta"), /*inline*/false);
+      std_meta_node = current_namespace;
+      pop_namespace ();
+    }

??? std_meta_node is a namespace, which are always typeless.

This was an ICE in is_std_substitution on:

      if (!(TYPE_LANG_SPECIFIC (type) && TYPE_TEMPLATE_INFO (type)))

where decl=namespace_decl meta and type was null due to the ordering
issue mentioned in the comment.

make_namespace uses void_type_node to build a namespace_decl, so all
other namespaces have a non-null type.

Why doesn't std_node have the same issue?

Because is_std_substitution has this early exit:

    if (!DECL_NAMESPACE_STD_P (CP_DECL_CONTEXT (decl)))
      return false;

So, basically by accident; is_std_substitution wants either a class or a
class template, but is looking at the type of any decl.  That seems worth
tightening up so we only look at the type of a class template. But not
urgent.

OK, I'll do it later.
Incidentally, why do we need to initialize std_meta_node here rather than in
init_reflection?

It seemed convenient since we just pushed into std:: which is where
meta should reside.

Sure, but if creating it here means we need to fix it up later perhaps it's
cleaner to just create it later.  Fine either way.

Fair enough, changed in:
<https://forge.sourceware.org/marek/gcc/commit/e6c0579c2b535c2f0ea2ed1596b6c6b0b0cc3e1d>

@@ -9808,6 +9918,20 @@ cp_finish_decl (tree decl, tree init, bool 
init_const_expr_p,
        record_types_used_by_current_var_decl (decl);
        }
+  /* CWG 3115: Every function of consteval-only type shall be an
+     immediate function.  */
+  if (TREE_CODE (decl) == FUNCTION_DECL
+      && !DECL_IMMEDIATE_FUNCTION_P (decl)
+      && consteval_only_p (decl)
+      /* But if the function can be escalated, merrily we roll along.  */
+      && !immediate_escalating_function_p (decl))
+    {
+      error_at (DECL_SOURCE_LOCATION (decl),
+               "function of consteval-only type must be declared %qs",
+               "consteval");
+      return;
+    }
@@ -20471,6 +20641,16 @@ finish_function (bool inline_p)
          unused_but_set_errorcount = errorcount;
        }
+  /* CWG 3115: Every function of consteval-only type shall be an immediate
+     function.  */
+  if (!DECL_IMMEDIATE_FUNCTION_P (fndecl)
+      && consteval_only_p (fndecl)
+      && !immediate_escalating_function_p (fndecl)
+      && !is_std_allocator_allocate (fndecl))
+    error_at (DECL_SOURCE_LOCATION (fndecl),
+             "function of consteval-only type must be declared %qs",
+             "consteval");

Do we need this in both places?

I think so, one is for fn decls, the other one for fn defs.

But every function definition is also a declaration.  And this condition
seems like something you can check before finish_function.

Maybe in grokfndecl?  Though that won't help with instantiations, so perhaps
you need a separate check function to call from there and
tsubst_function_decl?

Reworked in
<https://forge.sourceware.org/marek/gcc/commit/21332410abe133b726172248e44825e48bfd4b00>

I couldn't call the new function in tsubst_function_decl though -- it
caused too many errors on code such as:

    indirect_result_t<_Proj&, _Iter> operator*() const; // not defined

in bits/iterator_concepts.h, or

    _UninitDestroyGuard(const _UninitDestroyGuard&);

in bits/stl_uninitialized.h.

Why?

It complains that they're not declared consteval, which...they're not.
And they are not immediate-escalatable (defaulted special members of
consteval-only type should be though -- PR123404).

Hmm, yeah, in 123404 DECL_DEFAULTED_FN hasn't been set yet, so immediate_escalating_function_p gives the wrong answer. So either we need to set it sooner, or go back to checking this in cp_finish_decl. But maybe in start_function rather than finish_function.

But it worked to call it in instantiate_body -- that is, when we
instantiate a fn def.

I had to add consteval to 4 std::meta::exception member fns.
(I think this needs an LWG issue.)

+           c = BINFO_INHERITANCE_CHAIN (c);
+         tpvis = type_visibility (BINFO_TYPE (c));
+         *walk_subtrees = 0;
+         break;
+       case REFLECT_DATA_MEMBER_SPEC:
+         /* For data member description determine visibility
+            from the type.  */
+         tpvis = type_visibility (TREE_VEC_ELT (r, 0));
+         *walk_subtrees = 0;
+         break;
+       case REFLECT_PARM:
+         /* For function parameter reflection determine visibility
+            based on parent_of.  */
+         tpvis = expr_visibility (DECL_CONTEXT (r));
+         *walk_subtrees = 0;
+         break;
+       case REFLECT_OBJECT:
+         c = get_base_address (r);
+         if ((VAR_P (c) && decl_function_context (c))
+             || TREE_CODE (c) == PARM_DECL)
+           {
+             /* Make reflect_object of block scope variables
+                subobjects local.  */
+             tpvis = VISIBILITY_ANON;
+             *walk_subtrees = 0;
+           }

We just stopped treating local declarations as VISIBILITY_ANON in
r16-5024-gf062a6b7985fce -- for this to differ from that, we need more
rationale.

E.g. in

     template <int N, decltype(^^::) I>
     void
     foo ()
     {
     }

     static void
     bar ()
     {
       int v = 42;
       foo <101, ^^v> (); // #1
     }

#1 shouldn't be exported.  If I follow r16-5024, decl_linkage will
give me lk_none for 'v', as it should (it's a var at block scope), but
type_visibility of its type says VISIBILITY_DEFAULT

v is TU-local under https://eel.is/c++draft/basic.link#15.1.2 because it's
defined within TU-local bar().

So perhaps r16-5024 went too far one way, and this goes too far the other
way: for a decl with no linkage, we need to refer to the linkage of the
containing entity that does have a name with linkage.

So like this?  I haven't pushed this because I'm not sure that this
is what we want, although it seems OK to me -- it changes places with
a TODO in the testcase.  (I think this could be checked in post-merge.)

Agreed.

diff --git a/gcc/cp/decl2.cc b/gcc/cp/decl2.cc
index 1e717b3801f..07ee5568efd 100644
--- a/gcc/cp/decl2.cc
+++ b/gcc/cp/decl2.cc
@@ -2965,8 +2965,13 @@ min_vis_expr_r (tree *tp, int *walk_subtrees, void *data)
          if ((VAR_P (r) && decl_function_context (r))

I think we want to drop the decl_function_context check here and make ^^var
unconditionally expr_visibility (r)...

              || TREE_CODE (r) == PARM_DECL)
            {
-             /* Block scope variables are local to the TU.  */
-             tpvis = VISIBILITY_ANON;
+             /* For _DECLs with no linkage refer to the linkage of the
+                containing entity that does have a name with linkage.
+                ??? The r16-5024 change should perhaps follow this logic.  */
+             if (decl_linkage (r) == lk_none)
+               tpvis = expr_visibility (DECL_CONTEXT (r));

..., and make this DECL_CONTEXT change under case VAR_DECL (adjusting the
r16-5024 change).

Okay, so like this (dg.exp passes)?  I still haven't pushed this.

diff --git a/gcc/cp/decl2.cc b/gcc/cp/decl2.cc
index 4d1e15f1d5e..c4647e56aad 100644
--- a/gcc/cp/decl2.cc
+++ b/gcc/cp/decl2.cc
@@ -2906,7 +2906,7 @@ min_vis_expr_r (tree *tp, int *walk_subtrees, void *data)
        }
      addressable:
        if (decl_linkage (t) == lk_none)
-       tpvis = type_visibility (TREE_TYPE (t));
+       tpvis = expr_visibility (DECL_CONTEXT (t));

This change looks good, and should be pushed by itself as an update to r16-5024.

        /* Decls that have had their visibility constrained will report
         as external linkage, but we still want to transitively constrain
         if we refer to them, so just check TREE_PUBLIC instead.  */
@@ -2961,11 +2961,20 @@ min_vis_expr_r (tree *tp, int *walk_subtrees, void 
*data)
              *walk_subtrees = 0;
              break;
            }
-         if ((VAR_P (r) && decl_function_context (r))
-             || TREE_CODE (r) == PARM_DECL)
+         /* For _DECLs with no linkage refer to the linkage of the
+            containing entity that does have a name with linkage.  */
+         if (VAR_P (r))
            {
-             /* Block scope variables are local to the TU.  */
-             tpvis = VISIBILITY_ANON;
+             tpvis = expr_visibility (r);

Actually, maybe we want goto addressable here to avoid the non-ODR-use case. Which would also work for PARM_DECL.

+             *walk_subtrees = 0;
+             break;
+           }
+         if (TREE_CODE (r) == PARM_DECL)
+           {
+             if (decl_linkage (r) == lk_none)
+               tpvis = expr_visibility (DECL_CONTEXT (r));
+             else
+               tpvis = VISIBILITY_ANON;
              *walk_subtrees = 0;
              break;
            }
diff --git a/gcc/testsuite/g++.dg/reflect/visibility1.C 
b/gcc/testsuite/g++.dg/reflect/visibility1.C
index c47817b9b4e..331e8353e4a 100644
--- a/gcc/testsuite/g++.dg/reflect/visibility1.C
+++ b/gcc/testsuite/g++.dg/reflect/visibility1.C
@@ -49,8 +49,8 @@ inline void
  qux (int x)
  {
    int v = 42;
-  foo <106, ^^x> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi106EL" } } - var in inline fn - TODO, shall this be exported?
-  foo <107, ^^v> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi107EL" } } - var in inline fn - TODO, shall this be exported?
+  foo <106, ^^x> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi106EL" } } - var in inline fn
+  foo <107, ^^v> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi107EL" } } - var in inline fn
  }
template <int N>
@@ -58,8 +58,8 @@ void
  plugh (int x)
  {
    int v = 42;
-  foo <132, ^^x> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi132EL" } } - var in public fn template instantiation - TODO, shall 
this be exported?
-  foo <133, ^^v> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi133EL" } } - var in public fn template instantiation - TODO, shall 
this be exported?
+  foo <132, ^^x> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi132EL" } } - var in public fn template instantiation
+  foo <133, ^^v> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi133EL" } } - var in public fn template instantiation
    foo <134, parameters_of (parent_of (^^v))[0]> (); // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi134EL" { target *-*-linux* } } } - fn parm of public fn template 
instantiation
  }
@@ -80,8 +80,8 @@ void
  fred (int x)
  {
    int v = 42;
-  foo <138, ^^x> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi138EL" } } - var in TU-local fn template instantiation
-  foo <139, ^^v> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi139EL" } } - var in Tu-local fn template instantiation
+  foo <138, ^^x> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi138EL" } } - var in TU-local fn template instantiation
+  foo <139, ^^v> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi139EL" } } - var in Tu-local fn template instantiation
    foo <140, parameters_of (parent_of (^^v))[0]> (); // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi140EL" { xfail *-*-* } } } - fn parm of TU-local fn template 
instantiation - TODO, I think this shouldn't be exported and the mangling of these 3 doesn't 
include the template parameter
  }
@@ -108,8 +108,8 @@ xyzzy (int x)
    foo <122, ^^G <int>> ();                        // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi122EL" } } - specialization of TU-local template
    foo <123, ^^G <A>> ();                  // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi123EL" } } - specialization of TU-local template
    foo <124, ^^G <B>> ();                  // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi124EL" } } - specialization of TU-local template
-  foo <125, ^^x> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi125EL" } } - var in public fn but non-comdat - TODO, shall this be 
exported?
-  foo <126, ^^v> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi126EL" } } - var in public fn but non-comdat - TODO, shall this be 
exported?
+  foo <125, ^^x> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi125EL" } } - var in public fn but non-comdat
+  foo <126, ^^v> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi126EL" } } - var in public fn but non-comdat
    foo <127, std::meta::info {}> ();             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi127EL" { target *-*-linux* } } } - null reflection
    foo <128, ^^b> ();                            // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi128EL" { target *-*-linux* } } } - public variable
    foo <129, ^^c> ();                            // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi129EL" } } - TU-local variable
@@ -80,8 +80,8 @@ void
   fred (int x)
   {
     int v = 42;
-  foo <138, ^^x> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi138EL" } } - var in TU-local fn template instantiation
-  foo <139, ^^v> ();                             // { dg-final { scan-assembler-not 
"\t.weak\t_Z3fooILi139EL" } } - var in Tu-local fn template instantiation
+  foo <138, ^^x> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi138EL" } } - var in TU-local fn template instantiation
+  foo <139, ^^v> ();                             // { dg-final { scan-assembler 
"\t.weak\t_Z3fooILi139EL" } } - var in Tu-local fn template instantiation

??? Since the template is TU-local, the instantiations should be too,
regardless of the visibility of the reflection.
So here DECL_CONTEXT of x is the fn decl fred which is lk_external and its
DECL_VISIBILITY is VISIBILITY_DEFAULT.

I'm confused.  fred is not static and isn't in namespace {}, why should
it be TU-local?  I think the comment is wrong.

Agreed, the comment seems to be copied from TU-local function garply.

+       /* This can happen for std::meta::info(^^int) where the cast has no
+          meaning.  */
+       if (REFLECTION_TYPE_P (type) && REFLECT_EXPR_P (op))
+         {
+           r = op;
+           break;
+         }

Why is this needed?  I'd expect the existing code to work fine for a cast to
the same type.

This is to handle

    using info = decltype(^^int);
    void
    g ()
    {
      constexpr auto r = info(^^int);
    }

An analogical test would be:

    using T = int;
    void
    g ()
    {
      constexpr auto i = T(42);
    }

which is handled by

      if (same_type_ignoring_tlq_and_bounds_p (type, TREE_TYPE (op)))
        r = op;

later in that function.  But with info(^^int) we don't get that far
because we do:

      if (op == oldop && tcode != UNARY_PLUS_EXPR)
        /* We didn't fold at the top so we could check for ptr-int
         conversion.  */
        return fold (t);

because op and oldop are the same REFLECT_EXPR, whereas with T(42)
op == 42 and oldop == NON_LVALUE_EXPR <42>.

But why is taking that return a problem?

So t is a NOP_EXPR: (info) ^^int and fold leaves that NOP_EXPR be.

Aha, because fold doesn't understand REFLECT_EXPR I guess.

So let's keep the added special handling but move it to just before the
op==oldop code.

Done in
<https://forge.sourceware.org/marek/gcc/commit/1f6219c2033466f2d5ca118dde50b885a362de17>
+    {
+      /* We may have to instantiate; for instance, if we're dealing with
+        a variable template.  For &[: ^^S::x :], we have to create an
+        OFFSET_REF.  For a VAR_DECL, we need the convert_from_reference.  */
+      cp_unevaluated u;
+      /* CWG 3109 adjusted [class.protected] to say that checking access to
+        protected non-static members is disabled for members designated by a
+        splice-expression.  */
+      push_deferring_access_checks (dk_no_check);

I'm not sure where an access check would happen here, either?
finish_id_expression_1 already does this push/pop for the FIELD_DECL case.

This is for:

    class Base {
    private:
      int m;
    public:
      static constexpr auto rm = ^^m;
    };
    class Derived { static constexpr auto mp = &[:Base::rm:]; };

where in finish_id_expression_1 we don't go to the FIELD_DECL block,
but to the one above it with finish_qualified_id_expr because scope is
Base here.

I don't see how this applies; finish_id_expression_1 would be looking at
Base::rm, which is a public static member.

Ah!  So cp_parser_splice_expression for [:Base::rm:] here calls
cp_parser_splice_specifier which parses Base::rm which produces
VCE<info>(rm) but then splice does cxx_constant_value which evaluates
it to field_decl m.  So then when we reach the finish_id_expression back
in cp_parser_splice_expression, we're dealing with m and not rm.

It's not clear to me if splice can avoid cxx_constant_value.  I don't
think so.

That all makes sense.  But again, finish_id_expression_1 already does the
same push/pop for FIELD_DECL.  So the trouble comes because of

       /* We don't have the parser scope here, so figure out the context.
In                                        struct S { static constexpr
int i = 42; };
constexpr auto r = ^^S::i;
int i = [: r :];
we need to pass down 'S'.  */
       tree ctx = DECL_P (t) ? DECL_CONTEXT (t) : NULL_TREE;

diverting us to finish_qualified_id_expr.  Why do we need this?  Do we still
need it after the fixes to avoid repeating the lookup?

Looks like many testcases still need this.  E.g.,

   struct S {
     int m(this S) { return 42; }
   };
   auto p = &[: ^^S::m :];

crashes without the ctx: finish_id_expression_1 gets function_decl m and
goes to finish_class_member_access_expr -> lookup_member where we crash
because we don't have identifier_p.

And because spice_p is (wrongly) false.

   struct S {
     static constexpr int s_x = 11;
   };
   static_assert(&[:^^S::s_x:] == &S::s_x);

crashes without the ctx because we have a VAR_DECL but finish_id_expression_1
in one place expects only a CONST_DECL.

Sounds like it should be improved to handle a VAR_DECL as it already also handles FIELD_DECL and functions.

I've played with this more but then I ran into errors like
   invalid operands of types 'int*' and 'int S::*'
because without the ctx we failed to call build_offset_ref from
finish_qualified_id_expr.

We might need to call build_offset_ref separately for a splice.

So I guess we do need the context :/.

Marek


Reply via email to