[LispM-Hackers] Function calling

Paul Fuqua pf@ti.com
Mon, 15 Oct 2001 20:20:17 -0500


    Date: 15 Oct 2001 16:27:16 -0800
    From: james@unlambda.com (James A. Crippen)
    
    We need to implement M-memory and A-memory.

I wrestled with this question back when I was considering an
implementation of my own.  The key issue was whether it was possible to
faithfully emulate the Explorer if I never went below the
macroinstruction level.  Call instructions were part of the issue, as
were some of the weirder miscops and the whole GC thing.

Viewed from the macroop level, the Explorer is a simple stack machine.
A call instruction is either one of the specific ones (CALL-1-PUSH,
say), in which case the instruction itself indicates how many arguments
are supplied and how to dispose of the result, or one of the general
ones, in which case the preceding instructions have set up the stack,
including leaving the call-info word on top.

In that view, the function pointer can be derived from the instruction
(it's usually FEF-relative, so the initial pointer is in a slot in the
function header, and it'll either point to a functional object or be a
transparent pointer of some kind), the argument pointer (which points at
the first argument) can be calculated easily, and the various bits of
the call-info word are available when needed.

A call instruction does only a few things:  sets the function state to
point to the new function and its first instruction, adjusts the
argument pointer, pushes a call frame (five words containing the old
state), and adjusts the local pointer (top of stack after the call
frame).  The call-info word is just a way of condensing details into a
single word for the microcode, and the dispatching is just because we
allowed so many things to be funcalled.

I'm rambling.  Anyway, my point is that I'm not sure we really need to
get into the M-memory/VMA/microcode level to actually make things work.

                  Since these two must be kept synchronized I figure that
    implementing them as the same data structure would avoid the necessity
    of copying a lot of data back and forth with each emulation cycle.

Sure.  The only reason for M and A is to simulate a two-port register
file.

                        We need the PDL, PDL pointer, and PDL index.  The
    PDL is 1K by 32-bit words.  These shouldn't be difficult.  I may
    implement them in the next day or so.

The PDL *buffer* is a speed hack for the stack, a small cache of the top
of it inside the processor.  It ought to be transparent, too, and
shouldn't be visible at the macroop level, either.  The PDL as a whole
is just the system stack.

    The two components which trouble me are the MD and VMA registers.

At the microcode level, we'd write an address to VMA and a few cycles
later the data would appear in MD (or we'd put something in MD and write
to VMA a different way and the data would be written).  It's loads and
stores at a finer degree of detail.  At the macrocode level, you
wouldn't even see them.
    
    Another thing that is still troubling me is more related to the return
    instructions, the Indicators.

The indicators are more a concept than a thing.  Inside the microcode,
we had a dedicated register (M-T, for some reason) for the result of any
macroop.  A subsequent branch instruction looked at that register when
evaluating its condition.  I don't think we had anything more elaborate
than that.  In terms of macroop destinations, D-INDS just meant that the
result wasn't pushed, so the instruction was for effect rather than for
value;  however, you could still branch on the result in the next
instruction.

Thus, "MINUSP D-INDS" would do something like pop the top of the stack
and stick T in the magic register if it was a number less than zero, or
NIL if it was positive, or trap if it wasn't a number.  "MINUSP D-PDL"
would push the T or NIL on the stack.

(We might have had some hardware acceleration of this, but I frankly
can't remember.)

For calls, D-PDL and D-INDS work the same way.  D-RETURN and
D-REALLY-TAIL-RECURSIVE (I forget the official name) are different
implementations of tail-recursive calls;  the former is conservative and
sets a flag so returning will clean up the stack frame, while the latter
blows away the caller's frame during the call so returns are simple.

                              pf