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