Skip to content

Commit

Permalink
refactor[next] error handling (#1275)
Browse files Browse the repository at this point in the history
Replace exceptions and error handling with a consistent, improved model.
  • Loading branch information
petiaccja authored Jul 21, 2023
1 parent 8b3fc64 commit 1ebd302
Show file tree
Hide file tree
Showing 40 changed files with 1,044 additions and 809 deletions.
2 changes: 1 addition & 1 deletion src/gt4py/cartesian/frontend/node_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ def recurse(node: Node) -> Generator[Node, None, None]:
def location_to_source_location(loc: Optional[Location]) -> Optional[eve.SourceLocation]:
if loc is None or loc.line <= 0 or loc.column <= 0:
return None
return eve.SourceLocation(line=loc.line, column=loc.column, source=loc.scope)
return eve.SourceLocation(line=loc.line, column=loc.column, filename=loc.scope)
2 changes: 1 addition & 1 deletion src/gt4py/cartesian/gtc/dace/expansion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def get_dace_debuginfo(node: common.LocNode):
node.loc.column,
node.loc.line,
node.loc.column,
node.loc.source,
node.loc.filename,
)
else:
return dace.dtypes.DebugInfo(0)
Expand Down
47 changes: 15 additions & 32 deletions src/gt4py/eve/concepts.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

from __future__ import annotations

import ast
import copy
import re

Expand Down Expand Up @@ -58,58 +57,42 @@ class SymbolRef(ConstrainedStr, regex=_SYMBOL_NAME_RE):

@datamodels.datamodel(slots=True, frozen=True)
class SourceLocation:
"""Source code location (line, column, source)."""
"""File-line-column information for source code."""

filename: Optional[str]
line: int = datamodels.field(validator=_validators.ge(1))
column: int = datamodels.field(validator=_validators.ge(1))
source: str
end_line: Optional[int] = datamodels.field(validator=_validators.optional(_validators.ge(1)))
end_column: Optional[int] = datamodels.field(validator=_validators.optional(_validators.ge(1)))

@classmethod
def from_AST(cls, ast_node: ast.AST, source: Optional[str] = None) -> SourceLocation:
if (
not isinstance(ast_node, ast.AST)
or getattr(ast_node, "lineno", None) is None
or getattr(ast_node, "col_offset", None) is None
):
raise ValueError(
f"Passed AST node '{ast_node}' does not contain a valid source location."
)
if source is None:
source = f"<ast.{type(ast_node).__name__} at 0x{id(ast_node):x}>"
return cls(
ast_node.lineno,
ast_node.col_offset + 1,
source,
end_line=ast_node.end_lineno,
end_column=ast_node.end_col_offset + 1 if ast_node.end_col_offset is not None else None,
)

def __init__(
self,
filename: Optional[str],
line: int,
column: int,
source: str,
*,
end_line: Optional[int] = None,
end_column: Optional[int] = None,
) -> None:
assert end_column is None or end_line is not None
self.__auto_init__( # type: ignore[attr-defined] # __auto_init__ added dynamically
line=line, column=column, source=source, end_line=end_line, end_column=end_column
filename=filename, line=line, column=column, end_line=end_line, end_column=end_column
)

def __str__(self) -> str:
src = self.source or ""
filename_str = self.filename or "-"

end_line_str = self.end_line if self.end_line is not None else "-"
end_column_str = self.end_column if self.end_column is not None else "-"

end_part = ""
if self.end_line is not None:
end_part += f" to {self.end_line}"
end_str: Optional[str] = None
if self.end_column is not None:
end_part += f":{self.end_column}"
end_str = f"{end_line_str}:{end_column_str}"
elif self.end_line is not None:
end_str = f"{end_line_str}"

return f"<{src}:{self.line}:{self.column}{end_part}>"
if end_str is not None:
return f"<{filename_str}:{self.line}:{self.column} to {end_str}>"
return f"<{filename_str}:{self.line}:{self.column}>"


@datamodels.datamodel(slots=True, frozen=True)
Expand Down
38 changes: 0 additions & 38 deletions src/gt4py/next/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,41 +108,3 @@ class NeighborTable(Connectivity, Protocol):
class GridType(StrEnum):
CARTESIAN = "cartesian"
UNSTRUCTURED = "unstructured"


class GTError:
"""Base class for GridTools exceptions.
Notes:
This base class has to be always inherited together with a standard
exception, and thus it should not be used as direct superclass
for custom exceptions. Inherit directly from :class:`GTTypeError`,
:class:`GTTypeError`, ...
"""

...


class GTRuntimeError(GTError, RuntimeError):
"""Base class for GridTools run-time errors."""

...


class GTSyntaxError(GTError, SyntaxError):
"""Base class for GridTools syntax errors."""

...


class GTTypeError(GTError, TypeError):
"""Base class for GridTools type errors."""

...


class GTValueError(GTError, ValueError):
"""Base class for GridTools value errors."""

...
39 changes: 39 additions & 0 deletions src/gt4py/next/errors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# GT4Py - GridTools Framework
#
# Copyright (c) 2014-2023, ETH Zurich
# All rights reserved.
#
# This file is part of the GT4Py project and the GridTools framework.
# GT4Py is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or any later
# version. See the LICENSE.txt file at the top-level directory of this
# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Contains the exception classes and other utilities for error handling."""

from . import ( # noqa: module needs to be loaded for pretty printing of uncaught exceptions.
excepthook,
)
from .excepthook import set_verbose_exceptions
from .exceptions import (
DSLError,
InvalidParameterAnnotationError,
MissingAttributeError,
MissingParameterAnnotationError,
UndefinedSymbolError,
UnsupportedPythonFeatureError,
)


__all__ = [
"DSLError",
"InvalidParameterAnnotationError",
"MissingAttributeError",
"MissingParameterAnnotationError",
"UndefinedSymbolError",
"UnsupportedPythonFeatureError",
"set_verbose_exceptions",
]
87 changes: 87 additions & 0 deletions src/gt4py/next/errors/excepthook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# GT4Py - GridTools Framework
#
# Copyright (c) 2014-2023, ETH Zurich
# All rights reserved.
#
# This file is part of the GT4Py project and the GridTools framework.
# GT4Py is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or any later
# version. See the LICENSE.txt file at the top-level directory of this
# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""
Loading this module registers an excepthook that formats :class:`DSLError`.
The excepthook is necessary because the default hook prints :class:`DSLError`s
in an inconvenient way. The previously set excepthook is used to print all
other errors.
"""

import os
import sys
import warnings
from typing import Callable

from . import exceptions, formatting


def _get_verbose_exceptions_envvar() -> bool:
"""Detect if the user enabled verbose exceptions in the environment variables."""
env_var_name = "GT4PY_VERBOSE_EXCEPTIONS"
if env_var_name in os.environ:
false_values = ["0", "false", "off"]
true_values = ["1", "true", "on"]
value = os.environ[env_var_name].lower()
if value in false_values:
return False
elif value in true_values:
return True
else:
values = ", ".join([*false_values, *true_values])
msg = f"the 'GT4PY_VERBOSE_EXCEPTIONS' environment variable must be one of {values} (case insensitive)"
warnings.warn(msg)
return False


_verbose_exceptions: bool = _get_verbose_exceptions_envvar()


def set_verbose_exceptions(enabled: bool = False) -> None:
"""Programmatically set whether to use verbose printing for uncaught errors."""
global _verbose_exceptions
_verbose_exceptions = enabled


def _format_uncaught_error(err: exceptions.DSLError, verbose_exceptions: bool) -> list[str]:
if verbose_exceptions:
return formatting.format_compilation_error(
type(err),
err.message,
err.location,
err.__traceback__,
err.__cause__,
)
else:
return formatting.format_compilation_error(type(err), err.message, err.location)


def compilation_error_hook(fallback: Callable, type_: type, value: BaseException, tb) -> None:
"""
Format `CompilationError`s in a neat way.
All other Python exceptions are formatted by the `fallback` hook. When
verbose exceptions are enabled, the stack trace and cause of the error is
also printed.
"""
if isinstance(value, exceptions.DSLError):
exc_strs = _format_uncaught_error(value, _verbose_exceptions)
print("".join(exc_strs), file=sys.stderr)
else:
fallback(type_, value, tb)


_fallback = sys.excepthook
sys.excepthook = lambda ty, val, tb: compilation_error_hook(_fallback, ty, val, tb)
104 changes: 104 additions & 0 deletions src/gt4py/next/errors/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# GT4Py - GridTools Framework
#
# Copyright (c) 2014-2023, ETH Zurich
# All rights reserved.
#
# This file is part of the GT4Py project and the GridTools framework.
# GT4Py is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or any later
# version. See the LICENSE.txt file at the top-level directory of this
# distribution for a copy of the license or check <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""
The list of exception classes used in the library.
Exception classes that represent errors within an IR go here as a subclass of
:class:`DSLError`. Exception classes that represent other errors, like
the builtin ValueError, go here as well, although you should use Python's
builtin error classes if you can. Exception classes that are specific to a
certain submodule and have no use for the entire application may be better off
in that submodule as opposed to being in this file.
"""

from __future__ import annotations

import textwrap
from typing import Any, Optional

from gt4py.eve import SourceLocation

from . import formatting


class DSLError(Exception):
location: Optional[SourceLocation]

def __init__(self, location: Optional[SourceLocation], message: str) -> None:
self.location = location
super().__init__(message)

@property
def message(self) -> str:
return self.args[0]

def with_location(self, location: Optional[SourceLocation]) -> DSLError:
self.location = location
return self

def __str__(self) -> str:
if self.location:
loc_str = formatting.format_location(self.location, show_caret=True)
return f"{self.message}\n{textwrap.indent(loc_str, ' ')}"
return self.message


class UnsupportedPythonFeatureError(DSLError):
feature: str

def __init__(self, location: Optional[SourceLocation], feature: str) -> None:
super().__init__(location, f"unsupported Python syntax: '{feature}'")
self.feature = feature


class UndefinedSymbolError(DSLError):
sym_name: str

def __init__(self, location: Optional[SourceLocation], name: str) -> None:
super().__init__(location, f"name '{name}' is not defined")
self.sym_name = name


class MissingAttributeError(DSLError):
attr_name: str

def __init__(self, location: Optional[SourceLocation], attr_name: str) -> None:
super().__init__(location, f"object does not have attribute '{attr_name}'")
self.attr_name = attr_name


class TypeError_(DSLError):
def __init__(self, location: Optional[SourceLocation], message: str) -> None:
super().__init__(location, message)


class MissingParameterAnnotationError(TypeError_):
param_name: str

def __init__(self, location: Optional[SourceLocation], param_name: str) -> None:
super().__init__(location, f"parameter '{param_name}' is missing type annotations")
self.param_name = param_name


class InvalidParameterAnnotationError(TypeError_):
param_name: str
annotated_type: Any

def __init__(self, location: Optional[SourceLocation], param_name: str, type_: Any) -> None:
super().__init__(
location, f"parameter '{param_name}' has invalid type annotation '{type_}'"
)
self.param_name = param_name
self.annotated_type = type_
Loading

0 comments on commit 1ebd302

Please sign in to comment.