Implementation of Procedure Objects

In any language implementing closures, there are two types of procedures:

In abstract, these can be unified by emitting every procedure with a closure pointer argument, and nullifying that argument when it is not used. In practice, this is inefficient, because most procedure calls are statically resolvable, and in that case the decision (not) to pass a closure pointer can be made at compile time.

Complicating matters somewhat, we want procedure objects to be callable from C. This constrains the form of a procedure object in two ways:

  1. A procedure object may not be relocated during its lifetime.
  2. A procedure object must begin with code, so that a call to a procedure object that is initiated from C will have the desired effect. That is: procedure objects are executable, and they execute between frames.

The first requirement above means that in compacting implementations, procedure objects must be allocated from a non-compated heap. Type-based partitioning of procedure objects is probably indicated in this situation.

Types of Procedures

BitC constructs closures in such a way that globals are not incorporated into the closure and self-recursion is not incorporated into the closure. This results in two types of procedures: those requiring an explicit closure environment pointer and those that do not. If a procedure does not require an explicit closure environment pointer, then the procedure is implemented directly as a procedure label (i.e. a C-style function).

Procedure Objects and Stub Functions

When a procedure requires a closure environment pointer, four objects are generated:

In this case, the procedure object must not be relocated by the garbage collector (because there might be non-relocatable pointers in the C heap to them). The address of the procedure object serves as the procedure label. It is critical that the stack frame fabricated in any call to f_stub be compatible with the stack frame required for a call to f, and that the code of f (i.e. the code of the procedure object) not modify the stack frame in any way that would violate the calling convention.

If an f_stub() procedure is required, it will look (at the C level) something like:

    ReturnType f_stub(T1 arg1, ... Tn argn)
    {
      theClosedEnv = someplace_magical;
      return f_closed(theClosedEnv, arg1, ... argn);
    }
    

The choice of someplace_magical is (highly) architecture-dependent. Prefered choices are (in declining order of preference) are:

The job of the procedure object trampoline is to store the required closure environment pointer into one of these locations in a way that is compatible with the calling convention of the target architecture and transfer control to f_stub.

BitC Implementation

In the BitC implementation, the closure object consists of trampoline code that is interspersed with the environment pointer. This can be described by the C structure:

    union ProcedureObject {
      uint8_t code[?];      // length is arch-dependent
      struct {
        uint8_t prefix[?];  // size is arch-dependent
        void *ptr;          // pointer to closure environment
      } env;
    };
    

with the intended meaning that env.ptr is a naturally aligned overlay onto the code block.

Sample Implementations

On IA-32, the trampoline code sequence looks like:

    +-----------------+
    | code  (4 bytes) |
    +-----------------+
    |   env pointer   |
    +-----------------+
    |    more code    |
    +-----------------+
    union ProcedureObject {
      uint8_t code[13];      // length is arch-dependent
      struct {
        uint8_t prefix[4];  // size is arch-dependent
        void *ptr;          // pointer to closure environment
      } env;
    };
    
    NOP  ;; three NOPs for alignment
    NOP
    NOP
    MOVL  envP,eax
    J     dest
    

On SPARC, the trampoline code sequence looks like:

    +------------------+
    | code  (12 bytes) |
    +------------------+
    |    env pointer   |
    +------------------+
    
union ProcedureObject { uint8_t code[12]; // length is arch-dependent struct { uint8_t prefix[12]; // size is arch-dependent void *ptr; // pointer to closure environment } env; };
    SETHI r16 := destProc[31:10]
    JMPL  r16 := r16 + destProc[9:0]
    ;; trick: r16 now points to address of LD instr:
    LD    r16 := r16 + 4
    

Incidental Optimizations

There is an important op further optimization possible; most procedure calls are intra-file, and in these cases, or when whole-program optimization is possible, it is possible for the code emitter to emit calls to procedure objects in such a way as to call the f_closed version of the procedure directly. This is possible when the procedure can be statically resolved. The key point here is that there exists some heap-allocated stub strategy that is feasible on all target architectures.


Generated on Sat Feb 4 23:59:29 2012 for BitC Compiler by  doxygen 1.4.7