Extension: Lambda Expressions

Example

void connect(void (*handler)(void));

connect(fn void(void) { obj->on_clock(); });

Syntax & Semantics

The closure is implemented as heap-allocated executable code, generated on-the-fly. This makes closures binary compatible with existing functions because the function calling convention does not need to change. The body of the closure is allowed to refer to variables from its enclosing environment; however, it will be referring to copies made at the time of closure creation (thus avoiding the need for spaghetti stacks).

We might want to only allow closures to refer to variables from the enclosing scope if those variables are declared const. This would alleviate confusion that could arise from the copy semantics (in particular, mutations performed inside the closure won't affect the value of the variable in the enclosing scope). This is similar to Java's semantics (and implementation) for anonymous inner classes, whose methods are allowed to refer to local variables in the enclosing scope only if those variables are declared final. This would also benefit the implementation. Without this, it is necessary to make a local copy of everything in the environment on every invocation of a closure in case the body of the closure assigns to one of the variables contained in the environment. However, if constness is required, then this isn't a problem.

Because closures are heap-allocated, there must also be a mechanism for freeing them. We could just use free, but that would tie us down to a particular allocation scheme. Thus, I propose freefn as the deallocator associated with the fn allocator.

Implementation

Not implemented.

The function body has to be lifted to a fresh top-level function. The lambda lifting process stresses the approach to scoping because the lexical context of the body will change. The enclosing scope of the scope created for the lambda body becomes the global scope instead of the enclosing function scope, and any identifiers that existed between the global scope and the body scope (ie, the free identifiers of the lambda expression) need to be bundled into the closure and rewritten in the body so they don't refer to identifiers that are no longer accessible in the lifted body.

The lifted function will take two arguments in addition to the arguments expected by the lambda. One of these will contain the values bundled into the closure from the enclosing environment at the time of closure creation. References to such variables in the body must be rewritten to references to elements in this structure. The other additional argument is an implementation detail and should never be referenced in the lifted body.

freefn could simply be implemented as a call to free, but allows for certain optimizations. For example, if the body of the closure does not refer to any variables outside of it, then the "closure" pointer can simply be the pointer to the lifted function. Attempting to free such a closure would be a no-op.

Example transformation:

int foo() {
  int a = 1, b;
  int (*func)(int);
  func = fn int (int x) { x+a; };
  b = func(2);
  freefn(func);
  return b;
}

would become the following (where XXX is some uniquifier):

//
// Library routines
//

static const char[] closureTemplate = {
  // Machine code implementing
  //  push X
  //  call Y
  //  sub $4, %esp
  //  ret
  // Where X and Y are place-holders that will be filled in when a
  // closure is created later.  It's possible we want to be less
  // terse/efficient for the sake of stack traces, but stack traces
  // might be screwed anyways.
};

struct closureHeader {
  char code[sizeof closureTemplate];
};

void *allocClosure(size_t size, void *lifted, unsigned long envOffset) {
  struct closureTemplate *c = (closureTemplate*)malloc(size);
  memcpy(c->code, closureTemplate, sizeof closureTemplate);
  // Fill in X in code with (c + envOffset)
  // Fill in Y in code with address of lifted
  return c;
}

void freefn(void *closure) {
  free(closure);
}

//
// End library
//

// structure containing the closure environment
struct envXXX {
  int a;
};

// The closure.  This is heap-allocated executable code that also
// carries with it the closure environment.
struct closureXXX {
  struct closureHeader header;
  struct envXXX env;
};

// The lifted procedure
int lambdaXXX(struct envXXX *envXXX, int retXXX, int x) {
  int a = envXXX->a;
  return x+a;
}

int foo() {
  int a = 1, b;
  int (*func)(int);
  // Allocate the closure
  func = (int (*)(int))allocClosure(sizeof(struct closureXXX),
                                    lambdaXXX,
                                    offsetof(closureXXX, env));
  // Fill in the enviroment
  ((struct closureXXX*)func)->env.a = a;
  // Done creating closure
  b = func(2);
  freefn(func);
  return b;
}