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:

  1. 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.
  2. 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.
  3. Load R0 with the argument count.
  4. Load the PC register with the offset into the current code vector of the place to return to when the function call is complete.
  5. 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.
  6. 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):

  1. Load the active function register with a pointer to the compiled function object.
  2. The active frame register is set to the start of the current frame.
  3. 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:
    1. If K < minimum number of arguments, signal an error.
    2. If K > maximum number of arguments and there is no &rest argument, signal an error.
    3. 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.
    4. 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.
  4. 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:

  1. 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).
  2. Load the active function register with the list L.
  3. Load the PC register with 0.
  4. Allocate a frame on the control stack for the call to %SP-Internal-Apply.
  5. Move the contents of the argument registers into the local registers L0, L1, and L2 respectively.
  6. Store all the preserved registers in the frame.
  7. Place N, F, and L into argument registers A0, A1, and A2 respectively.
  8. 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:

  1. 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.
  2. 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.