On 05/13/2011 04:32 PM, Gwenael Casaccio wrote:
On 05/13/2011 08:46 AM, Paolo Bonzini wrote:
On 05/12/2011 05:24 PM, Holger Hans Peter Freyther wrote:
How many read-only objects do we have in a bootstrapped image? In a
typical
iliad application? Does it make sense to optimize for it? or will RO
handling
just become more easy and this is a cleanup?

I think the point is to optimize _non_ read-only objects at the cost of
making tenuring read-only objects a bit more expensive (you will have to
unprotect the page, allocate, copy, protect the page). We also win
read-only access to fixed instance variables, which GNU Smalltalk
doesn't have (only indexed instance variables are protected).

BTW, for the same reason I think that moving objects to read-only space
directly when you make them read-only (like you did for fixedspace) is a
good idea. Anyway, read-only objects are mostly literals so they are
long-living. Gwen, can you reorganize the patch along these lines?

The patch making readonly space, well, readonly should be a separate
one. For unprotection/protection around allocation you can add hooks to
alloc.c heaps, like we have after_allocating: that would be
before_freeing, after_freeing, before_allocating.

Paolo

I've made the new patch, the ro_old space is like fixed. I've fixed
two bugs one in the symbol allocation and another when the ro_old heap
should grow.

Gwen

Hi,

Paolo can you check the patch, here I've changed a bit the behavior of make_ro; I preserve F_FIXED, F_OLD, or young information thus when making an object writable I can restore it in the good space.
It seems to be the definitive version

Gwen
diff --git a/libgst/gstpriv.h b/libgst/gstpriv.h
index 68c34b9..2c6c49f 100644
--- a/libgst/gstpriv.h
+++ b/libgst/gstpriv.h
@@ -373,8 +373,7 @@ enum {
 
 /* Set whether an object, OOP, is readonly or readwrite.  */
 #define MAKE_OOP_READONLY(oop, ro) \
-  (((oop)->flags &= ~F_READONLY), \
-   ((oop)->flags |= (ro) ? F_READONLY : 0))
+  _gst_make_oop_readonly (oop, ro)
 
 #ifdef ENABLE_SECURITY
 
diff --git a/libgst/oop.c b/libgst/oop.c
index ad264d9..ad93d34 100644
--- a/libgst/oop.c
+++ b/libgst/oop.c
@@ -145,9 +145,18 @@ static OOP *queue_put (surv_space *q, OOP *src, int n);
 /* Move an object from survivor space to oldspace.  */
 static void tenure_one_object ();
 
+/* Initialize an allocation heap with a hooks set.  */
+static heap_data *init_space (void *function, size_t size);
+
 /* Initialize an allocation heap with the oldspace hooks set.  */
 static heap_data *init_old_space (size_t size);
 
+/* Initialize an allocation heap with the read-only oldspace hooks set.  */
+static heap_data *init_ro_old_space (size_t size);
+
+/* Initialize an allocation heap with the fixedspace hooks set.  */
+static heap_data *init_fixed_space (size_t size);
+
 /* Initialize a surv_space structure.  */
 static void init_survivor_space (struct surv_space *space, size_t size);
 
@@ -208,6 +217,15 @@ static int oldspace_sigsegv_handler (void* fault_address, int serious);
 #endif
 
 /* Hook that triggers garbage collection.  */
+static heap_data *grow_nomemory (heap_data *h, size_t sz);
+
+/* Hook that triggers garbage collection.  */
+static heap_data *ro_oldspace_nomemory (heap_data *h, size_t sz);
+
+/* Hook that triggers garbage collection.  */
+static heap_data *fixedspace_nomemory (heap_data *h, size_t sz);
+
+/* Hook that triggers garbage collection.  */
 static heap_data *oldspace_nomemory (heap_data *h, size_t sz);
 
 /* Answer the number of fields to be scanned in the object starting
@@ -235,6 +253,8 @@ static inline void mark_ephemeron_oops (void);
    not surviving the garbage collection.  Called by preare_for_sweep.  */
 static inline void check_weak_refs ();
 
+static gst_object _gst_alloc_new_obj (size_t size);
+
 
 void
 init_survivor_space (struct surv_space *space, size_t size)
@@ -247,16 +267,34 @@ init_survivor_space (struct surv_space *space, size_t size)
 }
 
 heap_data *
-init_old_space (size_t size)
+init_space (void *function, size_t size)
 {
   heap_data *h = _gst_mem_new_heap (0, size);
   h->after_prim_allocating = oldspace_after_allocating;
   h->before_prim_freeing = oldspace_before_freeing;
-  h->nomemory = oldspace_nomemory;
+  h->nomemory = function;
 
   return h;
 }
 
+heap_data *
+init_fixed_space (size_t size)
+{
+  return init_space (fixedspace_nomemory, size);
+}
+
+heap_data *
+init_ro_old_space (size_t size)
+{
+  return init_space (ro_oldspace_nomemory, size);
+}
+
+heap_data *
+init_old_space (size_t size)
+{
+  return init_space (oldspace_nomemory, size);
+}
+
 void
 _gst_init_mem_default ()
 {
@@ -331,8 +369,9 @@ _gst_init_mem (size_t eden, size_t survivor, size_t old,
     {
       if (old)
         {
+          _gst_mem.ro_old = init_ro_old_space (old);
           _gst_mem.old = init_old_space (old);
-          _gst_mem.fixed = init_old_space (old);
+          _gst_mem.fixed = init_fixed_space (old);
         }
 
       _gst_mem.active_half = &_gst_mem.surv[0];
@@ -695,15 +734,81 @@ _gst_swap_objects (OOP oop1,
     _gst_make_oop_weak (oop1);
 }
 
+mst_Boolean
+_gst_make_oop_readonly (OOP oop, mst_Boolean ro)
+{
+  gst_object newObj;
+  int size;
+
+  /* they are the same so I can return */
+  if (((oop->flags & F_READONLY) == F_READONLY) == ro)
+    return ro;
+
+  size = SIZE_TO_BYTES (TO_INT(oop->object->objSize));
+  if (ro)
+    {
+      /* become read only */
+      if ((oop->flags & F_LOADED) == 0)
+        {
+          newObj = (gst_object) _gst_mem_alloc (_gst_mem.ro_old, size);
+
+          if (!newObj)
+            abort ();
+
+          memcpy (newObj, oop->object, size);
+
+          if (oop->flags & F_FIXED)
+              _gst_mem_free (_gst_mem.fixed, oop->object);
+          else if (oop->flags & F_OLD)
+              _gst_mem_free (_gst_mem.old, oop->object);
+
+          oop->object = newObj;
+        }
+
+        oop->flags |= F_READONLY;
+    }
+  else
+    {
+      /* become writeable */
+      if ((oop->flags & F_LOADED) == 0)
+        {
+          if (oop->flags & F_FIXED)
+            newObj = (gst_object) _gst_mem_alloc (_gst_mem.fixed, size);
+          else if (oop->flags & F_OLD)
+            newObj = (gst_object) _gst_mem_alloc (_gst_mem.old, size);
+          else
+            newObj = (gst_object) _gst_alloc_new_obj (size);
+
+          if (!newObj)
+            abort ();
+
+          memcpy (newObj, oop->object, size);
+          _gst_mem_free (_gst_mem.ro_old, oop->object);
+
+          oop->object = newObj;
+        }
+
+      oop->flags &= ~(F_READONLY);
+    }
+  
+  return ro;
+}
 
 void
 _gst_make_oop_fixed (OOP oop)
 {
   gst_object newObj;
   int size;
+
   if (oop->flags & F_FIXED)
     return;
 
+  if (oop->flags & F_READONLY)
+    {
+      oop->flags |= F_FIXED;
+      return ;
+    }
+
   if ((oop->flags & F_LOADED) == 0)
     {
       size = SIZE_TO_BYTES (TO_INT(oop->object->objSize));
@@ -731,15 +836,17 @@ _gst_tenure_oop (OOP oop)
   if (oop->flags & F_OLD)
     return;
 
-  if (!(oop->flags & F_FIXED))
+  if (!((oop->flags & F_FIXED) || (oop->flags & F_READONLY)))
     {
       int size = SIZE_TO_BYTES (TO_INT(oop->object->objSize));
+      
       newObj = (gst_object) _gst_mem_alloc (_gst_mem.old, size);
+      _gst_mem.numOldOOPs++;
+
       if (!newObj)
         abort ();
 
       memcpy (newObj, oop->object, size);
-      _gst_mem.numOldOOPs++;
 
       oop->object = newObj;
     }
@@ -750,6 +857,31 @@ _gst_tenure_oop (OOP oop)
 
 
 gst_object
+_gst_alloc_new_obj (size_t size)
+{
+  OOP *newAllocPtr;
+  gst_object p_instance;
+
+  size = ROUNDED_BYTES (size);
+
+  /* We don't want to have allocPtr pointing to the wrong thing during
+     GC, so we use a local var to hold its new value */
+  newAllocPtr = _gst_mem.eden.allocPtr + BYTES_TO_SIZE (size);
+
+  if UNCOMMON (newAllocPtr >= _gst_mem.eden.maxPtr)
+    {
+      _gst_scavenge ();
+      newAllocPtr = _gst_mem.eden.allocPtr + size;
+    }
+
+  p_instance = (gst_object) _gst_mem.eden.allocPtr;
+  _gst_mem.eden.allocPtr = newAllocPtr;
+  p_instance->objSize = FROM_INT (BYTES_TO_SIZE (size));
+
+  return p_instance;
+}
+
+gst_object
 _gst_alloc_obj (size_t size,
 		OOP *p_oop)
 {
@@ -780,6 +912,39 @@ _gst_alloc_obj (size_t size,
 }
 
 gst_object
+_gst_alloc_ro_obj (size_t size,
+                    OOP *p_oop)
+{
+  gst_object p_instance;
+
+  size = ROUNDED_BYTES (size);
+
+  /* If the object is big enough, we put it directly in oldspace.  */
+  p_instance = (gst_object) _gst_mem_alloc (_gst_mem.ro_old, size);
+  if COMMON (p_instance)
+    goto ok;
+
+  _gst_global_gc (size);
+  p_instance = (gst_object) _gst_mem_alloc (_gst_mem.ro_old, size);
+  if COMMON (p_instance)
+    goto ok;
+
+  _gst_compact (0);
+  p_instance = (gst_object) _gst_mem_alloc (_gst_mem.ro_old, size);
+  if UNCOMMON (!p_instance)
+    {
+      /* !!! do something more reasonable in the future */
+      _gst_errorf ("Cannot recover, exiting...");
+      exit (1);
+    }
+
+ok:
+  *p_oop = alloc_oop (p_instance, F_OLD | F_FIXED | F_READONLY);
+  p_instance->objSize = FROM_INT (BYTES_TO_SIZE (size));
+  return p_instance;
+}
+
+gst_object
 _gst_alloc_old_obj (size_t size,
 		    OOP *p_oop)
 {
@@ -889,25 +1054,43 @@ oldspace_before_freeing (heap_data *h, heap_block *blk, size_t sz)
 }
 
 heap_data *
-oldspace_nomemory (heap_data *h, size_t sz)
+grow_nomemory (heap_data *h, size_t sz)
 {
   if (!_gst_gc_running)
     _gst_global_gc (sz);
   else
     {
       /* Already garbage collecting, emergency growth just to satisfy
-	 tenuring necessities.  */
-      int grow_amount_to_satisfy_rate = _gst_mem.old->heap_limit
+         tenuring necessities.  */
+      int grow_amount_to_satisfy_rate = h->heap_limit
            * (100.0 + _gst_mem.space_grow_rate) / 100;
-      int grow_amount_to_satisfy_threshold = 
-	   (sz + _gst_mem.old->heap_total)
-	   * 100.0 /_gst_mem.grow_threshold_percent;
+      int grow_amount_to_satisfy_threshold =
+           (sz + h->heap_total)
+           * 100.0 /_gst_mem.grow_threshold_percent;
 
-      _gst_mem.old->heap_limit = MAX (grow_amount_to_satisfy_rate,
-				      grow_amount_to_satisfy_threshold);
+      h->heap_limit = MAX (grow_amount_to_satisfy_rate,
+                           grow_amount_to_satisfy_threshold);
     }
 
-  return _gst_mem.old;
+  return h;
+}
+
+heap_data *
+ro_oldspace_nomemory (heap_data *h, size_t sz)
+{
+  return grow_nomemory (_gst_mem.ro_old, sz);
+}
+
+heap_data *
+fixedspace_nomemory (heap_data *h, size_t sz)
+{
+  return grow_nomemory (_gst_mem.fixed, sz);
+}
+
+heap_data *
+oldspace_nomemory (heap_data *h, size_t sz)
+{
+  return grow_nomemory (_gst_mem.old, sz);
 }
 
 #ifndef NO_SIGSEGV_HANDLING
@@ -979,6 +1162,7 @@ void
 grow_memory_no_compact (size_t new_heap_limit)
 {
   _gst_mem.old->heap_limit = new_heap_limit;
+  _gst_mem.ro_old->heap_limit = new_heap_limit;
   _gst_mem.fixed->heap_limit = new_heap_limit;
   _gst_mem.numGrowths++;
   update_stats (&stats.timeOfLastGrowth,
@@ -996,6 +1180,7 @@ _gst_compact (size_t new_heap_limit)
   if (new_heap_limit)
     {
       _gst_mem.fixed->heap_limit = new_heap_limit;
+      _gst_mem.ro_old->heap_limit = new_heap_limit;
       _gst_mem.numGrowths++;
       update_stats (&stats.timeOfLastGrowth,
 		    &_gst_mem.timeBetweenGrowths, NULL);
@@ -1037,7 +1222,7 @@ _gst_compact (size_t new_heap_limit)
        oop < &_gst_mem.ot[_gst_mem.ot_size]; oop++)
     {
       PREFETCH_LOOP (oop, PREF_READ | PREF_NTA);
-      if ((oop->flags & (F_OLD | F_FIXED | F_LOADED)) == F_OLD)
+      if ((oop->flags & (F_OLD | F_FIXED | F_LOADED | F_READONLY)) == F_OLD)
         {
           gst_object new;
           size_t size = SIZE_TO_BYTES (TO_INT (oop->object->objSize));
@@ -1049,6 +1234,7 @@ _gst_compact (size_t new_heap_limit)
     }
 
   xfree (_gst_mem.old);
+
   _gst_mem.old = new_heap;
   new_heap->nomemory = oldspace_nomemory;
 
@@ -1146,7 +1332,9 @@ _gst_global_gc (int next_allocation)
       if UNCOMMON ((next_allocation + _gst_mem.old->heap_total)
 	    * 100.0 / old_limit > _gst_mem.grow_threshold_percent
          || (next_allocation + _gst_mem.fixed->heap_total)
-	    * 100.0 / _gst_mem.fixed->heap_limit > _gst_mem.grow_threshold_percent)
+	    * 100.0 / _gst_mem.fixed->heap_limit > _gst_mem.grow_threshold_percent
+         || (next_allocation + _gst_mem.ro_old->heap_total)
+            * 100.0 / _gst_mem.ro_old->heap_limit > _gst_mem.grow_threshold_percent)
         {
           int grow_amount_to_satisfy_rate = old_limit
                * (100.0 + _gst_mem.space_grow_rate) / 100;
@@ -1190,7 +1378,9 @@ _gst_scavenge (void)
      || _gst_mem.old->heap_total * 100.0 / _gst_mem.old->heap_limit >
 	_gst_mem.grow_threshold_percent
      || _gst_mem.fixed->heap_total * 100.0 / _gst_mem.fixed->heap_limit >
-	_gst_mem.grow_threshold_percent)
+	_gst_mem.grow_threshold_percent
+     || _gst_mem.ro_old->heap_total * 100.0 / _gst_mem.ro_old->heap_limit >
+        _gst_mem.grow_threshold_percent)
     {
       _gst_global_gc (0);
       _gst_incremental_gc_step ();
@@ -1452,20 +1642,20 @@ _gst_sweep_oop (OOP oop)
     _gst_make_oop_non_weak (oop);
 
   /* Free unreachable oldspace objects.  */
-  if UNCOMMON (oop->flags & F_FIXED)
+  if (oop->flags & F_OLD)
     {
       _gst_mem.numOldOOPs--;
       stats.reclaimedOldSpaceBytesSinceLastGlobalGC +=
-	SIZE_TO_BYTES (TO_INT (OOP_TO_OBJ (oop)->objSize));
-      if ((oop->flags & F_LOADED) == 0)
-        _gst_mem_free (_gst_mem.fixed, oop->object);
+        SIZE_TO_BYTES (TO_INT (OOP_TO_OBJ (oop)->objSize));
     }
-  else if UNCOMMON (oop->flags & F_OLD)
+
+  if ((oop->flags & F_LOADED) == 0)
     {
-      _gst_mem.numOldOOPs--;
-      stats.reclaimedOldSpaceBytesSinceLastGlobalGC +=
-	SIZE_TO_BYTES (TO_INT (OOP_TO_OBJ (oop)->objSize));
-      if ((oop->flags & F_LOADED) == 0)
+      if UNCOMMON (oop->flags & F_READONLY)
+       _gst_mem_free (_gst_mem.ro_old, oop->object);
+      else if UNCOMMON (oop->flags & F_FIXED)
+       _gst_mem_free (_gst_mem.fixed, oop->object);
+      else if UNCOMMON (oop->flags & F_OLD)
         _gst_mem_free (_gst_mem.old, oop->object);
     }
 
@@ -1617,7 +1807,9 @@ tenure_one_object ()
     _gst_tenure_oop (oop);
 
   queue_get (&_gst_mem.tenuring_queue, 1);
-  queue_get (_gst_mem.active_half, TO_INT (oop->object->objSize));
+
+  if (!((oop->flags & F_FIXED) || (oop->flags & F_READONLY)))
+    queue_get (_gst_mem.active_half, TO_INT (oop->object->objSize));
 }
 
 void
@@ -2061,8 +2253,10 @@ _gst_copy_an_oop (OOP oop)
 #endif
 
       queue_put (&_gst_mem.tenuring_queue, &oop, 1);
-      obj = oop->object = (gst_object)
-	queue_put (_gst_mem.active_half, pData, TO_INT (obj->objSize));
+
+      if (!((oop->flags & F_FIXED) || (oop->flags & F_READONLY)))
+        obj = oop->object = (gst_object)
+	  queue_put (_gst_mem.active_half, pData, TO_INT (obj->objSize));
 
       oop->flags &= ~(F_SPACES | F_POOLED);
       oop->flags |= _gst_mem.active_flag;
diff --git a/libgst/oop.h b/libgst/oop.h
index 6816b3f..fd9ea9f 100644
--- a/libgst/oop.h
+++ b/libgst/oop.h
@@ -189,7 +189,7 @@ typedef struct cheney_scan_state {
 
 struct memory_space
 {
-  heap_data *old, *fixed;
+  heap_data *ro_old, *old, *fixed;
   struct new_space eden;
   struct surv_space surv[2], tenuring_queue;
 
@@ -414,6 +414,11 @@ extern gst_object _gst_alloc_old_obj (size_t size,
 				      OOP *p_oop) 
   ATTRIBUTE_HIDDEN;
 
+/* The same, but for a read-only oldspace object */
+extern gst_object _gst_alloc_ro_obj (size_t size,
+                                     OOP *p_oop)
+  ATTRIBUTE_HIDDEN;
+
 /* Allocate and return space for an object of SIZE words, without
    creating an OOP.  This is a special operation that is only needed
    at bootstrap time, so it does not care about garbage collection.  */
@@ -437,6 +442,10 @@ extern mst_Boolean _gst_realloc_oop_table (size_t newSize)
 extern void _gst_tenure_oop (OOP oop) 
   ATTRIBUTE_HIDDEN;
 
+/* Set whether an object, OOP, is readonly or readwrite.  */
+extern mst_Boolean _gst_make_oop_readonly (OOP oop, mst_Boolean ro)
+  ATTRIBUTE_HIDDEN;
+
 /* Move OOP to fixedspace.  */
 extern void _gst_make_oop_fixed (OOP oop) 
   ATTRIBUTE_HIDDEN;
diff --git a/libgst/save.c b/libgst/save.c
index 77cc8b9..70f41ed 100644
--- a/libgst/save.c
+++ b/libgst/save.c
@@ -646,7 +646,9 @@ load_normal_oops (int imageFd)
 
       else
 	{
-	  if (flags & F_FIXED)
+          if (flags & F_READONLY)
+            object = (gst_object) _gst_mem_alloc (_gst_mem.ro_old, size);
+	  else if (flags & F_FIXED)
 	    {
 	      _gst_mem.numFixedOOPs++;
               object = (gst_object) _gst_mem_alloc (_gst_mem.fixed, size);
diff --git a/libgst/sym.c b/libgst/sym.c
index a3cfd65..e0d7c8a 100644
--- a/libgst/sym.c
+++ b/libgst/sym.c
@@ -1460,11 +1460,10 @@ alloc_symbol_oop (const char *str, int len)
 
   numBytes = sizeof(gst_object_header) + len;
   alignedBytes = ROUNDED_BYTES (numBytes);
-  symbol = (gst_symbol) _gst_alloc_obj (alignedBytes, &symbolOOP);
+  symbol = (gst_symbol) _gst_alloc_ro_obj (alignedBytes, &symbolOOP);
   INIT_UNALIGNED_OBJECT (symbolOOP, alignedBytes - numBytes);
 
   memcpy (symbol->symString, str, len);
-  symbolOOP->flags |= F_READONLY;
   return symbolOOP;
 }
 
diff --git a/snprintfv/snprintfv/filament.h b/snprintfv/snprintfv/filament.h
index 4a91eb6..8a7ce6c 100644
--- a/snprintfv/snprintfv/filament.h
+++ b/snprintfv/snprintfv/filament.h
@@ -1,4 +1,4 @@
-#line 1 "../../../snprintfv/snprintfv/filament.in"
+#line 1 "./filament.in"
 /*  -*- Mode: C -*-  */
 
 /* filament.h --- a bit like a string but different =)O|
@@ -118,7 +118,7 @@ extern char * fildelete (Filament *fil);
 extern void _fil_extend (Filament *fil, size_t len, boolean copy);
 
 
-#line 61 "../../../snprintfv/snprintfv/filament.in"
+#line 61 "./filament.in"
 
 /* Save the overhead of a function call in the great majority of cases. */
 #define fil_maybe_extend(fil, len, copy)  \
diff --git a/snprintfv/snprintfv/printf.h b/snprintfv/snprintfv/printf.h
index 49a2e9f..1437dd5 100644
--- a/snprintfv/snprintfv/printf.h
+++ b/snprintfv/snprintfv/printf.h
@@ -1,4 +1,4 @@
-#line 1 "../../../snprintfv/snprintfv/printf.in"
+#line 1 "./printf.in"
 /*  -*- Mode: C -*-  */
 
 /* printf.in --- printf clone for argv arrays
@@ -266,7 +266,7 @@ enum
       } \
   } SNV_STMT_END
 
-#line 269 "../../../snprintfv/snprintfv/printf.in"
+#line 269 "./printf.in"
 /**
  * printf_generic_info:   
  * @pinfo: the current state information for the format
@@ -302,7 +302,7 @@ extern int printf_generic_info (struct printf_info *const pinfo, size_t n, int *
 extern int printf_generic (STREAM *stream, struct printf_info *const pinfo, union printf_arg const *args);
 
 
-#line 270 "../../../snprintfv/snprintfv/printf.in"
+#line 270 "./printf.in"
 /**
  * register_printf_function:  
  * @spec: the character which will trigger @func, cast to an unsigned int.
@@ -789,7 +789,7 @@ extern int snv_vasprintf (char **result, const char *format, va_list ap);
 extern int snv_asprintfv (char **result, const char *format, snv_constpointer const args[]);
 
 
-#line 271 "../../../snprintfv/snprintfv/printf.in"
+#line 271 "./printf.in"
 
 /* If you don't want to use snprintfv functions for *all* of your string
    formatting API, then define COMPILING_SNPRINTFV_C and use the snv_
diff --git a/snprintfv/snprintfv/stream.h b/snprintfv/snprintfv/stream.h
index 496bd33..0bebce1 100644
--- a/snprintfv/snprintfv/stream.h
+++ b/snprintfv/snprintfv/stream.h
@@ -1,4 +1,4 @@
-#line 1 "../../../snprintfv/snprintfv/stream.in"
+#line 1 "./stream.in"
 /*  -*- Mode: C -*-  */
 
 /* stream.h --- customizable stream routines
@@ -180,7 +180,7 @@ extern int stream_puts (char *s, STREAM *stream);
 extern int stream_get (STREAM *stream);
 
 
-#line 88 "../../../snprintfv/snprintfv/stream.in"
+#line 88 "./stream.in"
 #ifdef __cplusplus
 #if 0
 /* This brace is so that emacs can still indent properly: */
_______________________________________________
help-smalltalk mailing list
[email protected]
https://lists.gnu.org/mailman/listinfo/help-smalltalk

Reply via email to