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.
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?
Reveal 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
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.
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.
Predicting the message for input 2 followed by Enter
The user typed 2 and pressed Enter. getOperator is called next. What happens?
- 1Skip blanks
The method first skips any blanks. Here there are none to skip.
- 2Look at the next character
The method looks at the next character without consuming it. That character is the newline produced by pressing Enter.
- 3Test for an operator
A newline is not plus, minus, times, or divide, so the operator test fails. The method does not read anything.
- 4Test 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.
- 5Control leaves the method
The throw ends
getOperatorimmediately. No return statement runs, so the caller receives an exception rather than an operator.
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.
private static class ParseError extends Exception {
ParseError(String message) {
super(message);
}
}- 1The class header declares
ParseErroras a nested class that extends Exception. - 2The constructor accepts one String named message.
- 3The body calls
super(message), which passes the message to the Exception class. - 4The 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
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.
Error: expected an operator but reached end of line- try: the risky call to
getOperatorgoes here - catch (
ParseErrore): handles only the parse error type e.getMessage(): retrieves the text stored bysuper(message)
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 likeAfter seeing ? where an operator should be, the method returns a guessed operator and the calculation proceeds.
Why it failsA 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 insteadThrow 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 likeWriting a line after the throw in getOperator and assuming it executes when the bad character is found.
Why it failsControl leaves the method at the throw, so the remaining lines on that path do not run.
Do this insteadPlace 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 likeCalling getOperator without a try block and expecting the program to recover on its own.
Why it failsSome code further up the call chain must catch the error, or the program stops.
Do this insteadPut 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
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
getOperatorpeeks 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.
ParseErrorextends Exception, which makes it throwable, and its constructor passes the message to the superclass withsuper(message).- A method such as
getOperatorthrows aParseErrorwhen 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.