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:

[snipping to just active comments]

+/* Helper macro to set SPLICE_EXPR_EXPRESSION_P.  */
+#define SET_SPLICE_EXPR_EXPRESSION_P(NODE) \
+  (SPLICE_EXPR_EXPRESSION_P (TREE_CODE (NODE) == SPLICE_EXPR \
+                            ? NODE : TREE_OPERAND (NODE, 0)) = true)

What are these helpers for?  What is the node that we expect to have a
splice in op0?

These are to handle dependent_splice_p trees: either [:T:] or [:T:]<arg>,
and I wanted one macro for both.  The second one is a TEMPLATE_ID_EXPR.

OK, please mention dependent_splice_p in the comment.

@@ -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?

Incidentally, why do we need to initialize std_meta_node here rather than in init_reflection?

@@ -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?

+           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.

+  auto s = make_temp_override (cp_preserve_using_decl, true);

This flag seems kind of a kludge; since we only need it to apply to a single
lookup, I wonder how difficult it would be to use a LOOK_want flag instead?

I think not easy/possible, because strip_using_decls is called a lot
in different contexts, I don't think it can always access the lookup flags (?).

But it seems to me we only want to keep the USING_DECL for the single cp_parser_lookup_name in cp_parser_reflection_name, not for any other name lookup.

This can be addressed in a followup.

+       /* 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?

+cp_parser_splice_expression (cp_parser *parser, bool template_p,
+                            bool address_p, bool template_arg_p,
+                            bool member_access_p, cp_id_kind *idk)
+{
+  bool targs_p = false;
+
+  /* [class.access.base]/5: A member m is accessible at the point R when
+     designated in class N if
+     -- m is designated by a splice-expression  */
+  if (member_access_p)
+    push_deferring_access_checks (dk_no_check);
+
+  cp_expr expr = cp_parser_splice_specifier (parser, template_p, &targs_p);
+
+  if (member_access_p)
+    pop_deferring_access_checks ();

I don't understand messing with access around parsing the splice; where is
the access check that would be affected?

This is for

  struct S {
    int k;
  };
  class D : S {} d;
  int i = d.[:^^S::k:];
Without the push/pop we'd error in cp_parser_splice_specifier -> ... ->
cp_parser_reflect_expression -> cp_parser_parse_definitely ->
pop_to_parent_deferring_access_checks -> perform_access_checks ->
enforce_access and say:

  error: 'struct S S::S is inaccessible within this context
  error: 'int S::k' is inaccessible within this context

This suggests that we're doing the lookup in the context of "d.", which is wrong. That is, we should reject

struct A { static int x; };
int q = A().[:^^x:];  // error, no 'x' in scope

but the branch currently accepts this. Under https://eel.is/c++draft/basic.lookup.qual#general-2 'x' is not a member-qualified name.

+    {
+      /* 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.

+  /* ??? It'd be nice to use saved_token_sentinel, but its rollback
+     uses cp_lexer_previous_token, but we may be the first token in the
+     file so there are no previous tokens.  Sigh.  */
+  cp_lexer_save_tokens (parser->lexer);

That sounds pretty simple to fix?

Maybe.  saved_token_sentinel::rollback would have to fix

     cp_lexer_set_source_position_from_token
       (cp_lexer_previous_token (lexer));

when there are no previous tokens, maybe skip the _set_source_position_
call.

Sounds right. But this isn't necessary if you don't feel like pursuing it now.

+          [:^^B::fn:]()  // do not disable virtual dispatch
+          [:^^B:]::fn()  // disable virtual dispatch
+
+        so we check SPLICE_P.  */
+      if (parser->scope && !splice_p)
        *idk = CP_ID_KIND_QUALIFIED;

Hmm, it seems wrong for parser->scope to still be set in the former case.

So this is tested in reflect/member9.C.  I thought that d.[:^^B::fn:]()
should behave just like d.B::fn() which also leaves parser->scope set
to B.

I don't think it should, as above the splice is a separate lookup context.

I agree with the comments and the testcase, but we should be clearing parser->scope after the splice so we don't need any change here.

Jason

Reply via email to