Wednesday, June 30, 2021

Handling errors with grace (and sometimes without it)

Reading Time: 6 minutes
On addressing the dangers that waddle into your way, whether you like it or not.

A year and a half ago I picked up Crafting Interpreters. The project guides programmers through building their own interpreters for the Lox programming language. I started writing a blog series about my progress. You can see all the posts so far right here.

In the last post, we dove into chapter 7 to write an interpreter with the visitor pattern. That’s all well and good when our Lox code is written correctly. But how do we handle the cases where it isn’t?

At the interpretation stage, two things can go sideways:

  1. Lox encounters an operator that it does not know about, like $ or #.
  2. Lox encounters an operator it knows about with operands that don’t associate with that operator, like "two heads" > "one".

In our interpreter code, we have to account for those. We can do that by executing the appropriate checks at the appropriate times and then bubbling up any issues through the Lox run loop.

1. Catching Issues

Remember in the last post, when we looked at this method exemplifying the interpreter’s expression evaluation?

@Override
public Object visitUnaryExpr(Expr.Unary expr) {
        Object right = evaluate(expr.right);

        switch (expr.operator.type) {
            case MINUS:
                checkNumberOperand(expr.operator, right); //we'll get to this later
                return -(double) right;
            case BANG:
                return !isTruthy(right); //we'll also get to this later
        }

        ...
}

I promised to get back to checkNumberOperand() later. As it turns out, that method looks like this:

    private void checkNumberOperand(Token operator, Object operand) {
        if (operand instanceof Double) return;
        throw new RuntimeError(operator, "Operand must be a number.");
    }

We have a similar check embedded inside the interpreter method visitBinaryExpr() to check that both operands are numbers when the operator is something that operates exclusively on numbers, like one of these: > < >= <= * / -

A common lament about error handling is that there isn’t really a good way to completely separate it from the operational code. It sort of has to embed itself in each expression that could receive an input that produces an undesirable output.

At some point, most authors and teachers who talk about software architecture come around to this in one way or another. I’ve heard two solutions that I find myself returning to time and time again:

  1. Avdi Grimm in Confident Ruby: Accept dealing with nonnominal inputs as an expected and necessary part of your workflow, and group it as much as possible at the beginning of a function’s work rather than littering it all over the code (blog post here). That’s what you see exemplified above: we check that the operand is a number as soon as we know that the operator is a minus sign. This is also the idea behind the guard clause pattern.
  2. Michael Feathers, Edge Free Programming: Find ways to turn nonnominal inputs into nominal, expected inputs to keep your code as streamlined as possible (blog post here with examples of how to do this). That might be, to use one of Bob’s examples, taking an operator Lox doesn’t use (such as + as a unary operator a la +123 being the same as 123) and add it to the operators we do parse, surfacing the message “Lox does not support the + operator for just one operand” as the default behavior in the same way the other operators get evaluated as the default behavior.

Though the second of those solutions sounds preferable to the first, I wanted to start with the more familiar example. Also, even if the second one sounds more preferable and clean, from a practical perspective I find that I don’t usually arrive at ‘good’ executions of the second solution until I have noticed patterns in how I’m using the first solution, or come back to a case where I used the first solution after having some time to think.

2. Surfacing Issues

So now we know there’s a problem. What do we do?

In the prior post, I mentioned that I would show where we’re calling the interpreter’s evaluate method in this post, since it includes error handling. Here it is, also on the interpreter:

void interpret(Expr expression) {
     try {
         Object value = evaluate(expression);
     } catch (RuntimeError error) {
         Lox.runtimeError(error);
     }
}

So, when checkNumberOperand() throws that RuntimeError, this will catch it and call our own runtimeError method in the Lox runner:

    static void runtimeError(RuntimeError error) {
        System.err.println(error.getMessage() + "\n[line " + error.token.line + "]");
        hadRuntimeError = true;
    }

We’re calling that interpreter method in the Lox run loop itself. Lox has a static instance of the interpreter, and when some code is run, it scans, parses, and then interprets our code with this static method:

    private static void run(String source) {
        Scanner scanner = new Scanner(source);
        List<Token> tokens = scanner.scanTokens();

        for (Token token: tokens) {
            System.out.println(token);
        }
        Parser parser = new Parser(tokens);
        Expr expression = parser.parse();

        // Stop if there was a syntax error.
        if (hadError) return;

        interpreter.interpret(expression); // <===== HERE!!!
    }

We don’t catch the error that might have been thrown here. Instead, we let RuntimeError surface that message you see above. Worth noting: RuntimeError is a class of our own, which is different from (and inherits from) Java’s RuntimeException. We did this to maintain control over what the message is and to make it clear that this is an error coming from Lox, not the underlying implementing language:

class RuntimeError extends RuntimeException {
    final Token token;

    RuntimeError(Token token, String message) {
        super(message);
        this.token = token;
    }
}

Like many of the Gang of Four patterns or, say, the implementation of a decorator in Python, the process of catching, and throwing error messages as we rise through the call stack just kinda looks gnarly until you get familiar with it. At least, that has been my experience with it.

The next chapter of Crafting Interpreters is called “Statements and State.” I haven’t looked at it yet, but I’m excited about it. Expect more soon.

If you liked this post, you might also like…

The rest of the Crafting Interpreters series

This post about structural verification (I’m just figuring you’re into objectcraft, so)

This post about why use, or not use, an interface (specifically in Python because of the way Python does, or rather doesn’t exactly do, interfaces)

Like this:

Like Loading...



from Hacker News https://ift.tt/3gWG7sT

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.