------------------------------------------------------------------------------
MC logo
Dealing With Cycles
[^] Classes for Closures
------------------------------------------------------------------------------

Tom's Lisp uses reference counting for garbage collection, which cannot handle cycles. The interpreted language is such that interpreted code cannot create cyclic data structures, so those structures are not a problem. But garbage collection must deal with one internal data structure which does create cycles. Closures contain pointers to contexts, and contexts typically contain pointers to closures. Context objects themselves are linked in a stack arrangement: when a scope is entered, a new context is created which points to the previous one. This can create cycles, such as:

In the diagram, node (a) is the main context created when the program starts. The frames (b), (c) and (d) are contexts created by scope or by function invocation, but (c) and (d) have returned. While in scope (d), two closures, (e) and (f), were created and stored with set. Obviously, several pointer cycles are created.

Now, suppose the context (b) is popped, and then the pointer to (e) is removed from the main context. Regular reference counting will clean up (e), but the remaining cycle will keep (c), (d) and (f) alive, and the cycle keeps (b) around. All of these nodes are garbage which plain reference counting cannot clean up.

The solution leverages the stack-ordering of scopes, and involves some additional counting. First, pointers to closures are considered supporting or non-supporting; supporting is the usual case. Non-supporting pointers are any stored in a context which has exited. The red pointer in the diagram is non-supporting. The blue one is supporting, as are any pointers which are not stored in contexts. Pointers from closures to contexts may be discounted, meaning that they do not contribute to the reference count of what they point to. Closure objects count the number of non-supporting pointers to them. When all pointers to them are non-supporting (the non-support count equals the reference count), they discount their context pointer. For instance, assuming the diagram shows all pointers, the nodes have the following counts:
Node Reference Count Non-Support Count
(a) 2
(b) 2
(c) 1
(d) 1 The brown pointer is discounted.
(e) 1 0
(f) 1 1 The equal counts discount the brown pointer.

Now, again consider the change where (b) is popped, and the blue pointer to (e) is removed using the modified system. The new situation is:

As noted above, regular reference counting will delete only (e). However, in the modified scheme, the following happens:
  1. When the blue pointer is deleted, the reference count for (e) is reduced to zero, and that node is deleted, just as in vanilla reference counting.
  2. This causes the reference count for node (d) to also go to zero. Even though there are two pointers to this node, the brown one is discounted, so loss of the green one causes the (d) node to expire.
  3. This causes causes (c)'s count to reach zero and it is deleted.
  4. This zeros both (b) and (f), so they are deleted. Only node (a) is left.

Note that discounting the brown pointer has the effect of breaking the cycle through (c) (d), (f). Once (e) is gone, (d) goes and the dominoes fall.

The use careful use of nonsupporting and discounted pointers enables the reference counting to clean up the cyclic structures. Note that this relies on the stack-like behavior of context creation and destruction. It is not some long-sought general solution to collecting cycles with reference counting. It also depends on the fact that the Toms Lisp implements a very limited language, so it is impossible for the user program to create cyclic data structures. Therefore, the interpreter must clean up only its own well-understood messes, not figure out the user's.

Implementation

The class DiscRefPtr, derived from Ptr<Context>, represents a pointer which can be discounted. This is used in the Closure class to point to its context. It is not used elsewhere. Its discount method reduces the reference count of its target, and arranges that the actual destruction of the pointer will not perform that decrement.

Nonsupporting pointers are implemented by keeping to a count in each Closure object of the number of pointers pointing to it which are non-supporting. The Closure class has a method nonsup which increments the discount count and compares it to the reference count, then discounts its context pointer if needed. The Context class has a method last_rites which calls nonsup on each on each closure it contains. Last_rites is called from various places just before removing the context from the current context stack.