[comp.unix.wizards] alloca

schwartz@swatsun.uucp (Scott Schwartz) (06/06/88)

In article <8017@brl-smoke.ARPA> gwyn@brl.arpa (Doug Gwyn (VLD/VMB) <gwyn>) writes:
>For a limited class of applications, alloca() can be useful.
>However, I don't use it myself.  My applications all use a more
>disciplined approach to memory allocation.

One nice C extension that gcc provides is variable sized automatic
arrays. I.e.
	foo(s)
	int s;
	{
		int a[s];
	}

I think this obviates the need for alloca() in most (all?) cases.
Too late to get it in ansi, I suppose.

-- 
Scott Schwartz,  schwartz@swarthmore.edu,  psuvax1!vu-vlsi!swatsun!schwartz

karl@haddock.ISC.COM (Karl Heuer) (06/06/88)

In article <1874@thebes.UUCP> schwartz@thebes.UUCP (Scott Schwartz) writes:
>[gcc allows "foo(s) int s; { int a[s]; ... }".]
>I think this obviates the need for alloca() in most (all?) cases.
>Too late to get it in ansi, I suppose.

Yes, I'm sure it's much too late to get it into ANSI C-88, if indeed it ever
had a chance.  But this is exactly the right way to get it into C-99 (or
whenever __STDC__==2 comes out): by including it as a nonconforming extension
in popular compilers, the ramifications ought to be well-understood by then.

Karl W. Z. Heuer (ima!haddock!karl or karl@haddock.isc.com), The Walking Lint
Followups to comp.lang.c.

ted%nmsu.csnet@relay.cs.net (06/10/88)

With respect to alloca,

    From: Neil Webber <nw@palladium.uucp>
.
    I believe that the real solution to this class of problem lies in
    the constructor/destructor model of C++ and other such languages.

But isn't the following idiom for co-routines useful? (and how would it
be done with c++ constructors, without alloca)?

#include <stdio.h>
#include <setjmp.h>

/* set up a continuation with some free stack space */
#define cospawn(jb,fun,space) {if (_setjmp(jb)) fun();else alloca(space);}

/* transfer from one co-routine to another, saving our context */
#define cojmp(us,them) {if (!_setjmp(us)) _longjmp(them,1);}

jmp_buf A,B,C;

main()
{
    int i;
    void printa();
    void printb();
    
    cospawn(A,printa,1000);
    cospawn(B,printb,1000);
    for (i=0;i<5;i++) {
	cojmp(C,A);		/* transfer to A */
	printf("c\n");		/* note the return */
	cojmp(C,A);
	printf("C\n");
    }
}

/* print an 'a', transfer to B, then print an 'A' and transfer to B */
void printa(them)
continuation them;
{
    while (1) {
	printf("a ");fflush(stdout);
	cojmp(A,B);
	printf("A ");fflush(stdout);
	cojmp(A,B);
    }
}

/* print an 'b', transfer to C, then print an 'B' and transfer to C */
void printb(them)
continuation them;
{
    while (1) {
	printf("b ");fflush(stdout);
	cojmp(B,C);
	printf("B ");fflush(stdout);
	cojmp(B,C);
    }
}

It is of course possible to do the same by using malloc and munging on
the contents of the jmp_buf, but many implementations get very
paranoid when they suddenly find their stack pointer in the middle of
the heap.  Another alternative is for main() to allocate separate
stacks as local variables of type char [].  This still requires
munging the jmp_buf.  All techniques which require changing the
jmp_buf need to know which direction the stack grows, and something
about whether a push involves pre or post de(in)crement.  This idiom
has the advantage of not depending on the structure of a jmp_buf, nor
does it depend on the detailed operation of alloca (only on the
correct operation).

chris@mimsy.UUCP (Chris Torek) (06/10/88)

In article <16126@brl-adm.ARPA> ted%nmsu.csnet@relay.cs.net writes:
[alloca + setjmp/longjmp for coroutines]

longjmp is not suitable for coroutines because it is valid for longjmp
to attempt to unwind the stack in order to find the corresponding
setjmp, and it is therefore legal for longjmp to abort if it is
attempting to jump the `wrong way' on the stack.
-- 
In-Real-Life: Chris Torek, Univ of MD Comp Sci Dept (+1 301 454 7163)
Domain:	chris@mimsy.umd.edu	Path:	uunet!mimsy!chris

djones@megatest.UUCP (Dave Jones) (06/11/88)

From article <11902@mimsy.UUCP>, by chris@mimsy.UUCP (Chris Torek):
> In article <16126@brl-adm.ARPA> ted%nmsu.csnet@relay.cs.net writes:
> [alloca + setjmp/longjmp for coroutines]
> 
> longjmp is not suitable for coroutines because it is valid for longjmp
> to attempt to unwind the stack in order to find the corresponding
> setjmp, and it is therefore legal for longjmp to abort if it is
> attempting to jump the `wrong way' on the stack.
> -- 
> In-Real-Life: Chris Torek, Univ of MD Comp Sci Dept (+1 301 454 7163)
> Domain:	chris@mimsy.umd.edu	Path:	uunet!mimsy!chris

I'm no C wizard -- BSD4.2 and Sun3-OS are the only C's I've ever used --  
but it seems to me that longjmp is the most suitable technique going,
by default.

What else is there?  You could use [abuse?] sigvec and kill.  But if you 
use separate stacks of fixed sizes, they can overflow with disastrous 
consequences.  And -- correct me if I'm wrong -- more systems have
setjmp/longjmp/alloca than have sigvec and kill.

Or you could use a smattering of assembler.   But, it will certainly run 
on more kinds of machines if written in C than it would if written in 
assembler.

And to answer your objection about unwinding the stack,  you can see to 
it that the stack is restored _before_ you do the longjmp, so the longjmp 
can "unwind" as it pleases.

I recently wrote a little discrete-event-simulator using 
setjmp/longjmp/alloca to do lightweight processes.

We hope to run it on both Sun3s and IBM PCs.  Haven't tried it on the PCs 
yet, so I don't know if it works there or not.  Do I have a surprise in
store for me?

Here's what I did.  The main simulator loop calls alloca(1) to find
the bottom of the part of the stack that lightweight processes will
be using.  It squirrels that address away in the variable {stack_bottom}.
To start a process initially, it just calls the process's procedure.
Then the simulator and the process trade setjmp/longjmp cycles through
a couple of jmpbufs.

Well, you'll see.

Is there some gotcha that will break this code on some systems?
If so, is there a better [more machine independent] way?


/***********************************************************************
** Run the simulation, stopping after some number of ticks, unless
** all processes exit, or some process calls PSim_stop() first.
***********************************************************************/

unsigned long
PSim_run(obj, ticks)
  Simulation* obj;
  unsigned long ticks;
{
  obj->stack_bottom = (char*)alloca(1);
  obj->stop_time += ticks;

  while(!obj->quit)
    {
      /* Get a busy process from the busy-queue */
      obj->active = (Process*) PQ_pop(&obj->busy);
      
      /* If all processes are finished, or are waiting on 
      ** a semaphore, we are blocked, and must exit the simulation.
      */
      
      if(obj->active==0)
	goto end_simulation;
      
      { register Process *active = obj->active;
	
	/* Update the time to the time of the active process */
	obj->time = active->busy_until;
	
	if( obj->time >= obj->stop_time)
	  goto end_simulation;
	
	if(setjmp(active->suspend) == 0)
	  if(active->stack_save == 0)
	    /* Process has not yet started. Call its start-procedure. */
	    active->return_value =
	      (*(active->start))(obj);
	  else
	    { /* Process has been suspended, and will now be restarted. */
	      
	      /* allocate the restarting process's stack. */
	      alloca( active->stack_size );
	      
	      /* restore it */
	      bcopy(  active->stack_save, active->stack_real,
		    active->stack_size);
	      sfree(active->stack_save);
	      active->stack_save = 0;
	      
	      /* restart the process */
	      longjmp(active->restart, 1);
	    }
      }
    }
  
 end_simulation:
  cleanup(obj);
  return obj->time;
}


static 
suspend_active_proc(obj)
  register Simulation* obj;
{
  char* stack_top = (char*)alloca(1);
  long size = abs(obj->stack_bottom - stack_top);
  register Process* active = obj->active;
  
  active->stack_save = (char*)smalloc(size);
  active->stack_real = min(stack_top, obj->stack_bottom);
  active->stack_size = size;

  if(setjmp(active->restart) == 0)
    {       
      /* copy the stack and return to the simulator. */
      bcopy( active->stack_real, active->stack_save, size);
      longjmp(active->suspend, 1);
    }
}

chris@mimsy.UUCP (Chris Torek) (06/11/88)

Put it this way:  If you have a stack or an emulation of a stack (as
required by recursive functions), and if `alloca' is a compiler
builtin, the concept can be implemented.  Hence alloca can be *made*
portable to any C compiler, if only by fiat (declare that henceforth
`alloca' is a keyword or is otherwise reserved).

Now the problem becomes one of convincing compiler writers that
alloca (possibly by some other name) is worth adding as a reserved
word, or (on some systems) simply writing it in assembly (either as
a routine or with `inline assembly').

Note that alloca is not a panacea, and that it can largely be simulated
with dynamically sized arrays, as in

	int n = get_n();
	{
		char like_alloca[n];
		...
	}

These are not identical concepts, but they are interrelated.  Whether
one is `more important' or `more useful' than another I will not venture
to say.
-- 
In-Real-Life: Chris Torek, Univ of MD Comp Sci Dept (+1 301 454 7163)
Domain:	chris@mimsy.umd.edu	Path:	uunet!mimsy!chris

henry@utzoo.uucp (Henry Spencer) (06/14/88)

> I'm no C wizard... but it seems to me that longjmp is the most suitable 
> technique going, by default...  What else is there?  You could use [abuse?]
> sigvec and kill...

In the same way that you're abusing longjmp, you mean?  The fact is, there
is -->**NO**<-- portable way to accomplish what you're trying to do.  None.
Coroutines simply cannot be done without some degree of cooperation from
the language or the compiler.  C doesn't cooperate on the language level,
so you're stuck.  Yes, you can write code that will more-or-less work, so
long as the compiler doesn't spring any surprises on you.  Portable it isn't.
That being the case, the proper approach is to admit defeat, study your
compiler carefully, and write twenty lines of assembler to do things right.
-- 
Man is the best computer we can      |  Henry Spencer @ U of Toronto Zoology
put aboard a spacecraft. --Von Braun | {ihnp4,decvax,uunet!mnetor}!utzoo!henry

mouse@mcgill-vision.UUCP (der Mouse) (06/18/88)

In article <16100@brl-adm.ARPA>, rbj@icst-cmr.arpa (Root Boy Jim) writes:
[Other attributions are Neil Webber <nw@palladium.uucp> and
ted%nmsu.csnet@relay.cs.net, but it's not clear who wrote what.]
> 	   2) function calling conventions -- unless alloca() is
> 	      built into the C compiler it has to implemented as
> 	      a C callable function, not an inline stack adjustment.
> It's hard to figure out exactly what you mean here.

I would imagine that he means more or less what he said.  If alloca()
is not specially known to the compiler (ie, "built in"), it will
compile into a call to a function.  It therefore can't be an inline
stack adjustment.  That's all.  (The function called will wind up
adjusting the stack, yes, but that's not "inline".)

					der Mouse

			uucp: mouse@mcgill-vision.uucp
			arpa: mouse@larry.mcrcim.mcgill.edu

gallen@apollo.uucp (Gary Allen) (06/20/88)

In article <1169@mcgill-vision.UUCP> mouse@mcgill-vision.UUCP (der Mouse) writes:
>In article <16100@brl-adm.ARPA>, rbj@icst-cmr.arpa (Root Boy Jim) writes:
>[Other attributions are Neil Webber <nw@palladium.uucp> and
>ted%nmsu.csnet@relay.cs.net, but it's not clear who wrote what.]
>> 	   2) function calling conventions -- unless alloca() is
>> 	      built into the C compiler it has to implemented as
>> 	      a C callable function, not an inline stack adjustment.
>> It's hard to figure out exactly what you mean here.
>
>I would imagine that he means more or less what he said.  If alloca()
>is not specially known to the compiler (ie, "built in"), it will
>compile into a call to a function.  It therefore can't be an inline
>stack adjustment.  That's all.  (The function called will wind up
>adjusting the stack, yes, but that's not "inline".)

Not necessarily. If your C compiler supports the 'asm' statement, it is possible
to implement 'alloca' as a macro to adjust the stack pointer and then jam its
contents into a register variable. It obviously ain't pretty and it obviously
ain't portable, but it does work.

Gary Allen
Apollo Computer
Chelmsford, MA
{decvax,umix,yale}!apollo!gallen

"I don't need life, I'm high on drugs."

gwyn@smoke.BRL.MIL (Doug Gwyn) (04/22/89)

In article <15945@rpp386.Dallas.TX.US> jfh@rpp386.Dallas.TX.US (John F. Haugh II) writes:
>In article <1883@thor.acc.stolaf.edu> mike@mike@stolaf.edu writes:
>>To repeat: alloca is EVIL.
>It doesn't have to be.

You appear to have missed the previous discussion.
alloca() simply cannot be reasonably provided in some C environments.
If your application depends on alloca(), you have limited its portability.
There is no situation I know of when use of alloca() is necessary.

les@chinet.chi.il.us (Leslie Mikesell) (04/22/89)

In article <10090@smoke.BRL.MIL> gwyn@brl.arpa (Doug Gwyn) writes:

>>>To repeat: alloca is EVIL.
>>It doesn't have to be.

>You appear to have missed the previous discussion.
>alloca() simply cannot be reasonably provided in some C environments.
>If your application depends on alloca(), you have limited its portability.
>There is no situation I know of when use of alloca() is necessary.

Likewise there is no situation where the use of local variables is necessary.

There are two situations which are helped by the use of alloca():
1) dividing the dynamically allocated space into reasonably sized chunks
to reduce the time malloc() and free() need to search their lists, and
2) providing the ability to longjmp() out of a function (or just return)
with the no longer needed memory automatically released (just like local
variables).

In addition to this, alloca() is conceptually the same as using local
variables, malloc() is conceptually the same as using global varibles
and the same arguments for the use of each should apply.  Consider that
if you longjmp() out of a function that no longer needs its malloc()'ed
memory you *must* have stashed a global pointer in order to free it.
Now, why can't the machines that can dynamically allocate space off the
stack have an ordinary alloca() and those that can't provide a fake one
which uses malloc() but tags the blocks such that calling alloca() or
malloc() again will automatically free() any blocks alloca()'ed from
a function that is no longer active?  This would at least provide the
conceptual advantages if not the efficiency.

Les Mikesell

gwyn@smoke.BRL.MIL (Doug Gwyn) (04/23/89)

In article <8261@chinet.chi.il.us> les@chinet.chi.il.us (Leslie Mikesell) writes:
>Now, why can't the machines that can dynamically allocate space off the
>stack have an ordinary alloca() and those that can't provide a fake one
>which uses malloc() but tags the blocks such that calling alloca() or
>malloc() again will automatically free() any blocks alloca()'ed from
>a function that is no longer active?

That's indeed more or less how my alloca() emulation works, but there are
numerous practical details that have to be taken care of, and such an
emulation simply cannot do the "right thing" under all circumstances.

Having gone through the exercise, I can assure you that alloca() is not a
good idea in C as it exists.  Some similar functionality might be useful
in a new programming language design (not C).  In writing many hundreds of
thousands of lines of C source code, I've never felt a strong enough need
for alloca() to cause me to use it.  Normally when I dynamically allocate
storage, it needs to hang around after the scope of the function in which
it was allocated terminates.  When it doesn't, normally automatic variables
suffice.  The few remaining instances aren't a great deal of trouble and
I use malloc()/free() for those.

ka@june.cs.washington.edu (Kenneth Almquist) (04/24/89)

les@chinet.chi.il.us (Leslie Mikesell) writes:
> There are two situations which are helped by the use of alloca():
> 1) dividing the dynamically allocated space into reasonably sized chunks
> to reduce the time malloc() and free() need to search their lists, and
> 2) providing the ability to longjmp() out of a function (or just return)
> with the no longer needed memory automatically released (just like local
> variables).

Ash uses a memory allocator with stack-like semantics:

	struct stackmark smark;		/* stack mark saves stack location */
	setstackmark(&smark);		/* smark = stack pointer */
	for (;;) {
		...
		p = stalloc(n);		/* allocate space off stack */
		...
	}
	/* now free everything allocated by the stalloc calls */
	popstackmark(&smark);		/* stack pointer = smark */

This is more flexible than alloca because routines can return blocks
allocated via stalloc.  It solves the problems you mention.  (It deals
with problem 1 by allocating reasonably large blocks from malloc and
doling them out in smaller pieces.)  Its main advantage is that it can
be implemented reliably on any machine that can support C.

Ash will appear in comp.sources.unix next week if you want to steal the
code.
				Kenneth Almquist

kjones@talos.UUCP (Kyle Jones) (04/24/89)

In article <1883@thor.acc.stolaf.edu> mike@mike@stolaf.edu writes:
 > I'm the author of GNU grep, and I firmly believe that alloca is Evil.
 > Don't use it in new programs!

Why not?  So compiler vendors can stop supporting it?  Alloca is a very
useful function to have on systems that can support it.  I believe that
it is useful enough that it should be demanded on all systems capable of
supporting it.  For the others, I can cobble a replacement that uses
malloc and put wrappers around setjmp and longjmp calls to free this
memory.  I've done this before, it's not hard.  But using a the alloca
where available is a big win in terms of time taken to allocate the
memory and programming ease when cleaning up after a longjmp.

Why throw alloca away on systems that can support it?  It's a simple
idea, stupefyingly easy to implement on stack-based machines, and makes
a certain class of programming tasks easier.

jfh@rpp386.Dallas.TX.US (John F. Haugh II) (04/24/89)

In article <10090@smoke.BRL.MIL> gwyn@brl.arpa (Doug Gwyn) writes:
>There is no situation I know of when use of alloca() is necessary.

What about the situations when the use of an automatic variable is
necessary?
-- 
John F. Haugh II                        +-Quote of the Week:-------------------
VoiceNet: (214) 250-3311   Data: -6272  | "When everyone else complies
InterNet: jfh@rpp386.Dallas.TX.US       |  I will too ..."
UucpNet : <backbone>!killer!rpp386!jfh  +-------------------  -- Doug Davis  --

flee@shire.cs.psu.edu (Felix Lee) (04/25/89)

Are there uses of alloca() that can't be handled by variable-length
automatic arrays:
	{ int n = 10; { char s[n]; } }
which are allowed by GNU cc?

Are there architectures that will not allow you to implement this?
--
Felix Lee	flee@shire.cs.psu.edu	*!psuvax1!shire!flee

jdc@naucse.UUCP (John Campbell) (04/27/89)

From article <11344@tekecs.GWD.TEK.COM>, by andrew@frip.wv.tek.com (Andrew Klossner):
>> Are there architectures that will not allow you to implement this?
  (alloca)
 
> No, with compiler assist you can always implement it; but its
> complexity can approach that of marking all mallocs and teaching the
> compiler (and longjmp) to call un_malloc_temporaries on each procedure
> return.

Ah, but that's the rub.  What if you already have your compiler.  In
other words, what if you know of some nice useful software that *someone*
sprinkled alloca's in AND you have some silly vendor supplied C compiler.
In one case (at least) you'll have to write a stack manipulator (not in
'C') and even compile without optimizing.  Often, it turns out, you'll
find (with the vendor's compiler) that you're better off rewriting the
offending section in order to turn optimize back on. 

I don't remember alloca() making it to the ANSI standard library.  I hereby
request that only those who write *useless* code use alloca()! :-)
-- 
	John Campbell               ...!arizona!naucse!jdc
                                    CAMPBELL@NAUVAX.bitnet
	unix?  Sure send me a dozen, all different colors.

dag@per2.UUCP (Daniel A. Glasser) (04/29/89)

In article <1381@naucse.UUCP>, jdc@naucse.UUCP (John Campbell) writes:
> I don't remember alloca() making it to the ANSI standard library.  I hereby
> request that only those who write *useless* code use alloca()! :-)

Here here!

The standard does not have alloca() anywhere in it.  The rationale document
does mention the omission in 4.10.3 -- (quoted without permission)

    Some implementations provide a function (often called alloca) which
    allocates the requested object from automatic storage;  the object is
    automatically freed when the calling function exits.  _S_u_c_h _a _f_u_n_c_t_i_o_n
    _i_s _n_o_t _e_f_f_i_c_i_e_n_t_l_y _i_m_p_l_e_m_e_n_t_a_b_l_e _i_n _a _v_a_r_i_e_t_y _o_f _e_n_v_i_r_o_n_m_e_n_t_s_, _s_o _i_t
    _w_a_s _n_o_t _a_d_o_p_t_e_d _i_n _t_h_e _S_t_a_n_d_a_r_d.

-- 
 _____________________________________________________________________________
    Daniel A. Glasser                           One of those things that goes
    uwvax!per2!dag                              "BUMP!!!(ouch)" in the night. 
 ---Persoft, Inc.---------465 Science Drive-------Madison, WI 53711-----------

scs@adam.pika.mit.edu (Steve Summit) (04/29/89)

In article <8261@chinet.chi.il.us> les@chinet.chi.il.us (Leslie Mikesell) writes:
%In article <10090@smoke.BRL.MIL> gwyn@brl.arpa (Doug Gwyn) writes:
%>[earlier posters wrote:]
%>>>To repeat: alloca is EVIL.
%>>It doesn't have to be.
%>There is no situation I know of when use of alloca() is necessary.
%There are two situations which are helped by the use of alloca():
%...
%2) providing the ability to longjmp() out of a function (or just return)
%with the no longer needed memory automatically released

longjmp is evil.

gwyn@smoke.BRL.MIL (Doug Gwyn) (04/29/89)

In article <10939@bloom-beacon.MIT.EDU> scs@adam.pika.mit.edu (Steve Summit) writes:
>longjmp is evil.

Well, at least it's worse than "goto".
Some people have found reasonable (albeit obscure) uses for longjmp.
I don't use it myself.

les@chinet.chi.il.us (Leslie Mikesell) (05/02/89)

In article <10177@smoke.BRL.MIL> gwyn@brl.arpa (Doug Gwyn) writes:

>>longjmp is evil.

>Well, at least it's worse than "goto".
>Some people have found reasonable (albeit obscure) uses for longjmp.
>I don't use it myself.

Here is a use:
 A print spooler output process accepts commands to print files on a
 pipe from its parent (the scheduler).  An interactive control program
 can cause the scheduler to send signals to the output process.  One
 signal causes the current job to be restarted from the beginning,
 another causes the current job to be discarded and the next command to
 be read from the input pipe.  There are many sub-functions involved
 in the output process.  Using longjmp() out of the signal handler
 allows rapid response from anywhere in the program doing only the
 clean-up needed for the current state.
 Is there any other way to handle a situation like this?  Testing a
 global variable set in the signal handler at all the appropriate
 places would add quite a bit of overhead.

Les Mikesell

jdickson@spanza.jpl.nasa.gov (Jeff Dickson) (05/04/89)

	I agree with Root Boy. I use alloca() exclusively on Sun O.S. 4.0 to
set aside a large chunk of space in which to do my own memory allocation
-deallocation out of. I first tried to use malloc/free, but had chronic prob-
lems with it. It wasn't so much that those calls failed, it was that they
failed and/or caused other unexplainable problems when they were called many
times a second. My code doesn't use much memory, but it does do many allocations
and deallocations per second. I have found however, that I can do essentially
the same thing as alloca() by declaring a large automatic array of character. 
In that respect, it isn't totally necessary that I use alloca().
	I really don't understand what the big fuss over alloca() is. So what
that it does some weird things to the code. At least IT works. But then again,
my code may not use it in the insane sense that others are refering to. My 
code does not call it often. In fact, I largely call it in the beginning of
the process.

				jdickson@zook.jpl.nasa.gov

clive@ixi.UUCP (Clive) (05/05/89)

I don't know where all this alloca stuff came from, but BCPL had a
nice function (I forget the name, but let's call it stack_malloc for now)
which called another function with an array of specified size on
the stack - thus when you left this function (*however* you did,
including longjumps), the array went away.

Translating to C, the definition would be something like:

      int stack_malloc (f, n)
      int (*f) (/* char [], int */);
      int n;
      {
          /* The body of this function is equivalent to the
             (illegal) code:

          char a [n];  /* This is illegal */

          return (*f) (a, n);
      }

[BCPL was typeless - it was much less painful]

The library code for this would do something like:
(assume args in r1 and r2, and ascending stack)

      r3 = r1
      r1 = sp
      sp += r2
      block copy the stack frame at fp (the one generated
        by the call) up by r2
      fp += r2
      /* the 'sp just before call' field in the frame is unaltered */
      jump @r3

I forget the return sequence used on this system, but this had the
right effect - the array was effectively in the caller's stack frame,
but when they returned, the sp had dropped down past it.
-- 
Clive D.W. Feather           clive@ixi.uucp
IXI Limited                  ...!mcvax!ukc!acorn!ixi!clive (untested)
                             +44 223 462 131