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 <nathanielosh...@gmail.com> > > --- > > 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; >