public inbox for gcc-patches@gcc.gnu.org
 help / color / mirror / Atom feed
* [PATCH] [og12] OpenMP: Constructors and destructors for "declare target" static aggregates
@ 2023-03-27 18:54 Julian Brown
  2023-04-02  9:38 ` Thomas Schwinge
  0 siblings, 1 reply; 3+ messages in thread
From: Julian Brown @ 2023-03-27 18:54 UTC (permalink / raw)
  To: gcc-patches

This patch adds support for running constructors and destructors for
static (file-scope) aggregates for C++ objects which are marked with
"declare target" directives on OpenMP offload targets.

At present, space is allocated on the target for such aggregates, but
nothing ever constructs them properly, so they end up zero-initialised.

Tested with offloading to AMD GCN. I will apply to the og12 branch
shortly.

ChangeLog

2023-03-27  Julian Brown  <julian@codesourcery.com>

gcc/cp/
	* decl2.cc (priority_info): Add omp_tgt_initializations_p and
	omp_tgt_destructions_p.
	(start_objects, start_static_storage_duration_function,
	do_static_initialization_or_destruction,
	one_static_initialization_or_destruction,
	generate_ctor_or_dtor_function): Add 'omp_target' parameter.  Support
	"declare target" decls. Update forward declarations.
	(OMP_SSDF_IDENTIFIER): New macro.
	(omp_tgt_ssdf_decls): New vec.
	(get_priority_info): Initialize omp_tgt_initializations_p and
	omp_tgt_destructions_p fields.
	(handle_tls_init): Update call to
	omp_static_initialization_or_destruction.
	(c_parse_final_cleanups): Support constructors/destructors on OpenMP
	offload targets.

gcc/
	* omp-builtins.def (BUILT_IN_OMP_IS_INITIAL_DEVICE): New builtin.
	* tree.cc (get_file_function_name): Support names for on-target
	constructor/destructor functions.

libgomp/
	* testsuite/libgomp.c++/static-aggr-constructor-destructor-1.C: New
	test.
	* testsuite/libgomp.c++/static-aggr-constructor-destructor-2.C: New
	test.
---
 gcc/cp/decl2.cc                               | 225 +++++++++++++++---
 gcc/omp-builtins.def                          |   2 +
 gcc/tree.cc                                   |   6 +-
 .../static-aggr-constructor-destructor-1.C    |  28 +++
 .../static-aggr-constructor-destructor-2.C    |  31 +++
 5 files changed, 257 insertions(+), 35 deletions(-)
 create mode 100644 libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-1.C
 create mode 100644 libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-2.C

diff --git a/gcc/cp/decl2.cc b/gcc/cp/decl2.cc
index f1a6df375e8..042ae4df700 100644
--- a/gcc/cp/decl2.cc
+++ b/gcc/cp/decl2.cc
@@ -65,16 +65,19 @@ typedef struct priority_info_s {
   /* Nonzero if there have been any destructions at this priority
      throughout the translation unit.  */
   int destructions_p;
+  /* Again, but specifically for OpenMP "declare target" initializations.  */
+  int omp_tgt_initializations_p;
+  int omp_tgt_destructions_p;
 } *priority_info;
 
-static tree start_objects (int, int);
+static tree start_objects (int, int, bool);
 static void finish_objects (int, int, tree);
-static tree start_static_storage_duration_function (unsigned);
+static tree start_static_storage_duration_function (unsigned, bool);
 static void finish_static_storage_duration_function (tree);
 static priority_info get_priority_info (int);
-static void do_static_initialization_or_destruction (tree, bool);
-static void one_static_initialization_or_destruction (tree, tree, bool);
-static void generate_ctor_or_dtor_function (bool, int, location_t *);
+static void do_static_initialization_or_destruction (tree, bool, bool);
+static void one_static_initialization_or_destruction (tree, tree, bool, bool);
+static void generate_ctor_or_dtor_function (bool, int, location_t *, bool);
 static int generate_ctor_and_dtor_functions_for_priority (splay_tree_node,
 							  void *);
 static tree prune_vars_needing_no_initialization (tree *);
@@ -3791,7 +3794,7 @@ generate_tls_wrapper (tree fn)
    vtv_start_verification_constructor_init_function.  */
 
 static tree
-start_objects (int method_type, int initp)
+start_objects (int method_type, int initp, bool omp_target = false)
 {
   /* Make ctor or dtor function.  METHOD_TYPE may be 'I' or 'D'.  */
   int module_init = 0;
@@ -3806,7 +3809,16 @@ start_objects (int method_type, int initp)
     {
       char type[14];
 
-      unsigned len = sprintf (type, "sub_%c", method_type);
+      unsigned len;
+      if (omp_target)
+	/* Use "off_" signifying "offload" here.  The name must be distinct
+	   from the non-offload case.  The format of the name is scanned in
+	   tree.cc/get_file_function_name, so stick to the same length for
+	   both name variants.  */
+	len = sprintf (type, "off_%c", method_type);
+      else
+	len = sprintf (type, "sub_%c", method_type);
+
       if (initp != DEFAULT_INIT_PRIORITY)
 	{
 	  char joiner = '_';
@@ -3821,6 +3833,17 @@ start_objects (int method_type, int initp)
 
   tree fntype =	build_function_type (void_type_node, void_list_node);
   tree fndecl = build_lang_decl (FUNCTION_DECL, name, fntype);
+
+  if (omp_target)
+    {
+      DECL_ATTRIBUTES (fndecl)
+	= tree_cons (get_identifier ("omp declare target"), NULL_TREE,
+		     DECL_ATTRIBUTES (fndecl));
+      DECL_ATTRIBUTES (fndecl)
+	= tree_cons (get_identifier ("omp declare target nohost"), NULL_TREE,
+		     DECL_ATTRIBUTES (fndecl));
+    }
+
   DECL_CONTEXT (fndecl) = FROB_CONTEXT (global_namespace);
   if (module_init > 0)
     {
@@ -3911,6 +3934,7 @@ finish_objects (int method_type, int initp, tree body)
 /* The name of the function we create to handle initializations and
    destructions for objects with static storage duration.  */
 #define SSDF_IDENTIFIER "__static_initialization_and_destruction"
+#define OMP_SSDF_IDENTIFIER "__omp_target_static_init_and_destruction"
 
 /* The declaration for the __INITIALIZE_P argument.  */
 static GTY(()) tree initialize_p_decl;
@@ -3925,6 +3949,9 @@ static GTY(()) tree ssdf_decl;
    translation unit.  */
 static GTY(()) vec<tree, va_gc> *ssdf_decls;
 
+/* Same, but specifically for offloaded OpenMP "declare target" functions.  */
+static GTY(()) vec<tree, va_gc> *omp_tgt_ssdf_decls;
+
 /* A map from priority levels to information about that priority
    level.  There may be many such levels, so efficient lookup is
    important.  */
@@ -3943,24 +3970,37 @@ static splay_tree priority_info_map;
    translation unit.  */
 
 static tree
-start_static_storage_duration_function (unsigned count)
+start_static_storage_duration_function (unsigned count, bool omp_target)
 {
   tree type;
   tree body;
-  char id[sizeof (SSDF_IDENTIFIER) + 1 /* '\0' */ + 32];
+  tree name;
 
-  /* Create the identifier for this function.  It will be of the form
-     SSDF_IDENTIFIER_<number>.  */
-  sprintf (id, "%s_%u", SSDF_IDENTIFIER, count);
+  if (omp_target)
+    {
+      char id[sizeof (OMP_SSDF_IDENTIFIER) + 1 /* '\0' */ + 32];
+
+      /* Create the identifier for this function.  It will be of the form
+	 SSDF_IDENTIFIER_<number>.  */
+      sprintf (id, "%s_%u", OMP_SSDF_IDENTIFIER, count);
+      name = get_identifier (id);
+    }
+  else
+    {
+      char id[sizeof (SSDF_IDENTIFIER) + 1 /* '\0' */ + 32];
+
+      /* Create the identifier for this function.  It will be of the form
+	 SSDF_IDENTIFIER_<number>.  */
+      sprintf (id, "%s_%u", SSDF_IDENTIFIER, count);
+      name = get_identifier (id);
+    }
 
   type = build_function_type_list (void_type_node,
 				   integer_type_node, integer_type_node,
 				   NULL_TREE);
 
   /* Create the FUNCTION_DECL itself.  */
-  ssdf_decl = build_lang_decl (FUNCTION_DECL,
-			       get_identifier (id),
-			       type);
+  ssdf_decl = build_lang_decl (FUNCTION_DECL, name, type);
   TREE_PUBLIC (ssdf_decl) = 0;
   DECL_ARTIFICIAL (ssdf_decl) = 1;
 
@@ -3984,7 +4024,14 @@ start_static_storage_duration_function (unsigned count)
       get_priority_info (DEFAULT_INIT_PRIORITY);
     }
 
-  vec_safe_push (ssdf_decls, ssdf_decl);
+  if (omp_target && !omp_tgt_ssdf_decls)
+    /* Static constructors and destructors for "declare target" variables.  */
+    vec_alloc (omp_tgt_ssdf_decls, 32);
+
+  if (omp_target)
+    vec_safe_push (omp_tgt_ssdf_decls, ssdf_decl);
+  else
+    vec_safe_push (ssdf_decls, ssdf_decl);
 
   /* Create the argument list.  */
   initialize_p_decl = cp_build_parm_decl
@@ -3997,6 +4044,16 @@ start_static_storage_duration_function (unsigned count)
   DECL_CHAIN (initialize_p_decl) = priority_decl;
   DECL_ARGUMENTS (ssdf_decl) = initialize_p_decl;
 
+  if (omp_target)
+    {
+      DECL_ATTRIBUTES (ssdf_decl)
+	= tree_cons (get_identifier ("omp declare target"), NULL_TREE,
+		     DECL_ATTRIBUTES (ssdf_decl));
+      DECL_ATTRIBUTES (ssdf_decl)
+	= tree_cons (get_identifier ("omp declare target nohost"), NULL_TREE,
+		     DECL_ATTRIBUTES (ssdf_decl));
+    }
+
   /* Put the function in the global scope.  */
   pushdecl (ssdf_decl);
 
@@ -4048,6 +4105,8 @@ get_priority_info (int priority)
       pi = XNEW (struct priority_info_s);
       pi->initializations_p = 0;
       pi->destructions_p = 0;
+      pi->omp_tgt_initializations_p = 0;
+      pi->omp_tgt_destructions_p = 0;
       splay_tree_insert (priority_info_map,
 			 (splay_tree_key) priority,
 			 (splay_tree_value) pi);
@@ -4108,7 +4167,8 @@ fix_temporary_vars_context_r (tree *node,
    are destroying it.  */
 
 static void
-one_static_initialization_or_destruction (tree decl, tree init, bool initp)
+one_static_initialization_or_destruction (tree decl, tree init, bool initp,
+					  bool omp_target)
 {
   tree guard_if_stmt = NULL_TREE;
   tree guard;
@@ -4255,7 +4315,7 @@ one_static_initialization_or_destruction (tree decl, tree init, bool initp)
    Whether initialization or destruction is performed is specified by INITP.  */
 
 static void
-do_static_initialization_or_destruction (tree vars, bool initp)
+do_static_initialization_or_destruction (tree vars, bool initp, bool omp_target)
 {
   tree node, init_if_stmt, cond;
 
@@ -4298,10 +4358,14 @@ do_static_initialization_or_destruction (tree vars, bool initp)
        priority.  */
     priority = DECL_EFFECTIVE_INIT_PRIORITY (decl);
     pi = get_priority_info (priority);
-    if (initp)
+    if (initp && !omp_target)
       pi->initializations_p = 1;
-    else
+    else if (!omp_target)
       pi->destructions_p = 1;
+    else if (initp && omp_target)
+      pi->omp_tgt_initializations_p = 1;
+    else
+      pi->omp_tgt_destructions_p = 1;
 
     /* Conditionalize this initialization on being in the right priority
        and being initializing/finalizing appropriately.  */
@@ -4317,9 +4381,17 @@ do_static_initialization_or_destruction (tree vars, bool initp)
     for (; node
 	   && DECL_EFFECTIVE_INIT_PRIORITY (TREE_VALUE (node)) == priority;
 	 node = TREE_CHAIN (node))
-      /* Do one initialization or destruction.  */
-      one_static_initialization_or_destruction (TREE_VALUE (node),
-						TREE_PURPOSE (node), initp);
+      {
+	tree decl = TREE_VALUE (node);
+	tree init = TREE_PURPOSE (node);
+	/* We will emit 'init' twice, and it is modified in-place during
+	   gimplification.  Make a copy here.  */
+	if (omp_target)
+	  init = copy_node (init);
+	/* Do one initialization or destruction.  */
+	one_static_initialization_or_destruction (decl, init, initp,
+						  omp_target);
+      }
 
     /* Finish up the priority if-stmt body.  */
     finish_then_clause (priority_if_stmt);
@@ -4419,7 +4491,7 @@ write_out_vars (tree vars)
 
 static void
 generate_ctor_or_dtor_function (bool constructor_p, int priority,
-				location_t *locus)
+				location_t *locus, bool omp_target)
 {
   input_location = *locus;
 
@@ -4451,13 +4523,14 @@ generate_ctor_or_dtor_function (bool constructor_p, int priority,
      arguments.  */
   tree fndecl;
   size_t i;
-  FOR_EACH_VEC_SAFE_ELT (ssdf_decls, i, fndecl)
+  vec<tree, va_gc> *walk_decls = omp_target ? omp_tgt_ssdf_decls : ssdf_decls;
+  FOR_EACH_VEC_SAFE_ELT (walk_decls, i, fndecl)
     {
       /* Calls to pure or const functions will expand to nothing.  */
       if (! (flags_from_decl_or_type (fndecl) & (ECF_CONST | ECF_PURE)))
 	{
 	  if (! body)
-	    body = start_objects (function_key, priority);
+	    body = start_objects (function_key, priority, omp_target);
 
 	  tree call = cp_build_function_call_nary (fndecl, tf_warning_or_error,
 						   build_int_cst (NULL_TREE,
@@ -4487,9 +4560,17 @@ generate_ctor_and_dtor_functions_for_priority (splay_tree_node n, void * data)
   /* Generate the functions themselves, but only if they are really
      needed.  */
   if (pi->initializations_p)
-    generate_ctor_or_dtor_function (/*constructor_p=*/true, priority, locus);
+    generate_ctor_or_dtor_function (/*constructor_p=*/true, priority, locus,
+				    /*omp_target=*/false);
   if (pi->destructions_p)
-    generate_ctor_or_dtor_function (/*constructor_p=*/false, priority, locus);
+    generate_ctor_or_dtor_function (/*constructor_p=*/false, priority, locus,
+				    /*omp_target=*/false);
+  if (pi->omp_tgt_initializations_p)
+    generate_ctor_or_dtor_function (/*constructor_p=*/true, priority, locus,
+				    /*omp_target=*/true);
+  if (pi->omp_tgt_destructions_p)
+    generate_ctor_or_dtor_function (/*constructor_p=*/false, priority, locus,
+				    /*omp_target=*/true);
 
   /* Keep iterating.  */
   return 0;
@@ -4773,7 +4854,7 @@ handle_tls_init (void)
     {
       tree var = TREE_VALUE (vars);
       tree init = TREE_PURPOSE (vars);
-      one_static_initialization_or_destruction (var, init, true);
+      one_static_initialization_or_destruction (var, init, true, false);
 
       /* Output init aliases even with -fno-extern-tls-init.  */
       if (TARGET_SUPPORTS_ALIASES && TREE_PUBLIC (var))
@@ -5087,6 +5168,7 @@ c_parse_final_cleanups (void)
 
   int retries = 0;
   unsigned ssdf_count = 0;
+  unsigned omp_target_ssdf_count = 0;
   for (bool reconsider = true; reconsider; retries++)
     {
       reconsider = false;
@@ -5160,11 +5242,18 @@ c_parse_final_cleanups (void)
 	  /* Set the line and file, so that it is obviously not from
 	     the source file.  */
 	  input_location = locus_at_end_of_parsing;
-	  ssdf_body = start_static_storage_duration_function (ssdf_count);
+	  ssdf_body
+	    = start_static_storage_duration_function (ssdf_count, false);
 
 	  /* First generate code to do all the initializations.  */
 	  if (vars)
-	    do_static_initialization_or_destruction (vars, /*initp=*/true);
+	    do_static_initialization_or_destruction (vars, /*initp=*/true,
+						     /*omp_target=*/false);
+
+	  tree filtered_vars = NULL_TREE;
+
+	  if (flag_openmp)
+	    filtered_vars = copy_list (vars);
 
 	  /* Then, generate code to do all the destructions.  Do these
 	     in reverse order so that the most recently constructed
@@ -5175,7 +5264,8 @@ c_parse_final_cleanups (void)
 	  if (!flag_use_cxa_atexit && vars)
 	    {
 	      vars = nreverse (vars);
-	      do_static_initialization_or_destruction (vars, /*initp=*/false);
+	      do_static_initialization_or_destruction (vars, /*initp=*/false,
+						       /*omp_target=*/false);
 	    }
 	  else
 	    vars = NULL_TREE;
@@ -5185,6 +5275,74 @@ c_parse_final_cleanups (void)
 	  input_location = locus_at_end_of_parsing;
 	  finish_static_storage_duration_function (ssdf_body);
 
+	  if (flag_openmp)
+	    {
+	      /* Do all the above again for OpenMP "declare target" static
+		 storage duration decls.  */
+
+	      /* We're only interested in "declare target" variables now.  */
+	      tree *fvarsp = &filtered_vars;
+	      while (*fvarsp)
+		{
+		  tree decl = TREE_VALUE (*fvarsp);
+
+		  if (lookup_attribute ("omp declare target",
+					DECL_ATTRIBUTES (decl)))
+		    fvarsp = &OMP_CLAUSE_CHAIN (*fvarsp);
+		  else
+		    *fvarsp = OMP_CLAUSE_CHAIN (*fvarsp);
+		}
+
+	      input_location = locus_at_end_of_parsing;
+	      ssdf_body
+		= start_static_storage_duration_function (omp_target_ssdf_count,
+							  /*omp_target=*/true);
+
+	      /* As above, first generate code to do all the
+		 initializations.  */
+	      if (filtered_vars)
+		{
+		  tree nonhost_if_stmt = NULL_TREE;
+		  nonhost_if_stmt = begin_if_stmt ();
+
+		  /* We add an "omp declare target nohost" attribute, but (for
+		     now) we still get a copy of the constructor/destructor on
+		     the host.  Make sure it does nothing unless we're on the
+		     target device.  */
+		  tree fn
+		    = builtin_decl_explicit (BUILT_IN_OMP_IS_INITIAL_DEVICE);
+		  tree initial_dev = build_call_expr (fn, 0);
+		  tree target_dev_p
+		    = cp_build_binary_op (input_location, NE_EXPR, initial_dev,
+					  build_int_cst (NULL_TREE, 1),
+					  tf_warning_or_error);
+		  finish_if_stmt_cond (target_dev_p, nonhost_if_stmt);
+
+		  do_static_initialization_or_destruction (filtered_vars,
+							   /*initp=*/true,
+							   /*omp_target=*/true);
+		  if (!flag_use_cxa_atexit && filtered_vars)
+		    {
+		      filtered_vars = nreverse (filtered_vars);
+		      do_static_initialization_or_destruction (filtered_vars,
+							       /*initp=*/false,
+							       /*omp_target=*/
+							       false);
+		    }
+		  else
+		    filtered_vars = NULL_TREE;
+
+		  /* Finish up nonhost if-stmt body.  */
+		  finish_then_clause (nonhost_if_stmt);
+		  finish_if_stmt (nonhost_if_stmt);
+		}
+
+	      input_location = locus_at_end_of_parsing;
+	      finish_static_storage_duration_function (ssdf_body);
+
+	      omp_target_ssdf_count++;
+	    }
+
 	  /* All those initializations and finalizations might cause
 	     us to need more inline functions, more template
 	     instantiations, etc.  */
@@ -5365,7 +5523,8 @@ c_parse_final_cleanups (void)
 	   || module_initializer_kind ())
     generate_ctor_or_dtor_function (/*constructor_p=*/true,
 				    DEFAULT_INIT_PRIORITY,
-				    &locus_at_end_of_parsing);
+				    &locus_at_end_of_parsing,
+				    /*omp_target=*/false);
 
   /* We're done with the splay-tree now.  */
   if (priority_info_map)
diff --git a/gcc/omp-builtins.def b/gcc/omp-builtins.def
index d257278b9e5..b3715b91cbb 100644
--- a/gcc/omp-builtins.def
+++ b/gcc/omp-builtins.def
@@ -68,6 +68,8 @@ DEF_GOACC_BUILTIN_ONLY (BUILT_IN_GOACC_SINGLE_COPY_START, "GOACC_single_copy_sta
 DEF_GOACC_BUILTIN_ONLY (BUILT_IN_GOACC_SINGLE_COPY_END, "GOACC_single_copy_end",
 			BT_FN_VOID_PTR, ATTR_NOTHROW_LEAF_LIST)
 
+DEF_GOMP_BUILTIN (BUILT_IN_OMP_IS_INITIAL_DEVICE, "omp_is_initial_device",
+		  BT_FN_INT, ATTR_CONST_NOTHROW_LEAF_LIST)
 DEF_GOMP_BUILTIN (BUILT_IN_OMP_GET_THREAD_NUM, "omp_get_thread_num",
 		  BT_FN_INT, ATTR_CONST_NOTHROW_LEAF_LIST)
 DEF_GOMP_BUILTIN (BUILT_IN_OMP_GET_NUM_THREADS, "omp_get_num_threads",
diff --git a/gcc/tree.cc b/gcc/tree.cc
index 13c23b67a43..aed566fcf0e 100644
--- a/gcc/tree.cc
+++ b/gcc/tree.cc
@@ -8769,9 +8769,11 @@ get_file_function_name (const char *type)
      will be local to this file and the name is only necessary for
      debugging purposes. 
      We also assign sub_I and sub_D sufixes to constructors called from
-     the global static constructors.  These are always local.  */
+     the global static constructors.  These are always local.
+     OpenMP "declare target" offloaded constructors/destructors use "off_I" and
+     "off_D" for the same purpose.  */
   else if (((type[0] == 'I' || type[0] == 'D') && targetm.have_ctors_dtors)
-	   || (startswith (type, "sub_")
+	   || ((startswith (type, "sub_") || startswith (type, "off_"))
 	       && (type[4] == 'I' || type[4] == 'D')))
     {
       const char *file = main_input_filename;
diff --git a/libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-1.C b/libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-1.C
new file mode 100644
index 00000000000..91d8469a150
--- /dev/null
+++ b/libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-1.C
@@ -0,0 +1,28 @@
+// { dg-do run }
+
+#include <cassert>
+
+#pragma omp declare target
+
+struct str {
+  str(int x) : _x(x) { }
+  int add(str o) { return _x + o._x; }
+  int _x;
+} v1(5);
+
+#pragma omp end declare target
+
+int main()
+{
+  int res = -1;
+  str v2(2);
+
+#pragma omp target map(from:res)
+  {
+    res = v1.add(v2);
+  }
+
+  assert (res == 7);
+
+  return 0;
+}
diff --git a/libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-2.C b/libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-2.C
new file mode 100644
index 00000000000..1bf3ee8e31c
--- /dev/null
+++ b/libgomp/testsuite/libgomp.c++/static-aggr-constructor-destructor-2.C
@@ -0,0 +1,31 @@
+// { dg-do run }
+
+#include <cassert>
+
+#pragma omp declare target
+
+template<typename T>
+struct str {
+  str(T x) : _x(x) { }
+  T add(str o) { return _x + o._x; }
+  T _x;
+};
+
+str<long> v1(5);
+
+#pragma omp end declare target
+
+int main()
+{
+  long res = -1;
+  str<long> v2(2);
+
+#pragma omp target map(from:res)
+  {
+    res = v1.add(v2);
+  }
+
+  assert (res == 7);
+
+  return 0;
+}
-- 
2.29.2


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2023-04-05 12:31 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-03-27 18:54 [PATCH] [og12] OpenMP: Constructors and destructors for "declare target" static aggregates Julian Brown
2023-04-02  9:38 ` Thomas Schwinge
2023-04-05 12:31   ` Julian Brown

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).