diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc
index f0485a95073..930d453dde2 100644
--- a/gcc/cp/coroutines.cc
+++ b/gcc/cp/coroutines.cc
@@ -353,10 +353,20 @@ coroutine_info_hasher::equal (coroutine_info *lhs, const compare_type& rhs)
   return lhs->function_decl == rhs;
 }
 
+/* Initialize the coroutine info table, to hold state per coroutine decl,
+   if not already created.  */
+
+static void
+create_coroutine_info_table ()
+{
+  if (!coroutine_info_table)
+    coroutine_info_table = hash_table<coroutine_info_hasher>::create_ggc (11);
+}
+
 /* Get the existing coroutine_info for FN_DECL, or insert a new one if the
    entry does not yet exist.  */
 
-coroutine_info *
+static coroutine_info *
 get_or_insert_coroutine_info (tree fn_decl)
 {
   gcc_checking_assert (coroutine_info_table != NULL);
@@ -375,7 +385,7 @@ get_or_insert_coroutine_info (tree fn_decl)
 
 /* Get the existing coroutine_info for FN_DECL, fail if it doesn't exist.  */
 
-coroutine_info *
+static coroutine_info *
 get_coroutine_info (tree fn_decl)
 {
   if (coroutine_info_table == NULL)
@@ -757,11 +767,7 @@ ensure_coro_initialized (location_t loc)
       if (!void_coro_handle_address)
 	return false;
 
-      /* A table to hold the state, per coroutine decl.  */
-      gcc_checking_assert (coroutine_info_table == NULL);
-      coroutine_info_table =
-	hash_table<coroutine_info_hasher>::create_ggc (11);
-
+      create_coroutine_info_table ();
       if (coroutine_info_table == NULL)
 	return false;
 
@@ -873,11 +879,13 @@ coro_promise_type_found_p (tree fndecl, location_t loc)
       coro_info->self_h_proxy
 	= build_lang_decl (VAR_DECL, coro_self_handle_id,
 			   coro_info->handle_type);
+      DECL_CONTEXT (coro_info->self_h_proxy) = fndecl;
 
       /* Build a proxy for the promise so that we can perform lookups.  */
       coro_info->promise_proxy
 	= build_lang_decl (VAR_DECL, coro_promise_id,
 			   coro_info->promise_type);
+      DECL_CONTEXT (coro_info->promise_proxy) = fndecl;
 
       /* Note where we first saw a coroutine keyword.  */
       coro_info->first_coro_keyword = loc;
@@ -902,6 +910,17 @@ coro_get_ramp_function (tree decl)
   return NULL_TREE;
 }
 
+/* Given a DECL, an actor or destroyer, build a link from that to the ramp
+   function.  Used by modules streaming.  */
+
+void
+coro_set_ramp_function (tree decl, tree ramp)
+{
+  if (!to_ramp)
+    to_ramp = hash_map<tree, tree>::create_ggc (10);
+  to_ramp->put (decl, ramp);
+}
+
 /* Given the DECL for a ramp function (the user's original declaration) return
    the actor function if it has been defined.  */
 
@@ -926,6 +945,27 @@ coro_get_destroy_function (tree decl)
   return NULL_TREE;
 }
 
+/* For a given ramp function DECL, set the actor and destroy functions.
+   This is only used by modules streaming.  */
+
+void
+coro_set_transform_functions (tree decl, tree actor, tree destroy)
+{
+  /* Only relevant with modules.  */
+  gcc_assert (modules_p ());
+
+  /* This should only be called for newly streamed declarations.  */
+  gcc_assert (!get_coroutine_info (decl));
+
+  /* This might be the first use of coroutine info in the TU, so
+     create the coroutine info table if needed.  */
+  create_coroutine_info_table ();
+
+  coroutine_info *coroutine = get_or_insert_coroutine_info (decl);
+  coroutine->actor_decl = actor;
+  coroutine->destroy_decl = destroy;
+}
+
 /* Given a CO_AWAIT_EXPR AWAIT_EXPR, return its resume call.  */
 
 tree
@@ -4393,15 +4433,19 @@ coro_build_actor_or_destroy_function (tree orig, tree fn_type,
     = build_lang_decl (FUNCTION_DECL, copy_node (DECL_NAME (orig)), fn_type);
 
   /* Allow for locating the ramp (original) function from this one.  */
-  if (!to_ramp)
-    to_ramp = hash_map<tree, tree>::create_ggc (10);
-  to_ramp->put (fn, orig);
+  coro_set_ramp_function (fn, orig);
 
   DECL_CONTEXT (fn) = DECL_CONTEXT (orig);
   DECL_SOURCE_LOCATION (fn) = loc;
   DECL_ARTIFICIAL (fn) = true;
   DECL_INITIAL (fn) = error_mark_node;
 
+  /* Copy linkage from the original function.  */
+  TREE_PUBLIC (fn) = TREE_PUBLIC (orig);
+  DECL_DECLARED_INLINE_P (fn) = DECL_DECLARED_INLINE_P (orig);
+  DECL_NOT_REALLY_EXTERN (fn) = DECL_NOT_REALLY_EXTERN (orig);
+  DECL_INTERFACE_KNOWN (fn) = DECL_INTERFACE_KNOWN (orig);
+
   tree id = get_identifier ("frame_ptr");
   tree fp = build_lang_decl (PARM_DECL, id, coro_frame_ptr);
   DECL_ARTIFICIAL (fp) = true;
diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
index b8470fc256c..1d40b387d6e 100644
--- a/gcc/cp/cp-tree.h
+++ b/gcc/cp/cp-tree.h
@@ -9144,6 +9144,10 @@ extern tree coro_get_ramp_function		(tree);
 
 extern tree co_await_get_resume_call		(tree await_expr);
 
+/* Only for use by modules.  */
+extern void coro_set_transform_functions	(tree, tree, tree);
+extern void coro_set_ramp_function		(tree, tree);
+
 /* Inline bodies.  */
 
 inline tree
diff --git a/gcc/cp/decl2.cc b/gcc/cp/decl2.cc
index e807eab1b8a..d50864b8f75 100644
--- a/gcc/cp/decl2.cc
+++ b/gcc/cp/decl2.cc
@@ -6480,6 +6480,19 @@ mark_used (tree decl, tsubst_flags_t complain /* = tf_warning_or_error */)
       return false;
     }
 
+  /* For coroutines, we need to mark the transform functions as used,
+     if they exist yet.  */
+  if (flag_coroutines
+      && TREE_CODE (decl) == FUNCTION_DECL
+      && DECL_COROUTINE_P (decl)
+      && DECL_RAMP_P (decl))
+    {
+      if (tree actor = DECL_ACTOR_FN (decl))
+	mark_used (actor);
+      if (tree destroy = DECL_DESTROY_FN (decl))
+	mark_used (destroy);
+    }
+
   /* If DECL has a deduced return type, we need to instantiate it now to
      find out its type.  For OpenMP user defined reductions, we need them
      instantiated for reduction clauses which inline them by hand directly.
diff --git a/gcc/cp/module.cc b/gcc/cp/module.cc
index af0730ba974..67fb1e4a22d 100644
--- a/gcc/cp/module.cc
+++ b/gcc/cp/module.cc
@@ -2969,6 +2969,7 @@ static char const *const merge_kind_name[MK_hwm] =
 /* Mergeable entity location data.  */
 struct merge_key {
   cp_ref_qualifier ref_q : 2;
+  unsigned coro_disc : 2;  /* Discriminator for coroutine transforms.  */
   unsigned index;
 
   tree ret;  /* Return type, if appropriate.  */
@@ -2977,7 +2978,7 @@ struct merge_key {
   tree constraints;  /* Constraints.  */
 
   merge_key ()
-    :ref_q (REF_QUAL_NONE), index (0),
+    :ref_q (REF_QUAL_NONE), coro_disc (0), index (0),
      ret (NULL_TREE), args (NULL_TREE),
      constraints (NULL_TREE)
   {
@@ -2999,6 +3000,7 @@ struct post_process_data {
   bool returns_null;
   bool returns_abnormally;
   bool infinite_loop;
+  bool coroutine_component;
 };
 
 /* Tree stream reader.  Note that reading a stream doesn't mark the
@@ -11696,6 +11698,24 @@ trees_in::decl_container ()
   return container;
 }
 
+/* Gets a 2-bit discriminator to distinguish coroutine actor or destroy
+   functions from a normal function.  */
+
+static int
+get_coroutine_discriminator (tree inner)
+{
+  if (tree ramp = DECL_RAMP_FN (inner))
+    {
+      if (DECL_ACTOR_FN (ramp) == inner)
+	return 1;
+      else if (DECL_DESTROY_FN (ramp) == inner)
+	return 2;
+      else
+	gcc_unreachable ();
+    }
+  return 0;
+}
+
 /* Write out key information about a mergeable DEP.  Does not write
    the contents of DEP itself.  The context has already been
    written.  The container has already been streamed.  */
@@ -11787,6 +11807,7 @@ trees_out::key_mergeable (int tag, merge_kind mk, tree decl, tree inner,
 	      tree fn_type = TREE_TYPE (inner);
 
 	      key.ref_q = type_memfn_rqual (fn_type);
+	      key.coro_disc = get_coroutine_discriminator (inner);
 	      key.args = TYPE_ARG_TYPES (fn_type);
 
 	      if (tree reqs = get_constraints (inner))
@@ -11923,7 +11944,12 @@ trees_out::key_mergeable (int tag, merge_kind mk, tree decl, tree inner,
       tree_node (name);
       if (streaming_p ())
 	{
-	  unsigned code = (key.ref_q << 0) | (key.index << 2);
+	  /* Check we have enough bits for the index.  */
+	  gcc_checking_assert (key.index < (1u << (sizeof (unsigned) * 8 - 4)));
+
+	  unsigned code = ((key.ref_q << 0)
+			   | (key.coro_disc << 2)
+			   | (key.index << 4));
 	  u (code);
 	}
 
@@ -11947,8 +11973,8 @@ trees_out::key_mergeable (int tag, merge_kind mk, tree decl, tree inner,
     }
 }
 
-/* DECL is a new declaration that may be duplicated in OVL.  Use RET &
-   ARGS to find its clone, or NULL.  If DECL's DECL_NAME is NULL, this
+/* DECL is a new declaration that may be duplicated in OVL.  Use KEY
+   to find its clone, or NULL.  If DECL's DECL_NAME is NULL, this
    has been found by a proxy.  It will be an enum type located by its
    first member.
 
@@ -12008,6 +12034,9 @@ check_mergeable_decl (merge_kind mk, tree decl, tree ovl, merge_key const &key)
 		&& (!DECL_IS_UNDECLARED_BUILTIN (m_inner)
 		    || !DECL_EXTERN_C_P (m_inner)
 		    || DECL_EXTERN_C_P (d_inner))
+		/* Reject if we're not the same kind of coroutine function.  */
+		&& (!flag_coroutines
+		    || key.coro_disc == get_coroutine_discriminator (m_inner))
 		/* Reject if one is a different member of a
 		   guarded/pre/post fn set.  */
 		&& (!flag_contracts
@@ -12125,7 +12154,8 @@ trees_in::key_mergeable (int tag, merge_kind mk, tree decl, tree inner,
       merge_key key;
       unsigned code = u ();
       key.ref_q = cp_ref_qualifier ((code >> 0) & 3);
-      key.index = code >> 2;
+      key.coro_disc = (code >> 2) & 3;
+      key.index = code >> 4;
 
       if (mk == MK_enum)
 	key.ret = tree_node ();
@@ -13031,6 +13061,8 @@ trees_out::write_function_def (tree decl)
 	  flags |= 8 * f->language->returns_null;
 	  flags |= 16 * f->language->returns_abnormally;
 	  flags |= 32 * f->language->infinite_loop;
+	  /* Set for coroutines.  */
+	  flags |= 64 * f->coroutine_component;
 	}
 
       u (flags);
@@ -13041,6 +13073,17 @@ trees_out::write_function_def (tree decl)
       state->write_location (*this, f->function_start_locus);
       state->write_location (*this, f->function_end_locus);
     }
+
+  if (f && f->coroutine_component)
+    {
+      tree ramp = DECL_RAMP_FN (decl);
+      tree_node (ramp);
+      if (!ramp)
+	{
+	  tree_node (DECL_ACTOR_FN (decl));
+	  tree_node (DECL_DESTROY_FN (decl));
+	}
+    }
 }
 
 void
@@ -13056,13 +13099,13 @@ trees_in::read_function_def (tree decl, tree maybe_template)
   tree initial = tree_node ();
   tree saved = tree_node ();
   tree context = tree_node ();
-  constexpr_fundef cexpr;
   post_process_data pdata {};
   pdata.decl = maybe_template;
 
   tree maybe_dup = odr_duplicate (maybe_template, DECL_SAVED_TREE (decl));
   bool installing = maybe_dup && !DECL_SAVED_TREE (decl);
 
+  constexpr_fundef cexpr;
   if (u ())
     {
       cexpr.parms = chained_decls ();
@@ -13074,7 +13117,6 @@ trees_in::read_function_def (tree decl, tree maybe_template)
     cexpr.decl = NULL_TREE;
 
   unsigned flags = u ();
-
   if (flags & 2)
     {
       pdata.start_locus = state->read_location (*this);
@@ -13083,6 +13125,22 @@ trees_in::read_function_def (tree decl, tree maybe_template)
       pdata.returns_null = flags & 8;
       pdata.returns_abnormally = flags & 16;
       pdata.infinite_loop = flags & 32;
+      pdata.coroutine_component = flags & 64;
+    }
+
+  tree coro_actor = NULL_TREE;
+  tree coro_destroy = NULL_TREE;
+  tree coro_ramp = NULL_TREE;
+  if (pdata.coroutine_component)
+    {
+      coro_ramp = tree_node ();
+      if (!coro_ramp)
+	{
+	  coro_actor = tree_node ();
+	  coro_destroy = tree_node ();
+	  if ((coro_actor == NULL_TREE) != (coro_destroy == NULL_TREE))
+	    set_overrun ();
+	}
     }
 
   if (get_overrun ())
@@ -13100,6 +13158,11 @@ trees_in::read_function_def (tree decl, tree maybe_template)
       if (cexpr.decl)
 	register_constexpr_fundef (cexpr);
 
+      if (coro_ramp)
+	coro_set_ramp_function (decl, coro_ramp);
+      else if (coro_actor && coro_destroy)
+	coro_set_transform_functions (decl, coro_actor, coro_destroy);
+
       if (DECL_LOCAL_DECL_P (decl))
 	/* Block-scope OMP UDRs aren't real functions, and don't need a
 	   function structure to be allocated or to be expanded.  */
@@ -17556,6 +17619,7 @@ module_state::read_cluster (unsigned snum)
       cfun->language->returns_null = pdata.returns_null;
       cfun->language->returns_abnormally = pdata.returns_abnormally;
       cfun->language->infinite_loop = pdata.infinite_loop;
+      cfun->coroutine_component = pdata.coroutine_component;
 
       /* Make sure we emit explicit instantiations.
 	 FIXME do we want to do this in expand_or_defer_fn instead?  */
diff --git a/gcc/testsuite/g++.dg/modules/coro-1_a.C b/gcc/testsuite/g++.dg/modules/coro-1_a.C
new file mode 100644
index 00000000000..ec2c8300a80
--- /dev/null
+++ b/gcc/testsuite/g++.dg/modules/coro-1_a.C
@@ -0,0 +1,28 @@
+// { dg-do compile { target c++20 } }
+// { dg-additional-options "-fmodules" }
+// { dg-module-cmi M }
+
+module;
+#include <coroutine>
+export module M;
+
+struct simple_promise;
+struct simple_coroutine : std::coroutine_handle<simple_promise> {
+  using promise_type = ::simple_promise;
+};
+struct simple_promise {
+  simple_coroutine get_return_object() { return { simple_coroutine::from_promise(*this) }; }
+  std::suspend_always initial_suspend() noexcept { return {}; }
+  std::suspend_always final_suspend() noexcept { return {}; }
+  void return_void() {}
+  void unhandled_exception() {}
+};
+export simple_coroutine coroutine() {
+  co_return;
+}
+export inline simple_coroutine inline_coroutine() {
+  co_return;
+}
+export template <typename T> simple_coroutine template_coroutine() {
+  co_return;
+}
diff --git a/gcc/testsuite/g++.dg/modules/coro-1_b.C b/gcc/testsuite/g++.dg/modules/coro-1_b.C
new file mode 100644
index 00000000000..e1384a7d639
--- /dev/null
+++ b/gcc/testsuite/g++.dg/modules/coro-1_b.C
@@ -0,0 +1,19 @@
+// { dg-module-do run { target c++20 } }
+// { dg-additional-options "-fmodules" }
+
+#include <coroutine>
+import M;
+
+int main() {
+  auto a = coroutine();
+  a.resume();
+  a.destroy();
+
+  auto b = inline_coroutine();
+  b.resume();
+  b.destroy();
+
+  auto c = template_coroutine<int>();
+  c.resume();
+  c.destroy();
+}
