Error Reporting

So far our error reporting doesn’t work so well. We can report when an error occurs, and give a vague notion of what the problem was, but we don’t give the user much information about what exactly has gone wrong. For example if there is an unbound symbol we should be able to report exactly which symbol was unbound. This can help the user track down errors, typos, and other trivial problems.

Wouldn’t it be great if we could write a function that can report errors in a similar way to how printf works. It would be ideal if we could pass in strings, integers, and other data to make our error messages richer.

The printf function is a special function in C because it takes a variable number of arguments. We can create our own variable argument functions, which is what we’re going to do to make our error reporting better.

We’ll modify lval_err to act in the same way as printf, taking in a format string, and after that a variable number of arguments to match into this string.

To declare that a function takes variables arguments in the type signature you use the special syntax of ellipsis ..., which represent the rest of the arguments.

  1. lval* lval_err(char* fmt, ...);

Then, inside the function there are standard library functions we can use to examine what the caller has passed in.

The first step is to create a va_list struct and initialise it with va_start, passing in the last named argument. For other purposes it is possible to examine each argument passed in using va_arg, but we are going to pass our whole variable argument list directly to the vsnprintf function. This function acts like printf but instead writes to a string and takes in a va_list. Once we are done with our variable arguments, we should call va_end to cleanup any resources used.

The vsnprintf function outputs to a string, which we need to allocate some first. Because we don’t know the size of this string until we’ve run the function we first allocate a buffer 512 characters big and then reallocate to a smaller buffer once we’ve output to it. If an error message is going to be longer than 512 characters it will just get cut off, but hopefully this won’t happen.

Putting it all together our new error function looks like this.

  1. lval* lval_err(char* fmt, ...) {
  2. lval* v = malloc(sizeof(lval));
  3. v->type = LVAL_ERR;
  4. /* Create a va list and initialize it */
  5. va_list va;
  6. va_start(va, fmt);
  7. /* Allocate 512 bytes of space */
  8. v->err = malloc(512);
  9. /* printf the error string with a maximum of 511 characters */
  10. vsnprintf(v->err, 511, fmt, va);
  11. /* Reallocate to number of bytes actually used */
  12. v->err = realloc(v->err, strlen(v->err)+1);
  13. /* Cleanup our va list */
  14. va_end(va);
  15. return v;
  16. }

Using this we can then start adding in some better error messages to our functions. As an example we can look at lenv_get. When a symbol can’t be found, rather than reporting a generic error, we can actually report the name that was not found.

  1. return lval_err("Unbound Symbol '%s'", k->sym);

We can also adapt our LASSERT macro such that it can take variable arguments too. Because this is a macro and not a standard function the syntax is slightly different. On the left hand side of the definition we use the ellipses notation again, but on the right hand side we use a special variable __VA_ARGS__ to paste in the contents of all the other arguments.

We need to prefix this special variable with two hash signs ##. This ensures that it is pasted correctly when the macro is passed no extra arguments. In essence what this does is make sure to remove the leading comma , to appear as if no extra arguments were passed in.

Because we might use args in the construction of the error message we need to make sure we don’t delete it until we’ve created the error value.

  1. #define LASSERT(args, cond, fmt, ...) \
  2. if (!(cond)) { \
  3. lval* err = lval_err(fmt, ##__VA_ARGS__); \
  4. lval_del(args); \
  5. return err; \
  6. }

Now we can update some of our error messages to make them more informative. For example if the incorrect number of arguments were passed we can specify how many were required and how many were given.

  1. LASSERT(a, a->count == 1,
  2. "Function 'head' passed too many arguments. "
  3. "Got %i, Expected %i.",
  4. a->count, 1);

We can also improve our error reporting for type errors. We should attempt to report what type was expected by a function and what type it actually got. Before we can do this it would be useful to have a function that took as input some type enumeration and returned a string representation of that type.

  1. char* ltype_name(int t) {
  2. switch(t) {
  3. case LVAL_FUN: return "Function";
  4. case LVAL_NUM: return "Number";
  5. case LVAL_ERR: return "Error";
  6. case LVAL_SYM: return "Symbol";
  7. case LVAL_SEXPR: return "S-Expression";
  8. case LVAL_QEXPR: return "Q-Expression";
  9. default: return "Unknown";
  10. }
  11. }
  1. LASSERT(a, a->cell[0]->type == LVAL_QEXPR,
  2. "Function 'head' passed incorrect type for argument 0. "
  3. "Got %s, Expected %s.",
  4. ltype_name(a->cell[0]->type), ltype_name(LVAL_QEXPR));

Go ahead and use LASSERT to report errors in greater depth throughout the code. This should make debugging many of the next stages much easier as we begin to write complicated code using our new language. See if you can use macros to save on typing and automatically generate code for common methods of error reporting.

  1. lispy> + 1 {5 6 7}
  2. Error: Function '+' passed incorrect type for argument 1. Got Q-Expression, Expected Number.
  3. lispy> head {1 2 3} {4 5 6}
  4. Error: Function 'head' passed incorrect number of arguments. Got 2, Expected 1.
  5. lispy>