Skip to content

Commit

Permalink
Improved error handling.
Browse files Browse the repository at this point in the history
- Separate error classes for scanning/parsing errors depending on whether the error is recoverable by adding more code at the end
- die() now puts the error message in the exception rather than printing it directly
- Improved a couple of error messages
  • Loading branch information
dloscutoff committed Mar 19, 2022
1 parent 02f985e commit b2ba974
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 56 deletions.
16 changes: 12 additions & 4 deletions errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@

class FatalError(Exception):
"""Class for throwing fatal errors."""
def __str__(self):
return " ".join(map(str, self.args))

class BadSyntax(FatalError):
"""Unrecoverable syntax error, e.g. starting with a binary operator."""
pass

class IncompleteSyntax(FatalError):
"""Recoverable syntax error, e.g. unmatched open parenthesis."""
pass

class ErrorReporter:
Expand All @@ -19,10 +28,9 @@ def warn(self, *message):
if self._warnings:
print(*map(rewritePtypes, message), file=sys.stderr)

def die(self, *message):
"""Print a fatal error and exit."""
print(*map(rewritePtypes, message), file=sys.stderr)
raise FatalError()
def die(self, *message, errorClass=FatalError):
"""Raise a fatal error."""
raise errorClass(*map(rewritePtypes, message))


def rewritePtypes(message):
Expand Down
10 changes: 6 additions & 4 deletions execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,12 +1121,14 @@ def EVAL(self, code, argList=None):
# Scan, parse, and convert to Block first
try:
tkns = scanning.scan(str(code) + "\n")
except FatalError:
self.err.die("Fatal scanning error while evaluating", code)
except FatalError as err:
self.err.die(f"Scanning error while evaluating {code!r}:",
err)
try:
tree = parsing.parse(tkns)
except FatalError:
self.err.die("Fatal parsing error while evaluating", code)
except FatalError as err:
self.err.die(f"Parsing error while evaluating {code!r}:",
err)
code = self.BLOCK(tree)
if isinstance(code, Block) and argList is not None:
return self.functionCall(code, argList)
Expand Down
39 changes: 25 additions & 14 deletions parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import ptypes # TBD: add another class or two to tokens and refactor this
# dependency? Would allow ptypes to import isExpr, which might
# be helpful for Blocks...?
from errors import ErrorReporter
from errors import ErrorReporter, BadSyntax, IncompleteSyntax

assignOp = operators.opsByArity[2][":"]
err = ErrorReporter(warnings=True) # TODO: get this setting from the args?
Expand All @@ -23,7 +23,8 @@ def parse(tokenList):
def parseStatement(tokenList):
"Parse a statement from the beginning of the token list."
if tokenList[0] is None:
err.die("Hit end of tokens while parsing statement")
err.die("Hit end of tokens while parsing statement",
errorClass=IncompleteSyntax)
elif isinstance(tokenList[0], tokens.Command):
token = tokenList.pop(0)
command = operators.commands[token]
Expand Down Expand Up @@ -72,12 +73,14 @@ def parseNameList(tokenList):
tokenList.pop(0)
if len(nameList) == 1:
# No names in the list, just the enlist operator
err.die("List of names in for-loop header cannot be empty")
err.die("List of names in for-loop header cannot be empty",
errorClass=BadSyntax)
elif tokenList[0] is None:
err.die("Unterminated list of names in for-loop header")
err.die("Unterminated list of names in for-loop header",
errorClass=IncompleteSyntax)
else:
err.die("For-loop header must be name or list of names, not",
tokenList[0])
tokenList[0], errorClass=BadSyntax)
# A semicolon after the name list is unnecessary but legal
if tokenList[0] == ";":
tokenList.pop(0)
Expand All @@ -93,9 +96,10 @@ def parseBlock(tokenList):
if tokenList[0] == "}":
tokenList.pop(0)
elif tokenList[0] is None:
err.die("Unterminated block")
err.die("Unterminated block", errorClass=IncompleteSyntax)
else:
err.die("Expecting } at end of block, got", tokenList[0])
err.die("Expecting } at end of block, got", tokenList[0],
errorClass=BadSyntax)
else:
# Single statement
# Have to wrap it in a list to make it a code block
Expand Down Expand Up @@ -250,7 +254,8 @@ def parseOperand(tokenList):
expressions = []
while tokenList[0] != ")":
if tokenList[0] is None:
err.die("Unterminated parenthesis")
err.die("Unterminated parenthesis",
errorClass=IncompleteSyntax)
else:
expressions.append(parseExpr(tokenList))
# Remove the closing parenthesis
Expand All @@ -271,7 +276,7 @@ def parseOperand(tokenList):
subExpression = [operators.enlist]
while tokenList[0] != "]":
if tokenList[0] is None:
err.die("Unterminated list")
err.die("Unterminated list", errorClass=IncompleteSyntax)
else:
subExpression.append(parseExpr(tokenList))
tokenList.pop(0)
Expand All @@ -292,9 +297,12 @@ def parseOperand(tokenList):
op = op.copy()
op.fold = True
op.arity = 1
elif tokenList[0] is None:
err.die("Missing operator for $ meta-operator",
errorClass=IncompleteSyntax)
else:
err.die("Missing/wrong operator for $ meta-operator: got",
tokenList[0], "instead")
err.die("Wrong operator for $ meta-operator: got",
tokenList[0], "instead", errorClass=BadSyntax)
else:
op = operators.opsByArity[1][token]
# Check for the * and : meta-operators
Expand All @@ -314,12 +322,15 @@ def parseOperand(tokenList):

# If control reaches here, we've got a problem
if tokenList[0] is None:
err.die("Hit end of tokens while parsing expression")
err.die("Hit end of tokens while parsing expression",
errorClass=IncompleteSyntax)
elif (tokenList[0] in operators.opsByArity[2] or
tokenList[0] in operators.opsByArity[3]):
err.die(tokenList[0], "is not a unary operator")
err.die(tokenList[0], "is not a unary operator",
errorClass=BadSyntax)
else:
err.die("Expected expression, got", repr(tokenList[0]))
err.die("Expected expression, got", repr(tokenList[0]),
errorClass=BadSyntax)


def unparse(tree, statementSep=""):
Expand Down
56 changes: 25 additions & 31 deletions pip.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -185,18 +185,18 @@ def pip(code=None, argv=None, interactive=True):
print()
try:
tokens = scan(program)
except FatalError:
print("Fatal error while scanning, execution aborted.",
file=sys.stderr)
except FatalError as err:
print("Fatal error while scanning:", err, file=sys.stderr)
print("Execution aborted.", file=sys.stderr)
sys.exit(1)
if options.verbose:
print(addSpaces(tokens))
print()
try:
parse_tree = parse(tokens)
except FatalError:
print("Fatal error while parsing, execution aborted.",
file=sys.stderr)
except FatalError as err:
print("Fatal error while parsing:", err, file=sys.stderr)
print("Execution aborted.", file=sys.stderr)
sys.exit(1)
if options.verbose:
pprint.pprint(parse_tree)
Expand All @@ -217,55 +217,49 @@ def pip(code=None, argv=None, interactive=True):
for arg in raw_args:
try:
arg_tokens = scan(arg)
except FatalError:
print(f"Fatal error while scanning argument {arg!r}, "
"execution aborted.",
file = sys.stderr)
except FatalError as err:
print(f"Fatal error while scanning argument {arg!r}:",
err, file=sys.stderr)
print("Execution aborted.", file=sys.stderr)
sys.exit(1)
try:
arg_parse_tree = parse(arg_tokens)
except FatalError:
print(f"Fatal error while parsing argument {arg!r}, "
"execution aborted.",
file = sys.stderr)
except FatalError as err:
print(f"Fatal error while parsing argument {arg!r}:",
err, file=sys.stderr)
print("Execution aborted.", file=sys.stderr)
sys.exit(1)
parsed_arg = arg_parse_tree[0]
try:
program_args.append(state.executeStatement(parsed_arg))
except FatalError:
print(f"Fatal error while evaluating argument {arg!r}, "
"execution aborted.",
file = sys.stderr)
except (FatalError, RuntimeError) as err:
# RuntimeError probably means we exceeded Python's
# max recursion depth
print(f"Fatal error while evaluating argument {arg!r}:",
err, file=sys.stderr)
print("Execution aborted.", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("Program terminated by user while evaluating "
f"argument {arg!r}.",
file=sys.stderr)
sys.exit(1)
except RuntimeError as err:
# Probably exceeded Python's max recursion depth
print(f"Fatal error while evaluating argument {arg!r}:",
err,
file=sys.stderr)
sys.exit(1)
else:
# Treat each argument as a Scalar
program_args = [Scalar(arg) for arg in raw_args]
if interactive:
print("Executing...")
try:
state.executeProgram(parse_tree, program_args)
except FatalError:
print("Fatal error during execution, program terminated.",
file=sys.stderr)
except (FatalError, RuntimeError) as err:
# RuntimeError probably means we exceeded Python's max
# recursion depth
print("Fatal error during execution:", err, file=sys.stderr)
print("Program terminated.", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("Program terminated by user.", file=sys.stderr)
sys.exit(1)
except RuntimeError as err:
# Probably exceeded Python's max recursion depth
print("Fatal error:", err, file=sys.stderr)
sys.exit(1)

if __name__ == "__main__":
if len(sys.argv) == 1:
Expand Down
4 changes: 2 additions & 2 deletions scanning.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
import tokens
import operators
from errors import ErrorReporter
from errors import ErrorReporter, IncompleteSyntax


err = ErrorReporter(warnings=True) # TODO: get this setting from the args?
Expand Down Expand Up @@ -110,7 +110,7 @@ def tokenize(code):
code = code[index:]
elif code[0] in '"`\'' or code[:2] == '\\"':
err.die("Unterminated string or pattern literal:",
code.strip())
code.strip(), errorClass=IncompleteSyntax)
else:
if code[0] == "j":
err.warn("While scanning, ignored 'j' "
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

VERSION = "1.0.2"
COMMIT_DATE = "2021-12-29"
COMMIT_DATE = "2022-03-19"

0 comments on commit b2ba974

Please sign in to comment.