6.1 Function Calls ¶
On the Perq function calling is done by micro-coded instructions. The
instructions perform a large number of operations, including determining
whether the function being called is compiled or interpreted, determining that
a legal number of arguments are passed, and branching to the correct entry
point in the function. To do all this on the IBM RT PC would
involve a large amount of computation. In the general case, it is necessary to
do all this, but in some common cases, it is possible to short circuit most of
this work.
To perform a function call in the general case, the following steps occur:
- Allocate space on the control stack for the fix-sized part of a call
frame. This space will be used to store all the registers that must
be preserved across a function call.
- Arguments to the function are now evaluated. The first three
arguments are stored in the argument registers A0, A1, and A2. The
rest of the arguments are stored on the stack as they are evaluated.
Note that during the evaluation of arguments, the argument registers
may be used and may have to be stored in local variables and restored
just before the called function is invoked.
- Load R0 with the argument count.
- Load the PC register with the offset into the current code vector of
the place to return to when the function call is complete.
- If this call is for multiple values, mark the frame as accepting
multiple values, by making the fixnum offset above negative by oring
in the negative fixnum type code.
- Store all the registers that must be preserved over the function call in the
current frame.
At this point, all the arguments are set up and all the registers have been
saved. All the code to this point is done inline. If the object being called
as a function is a symbol, we get the definition from the definition cell of
the symbol. If this definition is the trap object, an undefined symbol error
is generated. The function calling mechanism diverges at this point depending
on the type of function being called, i.e., whether it is a compiled function
object or a list.
If we have a compiled function object, the following steps are performed (this
code is out of line):
- Load the active function register with a pointer to the compiled function
object.
- The active frame register is set to the start of the current frame.
- Note the number of arguments evaluated. Let this be K. The correct
entry point in the called function’s code vector must be computed as
a function of K and the number of arguments the called function
wants:
- If K < minimum number of arguments, signal an error.
- If K > maximum number of arguments and there is no &rest argument,
signal an error.
- If K > maximum number of arguments and there is a &rest argument,
start at offset 0 in the code vector. This entry point must collect
the excess arguments into a list and leave the &rest argument in the
appropriate argument register or on the stack as appropriate.
- If K is between the minimum and maximum arguments (inclusive), get
the starting offset from the appropriate slot of the called
function’s function object. This is stored as a fixnum in slot K -
MIN + 6 of the function object.
- Load one of the Non-Lisp temporary registers with the address of the
code vector and add in the offset calculated above. Then do a branch
register instruction with this register as the operand. The called
function is now executing at the appropriate place.
If the function being called is a list, %SP-Internal-Apply must be called to
interpret the function with the given arguments. Proceed as follows:
- Note the number of arguments evaluated for the current open frame (call this N)
and the frame pointer for the frame (call it F). Also remember the lambda
expression in this frame (call it L).
- Load the active function register with the list L.
- Load the PC register with 0.
- Allocate a frame on the control stack for the call to %SP-Internal-Apply.
- Move the contents of the argument registers into the local registers L0, L1,
and L2 respectively.
- Store all the preserved registers in the frame.
- Place N, F, and L into argument registers A0, A1, and A2 respectively.
- Do the equivalent of a start call on %SP-Internal-Apply.
%SP-Internal-Apply, a function of three arguments,
now evaluates the call to the lambda-expression or interpreted
lexical closure L, obtaining the arguments from the frame pointed to
by F. The first three arguments must be obtained from the frame that
%SP-Internal-Apply runs in, since they are stored in its stack frame
and not on the stack as the rest of the arguments are. Prior to
returning %SP-Internal-Apply sets the Active-Frame register to F, so
that it returns from frame F.
The above is the default calling mechanism. However, much of the
overhead can be reduced. Most of the overhead is incurred by having
to check the legality of the function call everytime the function is
called. In many situations where the function being called is a
symbol, this checking can be done only once per call site by
introducing a data structure called a link table. The one exception
to this rule is when the function apply is used with a symbol. In
this situation, the argument count checks are still necessary, but
checking for whether the function is a list or compiled function
object can be bypassed.
The link table is a hash table whose key is based on the name of the
function, the number of arguments supplied to the call and a flag
specifying whether the call is done through apply or not. Each entry
of the link table consists of two words:
- The address of the function object associated with the symbol being
called. This is here, so that double indirection is not needed to
access the function object which must be loaded into the active
function register. Initially, the symbol is stored in this slot.
- The address of the instruction in the function being called to start
executing when this table entry is used. Initially, this points to
an out of line routine that checks the legality of the call and
calculates the correct place to jump to in the called function. This
out of line routine replaces the contents of this word with the
correct address it calculated. In the case when the call is caused
by apply, this will often be an out of line routine that checks the
argument count and calculates where to jump. In the case where the
called function accepts &rest arguments and the minimum number of
arguments passed is guaranteed to be greater than the maximum number
of arguments, then a direct branch to the &rest arg entry point is
made.
When a compiled file is loaded into the lisp environment, all the
entries for the newly loaded functions will be set to an out of line
routine mentioned above. Also, during a garbage collection the
entries in this table must be updated when a function object for a
symbol is moved.
The IBM RT PC code to perform a function call using the link table
becomes:
cal CS,CS,%Frame-Size ; Alloc. space on control st.
<Code to evaluate arguments to the function>
cau NL1,0,high-half-word(lte(function nargs flag))
oil NL1,0,low-half-word(lte(function nargs flag))
cal PC,0,return-tag ; Offset into code vector.
<oiu PC,PC,#xF800 ; Mark if call-multiple frame>
stm L0,CS,-(%Frame-Size-4) ; Save preserved regs.
lm AF,NL1,0 ; Link table entry contents.
bnbrx pz,R15 ; Branch to called routine.
cal FP,CS,-(%Frame-Size-4) ; Get pointer to frame.
return-tag:
The first two instructions after the arguments are evaled get the
address of the link table entry into a register. The two 16-bit half
word entries are filled in at load time. The rest of the
instructions should be fairly straight forward.