On Wed, Aug 07, 2024 at 04:18:47PM -0400, Jason Merrill wrote:
> On 8/5/24 9:16 AM, Nathaniel Shead wrote:
> > Bootstrapped and regtested (so far just modules.exp) on
> > x86_64-pc-linux-gnu, OK for trunk if full regtest passes?
>
> OK.
>
> > -- >8 --
> >
> > Currently name lookup generally seems to assume that all entities
> > declared within a named module (partition) are attached to said module,
> > which is not true for GM entities (e.g. via extern "C++"), and causes
> > issues with deduplication.
> >
> > This patch fixes the issue by ensuring that module attachment of a
> > declaration is consistently used to handling merging. Handling this
> > exposes some issues with deduplicating temploid friends; to resolve this
> > we always create the BINDING_SLOT_PARTITION slot so that we have
> > somewhere to place attached names (from any module).
> >
> > PR c++/114950
> >
> > gcc/cp/ChangeLog:
> >
> > * module.cc (trees_out::decl_value): Stream bit indicating
> > imported temploid friends early.
> > (trees_in::decl_value): Use this bit with key_mergeable.
> > (trees_in::key_mergeable): Allow merging attached declarations
> > if they're imported temploid friends.
> > (module_state::read_cluster): Check for GM entities that may
> > require merging even when importing from partitions.
> > * name-lookup.cc (enum binding_slots): Adjust comment.
> > (get_fixed_binding_slot): Always create partition slot.
> > (name_lookup::search_namespace_only): Support binding vectors
> > with both partition and GM entities to dedup.
> > (walk_module_binding): Likewise.
> > (name_lookup::adl_namespace_fns): Likewise.
> > (set_module_binding): Likewise.
> > (check_module_override): Use attachment of the decl when
> > checking overrides rather than named_module_p.
> > (lookup_imported_hidden_friend): Use partition slot for finding
> > mergeable template bindings.
> > * name-lookup.h (set_module_binding): Split mod_glob_flag
> > parameter into separate global_p and partition_p params.
> >
> > gcc/testsuite/ChangeLog:
> >
> > * g++.dg/modules/tpl-friend-13_e.C: Adjust error message.
> > * g++.dg/modules/ambig-2_a.C: New test.
> > * g++.dg/modules/ambig-2_b.C: New test.
> > * g++.dg/modules/part-9_a.C: New test.
> > * g++.dg/modules/part-9_b.C: New test.
> > * g++.dg/modules/part-9_c.C: New test.
> > * g++.dg/modules/tpl-friend-15.h: New test.
> > * g++.dg/modules/tpl-friend-15_a.C: New test.
> > * g++.dg/modules/tpl-friend-15_b.C: New test.
> > * g++.dg/modules/tpl-friend-15_c.C: New test.
> >
> > Signed-off-by: Nathaniel Shead <[email protected]>
> > ---
> > gcc/cp/module.cc | 55 ++++++++++------
> > gcc/cp/name-lookup.cc | 65 ++++++++++---------
> > gcc/cp/name-lookup.h | 2 +-
> > gcc/testsuite/g++.dg/modules/ambig-2_a.C | 7 ++
> > gcc/testsuite/g++.dg/modules/ambig-2_b.C | 10 +++
> > gcc/testsuite/g++.dg/modules/part-9_a.C | 10 +++
> > gcc/testsuite/g++.dg/modules/part-9_b.C | 10 +++
> > gcc/testsuite/g++.dg/modules/part-9_c.C | 8 +++
> > .../g++.dg/modules/tpl-friend-13_e.C | 4 +-
> > gcc/testsuite/g++.dg/modules/tpl-friend-15.h | 11 ++++
> > .../g++.dg/modules/tpl-friend-15_a.C | 8 +++
> > .../g++.dg/modules/tpl-friend-15_b.C | 8 +++
> > .../g++.dg/modules/tpl-friend-15_c.C | 7 ++
> > 13 files changed, 150 insertions(+), 55 deletions(-)
> > create mode 100644 gcc/testsuite/g++.dg/modules/ambig-2_a.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/ambig-2_b.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/part-9_a.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/part-9_b.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/part-9_c.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/tpl-friend-15.h
> > create mode 100644 gcc/testsuite/g++.dg/modules/tpl-friend-15_a.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/tpl-friend-15_b.C
> > create mode 100644 gcc/testsuite/g++.dg/modules/tpl-friend-15_c.C
> >
> > diff --git a/gcc/cp/module.cc b/gcc/cp/module.cc
> > index d1607a06757..e6b569ebca5 100644
> > --- a/gcc/cp/module.cc
> > +++ b/gcc/cp/module.cc
> > @@ -2955,7 +2955,8 @@ private:
> > public:
> > tree decl_container ();
> > tree key_mergeable (int tag, merge_kind, tree decl, tree inner, tree
> > type,
> > - tree container, bool is_attached);
> > + tree container, bool is_attached,
> > + bool is_imported_temploid_friend);
> > unsigned binfo_mergeable (tree *);
> > private:
> > @@ -7803,6 +7804,7 @@ trees_out::decl_value (tree decl, depset *dep)
> > || !TYPE_PTRMEMFUNC_P (TREE_TYPE (decl)));
> > merge_kind mk = get_merge_kind (decl, dep);
> > + bool is_imported_temploid_friend = imported_temploid_friends->get (decl);
> > if (CHECKING_P)
> > {
> > @@ -7838,13 +7840,11 @@ trees_out::decl_value (tree decl, depset *dep)
> > && DECL_MODULE_ATTACH_P (not_tmpl))
> > is_attached = true;
> > - /* But don't consider imported temploid friends as attached,
> > - since importers will need to merge this decl even if it was
> > - attached to a different module. */
> > - if (imported_temploid_friends->get (decl))
> > - is_attached = false;
> > -
> > bits.b (is_attached);
> > +
> > + /* Also tell the importer whether this is an imported temploid
> > + friend, which has implications for merging. */
> > + bits.b (is_imported_temploid_friend);
> > }
> > bits.b (dep && dep->has_defn ());
> > }
> > @@ -8021,13 +8021,12 @@ trees_out::decl_value (tree decl, depset *dep)
> > }
> > }
> > - if (TREE_CODE (inner) == FUNCTION_DECL
> > - || TREE_CODE (inner) == TYPE_DECL)
> > + if (is_imported_temploid_friend)
> > {
> > /* Write imported temploid friends so that importers can reconstruct
> > this information on stream-in. */
> > tree* slot = imported_temploid_friends->get (decl);
> > - tree_node (slot ? *slot : NULL_TREE);
> > + tree_node (*slot);
> > }
> > bool is_typedef = false;
> > @@ -8106,6 +8105,7 @@ trees_in::decl_value ()
> > {
> > int tag = 0;
> > bool is_attached = false;
> > + bool is_imported_temploid_friend = false;
> > bool has_defn = false;
> > unsigned mk_u = u ();
> > if (mk_u >= MK_hwm || !merge_kind_name[mk_u])
> > @@ -8126,7 +8126,10 @@ trees_in::decl_value ()
> > {
> > bits_in bits = stream_bits ();
> > if (!(mk & MK_template_mask) && !state->is_header ())
> > - is_attached = bits.b ();
> > + {
> > + is_attached = bits.b ();
> > + is_imported_temploid_friend = bits.b ();
> > + }
> > has_defn = bits.b ();
> > }
> > @@ -8231,7 +8234,7 @@ trees_in::decl_value ()
> > parm_tag = fn_parms_init (inner);
> > tree existing = key_mergeable (tag, mk, decl, inner, type, container,
> > - is_attached);
> > + is_attached, is_imported_temploid_friend);
> > tree existing_inner = existing;
> > if (existing)
> > {
> > @@ -8336,8 +8339,7 @@ trees_in::decl_value ()
> > }
> > }
> > - if (TREE_CODE (inner) == FUNCTION_DECL
> > - || TREE_CODE (inner) == TYPE_DECL)
> > + if (is_imported_temploid_friend)
> > if (tree owner = tree_node ())
> > if (is_new)
> > imported_temploid_friends->put (decl, owner);
> > @@ -11174,7 +11176,8 @@ check_mergeable_decl (merge_kind mk, tree decl,
> > tree ovl, merge_key const &key)
> > tree
> > trees_in::key_mergeable (int tag, merge_kind mk, tree decl, tree inner,
> > - tree type, tree container, bool is_attached)
> > + tree type, tree container, bool is_attached,
> > + bool is_imported_temploid_friend)
> > {
> > const char *kind = "new";
> > tree existing = NULL_TREE;
> > @@ -11316,6 +11319,7 @@ trees_in::key_mergeable (int tag, merge_kind mk,
> > tree decl, tree inner,
> > case NAMESPACE_DECL:
> > if (is_attached
> > + && !is_imported_temploid_friend
>
> How can a namespace be an imported temploid friend?
>
Cut off by context, but this is
switch (TREE_CODE (container))
{
default:
gcc_unreachable ();
case NAMESPACE_DECL:
if (is_attached
&& !is_imported_temploid_friend
&& !(state->is_module () || state->is_partition ()))
kind = "unique";
i.e. the NAMESPACE_DECL is referring to the container that the decl is
attached to for merging purposes.
> > && !(state->is_module () || state->is_partition ()))
> > kind = "unique";
> > else
> > @@ -11347,7 +11351,9 @@ trees_in::key_mergeable (int tag, merge_kind mk,
> > tree decl, tree inner,
> > break;
> > case TYPE_DECL:
> > - if (is_attached && !(state->is_module () || state->is_partition ())
> > + if (is_attached
> > + && !is_imported_temploid_friend
This is the one that may perhaps be unnecessary (on thinking over this
again I would expect any class-scope friends to not be redeclared
outside of their named module, even for imported templates?), so I'll
actually re-test this patch without this hunk.
> > + && !(state->is_module () || state->is_partition ())
> > /* Implicit member functions can come from
> > anywhere. */
> > && !(DECL_ARTIFICIAL (decl)
> > @@ -15291,6 +15297,7 @@ module_state::read_cluster (unsigned snum)
> > tree visible = NULL_TREE;
> > tree type = NULL_TREE;
> > bool dedup = false;
> > + bool global_p = false;
> > /* We rely on the bindings being in the reverse order of
> > the resulting overload set. */
> > @@ -15308,6 +15315,16 @@ module_state::read_cluster (unsigned snum)
> > if (sec.get_overrun ())
> > break;
> > + if (!global_p)
> > + {
> > + /* Check if the decl could require GM merging. */
> > + tree orig = get_originating_module_decl (decl);
> > + tree inner = STRIP_TEMPLATE (orig);
> > + if (!DECL_LANG_SPECIFIC (inner)
> > + || !DECL_MODULE_ATTACH_P (inner))
> > + global_p = true;
> > + }
> > +
> > if (decls && TREE_CODE (decl) == TYPE_DECL)
> > {
> > /* Stat hack. */
> > @@ -15394,10 +15411,8 @@ module_state::read_cluster (unsigned snum)
> > break; /* Bail. */
> > dump () && dump ("Binding of %P", ns, name);
> > - if (!set_module_binding (ns, name, mod,
> > - is_header () ? -1
> > - : is_module () || is_partition () ? 1
> > - : 0,
> > + if (!set_module_binding (ns, name, mod, global_p,
> > + is_module () || is_partition (),
> > decls, type, visible))
> > sec.set_overrun ();
> > }
> > diff --git a/gcc/cp/name-lookup.cc b/gcc/cp/name-lookup.cc
> > index 8823ab71c60..872f1af0b2e 100644
> > --- a/gcc/cp/name-lookup.cc
> > +++ b/gcc/cp/name-lookup.cc
> > @@ -51,8 +51,8 @@ enum binding_slots
> > {
> > BINDING_SLOT_CURRENT, /* Slot for current TU. */
> > BINDING_SLOT_GLOBAL, /* Slot for merged global module. */
> > - BINDING_SLOT_PARTITION, /* Slot for merged partition entities
> > - (optional). */
> > + BINDING_SLOT_PARTITION, /* Slot for merged partition entities or
> > + imported friends. */
> > /* Number of always-allocated slots. */
> > BINDING_SLOTS_FIXED = BINDING_SLOT_GLOBAL + 1
> > @@ -248,9 +248,10 @@ get_fixed_binding_slot (tree *slot, tree name,
> > unsigned ix, int create)
> > if (!create)
> > return NULL;
> > - /* The partition slot is only needed when we're a named
> > - module. */
> > - bool partition_slot = named_module_p ();
> > + /* The partition slot is always needed, in case we have imported
> > + temploid friends with attachment different from the module we
> > + imported them from. */
> > + bool partition_slot = true;
> > unsigned want = ((BINDING_SLOTS_FIXED + partition_slot + (create <
> > 0)
> > + BINDING_VECTOR_SLOTS_PER_CLUSTER - 1)
> > / BINDING_VECTOR_SLOTS_PER_CLUSTER);
> > @@ -937,7 +938,6 @@ name_lookup::search_namespace_only (tree scope)
> > stat_hack, then everything was exported. */
> > tree type = NULL_TREE;
> > -
> > /* If STAT_HACK_P is false, everything is visible, and
> > there's no duplication possibilities. */
> > if (STAT_HACK_P (bind))
> > @@ -947,9 +947,9 @@ name_lookup::search_namespace_only (tree scope)
> > /* Do we need to engage deduplication? */
> > int dup = 0;
> > if (MODULE_BINDING_GLOBAL_P (bind))
> > - dup = 1;
> > - else if (MODULE_BINDING_PARTITION_P (bind))
> > - dup = 2;
> > + dup |= 1;
> > + if (MODULE_BINDING_PARTITION_P (bind))
> > + dup |= 2;
> > if (unsigned hit = dup_detect & dup)
> > {
> > if ((hit & 1 && BINDING_VECTOR_GLOBAL_DUPS_P (val))
> > @@ -1275,9 +1275,9 @@ name_lookup::adl_namespace_fns (tree scope, bitmap
> > imports)
> > /* Do we need to engage deduplication? */
> > int dup = 0;
> > if (MODULE_BINDING_GLOBAL_P (bind))
> > - dup = 1;
> > - else if (MODULE_BINDING_PARTITION_P (bind))
> > - dup = 2;
> > + dup |= 1;
> > + if (MODULE_BINDING_PARTITION_P (bind))
> > + dup |= 2;
> > if (unsigned hit = dup_detect & dup)
> > if ((hit & 1 && BINDING_VECTOR_GLOBAL_DUPS_P (val))
> > || (hit & 2
> > @@ -3758,6 +3758,9 @@ check_module_override (tree decl, tree mvec, bool
> > hiding,
> > binding_cluster *cluster = BINDING_VECTOR_CLUSTER_BASE (mvec);
> > unsigned ix = BINDING_VECTOR_NUM_CLUSTERS (mvec);
> > + tree nontmpl = STRIP_TEMPLATE (decl);
> > + bool attached = DECL_LANG_SPECIFIC (nontmpl) && DECL_MODULE_ATTACH_P
> > (nontmpl);
> > +
> > if (BINDING_VECTOR_SLOTS_PER_CLUSTER == BINDING_SLOTS_FIXED)
> > {
> > cluster++;
> > @@ -3819,7 +3822,7 @@ check_module_override (tree decl, tree mvec, bool
> > hiding,
> > {
> > /* Look in the appropriate mergeable decl slot. */
> > tree mergeable = NULL_TREE;
> > - if (named_module_p ())
> > + if (attached)
> > mergeable = BINDING_VECTOR_CLUSTER (mvec, BINDING_SLOT_PARTITION
> > / BINDING_VECTOR_SLOTS_PER_CLUSTER)
> > .slots[BINDING_SLOT_PARTITION % BINDING_VECTOR_SLOTS_PER_CLUSTER];
> > @@ -3839,15 +3842,13 @@ check_module_override (tree decl, tree mvec, bool
> > hiding,
> > matched:
> > if (match != error_mark_node)
> > {
> > - if (named_module_p ())
> > + if (attached)
> > BINDING_VECTOR_PARTITION_DUPS_P (mvec) = true;
> > else
> > BINDING_VECTOR_GLOBAL_DUPS_P (mvec) = true;
> > }
> > return match;
> > -
> > -
> > }
> > /* Record DECL as belonging to the current lexical scope. Check for
> > @@ -4300,7 +4301,9 @@ walk_module_binding (tree binding, bitmap partitions,
> > cluster++;
> > }
> > - bool maybe_dups = BINDING_VECTOR_PARTITION_DUPS_P (binding);
> > + /* There could be duplicate module or GMF entries. */
> > + bool maybe_dups = (BINDING_VECTOR_PARTITION_DUPS_P (binding)
> > + || BINDING_VECTOR_GLOBAL_DUPS_P (binding));
> > for (; ix--; cluster++)
> > for (unsigned jx = 0; jx != BINDING_VECTOR_SLOTS_PER_CLUSTER; jx++)
> > @@ -4394,14 +4397,14 @@ import_module_binding (tree ns, tree name,
> > unsigned mod, unsigned snum)
> > }
> > /* An import of MODULE is binding NS::NAME. There should be no
> > - existing binding for >= MODULE. MOD_GLOB indicates whether MODULE
> > - is a header_unit (-1) or part of the current module (+1). VALUE
> > - and TYPE are the value and type bindings. VISIBLE are the value
> > - bindings being exported. */
> > + existing binding for >= MODULE. GLOBAL_P indicates whether the
> > + bindings include global module entities. PARTITION_P is true if
> > + it is part of the current module. VALUE and TYPE are the value
> > + and type bindings. VISIBLE are the value bindings being exported. */
> > bool
> > -set_module_binding (tree ns, tree name, unsigned mod, int mod_glob,
> > - tree value, tree type, tree visible)
> > +set_module_binding (tree ns, tree name, unsigned mod, bool global_p,
> > + bool partition_p, tree value, tree type, tree visible)
> > {
> > if (!value)
> > /* Bogus BMIs could give rise to nothing to bind. */
> > @@ -4419,19 +4422,19 @@ set_module_binding (tree ns, tree name, unsigned
> > mod, int mod_glob,
> > return false;
> > tree bind = value;
> > - if (type || visible != bind || mod_glob)
> > + if (type || visible != bind || partition_p || global_p)
> > {
> > bind = stat_hack (bind, type);
> > STAT_VISIBLE (bind) = visible;
> > - if ((mod_glob > 0 && TREE_PUBLIC (ns))
> > + if ((partition_p && TREE_PUBLIC (ns))
> > || (type && DECL_MODULE_EXPORT_P (type)))
> > STAT_TYPE_VISIBLE_P (bind) = true;
> > }
> > - /* Note if this is this-module or global binding. */
> > - if (mod_glob > 0)
> > + /* Note if this is this-module and/or global binding. */
> > + if (partition_p)
> > MODULE_BINDING_PARTITION_P (bind) = true;
> > - else if (mod_glob < 0)
> > + if (global_p)
> > MODULE_BINDING_GLOBAL_P (bind) = true;
> > *mslot = bind;
> > @@ -4540,10 +4543,8 @@ lookup_imported_hidden_friend (tree friend_tmpl)
> > || !DECL_MODULE_IMPORT_P (inner))
> > return NULL_TREE;
> > - /* Imported temploid friends are not considered as attached to this
> > - module for merging purposes. */
> > - tree bind = get_mergeable_namespace_binding (current_namespace,
> > - DECL_NAME (inner), false);
> > + tree bind = get_mergeable_namespace_binding
> > + (current_namespace, DECL_NAME (inner), DECL_MODULE_ATTACH_P (inner));
> > if (!bind)
> > return NULL_TREE;
> > diff --git a/gcc/cp/name-lookup.h b/gcc/cp/name-lookup.h
> > index 5cf6ae6374a..7c4193444dd 100644
> > --- a/gcc/cp/name-lookup.h
> > +++ b/gcc/cp/name-lookup.h
> > @@ -484,7 +484,7 @@ extern tree lookup_class_binding (tree ctx, tree name);
> > extern bool import_module_binding (tree ctx, tree name, unsigned mod,
> > unsigned snum);
> > extern bool set_module_binding (tree ctx, tree name, unsigned mod,
> > - int mod_glob_flag,
> > + bool global_p, bool partition_p,
> > tree value, tree type, tree visible);
> > extern void add_module_namespace_decl (tree ns, tree decl);
> > diff --git a/gcc/testsuite/g++.dg/modules/ambig-2_a.C
> > b/gcc/testsuite/g++.dg/modules/ambig-2_a.C
> > new file mode 100644
> > index 00000000000..d5dcc93584a
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/ambig-2_a.C
> > @@ -0,0 +1,7 @@
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi A }
> > +
> > +export module A;
> > +
> > +extern "C++" int foo ();
> > +extern "C++" char bar ();
> > diff --git a/gcc/testsuite/g++.dg/modules/ambig-2_b.C
> > b/gcc/testsuite/g++.dg/modules/ambig-2_b.C
> > new file mode 100644
> > index 00000000000..b94416aabbf
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/ambig-2_b.C
> > @@ -0,0 +1,10 @@
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi !B }
> > +
> > +export module B;
> > +import A;
> > +
> > +extern "C++" int foo ();
> > +extern "C++" int bar (); // { dg-error "ambiguating new declaration" }
> > +
> > +// { dg-prune-output "not writing module" }
> > diff --git a/gcc/testsuite/g++.dg/modules/part-9_a.C
> > b/gcc/testsuite/g++.dg/modules/part-9_a.C
> > new file mode 100644
> > index 00000000000..dc033d301ee
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/part-9_a.C
> > @@ -0,0 +1,10 @@
> > +// PR c++/114950
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi M:a }
> > +
> > +module M:a;
> > +
> > +struct A {};
> > +extern "C++" struct B {};
> > +void f(int) {}
> > +extern "C++" void f(double) {}
> > diff --git a/gcc/testsuite/g++.dg/modules/part-9_b.C
> > b/gcc/testsuite/g++.dg/modules/part-9_b.C
> > new file mode 100644
> > index 00000000000..7339da22d97
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/part-9_b.C
> > @@ -0,0 +1,10 @@
> > +// PR c++/114950
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi M:b }
> > +
> > +module M:b;
> > +
> > +struct A {};
> > +extern "C++" struct B {};
> > +void f(int) {}
> > +extern "C++" void f(double) {}
> > diff --git a/gcc/testsuite/g++.dg/modules/part-9_c.C
> > b/gcc/testsuite/g++.dg/modules/part-9_c.C
> > new file mode 100644
> > index 00000000000..26ac6777b7a
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/part-9_c.C
> > @@ -0,0 +1,8 @@
> > +// PR c++/114950
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi M }
> > +// Handle merging definitions of extern "C++" decls across partitions
> > +
> > +export module M;
> > +import :a;
> > +import :b;
> > diff --git a/gcc/testsuite/g++.dg/modules/tpl-friend-13_e.C
> > b/gcc/testsuite/g++.dg/modules/tpl-friend-13_e.C
> > index afbd0a39c23..b32fd98b756 100644
> > --- a/gcc/testsuite/g++.dg/modules/tpl-friend-13_e.C
> > +++ b/gcc/testsuite/g++.dg/modules/tpl-friend-13_e.C
> > @@ -1,8 +1,8 @@
> > // { dg-additional-options "-fmodules-ts" }
> > // 'import X' does not correctly notice that S has already been declared.
> > -struct S {}; // { dg-message "previously declared" "" { xfail *-*-* } }
> > -template <typename> struct T {}; // { dg-message "previously declared" }
> > +struct S {}; // { dg-message "previous declaration" "" { xfail *-*-* } }
> > +template <typename> struct T {}; // { dg-message "previous declaration" }
> > void f() {} // { dg-message "previously declared" }
> > template <typename T> void g() {} // { dg-message "previously declared" }
> > diff --git a/gcc/testsuite/g++.dg/modules/tpl-friend-15.h
> > b/gcc/testsuite/g++.dg/modules/tpl-friend-15.h
> > new file mode 100644
> > index 00000000000..e4d3fff4445
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/tpl-friend-15.h
> > @@ -0,0 +1,11 @@
> > +// PR c++/114950
> > +
> > +template <typename T>
> > +struct A {
> > + friend void x();
> > +};
> > +template <typename T>
> > +struct B {
> > + virtual void f() { A<T> r; }
> > +};
> > +template struct B<int>;
> > diff --git a/gcc/testsuite/g++.dg/modules/tpl-friend-15_a.C
> > b/gcc/testsuite/g++.dg/modules/tpl-friend-15_a.C
> > new file mode 100644
> > index 00000000000..04c800875f4
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/tpl-friend-15_a.C
> > @@ -0,0 +1,8 @@
> > +// PR c++/114950
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi M:a }
> > +
> > +module M:a;
> > +extern "C++" {
> > + #include "tpl-friend-15.h"
> > +}
> > diff --git a/gcc/testsuite/g++.dg/modules/tpl-friend-15_b.C
> > b/gcc/testsuite/g++.dg/modules/tpl-friend-15_b.C
> > new file mode 100644
> > index 00000000000..781882f97bc
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/tpl-friend-15_b.C
> > @@ -0,0 +1,8 @@
> > +// PR c++/114950
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi M:b }
> > +
> > +module M:b;
> > +extern "C++" {
> > + #include "tpl-friend-15.h"
> > +}
> > diff --git a/gcc/testsuite/g++.dg/modules/tpl-friend-15_c.C
> > b/gcc/testsuite/g++.dg/modules/tpl-friend-15_c.C
> > new file mode 100644
> > index 00000000000..ced7e87d993
> > --- /dev/null
> > +++ b/gcc/testsuite/g++.dg/modules/tpl-friend-15_c.C
> > @@ -0,0 +1,7 @@
> > +// PR c++/114950
> > +// { dg-additional-options "-fmodules-ts" }
> > +// { dg-module-cmi M }
> > +
> > +export module M;
> > +import :a;
> > +import :b;
>