11

I've read several papers, articles, and section 4.1.4, chapter 4 of Compilers: Principles, Techniques, and Tools (2nd Edition) (a.k.a "The Dragon Book") which all discuss the topic of syntactical compiler error recovery. However, after experimenting with several modern compilers, I've seen that they also recover from semantic errors, as well as syntactic errors.

I understand fairly well the algorithms and techniques behind compilers recovering from syntacticly related errors, however I don't exactly understand how a compiler can recover from a semantic error.

I'm currently using a slight variation of the visitor pattern to generate code from my abstract syntax tree. Consider my compiler compiling the following expressions:

1 / (2 * (3 + "4"))

The compiler would generate the following abstract syntax tree:

      op(/)
        |
     -------
    /       \ 
 int(1)    op(*)
             |
          -------
         /       \
       int(2)   op(+)
                  |
               -------
              /       \
           int(3)   str(4)

The code-generation phase would then use the visitor pattern to recursively traverse the abstract syntax tree and perform type checking. The abstract syntax tree would be traversed until the compiler came to the innermost part of the expression; (3 + "4"). The compiler then checks each side of the expressions and sees that they're not semantically equivalent. The compiler raises a type error. Here is where the problem lies. What now should the compiler do?

For the compiler to recover from this error and continue type checking the outer parts of the expressions, it would have to return some type (int or str) from evaluating the innermost part of the expression, to the next innermost part of the expression. But it simply doesn't have a type to return. Since a type error occurred, no type was deduced.

One possible solution I've postulated, is that if a type error does occur, an error should be raised, and a special value that signifies that a type error occurred, should be returned to previous abstract syntax tree traversal calls. If previous traversal calls encounter this value, they know that a type error occurred deeper in the abstract syntax tree, and should avoid trying to deduce a type. While this method does seem to work, it seems to be very inefficient. If the innermost part of an expression is deep in the abstract syntax tree, then the compiler will have to make many recursive calls only to realize that no real work can be done, and simply return from each one.

Is the method I described above used (I doubt it). If so, is it not efficient? If not, what exactly are the methods used when compilers recover from semantic errors?

1
  • 3
    Pretty sure that is what is used, and why don’t you think it’s efficient enough? To do the type checking, the compiler has to walk the entire tree anyways. A semantic failure is more efficient since it lets the compiler eliminate a branch once the error is found.
    – Telastyn
    Jan 18, 2018 at 23:50

4 Answers 4

9

Your proposed idea is essentially correct.

The key is that the type of an AST node is calculated only once and then stored. Whenever the type is needed again, it simply retrieves the stored type. If the resolution ends in an error, an error type is stored instead.

5

One interesting approach is to have a special type for errors. When such an error is first encountered, a diagnostic is logged, and the error type is returned as the type of the expression. This error type has some interesting properties:

  • Any operation that is performed on it succeeds (in order to prevent a cascade of error messages all caused by the same original fault)
  • The result of any operation performed on an object with error type also has error type
  • If an error type gets as far as code generation, the code generator spots the use and generates code that fails (e.g. throws an exception, aborts, or whatever is appropriate for your language)

With this combination, you can actually successfully compile code that contains type errors, and as long as that code isn't actually used no runtime error will occur. This can be useful, for example, to allow you to run unit tests for parts of the code that are unaffected.

1
  • Thanks for the answer Jules. Funny enough, this is the exact method I ended up using. Great minds think alike, eh? ;-) Apr 4, 2018 at 17:33
2

If there is a semantic error, a compile error message indicating such is issued to the user.

Once that is done, it is ok to abort compiling as the input program is in error — it is not a legal program in the language, so it can simply be rejected.

That's pretty harsh, though, so there are softer alternatives. Abort any code generation and output file generation, yet continue something to look for more errors.

For example, it can simply abort any further type analysis for the current expression tree, and continue processing expressions from subsequent statements.

2

Let's assume your language allows adding integers, and allows concatenation of strings with the + operator.

Since int + string is not allowed, evaluating the + will result in an error being reported. The compiler could just return error as the type. Or it might be more clever, since int + int -> int and string + string -> string are allowed, it might return "error, could be int or string".

Then comes the * operator, and we'll assume only int + int is allowed. The compiler may then decide that the + actually was supposed to return int, and the type returned for the * would then be int, without any error message.

3
  • I'm think I follow you, @gnasher, but what exactly do you mean by the "" operator? Was that typo? Jan 19, 2018 at 0:46
  • @ChristianDean there’s an asterisk in the quotes which is being interpreted as Markdown markup instead of being rendered.
    – JakeRobb
    Jan 19, 2018 at 4:01
  • I’ve submitted an edit to the answer which will resolve the problem as soon as my edit gets peer reviewed.
    – JakeRobb
    Jan 19, 2018 at 4:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.