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).
> > 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));
/* 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);
+ *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.
> > > > > > + /* 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.
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.
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.
So I guess we do need the context :/.
Marek