[m-rev.] more LLDS agc improvements
Fergus Henderson
fjh at cs.mu.OZ.AU
Sat Nov 15 04:48:21 AEDT 2003
Estimated hours taken: 8
Branches: main
More improvements to LLDS accurate GC.
runtime/mercury_accurate_gc.c:
Rewrite the LLDS accurate GC stack traversal code using
MR_stack_walk_step() and MR_traverse_nondet_stack_from_layout().
The previous code had some serious bugs.
runtime/mercury_stack_trace.h:
runtime/mercury_stack_trace.c:
Don't pass `level_number' to the traversal function for
MR_traverse_nondet_stack_from_layout(). It's not needed.
runtime/mercury_stack_trace.c:
Add a comment, and an assertion.
Workspace: /home/jupiter/fjh/ws-jupiter/mercury
Index: runtime/mercury_accurate_gc.c
===================================================================
RCS file: /home/mercury1/repository/mercury/runtime/mercury_accurate_gc.c,v
retrieving revision 1.31
diff -u -d -r1.31 mercury_accurate_gc.c
--- runtime/mercury_accurate_gc.c 13 Nov 2003 10:45:50 -0000 1.31
+++ runtime/mercury_accurate_gc.c 14 Nov 2003 12:35:39 -0000
@@ -57,19 +57,24 @@
static void MR_LLDS_garbage_collect(MR_Code *saved_success,
MR_Word *stack_pointer,
MR_Word *max_frame, MR_Word *current_frame);
- static void traverse_call_stack(MR_Code *success_ip, MR_Internal *label,
- const MR_Label_Layout *label_layout,
- MR_Word **ptr_to_stack_pointer,
- MR_Word **ptr_to_current_frame);
- static void traverse_nondet_stack(MR_Word *stack_pointer,
+ static void traverse_det_stack(const MR_Label_Layout *label_layout,
+ MR_Word *stack_pointer, MR_Word *current_frame);
+ static void traverse_nondet_stack(const MR_Label_Layout *label_layout,
+ MR_Word *stack_pointer,
MR_Word *max_frame, MR_Word *current_frame);
+ static void traverse_nondet_frame(void *user_data,
+ const MR_Label_Layout *label_layout,
+ MR_Word *stack_pointer, MR_Word *current_frame);
+ static void traverse_frame(MR_bool is_first_frame,
+ const MR_Label_Layout *label_layout,
+ MR_Word *stack_pointer, MR_Word *current_frame);
static void resize_and_reset_redzone(MR_MemoryZone *old_heap,
MR_MemoryZone *new_heap);
static void copy_long_value(MR_Long_Lval locn, MR_TypeInfo type_info,
- MR_bool copy_regs, MR_Word *stack_pointer,
+ MR_bool is_first_frame, MR_Word *stack_pointer,
MR_Word *current_frame);
static void copy_short_value(MR_Short_Lval locn, MR_TypeInfo type_info,
- MR_bool copy_regs, MR_Word *stack_pointer,
+ MR_bool is_first_frame, MR_Word *stack_pointer,
MR_Word *current_frame);
#endif
@@ -434,15 +439,9 @@
{
MR_MemoryZone *old_heap, *new_heap;
MR_Word *old_hp;
-
MR_Internal *label;
const MR_Label_Layout *label_layout;
- MR_Internal *first_label;
- MR_Word *first_stack_pointer;
- MR_Word *first_max_frame;
- MR_Word *first_current_frame;
-
old_heap = MR_ENGINE(MR_eng_heap_zone);
new_heap = MR_ENGINE(MR_eng_heap_zone2);
old_hp = MR_virtual_hp;
@@ -468,17 +467,13 @@
label_layout = label->i_layout;
if (MR_agc_debug) {
- first_label = label;
- first_stack_pointer = stack_pointer;
- first_current_frame = current_frame;
- first_max_frame = max_frame;
fprintf(stderr, "BEFORE:\n");
- MR_agc_dump_stack_frames(first_label, old_heap, first_stack_pointer,
- first_current_frame);
- MR_agc_dump_nondet_stack_frames(first_label, old_heap,
- first_stack_pointer, first_current_frame, first_max_frame);
+ MR_agc_dump_stack_frames(label, old_heap, stack_pointer,
+ current_frame);
+ MR_agc_dump_nondet_stack_frames(label, old_heap,
+ stack_pointer, current_frame, max_frame);
MR_agc_dump_roots(root_list);
/*
@@ -491,20 +486,12 @@
}
/*
- ** First, traverse the call stack. This includes all of the det stack
- ** and the success continuation frames on the nondet stack. It does not
- ** include the failure continuation frames on the nondet stack.
- ** (We really only need to traverse the det stack here, but there's no
- ** way to do that without traversing the success continuation frames of
- ** the nondet stack too.)
- */
- traverse_call_stack(success_ip, label, label_layout,
- &stack_pointer, ¤t_frame);
-
- /*
- ** Next, traverse the whole of the nondet stack.
+ ** Traverse the stacks, copying the live data in the old heap to the
+ ** new heap.
*/
- traverse_nondet_stack(stack_pointer, max_frame, current_frame);
+ traverse_det_stack(label_layout, stack_pointer, current_frame);
+ traverse_nondet_stack(label_layout, stack_pointer, max_frame,
+ current_frame);
/*
** Copy any roots that are not on the stack.
@@ -534,10 +521,10 @@
/* XXX save this, it appears to get clobbered */
new_hp = MR_virtual_hp;
- MR_agc_dump_stack_frames(first_label, new_heap, first_stack_pointer,
- first_current_frame);
- MR_agc_dump_nondet_stack_frames(first_label, new_heap,
- first_stack_pointer, first_current_frame, first_max_frame);
+ MR_agc_dump_stack_frames(label, new_heap, stack_pointer,
+ current_frame);
+ MR_agc_dump_nondet_stack_frames(label, new_heap,
+ stack_pointer, current_frame, max_frame);
/* XXX restore this, it appears to get clobbered */
MR_virtual_hp = new_hp;
@@ -547,250 +534,161 @@
}
/*
-** Traverse the call stack. This includes all of the det stack
-** and the success continuation frames on the nondet stack. It does not
-** include the failure continuation frames on the nondet stack.
-** (We really only need to traverse the det stack here, but there's no
-** way to do that without traversing the success continuation frames of
-** the nondet stack too.)
+** Traverse the det stack. In order to do this, we need to traverse
+** the call stack, which includes all of the det stack _plus_ the
+** success continuation frames on the nondet stack (but not the failure
+** continuation frames on the nondet stack). But we skip all the nondet
+** frames.
*/
static void
-traverse_call_stack(MR_Code *success_ip, MR_Internal *label,
- const MR_Label_Layout *label_layout,
- MR_Word **ptr_to_stack_pointer, MR_Word **ptr_to_current_frame)
+traverse_det_stack(const MR_Label_Layout *label_layout,
+ MR_Word *stack_pointer, MR_Word *current_frame)
{
- MR_bool top_frame = MR_TRUE;
+ /*
+ ** Record whether this is the first frame on the call stack.
+ ** The very first frame on the call stack is special because it may have
+ ** live registers (the other frames should only have live stack slots).
+ */
+ MR_bool is_first_frame = MR_TRUE;
+
/*
** For each stack frame ...
*/
do {
- const MR_Label_Layout *return_label_layout;
- int short_var_count, long_var_count;
- int i;
- MR_MemoryList allocated_memory_cells = NULL;
- MR_TypeInfoParams type_params;
const MR_Proc_Layout *proc_layout;
-
- if (MR_agc_debug) {
- MR_printlabel(stderr, (MR_Code *) (MR_Word) label->i_addr);
- fflush(NULL);
- }
-
- short_var_count = MR_short_desc_var_count(label_layout);
- long_var_count = MR_long_desc_var_count(label_layout);
-
- /* Get the type parameters from the stack frame. */
+ MR_Stack_Walk_Step_Result result;
+ const char *problem;
/*
- ** We must pass NULL here since the registers have not been saved;
- ** This should be OK, because none of the values used by a procedure
- ** will be stored in registers across a call, since we have no
- ** caller-save registers (they are all callee-save).
- ** XXX except for the topmost frame!
+ ** Traverse this stack frame, if it is a det frame.
*/
- type_params = MR_materialize_type_params_base(label_layout,
- NULL, *ptr_to_stack_pointer, *ptr_to_current_frame);
-
- /* Copy each live variable */
-
- for (i = 0; i < long_var_count; i++) {
- MR_Long_Lval locn;
- MR_PseudoTypeInfo pseudo_type_info;
- MR_TypeInfo type_info;
-
- locn = MR_long_desc_var_locn(label_layout, i);
- pseudo_type_info = MR_var_pti(label_layout, i);
-
- type_info = MR_make_type_info(type_params, pseudo_type_info,
- &allocated_memory_cells);
- copy_long_value(locn, type_info, top_frame,
- *ptr_to_stack_pointer, *ptr_to_current_frame);
- MR_deallocate(allocated_memory_cells);
- allocated_memory_cells = NULL;
- }
-
- for (; i < short_var_count; i++) {
- MR_Short_Lval locn;
- MR_PseudoTypeInfo pseudo_type_info;
- MR_TypeInfo type_info;
-
- locn = MR_short_desc_var_locn(label_layout, i);
- pseudo_type_info = MR_var_pti(label_layout, i);
-
- type_info = MR_make_type_info(type_params, pseudo_type_info,
- &allocated_memory_cells);
- copy_short_value(locn, type_info, top_frame,
- *ptr_to_stack_pointer, *ptr_to_current_frame);
- MR_deallocate(allocated_memory_cells);
- allocated_memory_cells = NULL;
- }
-
- MR_free(type_params);
-
proc_layout = label_layout->MR_sll_entry;
-
- {
- MR_Long_Lval location;
- MR_Long_Lval_Type type;
- int number;
-
- location = proc_layout->MR_sle_succip_locn;
- if (MR_DETISM_DET_STACK(proc_layout->MR_sle_detism)) {
- type = MR_LONG_LVAL_TYPE(location);
- number = MR_LONG_LVAL_NUMBER(location);
- if (type != MR_LONG_LVAL_TYPE_STACKVAR) {
- MR_fatal_error("can only handle stackvars");
- }
- success_ip = (MR_Code *)
- MR_based_stackvar(*ptr_to_stack_pointer, number);
- *ptr_to_stack_pointer = *ptr_to_stack_pointer -
- proc_layout->MR_sle_stack_slots;
- } else {
- /*
- ** Note that curfr always points to an ordinary
- ** procedure frame, never to a temp frame, and
- ** this property continues to hold while we traverse
- ** the nondet stack via the succfr slot. So it is
- ** safe to access the succip and succfr slots
- ** without checking what kind of frame it is.
- */
- /* succip is saved in succip_slot */
- assert(location == -1);
- success_ip = MR_succip_slot(*ptr_to_current_frame);
- *ptr_to_current_frame = MR_succfr_slot(*ptr_to_current_frame);
- }
- label = MR_lookup_internal_by_addr(success_ip);
+ if (MR_DETISM_DET_STACK(proc_layout->MR_sle_detism)) {
+ traverse_frame(is_first_frame, label_layout, stack_pointer,
+ current_frame);
}
-/*
- we should use this code eventually, but it requires a bit of
- a redesign of the code around here.
-
+ /*
+ ** Get the next stack frame
+ */
result = MR_stack_walk_step(proc_layout, &label_layout,
- ptr_to_stack_pointer, ptr_to_current_frame, &problem);
-
+ &stack_pointer, ¤t_frame, &problem);
if (result == MR_STEP_ERROR_BEFORE || result == MR_STEP_ERROR_AFTER) {
MR_fatal_error(problem);
}
-*/
- if (label == NULL) {
- break;
- }
- return_label_layout = label->i_layout;
- label_layout = return_label_layout;
- top_frame = MR_FALSE;
+ is_first_frame = MR_FALSE;
+
} while (label_layout != NULL); /* end for each stack frame... */
}
/*
** Traverse the whole of the nondet stack.
-**
-** XXX The code below is broken. We should use
-** MR_traverse_nondet_stack_from_layout() instead.
-**
-** XXX Will we need correct value of stack_pointer (the pointer
-** to the det stack)?
-** Hopefully not, since I think that for nondet code, all values
-** should be saved on the nondet stack, not the det stack?
*/
+
+struct first_frame_data {
+ const MR_Label_Layout *first_frame_layout;
+ MR_Word *first_frame_curfr;
+};
+
static void
-traverse_nondet_stack(MR_Word *stack_pointer,
- MR_Word *max_frame, MR_Word *current_frame)
+traverse_nondet_stack(const MR_Label_Layout *first_frame_layout,
+ MR_Word *stack_pointer, MR_Word *max_frame, MR_Word *current_frame)
{
- MR_bool top_frame = MR_FALSE; /* XXX always false... is this wrong? */
+ struct first_frame_data data;
+ data.first_frame_layout = first_frame_layout;
+ data.first_frame_curfr = current_frame;
+ MR_traverse_nondet_stack_from_layout(max_frame, first_frame_layout,
+ stack_pointer, current_frame, traverse_nondet_frame,
+ &data);
+}
- while (max_frame > MR_nondet_stack_trace_bottom) {
- MR_Internal *label;
-#if 0
- MR_bool registers_valid;
- int frame_size;
+static void
+traverse_nondet_frame(void *user_data,
+ const MR_Label_Layout *label_layout, MR_Word *stack_pointer,
+ MR_Word *current_frame)
+{
+ MR_bool is_first_frame;
+ struct first_frame_data *data = user_data;
+
+ /*
+ ** Determine whether this is the first frame on the call stack.
+ ** The very first frame on the call stack is special because it may have
+ ** live registers (the other frames should only have live stack slots).
+ */
+ is_first_frame = (current_frame == data->first_frame_curfr
+ && !MR_DETISM_DET_STACK(
+ data->first_frame_layout->MR_sll_entry->MR_sle_detism));
- registers_valid = (max_frame == current_frame);
- frame_size = max_frame - MR_prevfr_slot(max_frame);
+ traverse_frame(is_first_frame, label_layout, stack_pointer, current_frame);
+}
- if (frame_size == MR_NONDET_TEMP_SIZE) {
- if (MR_agc_debug) {
- MR_printlabel(stderr, MR_redoip_slot(max_frame));
- fflush(NULL);
- }
- } else if (frame_size == MR_DET_TEMP_SIZE) {
- if (MR_agc_debug) {
- MR_printlabel(stderr, MR_redoip_slot(max_frame));
- fflush(NULL);
- }
- stack_pointer = MR_tmp_detfr_slot(max_frame); /* XXX ??? */
- } else {
- if (MR_agc_debug) {
- MR_printlabel(stderr, MR_redoip_slot(max_frame));
- fflush(NULL);
- }
- }
-#endif
- label = MR_lookup_internal_by_addr(MR_redoip_slot(max_frame));
- stack_pointer = NULL; /* XXX ??? */
+/*
+** Traverse a stack frame (it could be either a det frame or a nondet frame).
+*/
+static void
+traverse_frame(MR_bool is_first_frame, const MR_Label_Layout *label_layout,
+ MR_Word *stack_pointer, MR_Word *current_frame)
+{
+ int short_var_count, long_var_count;
+ int i;
+ MR_MemoryList allocated_memory_cells = NULL;
+ MR_TypeInfoParams type_params;
+ MR_Short_Lval locn;
+ MR_PseudoTypeInfo pseudo_type_info;
+ MR_TypeInfo type_info;
- if (label != NULL) {
- const MR_Label_Layout *label_layout;
- int short_var_count, long_var_count;
- int i;
- MR_MemoryList allocated_memory_cells = NULL;
- MR_TypeInfoParams type_params;
+ if (MR_agc_debug) {
+ /* XXX we used to print the label name here, but that's
+ not available anymore */
+ printf("traverse_frame: traversing frame with label layout %p\n",
+ (const void *) label_layout);
+ fflush(NULL);
+ }
- label_layout = label->i_layout;
- short_var_count = MR_short_desc_var_count(label_layout);
- long_var_count = MR_long_desc_var_count(label_layout);
- /* var_count = label_layout->MR_sll_var_count; */
+ short_var_count = MR_short_desc_var_count(label_layout);
+ long_var_count = MR_long_desc_var_count(label_layout);
- /*
- ** We must pass NULL here since the registers have not been
- ** saved; This should be OK, because none of the values used
- ** by a procedure will be stored in registers across a call,
- ** since we have no caller-save registers (they are all
- ** callee-save). XXX except for frame which was active
- ** when we entered the garbage collector!
- **
- ** XXX Is it right to pass MR_redofr_slot(max_frame) here?
- ** Why not just max_frame?
- */
- type_params = MR_materialize_type_params_base(label_layout,
- NULL, stack_pointer, MR_redofr_slot(max_frame));
-
- /* Copy each live variable */
- for (i = 0; i < long_var_count; i++) {
- MR_Long_Lval locn;
- MR_PseudoTypeInfo pseudo_type_info;
- MR_TypeInfo type_info;
+ /* Get the type parameters from the stack frame. */
- locn = MR_long_desc_var_locn(label_layout, i);
- pseudo_type_info = MR_var_pti(label_layout, i);
+ /*
+ ** For frames other than the first frame, we must pass NULL for the
+ ** registers here, since the registers have not been saved;
+ ** This should be OK, because none of the values used by a procedure
+ ** will be stored in registers across a call, since we have no
+ ** caller-save registers (they are all callee-save).
+ */
+ type_params = MR_materialize_type_params_base(label_layout,
+ (is_first_frame ? MR_fake_reg : NULL),
+ stack_pointer, current_frame);
+
+ /* Copy each live variable */
- type_info = MR_make_type_info(type_params, pseudo_type_info,
- &allocated_memory_cells);
- copy_long_value(locn, type_info, top_frame,
- stack_pointer, current_frame);
- MR_deallocate(allocated_memory_cells);
- allocated_memory_cells = NULL;
- }
+ for (i = 0; i < long_var_count; i++) {
+ locn = MR_long_desc_var_locn(label_layout, i);
+ pseudo_type_info = MR_var_pti(label_layout, i);
- for (; i < short_var_count; i++) {
- MR_Short_Lval locn;
- MR_PseudoTypeInfo pseudo_type_info;
- MR_TypeInfo type_info;
+ type_info = MR_make_type_info(type_params, pseudo_type_info,
+ &allocated_memory_cells);
+ copy_long_value(locn, type_info, is_first_frame,
+ stack_pointer, current_frame);
+ MR_deallocate(allocated_memory_cells);
+ allocated_memory_cells = NULL;
+ }
- locn = MR_short_desc_var_locn(label_layout, i);
- pseudo_type_info = MR_var_pti(label_layout, i);
+ for (; i < short_var_count; i++) {
+ locn = MR_short_desc_var_locn(label_layout, i);
+ pseudo_type_info = MR_var_pti(label_layout, i);
- type_info = MR_make_type_info(type_params, pseudo_type_info,
- &allocated_memory_cells);
- copy_short_value(locn, type_info, top_frame,
- stack_pointer, current_frame);
- MR_deallocate(allocated_memory_cells);
- allocated_memory_cells = NULL;
- }
- }
- max_frame = MR_prevfr_slot(max_frame);
+ type_info = MR_make_type_info(type_params, pseudo_type_info,
+ &allocated_memory_cells);
+ copy_short_value(locn, type_info, is_first_frame,
+ stack_pointer, current_frame);
+ MR_deallocate(allocated_memory_cells);
+ allocated_memory_cells = NULL;
}
+
+ MR_free(type_params);
}
/*
@@ -805,15 +703,15 @@
*/
static void
-copy_long_value(MR_Long_Lval locn, MR_TypeInfo type_info, MR_bool copy_regs,
- MR_Word *stack_pointer, MR_Word *current_frame)
+copy_long_value(MR_Long_Lval locn, MR_TypeInfo type_info,
+ MR_bool is_first_frame, MR_Word *stack_pointer, MR_Word *current_frame)
{
int locn_num;
locn_num = MR_LONG_LVAL_NUMBER(locn);
switch (MR_LONG_LVAL_TYPE(locn)) {
case MR_LONG_LVAL_TYPE_R:
- if (copy_regs) {
+ if (is_first_frame) {
MR_virtual_reg(locn_num) = MR_agc_deep_copy(
MR_virtual_reg(locn_num), type_info,
MR_ENGINE(MR_eng_heap_zone2->min),
@@ -850,6 +748,14 @@
break;
case MR_LONG_LVAL_TYPE_HP:
+ /*
+ ** Currently we don't support heap reclamation on failure
+ ** with accurate GC, so we shouldn't get any saved HP values.
+ ** XXX To support this, saved heap pointer values would need to be
+ ** updated to point to the new heap... but this is tricky -- see the
+ ** CVS log message for revision 1.21 of runtime/mercury_accurate_gc.c.
+ */
+ MR_fatal_error("copy_long_value: MR_LONG_LVAL_TYPE_HP");
break;
case MR_LONG_LVAL_TYPE_SP:
@@ -857,9 +763,11 @@
case MR_LONG_LVAL_TYPE_INDIRECT:
/* XXX Tyson will have to write the code for this */
+ MR_fatal_error("NYI: copy_long_value on MR_LONG_LVAL_TYPE_INDIRECT");
break;
case MR_LONG_LVAL_TYPE_UNKNOWN:
+ MR_fatal_error("copy_long_value: MR_LONG_LVAL_TYPE_UNKNOWN");
break;
default:
@@ -870,14 +778,14 @@
static void
copy_short_value(MR_Short_Lval locn, MR_TypeInfo type_info,
- MR_bool copy_regs, MR_Word *stack_pointer,
+ MR_bool is_first_frame, MR_Word *stack_pointer,
MR_Word *current_frame)
{
int locn_num;
switch (MR_SHORT_LVAL_TYPE(locn)) {
case MR_SHORT_LVAL_TYPE_R:
- if (copy_regs) {
+ if (is_first_frame) {
locn_num = MR_SHORT_LVAL_NUMBER(locn);
MR_virtual_reg(locn_num) =
MR_agc_deep_copy(
Index: runtime/mercury_stack_trace.c
===================================================================
RCS file: /home/mercury1/repository/mercury/runtime/mercury_stack_trace.c,v
retrieving revision 1.58
diff -u -d -r1.58 mercury_stack_trace.c
--- runtime/mercury_stack_trace.c 13 Nov 2003 05:36:21 -0000 1.58
+++ runtime/mercury_stack_trace.c 14 Nov 2003 04:30:24 -0000
@@ -242,8 +242,8 @@
return MR_STEP_ERROR_BEFORE;
}
+ location = entry_layout->MR_sle_succip_locn;
if (MR_DETISM_DET_STACK(determinism)) {
- location = entry_layout->MR_sle_succip_locn;
type = MR_LONG_LVAL_TYPE(location);
number = MR_LONG_LVAL_NUMBER(location);
@@ -256,6 +256,16 @@
*stack_trace_sp_ptr = *stack_trace_sp_ptr -
entry_layout->MR_sle_stack_slots;
} else {
+ /* succip is always saved in succip_slot */
+ assert(location == -1);
+ /*
+ ** Note that curfr always points to an ordinary
+ ** procedure frame, never to a temp frame, and
+ ** this property continues to hold while we traverse
+ ** the nondet stack via the succfr slot. So it is
+ ** safe to access the succip and succfr slots
+ ** without checking what kind of frame it is.
+ */
success = MR_succip_slot(*stack_trace_curfr_ptr);
*stack_trace_curfr_ptr = MR_succfr_slot(*stack_trace_curfr_ptr);
}
@@ -578,8 +588,7 @@
{
MR_Traverse_Nondet_Frame_Func_Info *func_info = info;
if (category != MR_TERMINAL_TOP_FRAME_ON_SIDE_BRANCH) {
- func_info->func(func_info->func_data, top_layout, base_sp, base_curfr,
- level_number);
+ func_info->func(func_info->func_data, top_layout, base_sp, base_curfr);
}
}
Index: runtime/mercury_stack_trace.h
===================================================================
RCS file: /home/mercury1/repository/mercury/runtime/mercury_stack_trace.h,v
retrieving revision 1.30
diff -u -d -r1.30 mercury_stack_trace.h
--- runtime/mercury_stack_trace.h 11 Nov 2003 09:12:26 -0000 1.30
+++ runtime/mercury_stack_trace.h 14 Nov 2003 04:29:09 -0000
@@ -102,7 +102,7 @@
typedef void MR_Traverse_Nondet_Frame_Func(void *user_data,
const MR_Label_Layout *layout, MR_Word *base_sp,
- MR_Word *base_curfr, int level_number);
+ MR_Word *base_curfr);
extern void MR_traverse_nondet_stack_from_layout(
MR_Word *maxfr, const MR_Label_Layout *label_layout,
--
Fergus Henderson <fjh at cs.mu.oz.au> | "I have always known that the pursuit
The University of Melbourne | of excellence is a lethal habit"
WWW: <http://www.cs.mu.oz.au/~fjh> | -- the last words of T. S. Garp.
--------------------------------------------------------------------------
mercury-reviews mailing list
post: mercury-reviews at cs.mu.oz.au
administrative address: owner-mercury-reviews at cs.mu.oz.au
unsubscribe: Address: mercury-reviews-request at cs.mu.oz.au Message: unsubscribe
subscribe: Address: mercury-reviews-request at cs.mu.oz.au Message: subscribe
--------------------------------------------------------------------------
More information about the reviews
mailing list