Errors

Your implementation is required to report both compile-time and run-time errors. You must use the exceptions defined in include/CompileTimeExceptions.h and the functions defined in runtime/include/run_time_errors.h. Do not modify these files, you can pass a string to a constructor/function to provide more details about a particular error. You must pass the corresponding line number to the exceptions for compile-time errors but not run-time errors. Do not create new errors. Your compiler is only expected to report the first error it encounters.

Compile-time Errors

Compile-time errors must be handled by throwing the exceptions defined in include/CompileTimeExceptions.h. To throw an exception, use the throw keyword.

throw MainError(1, "program does not have a main procedure");

Here are the compile-time errors you need to report:

  • SyntaxError

    Raised during compilation if the parser encounters a syntactic error in the program.

  • SymbolError

    Raised during compilation if an undefined symbol is referenced or a defined symbol is re-defined in the same scope.

  • TypeError

    Raised during compilation if an operation or statement is applied to or betweeen expressions with invalid or incompatible types.

  • AliasingError

    Raised during compilation if the compiler detects that mutable memory locations are aliased.

  • AssignError

    Raised during compilation if the compiler detects an assignment to a const value or a tuple unpacking assignment with the number of lvalues different than the number of fields in the tuple rvalue.

  • MainError

    Raised during compilation if the program does not have a procedure named main or when the signature of main is invalid.

  • ReturnError

    Raised during compilation if the program detects a function or procedure with a return value that does not have a return statement reachable by all control flows.

  • GlobalError

    Raised during compilation if the program detects a var global declaration, a global declaration without an initializing expression, or a global declaration with an invalid initializing expression.

  • StatementError

    Raised during compilation if the program is syntactically valid but the compiler detects an invalid statement in some context. For example, continue or break outside of a loop body.

  • CallError

    Raised during compilation if the procedure call statement is used to call a function. Also raised if a procedure is called in an invalid context. For example, a procedure call in an output stream expression.

  • DefinitionError

    Raised during compilation if a procedure or function is declared but not defined.

  • LiteralError

    Raised during compilation if a literal value in the program does not fit into its corresponding data type.

  • SizeError

    Raised during compilation if the compiler detects an operation or statement is applied to or between vectors and matrices with invalid or incompatible sizes. Read more about when a SizeError should be raised at run-time instead of compile-time in the Compile-time vs Run-time Size Errors section.

Here is an example invalid program and a corresponding compile-time error:

1 procedure main() returns integer {
2     integer x;
3 }
ReturnError on line 1: procedure "main" does not have a return statement reachable by all control flows

Syntax Errors

ANTLR handles syntax errors automatically, but you are required to override the behavior and throw the SyntaxError exception from include/CompileTimeExceptions.h.

For example:

/* main.cpp */

class MyErrorListener : public antlr4::BaseErrorListener {
    void syntaxError(antlr4::Recognizer *recognizer, antlr4::Token * offendingSymbol,
                     size_t line, size_t charPositionInLine, const std::string &msg,
                     std::exception_ptr e) override {
        std::vector<std::string> rule_stack = ((antlr4::Parser*) recognizer)->getRuleInvocationStack();
        // The rule_stack may be used for determining what rule and context the error has occurred in.
        // You may want to print the stack along with the error message, or use the stack contents to
        // make a more detailed error message.

        throw SyntaxError(line, msg); // Throw our exception with ANTLR's error message. You can customize this as appropriate.
    }
};

int main(int argc, char **argv) {

    ...

    gazprea::GazpreaParser parser(&tokens);

    parser.removeErrorListeners(); // Remove the default console error listener
    parser.addErrorListener(new MyErrorListener()); // Add our error listener

    ...
}

For more information regarding the handling of syntax errors in ANTLR, refer to chapter 9 of The Definitive ANTLR 4 Reference.

Run-time Errors

Run-time errors must be handled by calling the functions defined in runtime/include/run_time_errors.h.

MathError("cannot divide by zero")

Here are the run-time errors you need to report:

  • SizeError

    Raised at runtime if an operation or statement is applied to or between vectors and matrices with invalid or incompatible sizes. Read more about when a SizeError should be raised at compile-time instead of run-time in the Compile-time vs Run-time Size Errors section.

  • IndexError

    Raised at runtime if an expression used to index a vector or matrix is an integer, but is invalid for the vector/matrix size.

  • MathError

    Raised at runtime if either zero to the power of zero or a division by zero is evaluated.

  • StrideError

    Raised at runtime if the by operation is used with a stride value <=0.

Here is an example invalid program and a corresponding run-time error:

1 procedure main() returns integer {
2     integer[3] x = [2, 4, 6];
3     return integer[4];
4 }
IndexError: invalid index "4" on vector with size 3

Compile-time vs Run-time Size Errors

While the size of vectors and matrices may not always be known at compile time, there are instances where the compiler can perform length checks at compile time. For instance:

integer[2] vec = 1..10;

For simplicity, this section defines a subset of the size errors detectable at compile-time for which your compiler should report a SizeError at compile-time.

In particular, your compiler should raise a SizeError at compile-time if and only if it finds one of the following five cases:

  1. An operation between vectors or matrices with compatible types such that

    1. each operand vector or matrix expression is formed by operations on literal expressions, and

    2. the sizes of the operand vectors or matrices do not match.

  2. A vector or matrix declaration statement such that

    1. the expressions used to declare the size of the vector or matrix are literal integers,

    2. the declaration is initialized with a vector or matrix expression with compatible type that is formed by operations on literal expressions, and

    3. the size of the initialization expression is larger, in some dimension, than the declared size.

  3. A vector or matrix declaration statement such that

    1. the declaration has no declared size and

    2. there is no initialization expression.

  4. A vector or matrix declaration statement such that

    1. the declaration has no declared size,

    2. the initialization expression has compatible type, and

    3. the initialization expression is not a vector or matrix type.

Here are some example statements that should raise a compile-time SizeError:

[1, 2, 3] + [1.3] -> std_output;
[[1, 2], [3, 4]] % [[2, 2]] -> std_output;
integer[2] vec = [1, 2, 3] + 1;
integer[2, 2] mat = [[1, 2, 3], [4, 5, 6]];
integer[2] vec = 1..10;
character[*] vec;
boolean[*] vec = true;
real[*] vec = 3;

Here are some example statements that should not raise a compile-time SizeError in your implementation, but may raise a run-time SizeError:

[1, 2, 3] + vec -> std_output;
integer[2] vec = [1, 2, 3] + scal;
integer[two] vec = [1, 2, 3];

How to Write an Error Test Case

Your compiler test-suite can include error test cases. An error test case can be a compile-time error test case or a run-time error test case. In either case, the corresponding expected output file should include exactly one line of text. The line text should be the substring of the expected error message preceding the colon. Since there is no standard order for reporting compile-time errors, a compile-time error test case cannot include more than one compile-time error. A run-time error test case can include more than one run-time error since only the first run-time error encountered should be raised.

Here is an example compile-time error test case and corresponding expected output file:

var integer x = 0;

procedure main() returns integer {
  return 0;
}
GlobalError on line 1

Here is an example of a run-time error test case and the corresponding expected output file:

procedure main() returns integer {
  1..1 by 0 -> std_output;
  return 0;
}
StrideError

For error test cases, the tester only inspects the first line of the output. Therefore, you must ensure that your run-time error test cases do not execute any output stream statements before they raise a run-time error. The tester assumes that the first output printed when attempting to compile and run an error test case is the error message.

To handle error test cases, the tester checks the output and reports pass if the output begins with the same substring as the line in the expected output file and fail otherwise.

To ensure that the tester does not falsely identify a regular test case as an error test case, you must not write test cases whose corresponding expected output file contains exactly one line and the substring “Error”.