Evaluating Errors


Now that we know how to work with the lval type, we need to change our evaluation functions to use it instead of long.

As well as changing the type signatures we need to change the functions such that they work correctly upon encountering either an error as input, or a number as input.

In our eval_op function, if we encounter an error we should return it right away, and only do computation if both the arguments are numbers. We should modify our code to return an error rather than attempt to divide by zero. This will fix the crash described at the beginning of this chapter.

  1. lval eval_op(lval x, char* op, lval y) {
  2. /* If either value is an error return it */
  3. if (x.type == LVAL_ERR) { return x; }
  4. if (y.type == LVAL_ERR) { return y; }
  5. /* Otherwise do maths on the number values */
  6. if (strcmp(op, "+") == 0) { return lval_num(x.num + y.num); }
  7. if (strcmp(op, "-") == 0) { return lval_num(x.num - y.num); }
  8. if (strcmp(op, "*") == 0) { return lval_num(x.num * y.num); }
  9. if (strcmp(op, "/") == 0) {
  10. /* If second operand is zero return error */
  11. return y.num == 0
  12. ? lval_err(LERR_DIV_ZERO)
  13. : lval_num(x.num / y.num);
  14. }
  15. return lval_err(LERR_BAD_OP);
  16. }

What is that ? doing there?

You’ll notice that for division to check if the second argument is zero we use a question mark symbol ?, followed by a colon :. This is called the ternary operator, and it allows you to write conditional expressions on one line.

It works something like this. <condition> ? <then> : <else>. In other words, if the condition is true it returns what follows the ?, otherwise it returns what follows :.

Some people dislike this operator because they believe it makes code unclear. If you are unfamiliar with the ternary operator, you may initially find it awkward to use; but once you get to know it there are rarely problems.

We need to give a similar treatment to our eval function. In this case because we’ve defined eval_op to robustly handle errors we just need to add the error conditions to our number conversion function.

In this case we use the strtol function to convert from string to long. This allows us to check a special variable errno to ensure the conversion goes correctly. This is a more robust way to convert numbers than our previous method using atoi.

  1. lval eval(mpc_ast_t* t) {
  2. if (strstr(t->tag, "number")) {
  3. /* Check if there is some error in conversion */
  4. errno = 0;
  5. long x = strtol(t->contents, NULL, 10);
  6. return errno != ERANGE ? lval_num(x) : lval_err(LERR_BAD_NUM);
  7. }
  8. char* op = t->children[1]->contents;
  9. lval x = eval(t->children[2]);
  10. int i = 3;
  11. while (strstr(t->children[i]->tag, "expr")) {
  12. x = eval_op(x, op, eval(t->children[i]));
  13. i++;
  14. }
  15. return x;
  16. }

The final small step is to change how we print the result found by our evaluation to use our newly defined printing function which can print any type of lval.

  1. lval result = eval(r.output);
  2. lval_println(result);
  3. mpc_ast_delete(r.output);

And we are done! Try running this new program and make sure there are no crashes when dividing by zero.

  1. lispy> / 10 0
  2. Error: Division By Zero!
  3. lispy> / 10 2
  4. 5