playgroup

We’ve given functions their own environment. In this environment we will place the values that their formal arguments are set to. When we come to evaluate the body of the function we can do it in this environment and know that those variables will have the correct values.

But ideally we also want these functions to be able to access variables which are in the global environment, such as our builtin functions.

We can solve this problem by changing the definition of our environment to contain a reference to some parent environment. Then, when we want to evaluate a function, we can set this parent environment to our global environment, which has all of our builtins defined within.

When we add this to our lenv struct, conceptually it will be a reference to a parent environment, not some sub-environment or anything like that. Because of this we shouldn’t delete it when our lenv gets deleted, or copy it when our lenv gets copied.

The way the parent environment works is simple. If someone calls lenv_get on the environment, and the symbol cannot be found. It will look then in any parent environment to see if the named value exists there, and repeat the process till either the variable is found or there are no more parents. To signify that an environment has no parent we set the reference to NULL.

The constructor function only require basic changes to allow for this.

  1. struct lenv {
  2. lenv* par;
  3. int count;
  4. char** syms;
  5. lval** vals;
  6. };
  7. lenv* lenv_new(void) {
  8. lenv* e = malloc(sizeof(lenv));
  9. e->par = NULL;
  10. e->count = 0;
  11. e->syms = NULL;
  12. e->vals = NULL;
  13. return e;
  14. }

To get a value from an environment we need to add in the search of the parent environment in the case that a symbol is not found.

  1. lval* lenv_get(lenv* e, lval* k) {
  2. for (int i = 0; i < e->count; i++) {
  3. if (strcmp(e->syms[i], k->sym) == 0) {
  4. return lval_copy(e->vals[i]);
  5. }
  6. }
  7. /* If no symbol check in parent otherwise error */
  8. if (e->par) {
  9. return lenv_get(e->par, k);
  10. } else {
  11. return lval_err("Unbound Symbol '%s'", k->sym);
  12. }
  13. }

Because we have a new lval type that has its own environment we need a function for copying environments, to use for when we copy lval structs.

  1. lenv* lenv_copy(lenv* e) {
  2. lenv* n = malloc(sizeof(lenv));
  3. n->par = e->par;
  4. n->count = e->count;
  5. n->syms = malloc(sizeof(char*) * n->count);
  6. n->vals = malloc(sizeof(lval*) * n->count);
  7. for (int i = 0; i < e->count; i++) {
  8. n->syms[i] = malloc(strlen(e->syms[i]) + 1);
  9. strcpy(n->syms[i], e->syms[i]);
  10. n->vals[i] = lval_copy(e->vals[i]);
  11. }
  12. return n;
  13. }

Having parent environments also changes our concept of defining a variable.

There are two ways we could define a variable now. Either we could define it in the local, innermost environment, or we could define it in the global, outermost environment. We will add functions to do both. We’ll leave the lenv_put method the same. It can be used for definition in the local environment. But we’ll add a new function lenv_def for definition in the global environment. This works by simply following the parent chain up before using lenv_put to define locally.

  1. void lenv_def(lenv* e, lval* k, lval* v) {
  2. /* Iterate till e has no parent */
  3. while (e->par) { e = e->par; }
  4. /* Put value in e */
  5. lenv_put(e, k, v);
  6. }

At the moment this distinction may seem useless, but later on we will use it to write partial results of calculations to local variables inside a function. We should add another builtin for local assignment. We’ll call this put in C, but give it the = symbol in Lisp. We can adapt our builtin_def function and re-use the common code, just like we do with our mathematical operators.

Then we need to register these as a builtins.

  1. lenv_add_builtin(e, "def", builtin_def);
  2. lenv_add_builtin(e, "=", builtin_put);
  1. lval* builtin_def(lenv* e, lval* a) {
  2. return builtin_var(e, a, "def");
  3. }
  1. lval* builtin_put(lenv* e, lval* a) {
  2. return builtin_var(e, a, "=");
  3. }
  1. lval* builtin_var(lenv* e, lval* a, char* func) {
  2. LASSERT_TYPE(func, a, 0, LVAL_QEXPR);
  3. lval* syms = a->cell[0];
  4. for (int i = 0; i < syms->count; i++) {
  5. LASSERT(a, (syms->cell[i]->type == LVAL_SYM),
  6. "Function '%s' cannot define non-symbol. "
  7. "Got %s, Expected %s.", func,
  8. ltype_name(syms->cell[i]->type),
  9. ltype_name(LVAL_SYM));
  10. }
  11. LASSERT(a, (syms->count == a->count-1),
  12. "Function '%s' passed too many arguments for symbols. "
  13. "Got %i, Expected %i.", func, syms->count, a->count-1);
  14. for (int i = 0; i < syms->count; i++) {
  15. /* If 'def' define in globally. If 'put' define in locally */
  16. if (strcmp(func, "def") == 0) {
  17. lenv_def(e, syms->cell[i], a->cell[i+1]);
  18. }
  19. if (strcmp(func, "=") == 0) {
  20. lenv_put(e, syms->cell[i], a->cell[i+1]);
  21. }
  22. }
  23. lval_del(a);
  24. return lval_sexpr();
  25. }