Concepts / Expression Evaluation with a Parser

Expression Evaluation with a Parser

A syntax error is an error in the syntax of a programming language, and a parser must respond to it deliberately rather than guess.

  • Programming

A Calculator Meets Unexpected Input

Imagine a small calculator program that reads a line such as 4 + 2 and works out what the user meant. The part of the program doing this reading is called a parser. A parser reads text, checks that the text follows the rules of the expression language, and then acts on it. Those rules are not always followed. A user might type a stray character, leave out an operator, or end the line too early. When the text breaks the rules of the language in this way, the problem is called a syntax error.

The central question of this article is what the parser should do at the moment it discovers the problem. Before reading on, try to decide for yourself what a well-designed calculator should do when its input is broken.

What do you think happens?

A user types 2 and presses Enter with no operator after it. The next character the parser sees is the newline. What should the parser do?

  • Assume the user meant to add zero and keep going

  • Stop and signal an error, because a newline is not one of the four operators

  • Ignore the newline and wait for more input silently

Reveal answer
Answer:

Stop and signal an error, because a newline is not one of the four operators.

The parser expects one of plus, minus, times, or divide after a number. A newline is neither a valid operator nor a valid continuation, so the parser throws an error rather than guessing.

Two Honest Choices When Input Breaks the Rules

Definition

A syntax error is an error in the syntax of a programming language, and a parser must respond to it deliberately rather than guess.

Consider a calculator whose parser expects, after each number, one of the four operators plus, minus, times, or divide. If the user types 4 ? 2, the character after the 4 is neither a valid operator nor the end of the line. The parser then has two honest choices. It can guess what the user meant, which is risky because the guess may be wrong. Or it can report that the operator is missing and refuse to continue.

The technique this article follows is one common response: stop the normal flow of work and signal the error by throwing an exception, using an exception class designed for this purpose. The rest of the article explains how that signal is created, where it is raised, and where it is handled.

Tracing getOperator on Bad Input

The source shows the error-signaling technique in a method named getOperator. Its job is to read the next operator from the input. The method first skips any blanks, then looks at the next character without consuming it. If that character is one of the four operators, the method reads it, removing it from the input, and returns it. Otherwise it throws a ParseError, and the message it carries depends on what it found.

Skip blanksLook at nextcharacternot yet consumedIs it an operator?plus, minus, times, divideRead and returnoperatorremoved from inputThrow ParseErrormessage describes missingoperatorIs it end of line?Throw ParseErrormessage names the badcharacteryesnoyesno
When the parser reads a character it does not recognize, which branch does execution take, and where does control stop or continue?

Read the diagram as a set of branches. A passing test leads to a return. Each failing test leads straight to a throw, and once a throw happens, the remaining lines of getOperator do not run. The method has no return statement on the failing paths. Control leaves the method at the throw, so nothing after it executes.

Worked example

Predicting the message for input 2 followed by Enter

The user typed 2 and pressed Enter. getOperator is called next. What happens?

  1. 1
    Skip blanks

    The method first skips any blanks. Here there are none to skip.

  2. 2
    Look at the next character

    The method looks at the next character without consuming it. That character is the newline produced by pressing Enter.

  3. 3
    Test for an operator

    A newline is not plus, minus, times, or divide, so the operator test fails. The method does not read anything.

  4. 4
    Test for end of line

    The character is the end of the line, so the method takes the branch that throws an error describing a missing operator.

  5. 5
    Control leaves the method

    The throw ends getOperator immediately. No return statement runs, so the caller receives an exception rather than an operator.

Result

getOperator throws a ParseError whose message describes the missing operator. The user sees that message rather than a guessed answer.

The ParseError Class

The source uses an exception class named ParseError. The name tells a reader exactly what kind of failure it stands for: a problem found while parsing. The class is defined as a nested class, so it lives inside the surrounding class and is marked private static, which means only the enclosing code can use it. The following sketch is an original illustration consistent with that description.

Code walkthrough
private static class ParseError extends Exception {
    ParseError(String message) {
        super(message);
    }
}
  1. 1
    The class header declares ParseError as a nested class that extends Exception.
  2. 2
    The constructor accepts one String named message.
  3. 3
    The body calls super(message), which passes the message to the Exception class.
  4. 4
    The class adds no new fields or behavior. Its only job is to carry the message.
  • private static: nested inside the parser class and usable only by the enclosing code
  • extends Exception: makes the class throwable
  • constructor parameter: the message text supplied where the error is created
  • super(message): hands the message to the superclass Exception
Exceptionbase classParseErrorcustom exception classParseError(Stringmessage)constructormessagetext describing the problemextendsbelongs topassed insuper(message)
What is the inheritance relationship between the custom exception, its base exception class, and the message that flows through the constructor into the base class?

Where the Error Is Caught and Shown

Throwing an error is only half of the technique. Some code further up the call chain must catch it, or the program stops. The usual Java pattern is a try-catch block: the risky call goes inside try, and the handling code goes inside catch. The example below is an original illustration, not taken from the source, showing how a caller of getOperator might respond.

java
Output
Error: expected an operator but reached end of line
Notes on the code
  • try: the risky call to getOperator goes here
  • catch (ParseError e): handles only the parse error type
  • e.getMessage(): retrieves the text stored by super(message)
Caller in try blockwaiting for getOperatorcatch (ParseError e)message shown to usergetOperatorthrows ParseError herecallsexception propagates
After getOperator throws, which methods are unwound, and where is the exception finally caught and shown to the user?

Notice how the message travels. The text was written inside getOperator at the moment the problem was detected. It moved through the throw, then through the exception object, and finally into the catch block, where the program can show it to the user. The caller never needs to know how getOperator scanned the characters. It only needs to know that a failure of this kind may happen and what to do about it.

Mistakes Beginners Make with Syntax Errors

  • Common mistake

    Treating a bad character as valid and continuing

    What it looks like

    After seeing ? where an operator should be, the method returns a guessed operator and the calculation proceeds.

    Why it fails

    A parser that cannot honestly treat the input as valid should not pretend it is. The guess may produce a wrong answer the user never sees as an error.

    Do this instead

    Throw a ParseError at the point of detection so the problem is reported instead of hidden.

  • Common mistake

    Expecting code after a throw to run

    What it looks like

    Writing a line after the throw in getOperator and assuming it executes when the bad character is found.

    Why it fails

    Control leaves the method at the throw, so the remaining lines on that path do not run.

    Do this instead

    Place any cleanup or alternative logic in the caller, inside a try or catch block, rather than after the throw.

  • Common mistake

    Assuming the caller does not need to change

    What it looks like

    Calling getOperator without a try block and expecting the program to recover on its own.

    Why it fails

    Some code further up the call chain must catch the error, or the program stops.

    Do this instead

    Put the risky call inside try and handle ParseError in a catch block.

  • Common mistake

    Believing the exception class adds behavior: ParseError only passes its message to the superclass with super(message)

Predicting the Message a User Sees

MEDIUM

A user types 4 ? 2 into the calculator. Trace getOperator step by step. Which branch is taken after the 4 is read, and what happens to control? Describe the kind of message the user should see, and explain why the parser does not guess that the user meant plus.

Hints
  • Remember that getOperator peeks at the next character without consuming it.
  • The character ? is neither one of the four operators nor the end of the line.
  • Ask which lines run after a throw and which do not.

Key Takeaways

  • A syntax error occurs when input breaks the rules of the language, and a parser must respond to it deliberately rather than guess what the user meant.
  • ParseError extends Exception, which makes it throwable, and its constructor passes the message to the superclass with super(message).
  • A method such as getOperator throws a ParseError when it finds a bad character, and control leaves the method at the throw without running the remaining lines on that path.
  • A caller places the risky call inside a try block and handles the error in a catch block, where the message can be shown to the user.