Blog 2020/1/12
<- previous | index | next ->
A Lisp interpreter in C, part 11: apply
Let's learn how to write a Lisp interpreter in C!
In this part, we implement funcion application.
Lisp stands for "list processing", which means that every list is treated like a function call.
The first element of the list is assumed to be the operator, and the rest of the elements are the operands.
The operator is applied to the operands, and thus (+ 1 1)
evaluates to 2
.
Built-ins
When a list is evaluated, the first element must be one of three things:
- a special-form,
- (something which evaluates to) a user-defined function (also called a lambda),
- (something which evaluates to) a built-in function (also called a primitive function).
Our first built-in function is bi_add()
(bi being a bit of Hungarian notation),
which we will bind to the symbol +
:
builtins.c (error handling elided):
/* Built-in addition, i.e. `(+ 1 2 3)`.
Calculates the sum of argsp into resultp.
Promotes CLong to CDouble where needed.
Returns 0 or error. */
int bi_add(List* argsp, Form** resultpp, List* envp) {
long accuml = 0;
double accumd = 0.0;
bool long_mode = true;
bool double_mode = false;
bool mode = long_mode;
while (!is_list_empty(argsp)) {
Form* argp = argsp->datap;
if (is_clong(argp)) {
long l = ((CLong*)argp)->value;
if (mode == long_mode) {
accuml += l;
} else {
accumd += (double)l;
}
} else if (is_cdouble(argp)) {
double d = ((CDouble*)argp)->value;
if (mode == double_mode) {
accumd += d;
} else {
/* switch to double-mode. */
mode = double_mode;
accumd = (double)accuml + d;
}
} else {
return E_bi_add__unsupported_arg_type;
}
argsp = argsp->nextp;
continue;
}
if (mode == long_mode) {
CLong* clp;
new_clong(&clp, accuml);
*resultpp = (Form*)clp;
return 0;
} else {
CDouble* cdp;
new_cdouble(&cdp, accumd);
*resultpp = (Form*)cdp;
return 0;
}
}
Note that bi_add()
promotes integers to doubles when it is given mixed-type arguments. Thus (+ 1 3.14159)
evaluates to 4.14159
.
Lisp forms
In order to bind a built-in function in the environment, we need to be able to represent it as a Lisp form.
For this, we add another struct
:
struct CFunc_ {
FormType type;
int (*f)(List*, Form**, List*);
};
typedef struct CFunc_ CFunc;
In the above, f
is a pointer to a function
which takes a List*
, a Form**
, and another List*
,
and returns an int
.
I use cdecl.org to deal with function pointers in C.
Notice that this matches the function signature of bi_add()
.
That's no coincidence: all of our built-in functions will need to share a common signature.
We update form_eq()
to handle CFunc
.
All we have to do is compare the two C-function pointers:
return a == b;
} else if (is_nil(a)) {
return true;
+ } else if (is_cfunc(a)) {
+ CFunc* af = (CFunc*)a;
+ CFunc* bf = (CFunc*)b;
+ return af->f == bf->f;
} else {
assert(false);
}
The evaluator
We update eval_list()
to perform function application:
it evaluates the operator, evaluates the operands, then calls apply()
.
eval.c (error handling elided):
}
/* this is a regular function call. */
- /* for now, we just return the list. */
- *resultpp = (Form*)lp;
- return 0;
+ Form* op_evaledp;
+ List* args_evaledp;
+ eval_form(op_formp, &op_evaledp, envp);
+ eval_forms(argsp, &args_evaledp, envp);
+ return apply(op_evaledp, args_evaledp, resultpp, envp);
}
Our initial implementation of apply()
is very simple: it calls the C function with the supplied arguments:
/* Function application: apply the operator to the arguments.
Returns 0 or error. */
static int apply(Form* opp, List* argsp, Form** resultpp, List* envp) {
if (is_cfunc(opp)) {
CFunc* cfp = (CFunc*)opp;
return (cfp->f)(argsp, resultpp, envp);
} else {
return E_apply__bad_op_type;
}
}
And eval_forms()
is simply a convenience loop around eval_form()
:
eval.c (error handling elided):
/* Evaluate a list of forms.
Returns 0 or error. */
static int eval_forms(List* formsp, List** resultspp, List* envp) {
List* resultsp = g_emptylist;
while (!is_list_empty(formsp)) {
Form* resultp;
eval_form(formsp->datap, &resultp, envp);
list_push(&resultsp, resultp);
formsp = formsp->nextp;
continue;
}
*resultspp = resultsp;
return 0;
}
main
We update new_default_env()
to bind +
to our bi_add()
built-in:
assert(env_bind_nil(envp, "nil") == 0);
assert(env_bind_cbool(envp, "true", g_true) == 0);
assert(env_bind_cbool(envp, "false", g_false) == 0);
+ assert(env_bind_cfunc(envp, "+", bi_add) == 0);
return envp;
}
forms.c (error handling elided):
/* Convenience function to push a built-in C function into the env.
Returns 0 or error. */
int env_bind_cfunc(List* envp, char* key, int (*f)(List*, Form**, List*)) {
Symbol* keyp;
new_symbol(&keyp, key);
CFunc* valuep;
new_cfunc(&valuep, f);
return env_bind(envp, keyp, (Form*)valuep);
}
The printer
We update print_form()
to handle CFunc
:
return print_cbool(cbp, fp);
} else if (is_nil(formp)) {
return print_nil(fp);
+ } else if (is_cfunc(formp)) {
+ CFunc* cfp = (CFunc*)formp;
+ return print_cfunc(cfp, fp);
} else {
assert(false);
}
printer.c (error handling elided):
/* Prints the CFunc in cfp into fp.
Returns 0 or errno. */
static int print_cfunc(CFunc* cfp, FILE* fp) {
fprintf(fp, "<C function @%p>", cfp->f);
return 0;
}
Try it out
Finally, we can call functions!
$ ./lisp
> (+ 1 1)
2
>
Glorious!
$ ./lisp
> (bind a 2)
> (bind b 3)
> (+ a b)
5
>
We also have a printable representation for CFunc
values:
$ ./lisp
> +
<C function @0x1038c4060>
>
Next time
In part 12, we'll implement the quote
special form.