Function Calling

We need to write the code that runs when an expression gets evaluated and a function lval is called.

When this function type is a builtin we can call it as before, using the function pointer, but we need to do something separate for our user defined functions. We need to bind each of the arguments passed in, to each of the symbols in the formals field. Once this is done we need to evaluate the body field, using the env field as an environment, and the calling environment as a parent.

A first attempt, without error checking, might look like this:

  1. lval* lval_call(lenv* e, lval* f, lval* a) {
  2. /* If Builtin then simply call that */
  3. if (f->builtin) { return f->builtin(e, a); }
  4. /* Assign each argument to each formal in order */
  5. for (int i = 0; i < a->count; i++) {
  6. lenv_put(f->env, f->formals->cell[i], a->cell[i]);
  7. }
  8. lval_del(a);
  9. /* Set the parent environment */
  10. f->env->par = e;
  11. /* Evaluate the body */
  12. return builtin_eval(f->env,
  13. lval_add(lval_sexpr(), lval_copy(f->body)));
  14. }

But this doesn’t act correctly when the number of arguments supplied, and the number of formal arguments differ. In this situation it will crash.

Actually this is an interesting case, and leaves us a couple of options. We could just throw an error when the argument count supplied is incorrect, but we can do something that is more fun. When too few arguments are supplied we could instead bind the first few formal arguments of the function and then return it, leaving the rest unbound.

This creates a function that has been partially evaluated and reflects our previous idea of a function being some kind of partial computation. If we start with a function that takes two arguments, and pass in a single argument, we can bind this first argument and return a new function with its first formal argument bound, and its second remaining empty.

This metaphor creates a cute image of how functions work. We can imagine a function at the front of an expression, repeatedly consuming inputs directly to its right. After consuming the first input to its right, if it is full (requires no more inputs), it evaluates and replaces itself with some new value. If instead, it is still it still requires more, it replaces itself with another, more complete function, with one of its variables bound. This process repeats until the final value for the program is created.

So you can imagine functions like a little Pac-Man, not consuming all inputs at once, but iteratively eating inputs to the right, getting bigger and bigger until it is full and explodes to create something new. This isn’t actually how we’re going to implement it in code, but it is still fun to imagine.

  1. lval* lval_call(lenv* e, lval* f, lval* a) {
  2. /* If Builtin then simply apply that */
  3. if (f->builtin) { return f->builtin(e, a); }
  4. /* Record Argument Counts */
  5. int given = a->count;
  6. int total = f->formals->count;
  7. /* While arguments still remain to be processed */
  8. while (a->count) {
  9. /* If we've ran out of formal arguments to bind */
  10. if (f->formals->count == 0) {
  11. lval_del(a); return lval_err(
  12. "Function passed too many arguments. "
  13. "Got %i, Expected %i.", given, total);
  14. }
  15. /* Pop the first symbol from the formals */
  16. lval* sym = lval_pop(f->formals, 0);
  17. /* Pop the next argument from the list */
  18. lval* val = lval_pop(a, 0);
  19. /* Bind a copy into the function's environment */
  20. lenv_put(f->env, sym, val);
  21. /* Delete symbol and value */
  22. lval_del(sym); lval_del(val);
  23. }
  24. /* Argument list is now bound so can be cleaned up */
  25. lval_del(a);
  26. /* If all formals have been bound evaluate */
  27. if (f->formals->count == 0) {
  28. /* Set environment parent to evaluation environment */
  29. f->env->par = e;
  30. /* Evaluate and return */
  31. return builtin_eval(
  32. f->env, lval_add(lval_sexpr(), lval_copy(f->body)));
  33. } else {
  34. /* Otherwise return partially evaluated function */
  35. return lval_copy(f);
  36. }
  37. }

The above function does exactly as we explained, with correct error handling added in too. First it iterates over the passed in arguments attempting to place each one in the environment. Then it checks if the environment is full, and if so evaluates, otherwise returns a copy of itself with some arguments filled.

If we update our evaluation function lval_eval_sexpr to call lval_call, we can give our new system a spin.

  1. lval* f = lval_pop(v, 0);
  2. if (f->type != LVAL_FUN) {
  3. lval* err = lval_err(
  4. "S-Expression starts with incorrect type. "
  5. "Got %s, Expected %s.",
  6. ltype_name(f->type), ltype_name(LVAL_FUN));
  7. lval_del(f); lval_del(v);
  8. return err;
  9. }
  10. lval* result = lval_call(e, f, v);

Try defining some functions and test out how partial evaluation works.

  1. lispy> def {add-mul} (\ {x y} {+ x (* x y)})
  2. ()
  3. lispy> add-mul 10 20
  4. 210
  5. lispy> add-mul 10
  6. (\ {y} {+ x (* x y)})
  7. lispy> def {add-mul-ten} (add-mul 10)
  8. ()
  9. lispy> add-mul-ten 50
  10. 510
  11. lispy>