[comp.lang.c++] Implementing LISP in C++

jordan@aero.org (Larry M. Jordan) (03/13/91)

I'm endeavoring to implement an interpreter/compiler for a LISP-like language
in C++.  The question I wish to have answered is "Can I implement a
type-less lang. like LISP in C++ without resorting at some point to
actually asking an object for its type?" I just don't see how not to.
Also, I'm implementing a simple mark/sweep garbage collector which takes 
liberties with Cons cell pointers to avoid recursion.  I don't see a 
reasonable way to do this without asking objects during the mark phase
'are you a Cons?'. (Of course, I will need to implement the predicate 
functions like 'null, consp, listp, etc.').   I'd also like some
remarks on the 'reasonableness' of the implementation scheme I've 
chosen (to be described next paragraph).

Techniques I've seen in the past for implementing LISP objects in
languages like C or Modula-2 define an enumeration type and create
a discriminated union using the enum as a type 'tag' or discriminant.
XLISP and XSCHEME are implemented this way and it seems perfectly
reasonable.  I've taken a different approach since I'm using C++.
I'm implementing LISP objects as C++ classes--Cons, Vector, Fixnum, 
Closure, Environment...--derived from a common base class Node.  Most
methods are virtual and defined in class Node with a call to an error 
routine.  It is each derived class's responsiblity to redefine the
methods which are meaningfull--i.e.  the Cons class redefines
'car() and cdr()':

  class Cons : Node {
    Node *theCar;
    Node *theCdr;
    virtual Node *car() { return theCar; }
    virtual Node *cdr() { return theCdr; }
    virtual void setCar(Node *n) { theCar = n; }
    virtual void setCdr(Node *n) { theCdr = n; }
    ...
  };

Consequently, there are a large number of virtual methods in the base
class Node--20+ and growing.  At first I thought there would be a lot
of space dedicated to large vft's--sizeof(char*) * 
NumberOfVirtualMethodsInBase for each LISP object
derived from Node!  I was concerned about using vast amounts of memory
for vft's.  But then I considered what the alternative would be--
using code space to check the type of the object I'm about to 
reference as a 'Cons' object (which would be an operation that
would widely distributed throughout the system) rather that just 
envoke the method.  I get the error checking at the expense of a
calling a virtual method.  I'm convinced that this experiment is 
worthwhile to continue.

Thanks in advance.

--Larry

dsr@mir.mitre.org (Douglas S. Rand) (03/13/91)

In article <1991Mar12.221015.22144@aero.org>, jordan@aero.org (Larry M. Jordan) writes:
> I'm endeavoring to implement an interpreter/compiler for a LISP-like language
> in C++.  The question I wish to have answered is "Can I implement a
> type-less lang. like LISP in C++ without resorting at some point to
> actually asking an object for its type?" I just don't see how not to.
> Also, I'm implementing a simple mark/sweep garbage collector which takes 
> liberties with Cons cell pointers to avoid recursion.  I don't see a 
> reasonable way to do this without asking objects during the mark phase
> 'are you a Cons?'. (Of course, I will need to implement the predicate 
> functions like 'null, consp, listp, etc.').   I'd also like some
> remarks on the 'reasonableness' of the implementation scheme I've 
> chosen (to be described next paragraph).
> 

Well to start with,  why not have a virtual function like GCmarkPhase
which does something with conses or their decendents but not with other
LISP objects?

> Techniques I've seen in the past for implementing LISP objects in
> languages like C or Modula-2 define an enumeration type and create
> a discriminated union using the enum as a type 'tag' or discriminant.
> XLISP and XSCHEME are implemented this way and it seems perfectly
> reasonable.  I've taken a different approach since I'm using C++.

It is if the language doesn't do this for you with virtuals.

> ... some text deleted ...
> Consequently, there are a large number of virtual methods in the base
> class Node--20+ and growing.  At first I thought there would be a lot
> of space dedicated to large vft's--sizeof(char*) * 
> NumberOfVirtualMethodsInBase for each LISP object
> derived from Node!  I was concerned about using vast amounts of memory
> for vft's.  But then I considered what the alternative would be--

Why, do you think each instance would have a copy of the vtab?  The
vtab is equivalent to a class variable.  There is one copy per class.

> using code space to check the type of the object I'm about to 
> reference as a 'Cons' object (which would be an operation that
> would widely distributed throughout the system) rather that just 
> envoke the method.  I get the error checking at the expense of a

And the type check via the virtual function call is much faster and
more foolproof.

> calling a virtual method.  I'm convinced that this experiment is 
> worthwhile to continue.
> 

Have fun.

-- 
Douglas S. Rand 
Internet:   <dsrand@mitre.org>
Snail:	    MITRE, Burlington Road, Bedford, MA 
Disclaimer: MITRE might agree with me - then again...
Amateur Radio: KC1KJ

jordan@aero.org (Larry M. Jordan) (03/14/91)

Newsgroups: comp.lang.c++
Subject: Re: Implementing LISP in C++ (type discrimination)
Summary: 
Expires: 
References: <17238@cadillac.CAD.MCC.COM> <1991Mar12.221015.22144@aero.org> <1991Mar13.145107.19724@linus.mitre.org>
Sender: 
Followup-To: 
Distribution: 
Organization: The Aerospace Corporation, El Segundo, CA
Keywords: 

Doug,

Could you elaborate on the marking technique via 'GCmarkPhase'.  I
just don't see what you are driving at.  What I'm currently planning
to do is this:

  [1] for each LISP object class (I realize there is only one vtab per
      class) I have overloaded 'new' and 'delete'.  Each class
      'manages' its own object space. (i.e. when a new Cons cell is 
      'allocated', the method Cons::new() will fetch and return a Cons
      cell from Cons::FreeList.  If none are available, the
      garbage collector will be called.  It (gc) will need to
      mark all objects in use, won't it?   See [2].  Then sweep.  If
      Cons::FreeList is still empty, then the Cons object
      space manager will allocate a new chuck of Cons's
      and return one of these.

  [2] during the mark phase, each LISP object class will be asked to
      mark all its internal objects which are in use. (i.e. the Symbol
      class maintains the Obarray.  The Interpreter maintains the 
      evaluation stack, registers, etc.).
      
  [3] each LISP object class will be asked (after marking) to
      sweep, collecting objects into free lists.

'sweep' will be a static member in each class.  There will be a static
'mark' as well, but this will call a generic mark function 'void mark(Node*)'
which knows how to traverse LISP structures, as I indicated before
in a non recursive way, saving backpointer when chasing car's and
cdr's and restoring pointers when unwinding.  Writing such a generic
mark function seems to require doing a case analysis, doesn't it?

I have envisioned two methods one for marking during descent--
'int markAndDecend(?)' which marks and does the pointer reversal--
and a second on ascent--'int ascend(?)' which will restore pointers
up to a point where descending can continue or the marking phase is
complete.  These are two more virtual functions I will need to add
to the base class 'Node'! (The number continues to grow).  But, more
importantly, there are a lot of calls to virtual methods during
the marking phase.  How can this be as efficient as an up front
case analysis?!  I am concerned about the overhead.  Is the technique 
you've intimated substantially different?  Maybe more sophisticated
storage reclamation techniques are called for?!

--Larry
 

chip@tct.uucp (Chip Salzenberg) (03/16/91)

According to jordan@aero.org (Larry M. Jordan):
>... a generic mark function 'void mark(Node*)' which knows how to
>traverse LISP structures, as I indicated before in a non recursive
>way, saving backpointer when chasing car's and cdr's and restoring
>pointers when unwinding.  Writing such a generic mark function seems
>to require doing a case analysis, doesn't it?

In general, an "isKindOf()" test can almost always be replaced by a
virtual function.  For example:

     switch (p->type()) {
	case Cons:    do_cons_thing((Cons *)p);  break;
        case String:  do_string_thing((String *)p); break;
     }

can be replaced with a virtual do_thing() function, with
implementations Cons::do_thing() and String::do_thing().  Then the
above code becomes simply:

     p->do_thing();

>there are a lot of calls to virtual methods during the marking phase.
>How can this be as efficient as an up front case analysis?!

A switch() is typically a table lookup.  So is a virtual function
lookup.  Any more questions?
-- 
Chip Salzenberg at Teltronics/TCT     <chip@tct.uucp>, <uunet!pdn!tct!chip>
 "Most of my code is written by myself.  That is why so little gets done."
                 -- Herman "HLLs will never fly" Rubin

jordan@aero.org (Larry M. Jordan) (03/20/91)

I'm implementing a simple LISP interpreter in C++...

I've succeeded in implementing a marking routine (for a mark and sweep
garbage collector) WITOUT type discrimination!

I'll present type-discrimating version first (which is implemented in Ada)
and follow with the C++ version.

--Ada

  type NodeKind is (Free, Cons, Fixnum, ...);
  type MarkSet is (Unmarked, Marked, MarkLeft);

  type NodePtr is access Node;
  type Node(kind: NodeKind) is
    record
      mark: MarkSet;
      case kind is
	when Free => null;
	when Cons => car, cdr: NodePtr;
	when Fixnum => fix: FixnumType;
	...
      end case;
    end record;
 
Here's the mark function, a tricky non-recursive, routine for marking
all active structures (alg. thanks to David Betz):

  procedure mark(p: NodePtr) is
    current: NodePtr := p;
    previous: NodePtr := null;
  begin
    --mark this node

    loop
	--descend as far as we can

	while not current.mark = Marked loop

	    --mark this node and trace its children
	    case current.kind is
	      when Cons =>
		current.mark := Marked;
		if car(current) /= null then
		  declare
		    down: NodePtr := car(current);
		  begin
		    current.mark := MarkLeft;
		    rplaca(current, previous);
		    previous = current;
		    current = down;
		  end;
		elsif cdr(current) /= null then
		  declare
		    down: NodePtr := car(current);
		  begin
		    rplacd(current, previous);
		    previous = current;
		    current = down;
		  end;
		end if;

	      -- mark other nodes that require tracing
	      when ...
	    
	      -- mark objects that don't contain pointers
	      when Fixnu =>
		current.mark := Marked;
	      when ...
	    end case;

        end loop;


	--backup to a point where we can continue descending
	loop

	    --make sure there is a previous node
	    if previous /= null then
	       if previous.mark = MarkLeft then  --came from left side
		 previous.mark := Marked;
		 declare
		   up: NodePtr := car(previous);
		 begin
		   rplaca(previous, current);
		   if cdr(previous) /= null then
		     rplacd(previous,  up);
		     exit;
		   else			
		     current := cdr(previous); 	--step back up the branch
		     previous := up;
		   end if;
		 end
	       else				--came from right side
		 declare
		   up: NodePtr := cdr(previous);
		 begin
		   rplacd(previous, current);
		   current := previous; 	--step back up the branch
		   previous := up;
		 end;
	       end if;
	    else
	      -- no previous node, must be done
		return;
    	    end if;
	 end loop;

    end loop;

end mark;


Granted this is tricky to follow and the logic of what is really
happening here is hard to follow.  Worse, every time I add a new kind of
object to the LISP system, say Symbols or Environments, I must
change this marking procedure (not to mention the many other places where
case analysis is going on).  

Here's the not-type-descriminating version implemented in C++!
I've made Node a base class and derive all Node kinds from this class.  
The mark routine is implemented entirely abstractly in terms of Nodes 
and the case anaysis is replaced with dynamic binding--virtual method calls.  

class Node;

typedef boolean int;
enum { FALSE, TRUE };

struct MarkState {
  Node *current;
  Node *previous;
};

class Node {
  enum MarkSet { Unmarked, Marked, MarkLeft };
  
  virtual boolean descend(MarkState &m) = 0;	--implementation deferred
  virtual boolean ascend(MarkState &m) = 0;	--implementation deferred
}


void mark(Node *p)
{
   MarkState m;
   m.previous = NIL;
   m.current = p;

   while (1) {

     while (m.current->descend(m))
       ;
     while (1) {
       if (m.previous == NIL)
	  return;
       if (!m.previous->ascend(m))
	  break;
     }
   }
}

// that's it!

class Cons : Node {

  ...
  virtual boolean descend(MarkState &m)
  {
    // mark and save back pointers
    // return TRUE if still descending else return FALSE
  }

  virtual boolean ascend(MarkState &m)
  {
    // ascend (restoring pointers) to a point where descending can continue
    //    and return TRUE
    // else return FALSE if cannot ascend
  }
}



Chip Salzenburg raised an interesting point: 'switch() is a table
lookup'.  Has anyone done a study to see just how much storage is
used in switch() jump tables for a large non-OOP implementation vs. an
OOP implementation?  I'm less concern about the storage occupied by vft's
than I earlier--I'd rather pay the price for the dispatch table once
(in the class) than many times (at each switch()!?).

--Larry

roger@eos.eos.scg.hac.com (Roger Sheldon (Loral)) (04/12/91)

In article <1991Mar12.221015.22144@aero.org> jordan@aero.org (Larry M. Jordan) writes:
>I'm endeavoring to implement an interpreter/compiler for a LISP-like language
>in C++.  

Larry,
I've been working on Lisp in C++ also.  I've discovered a garbage collection
technique which you and others may find useful.  Here's a summary
of the technique.

The class hierarchy is similar to yours:

                        Base -------------- Object
       |-----------------|---------------|
      Cons             Symbol         Integer
       |           |-----|-----|
      Null  Static_symbol    Function

(The relationship of the Object class will be explained below.)
Garbage collection is accomplished via a combination of reference maintenance
and temporary objects.  Reference maintenance is done with a
few member functions:

Base *Base::Ref()       { refs++; return this; }
void Base::Deref()      { if (--refs == 0) delete this; }
void Cons::Deref() {                 // hides inhereted Base::Deref()
    Base *a = this;
    while (a != nil.value && --a->refs==0) {
        a->Car().Deref();            // delete car obj (clobber if last ref)
        Base *save = & a->Cdr();
        delete a;                    // may be deleting *this* !
        a = save;
    }
}

The Cons::Deref() method is a variation of the deref() method found in
Gnu's libg++ parameterized List class. (Thanks FSF) That's pretty much it 
for reference maintenance.  The Null and Static_symbol classes override the 
inherited Deref() method with no ops so there's no need to worry about 
refs and derefs to nil and t.

Now, the temporary objects work with the reference maintenance as follows.
The Object class is defined as: 

class Object { 
    Base *value;
public:
    Object() 			{ value = nil.value; }
    Object(Object &a) 	{ value = a.value->Ref(); }
    Object(Base &a) 	{ value = a.Ref(); }
    ~Object() 			{ value->Deref(); }
    operator=(Object &a){ 
        Base *save=value; 
        value = a.value->Ref();
        save->Deref();
    }
    ...
};

User code manipulates Lisp objects only through the Object class.  Specifically,
all user functions must use the same signature:
	Object foo(Object &a, Object &b <possibly more args>);
The main point is that Lisp objects are always returned through copied objects.
This allows user code to be written without the need to do reference 
maintenance -- the Object class takes care of reference maintenance 
automatically.  Here's an example of some user code:

Object shove_pl(Object &variable, Object &item, Object &alist) {
    if (!alist)
        return list(list(variable, list(item)));
    else if (variable == caar(alist))
        return cons(list(variable, append(cadar(alist), list(item))),
                    cdr(alist));
    else
        return cons(car(alist), shove_pl(variable, item, cdr(alist)));
    }

This is a translation of the Common Lisp shove_pl function found in
Winston's Lisp book:

(defun shove-pl (variable item a-list)
  (cond ((null a-list) (list (list variable (list item))))
        ((equal variable (caar a-list))
         (cons (list variable (append (cadar a-list) (list item)))
               (cdr a-list)))
        (t (cons (car a-list) (shove-pl variable item (cdr a-list))))))

OK, so this approach allows you to write elegant code.  But how
expensive is this garbage collection technique (which I'm calling
"Immediate" garbage collection since objects are clobbered immediately
after the last reference to it is removed) ?
I'm not going to get into a big discussion on performance.  I will
say that I've converted user functions to do their own reference maintenance 
(they deal with Base *'s rather than Objects) and the performance
improvement has ranged from 0 to 3 times.

Feel free to call me at 301 805-0463.

Roger Sheldon

bpendlet@bambam.dsd.es.com (Bob Pendleton) (04/18/91)

In article <14342@hacgate.UUCP>, roger@eos.eos.scg.hac.com (Roger Sheldon (Loral)) writes:

> OK, so this approach allows you to write elegant code.  But how
> expensive is this garbage collection technique (which I'm calling
> "Immediate" garbage collection since objects are clobbered immediately
> after the last reference to it is removed) ?

It's called reference counting. It's been used since the dawn of time.
Reference counting usually uses more total time than traditional
garbage collection. The other cost of reference counting is the
storage used by the count associated with each object. 

For cons cells the counts add 2 to 4 bytes to the size of each cell.
Usually a cons cell is only 8 bytes to begin with. A cons cell can be
as small as 4 bytes if it contains indexes instead of pointers. So a
reference count can chew up a lot of memory.

In older languages keeping track of the reference count was error
prone. But C++ classes let you hide the details and be sure that the
reference count is incremented and decrement at the right times.

(Some of the folks around here have coffee cups with "How could the
reference count be zero?" (Hi Janet) on them. They're left over from a
project written in C that used reference counting. It was a famous and
much dreaded runtime error message.)

The advantage of reference counting is that the program never "goes to
lunch" garbage collecting. Reference counting gives the user
consistent response times. Which is something that users love. I think
that this one benefit more than justifies the use of reference counts.

One thing to consider doing; When the reference count goes to zero put
the object on a type specific free list. When you need a new object of
a given type try to get it from its type specific free list first.
This way your not always going through the general purpose heap code.
Avoiding the general purpose code can give you a real performance
boost.

-- 
              Bob Pendleton, speaking only for myself.
   bpendlet@dsd.es.com or decwrl!esunix!bpendlet or utah-cs!esunix!bpendlet

                         Tools, not rules.

davis@barbes.ilog.fr (Harley Davis) (04/22/91)

In article <1991Apr17.233653.25149@dsd.es.com> bpendlet@bambam.dsd.es.com (Bob Pendleton) writes:

   It's called reference counting. It's been used since the dawn of time.
   Reference counting usually uses more total time than traditional
   garbage collection. The other cost of reference counting is the
   storage used by the count associated with each object. 

One of the most important problems with reference counting is that it
doesn't collect all the garbage.  In particular, circular data
structures never get collected.  Consider the following program:

(defvar *some-list* '(a b))
(setf (cddr *some-list*) *some-list*)
(setf *some-list* ())

Now, despite the fact that there is no way to access any of the cells
in *some-list*, all of them have a reference count of 1.  Oops.

-- Harley Davis

--
------------------------------------------------------------------------------
nom: Harley Davis			ILOG S.A.
net: davis@ilog.fr			2 Avenue Gallie'ni, BP 85
tel: (33 1) 46 63 66 66			94253 Gentilly Cedex, France

gintera@fsd.cpsc.ucalgary.ca (Andrew Ginter) (04/22/91)

In article <DAVIS.91Apr22105508@barbes.ilog.fr> davis@barbes.ilog.fr (Harley Davis) writes:
>One of the most important problems with reference counting is that it
>doesn't collect all the garbage.  In particular, circular data
>structures never get collected.

This was true of reference counting schemes until 1986.  An article
published in Software-Practice and Experience that year describes how
reference counting systems can reclaim circular lists fairly simply.
The exact name and author of the article escape me right now - they're
in my other office.  Drop me a note if you're desperate and I can dig
them up for you.

The circular list collection technique goes something like this:

* Scan the collected heap.  For each object in the heap, chase all
  pointers in the object and decrement the refcount on the target
  object.  At the end of the scan, all of the contribution of internal
  pointers to refcount fields has been eliminated.

* Scan the heap again.  Mark any object with a non-zero refcount
  field.  These are the objects which are referenced by "root
  pointers" (pointers which reside outside of the heap and which refer
  to collected objects).

* Scan the heap again.  Recursively mark any object with a non-zero
  refcount, incrementing reference counts as you traverse pointers in
  objects.

* Scan the heap again.  Any objects in the heap which still has a zero
  reference count can be reclaimed - these are the objects which were
  part of unreachable circular lists.

I'm writing this from memory, so this may not be exactly right, but it
gives the general idea.  I suspect that some of the heap scans can be
collapsed too, but again, I don't remember the details.

Andrew Ginter, 403-220-6320, gintera@cpsc.ucalgary.ca

tmb@ai.mit.edu (Thomas M. Breuel) (04/23/91)

   From: davis@barbes.ilog.fr (Harley Davis)
   In article <1991Apr17.233653.25149@dsd.es.com> bpendlet@bambam.dsd.es.com (Bob Pendleton) writes:
      It's called reference counting. It's been used since the dawn of time.
      Reference counting usually uses more total time than traditional
      garbage collection. The other cost of reference counting is the
      storage used by the count associated with each object. 

   One of the most important problems with reference counting is that it
   doesn't collect all the garbage.  In particular, circular data
   structures never get collected.  [...]

Reference counting is, however, ideal for garbage collecting large,
non-recursive objects like arrays of type real, complex, bit, etc.
Any other kind of garbage collection will have much higher overhead
in this case, and you can never build circular data structures out
of them.

hoelzle@neon.Stanford.EDU (Urs Hoelzle) (04/23/91)

tmb@ai.mit.edu (Thomas M. Breuel) writes:

>   [Others saying that reference counting is slower than other forms
>   of GC]

>Reference counting is, however, ideal for garbage collecting large,
>non-recursive objects like arrays of type real, complex, bit, etc.
>Any other kind of garbage collection will have much higher overhead
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>in this case, and you can never build circular data structures out
>of them.

What is the reason why this must be so?  Do you have any numbers to
show that reference counting is superior in this case?  (Read: I don't
believe it.)

-- 
------------------------------------------------------------------------------
Urs Hoelzle                                            hoelzle@cs.stanford.EDU
Center for Integrated Systems, CIS 42, Stanford University, Stanford, CA 94305

marti@mint.inf.ethz.ch (Robert Marti) (04/23/91)

In article <1991Apr23.000632.9175@neon.Stanford.EDU> hoelzle@neon.Stanford.EDU
(Urs Hoelzle) writes in response to an earlier message by tmb@ai.mit.edu
(Thomas M. Breuel):
>>[Any kind of garbage collection other than reference counting] will have
>>much higher overhead [in the case of non-recursive data structures].
>What is the reason why this must be so?  Do you have any numbers to
>show that reference counting is superior in this case?  (Read: I don't
>believe it.)

Neither do I.  In fact, I was surprised to find that using the conservative
garbage collector due to Hans Boehm and Mark Weiser (and described in
Software Practice and Experience in 1988?) instead of doing reference
counting myself was _considerably_ faster in a C++ implementation which
used many dynamically allocated arrays.  If my memory serves me correctly,
the running times dropped to about 60% of the original ones.  (And, yes,
the application ran long enough to incur several garbage collections!)

Robert Marti                      |  Phone:    +41 1 254 72 60
Institut fur Informationssysteme  |  FAX:      +41 1 262 39 73
ETH-Zentrum                       |  E-Mail:   marti@inf.ethz.ch
CH-8092 Zurich, Switzerland       |

davis@barbes.ilog.fr (Harley Davis) (04/24/91)

In article <1991Apr22.165825.13552@cpsc.ucalgary.ca> gintera@fsd.cpsc.ucalgary.ca (Andrew Ginter) writes:

   In article <DAVIS.91Apr22105508@barbes.ilog.fr> davis@barbes.ilog.fr (Harley Davis) writes:
   >One of the most important problems with reference counting is that it
   >doesn't collect all the garbage.  In particular, circular data
   >structures never get collected.

   This was true of reference counting schemes until 1986.  An article
   published in Software-Practice and Experience that year describes how
   reference counting systems can reclaim circular lists fairly simply.
   The exact name and author of the article escape me right now - they're
   in my other office.  Drop me a note if you're desperate and I can dig
   them up for you.

   The circular list collection technique goes something like this:

    [mark and sweep too]

Of course, if you use other gc methods with reference count, the
problem can be fixed.  But this was obvious way before 1986.  I think
Danny Bobrow did an implementation of a mixed ref count/full scan gc
in InterLisp in the late 70s/early 80s.

-- Harley Davis
--
------------------------------------------------------------------------------
nom: Harley Davis			ILOG S.A.
net: davis@ilog.fr			2 Avenue Gallie'ni, BP 85
tel: (33 1) 46 63 66 66			94253 Gentilly Cedex, France