Retain compiled source and file information for exceptions

This commit refactors the exception/error classes and their handling.
It also retains Hy source strings and their originating file information, when
available, all throughout the core parser and compiler functions.

As well, with these changes, calling code is no longer responsible for providing
source and file details to exceptions,

Closes hylang/hy#657.
This commit is contained in:
Brandon T. Willard 2018-10-28 20:43:17 -05:00 committed by Kodi Arfer
parent 902926c543
commit 51c7efe6e8
14 changed files with 525 additions and 278 deletions

View File

@ -6,7 +6,7 @@ try:
import __builtin__ as builtins
except ImportError:
import builtins # NOQA
import sys, keyword
import sys, keyword, textwrap
PY3 = sys.version_info[0] >= 3
PY35 = sys.version_info >= (3, 5)
@ -22,11 +22,39 @@ bytes_type = bytes if PY3 else str # NOQA
long_type = int if PY3 else long # NOQA
string_types = str if PY3 else basestring # NOQA
#
# Inspired by the same-named `six` functions.
#
if PY3:
exec('def raise_empty(t, *args): raise t(*args) from None')
raise_src = textwrap.dedent('''
def raise_from(value, from_value):
try:
raise value from from_value
finally:
traceback = None
''')
def reraise(exc_type, value, traceback=None):
try:
raise value.with_traceback(traceback)
finally:
traceback = None
else:
def raise_empty(t, *args):
raise t(*args)
def raise_from(value, from_value=None):
raise value
raise_src = textwrap.dedent('''
def reraise(exc_type, value, traceback=None):
try:
raise exc_type, value, traceback
finally:
traceback = None
''')
raise_code = compile(raise_src, __file__, 'exec')
exec(raise_code)
def isidentifier(x):
if x in ('True', 'False', 'None', 'print'):

View File

@ -19,9 +19,9 @@ import astor.code_gen
import hy
from hy.lex import hy_parse, mangle
from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy.lex.exceptions import PrematureEndOfInput
from hy.compiler import HyASTCompiler, hy_compile, hy_eval
from hy.errors import HyTypeError
from hy.errors import HyTypeError, HyLanguageError, HySyntaxError
from hy.importer import runhy
from hy.completer import completion, Completer
from hy.macros import macro, require
@ -101,15 +101,11 @@ class HyREPL(code.InteractiveConsole, object):
self.showtraceback()
try:
try:
do = hy_parse(source)
except PrematureEndOfInput:
return True
except LexException as e:
if e.source is None:
e.source = source
e.filename = filename
error_handler(e, use_simple_traceback=True)
do = hy_parse(source, filename=filename)
except PrematureEndOfInput:
return True
except HySyntaxError as e:
error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
return False
try:
@ -121,9 +117,12 @@ class HyREPL(code.InteractiveConsole, object):
[ast.Expr(expr_ast.body)])
print(astor.to_source(new_ast))
value = hy_eval(do, self.locals,
value = hy_eval(do, self.locals, self.module,
ast_callback=ast_callback,
compiler=self.hy_compiler)
compiler=self.hy_compiler,
filename=filename,
source=source)
except HyTypeError as e:
if e.source is None:
e.source = source
@ -131,7 +130,7 @@ class HyREPL(code.InteractiveConsole, object):
error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
return False
except Exception as e:
error_handler(e)
error_handler(e, use_simple_traceback=SIMPLE_TRACEBACKS)
return False
if value is not None:
@ -208,17 +207,19 @@ SIMPLE_TRACEBACKS = True
def pretty_error(func, *args, **kw):
try:
return func(*args, **kw)
except (HyTypeError, LexException) as e:
except HyLanguageError as e:
if SIMPLE_TRACEBACKS:
print(e, file=sys.stderr)
sys.exit(1)
raise
def run_command(source):
tree = hy_parse(source)
require("hy.cmdline", "__main__", assignments="ALL")
pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
def run_command(source, filename=None):
tree = hy_parse(source, filename=filename)
__main__ = importlib.import_module('__main__')
require("hy.cmdline", __main__, assignments="ALL")
pretty_error(hy_eval, tree, None, __main__, filename=filename,
source=source)
return 0
@ -340,7 +341,7 @@ def cmdline_handler(scriptname, argv):
if options.command:
# User did "hy -c ..."
return run_command(options.command)
return run_command(options.command, filename='<string>')
if options.mod:
# User did "hy -m ..."
@ -356,7 +357,7 @@ def cmdline_handler(scriptname, argv):
if options.args:
if options.args[0] == "-":
# Read the program from stdin
return run_command(sys.stdin.read())
return run_command(sys.stdin.read(), filename='<stdin>')
else:
# User did "hy <filename>"
@ -447,11 +448,12 @@ def hy2py_main():
if options.FILE is None or options.FILE == '-':
source = sys.stdin.read()
hst = pretty_error(hy_parse, source, filename='<stdin>')
else:
with io.open(options.FILE, 'r', encoding='utf-8') as source_file:
source = source_file.read()
hst = hy_parse(source, filename=options.FILE)
hst = pretty_error(hy_parse, source)
if options.with_source:
# need special printing on Windows in case the
# codepage doesn't support utf-8 characters

View File

@ -9,20 +9,21 @@ from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
notpexpr, dolike, pexpr, times, Tag, tag, unpack)
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
from hy.errors import HyCompileError, HyTypeError
from hy.errors import (HyCompileError, HyTypeError, HyEvalError,
HyInternalError)
from hy.lex import mangle, unmangle
from hy._compat import (str_type, string_types, bytes_type, long_type, PY3,
PY35, raise_empty)
from hy._compat import (string_types, str_type, bytes_type, long_type, PY3,
PY35, reraise)
from hy.macros import require, load_macros, macroexpand, tag_macroexpand
import hy.core
import pkgutil
import traceback
import importlib
import inspect
import pkgutil
import types
import ast
import sys
@ -340,22 +341,33 @@ def is_unpack(kind, x):
class HyASTCompiler(object):
"""A Hy-to-Python AST compiler"""
def __init__(self, module):
def __init__(self, module, filename=None, source=None):
"""
Parameters
----------
module: str or types.ModuleType
Module in which the Hy tree is evaluated.
Module name or object in which the Hy tree is evaluated.
filename: str, optional
The name of the file for the source to be compiled.
This is optional information for informative error messages and
debugging.
source: str, optional
The source for the file, if any, being compiled. This is optional
information for informative error messages and debugging.
"""
self.anon_var_count = 0
self.imports = defaultdict(set)
self.temp_if = None
if not inspect.ismodule(module):
module = importlib.import_module(module)
self.module = importlib.import_module(module)
else:
self.module = module
self.module = module
self.module_name = module.__name__
self.module_name = self.module.__name__
self.filename = filename
self.source = source
# Hy expects these to be present, so we prep the module for Hy
# compilation.
@ -431,13 +443,15 @@ class HyASTCompiler(object):
# nested; so let's re-raise this exception, let's not wrap it in
# another HyCompileError!
raise
except HyTypeError:
raise
except HyTypeError as e:
reraise(type(e), e, None)
except Exception as e:
raise_empty(HyCompileError, e, sys.exc_info()[2])
f_exc = traceback.format_exc()
exc_msg = "Internal Compiler Bug 😱\n{}".format(f_exc)
reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
def _syntax_error(self, expr, message):
return HyTypeError(expr, message)
return HyTypeError(message, self.filename, expr, self.source)
def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
oldpy_unpack=False):
@ -1614,7 +1628,29 @@ class HyASTCompiler(object):
def compile_eval_and_compile(self, expr, root, body):
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
hy_eval(new_expr + body, self.module.__dict__, self.module)
try:
hy_eval(new_expr + body,
self.module.__dict__,
self.module,
filename=self.filename,
source=self.source)
except HyInternalError:
# Unexpected "meta" compilation errors need to be treated
# like normal (unexpected) compilation errors at this level
# (or the compilation level preceding this one).
raise
except Exception as e:
# These could be expected Hy language errors (e.g. syntax errors)
# or regular Python runtime errors that do not signify errors in
# the compilation *process* (although compilation did technically
# fail).
# We wrap these exceptions and pass them through.
reraise(HyEvalError,
HyEvalError(str(e),
self.filename,
body,
self.source),
sys.exc_info()[2])
return (self._compile_branch(body)
if ast_str(root) == "eval_and_compile"
@ -1798,8 +1834,13 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False):
def hy_eval(hytree, locals=None, module=None, ast_callback=None,
compiler=None):
compiler=None, filename='<string>', source=None):
"""Evaluates a quoted expression and returns the value.
If you're evaluating hand-crafted AST trees, make sure the line numbers
are set properly. Try `fix_missing_locations` and related functions in the
Python `ast` library.
Examples
--------
=> (eval '(print "Hello World"))
@ -1812,8 +1853,8 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
Parameters
----------
hytree: a Hy expression tree
Source code to parse.
hytree: HyObject
The Hy AST object to evaluate.
locals: dict, optional
Local environment in which to evaluate the Hy tree. Defaults to the
@ -1835,6 +1876,19 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
An existing Hy compiler to use for compilation. Also serves as
the `module` value when given.
filename: str, optional
The filename corresponding to the source for `tree`. This will be
overridden by the `filename` field of `tree`, if any; otherwise, it
defaults to "<string>". When `compiler` is given, its `filename` field
value is always used.
source: str, optional
A string containing the source code for `tree`. This will be
overridden by the `source` field of `tree`, if any; otherwise,
if `None`, an attempt will be made to obtain it from the module given by
`module`. When `compiler` is given, its `source` field value is always
used.
Returns
-------
out : Result of evaluating the Hy compiled tree.
@ -1849,36 +1903,53 @@ def hy_eval(hytree, locals=None, module=None, ast_callback=None,
if not isinstance(locals, dict):
raise TypeError("Locals must be a dictionary")
_ast, expr = hy_compile(hytree, module=module, get_expr=True,
compiler=compiler)
# Does the Hy AST object come with its own information?
filename = getattr(hytree, 'filename', filename) or '<string>'
source = getattr(hytree, 'source', source)
# Spoof the positions in the generated ast...
for node in ast.walk(_ast):
node.lineno = 1
node.col_offset = 1
for node in ast.walk(expr):
node.lineno = 1
node.col_offset = 1
_ast, expr = hy_compile(hytree, module, get_expr=True,
compiler=compiler, filename=filename,
source=source)
if ast_callback:
ast_callback(_ast, expr)
globals = module.__dict__
# Two-step eval: eval() the body of the exec call
eval(ast_compile(_ast, "<eval_body>", "exec"), globals, locals)
eval(ast_compile(_ast, filename, "exec"),
module.__dict__, locals)
# Then eval the expression context and return that
return eval(ast_compile(expr, "<eval>", "eval"), globals, locals)
return eval(ast_compile(expr, filename, "eval"),
module.__dict__, locals)
def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
compiler=None):
"""Compile a Hy tree into a Python AST tree.
def _module_file_source(module_name, filename, source):
"""Try to obtain missing filename and source information from a module name
without actually loading the module.
"""
if filename is None or source is None:
mod_loader = pkgutil.get_loader(module_name)
if mod_loader:
if filename is None:
filename = mod_loader.get_filename(module_name)
if source is None:
source = mod_loader.get_source(module_name)
# We need a non-None filename.
filename = filename or '<string>'
return filename, source
def hy_compile(tree, module, root=ast.Module, get_expr=False,
compiler=None, filename=None, source=None):
"""Compile a HyObject tree into a Python AST Module.
Parameters
----------
tree: HyObject
The Hy AST object to compile.
module: str or types.ModuleType, optional
Module, or name of the module, in which the Hy tree is evaluated.
The module associated with `compiler` takes priority over this value.
@ -1893,18 +1964,43 @@ def hy_compile(tree, module=None, root=ast.Module, get_expr=False,
An existing Hy compiler to use for compilation. Also serves as
the `module` value when given.
filename: str, optional
The filename corresponding to the source for `tree`. This will be
overridden by the `filename` field of `tree`, if any; otherwise, it
defaults to "<string>". When `compiler` is given, its `filename` field
value is always used.
source: str, optional
A string containing the source code for `tree`. This will be
overridden by the `source` field of `tree`, if any; otherwise,
if `None`, an attempt will be made to obtain it from the module given by
`module`. When `compiler` is given, its `source` field value is always
used.
Returns
-------
out : A Python AST tree
"""
module = get_compiler_module(module, compiler, False)
if isinstance(module, string_types):
if module.startswith('<') and module.endswith('>'):
module = types.ModuleType(module)
else:
module = importlib.import_module(ast_str(module, piecewise=True))
if not inspect.ismodule(module):
raise TypeError('Invalid module type: {}'.format(type(module)))
filename = getattr(tree, 'filename', filename)
source = getattr(tree, 'source', source)
tree = wrap_value(tree)
if not isinstance(tree, HyObject):
raise HyCompileError("`tree` must be a HyObject or capable of "
"being promoted to one")
raise TypeError("`tree` must be a HyObject or capable of "
"being promoted to one")
compiler = compiler or HyASTCompiler(module)
compiler = compiler or HyASTCompiler(module, filename=filename, source=source)
result = compiler.compile(tree)
expr = result.force_expr

View File

@ -14,15 +14,14 @@
(if* (not (isinstance macro-name hy.models.HySymbol))
(raise
(hy.errors.HyTypeError
macro-name
(% "received a `%s' instead of a symbol for macro name"
(. (type name)
__name__)))))
(. (type name) --name--))
--file-- macro-name None)))
(for [kw '[&kwonly &kwargs]]
(if* (in kw lambda-list)
(raise (hy.errors.HyTypeError macro-name
(% "macros cannot use %s"
kw)))))
(raise (hy.errors.HyTypeError (% "macros cannot use %s"
kw)
--file-- macro-name None))))
;; this looks familiar...
`(eval-and-compile
(import hy)
@ -45,9 +44,9 @@
(if (and (not (isinstance tag-name hy.models.HySymbol))
(not (isinstance tag-name hy.models.HyString)))
(raise (hy.errors.HyTypeError
tag-name
(% "received a `%s' instead of a symbol for tag macro name"
(. (type tag-name) __name__)))))
(. (type tag-name) --name--))
--file-- tag-name None)))
(if (or (= tag-name ":")
(= tag-name "&"))
(raise (NameError (% "%s can't be used as a tag macro name" tag-name))))
@ -58,9 +57,8 @@
((hy.macros.tag ~tag-name)
(fn ~lambda-list ~@body))))
(defmacro macro-error [location reason]
"Error out properly within a macro at `location` giving `reason`."
`(raise (hy.errors.HyMacroExpansionError ~location ~reason)))
(defmacro macro-error [expression reason &optional [filename '--name--]]
`(raise (hy.errors.HyMacroExpansionError ~reason ~filename ~expression None)))
(defmacro defn [name lambda-list &rest body]
"Define `name` as a function with `lambda-list` signature and body `body`."

View File

@ -5,41 +5,65 @@
import traceback
from functools import reduce
from clint.textui import colored
class HyError(Exception):
"""
Generic Hy error. All internal Exceptions will be subclassed from this
Exception.
"""
pass
class HyCompileError(HyError):
def __init__(self, exception, traceback=None):
self.exception = exception
self.traceback = traceback
def __str__(self):
if isinstance(self.exception, HyTypeError):
return str(self.exception)
if self.traceback:
tb = "".join(traceback.format_tb(self.traceback)).strip()
else:
tb = "No traceback available. 😟"
return("Internal Compiler Bug 😱\n%s: %s\nCompilation traceback:\n%s"
% (self.exception.__class__.__name__,
self.exception, tb))
class HyTypeError(TypeError):
def __init__(self, expression, message):
super(HyTypeError, self).__init__(message)
self.expression = expression
def __init__(self, message, *args):
self.message = message
self.source = None
self.filename = None
super(HyError, self).__init__(message, *args)
class HyInternalError(HyError):
"""Unexpected errors occurring during compilation or parsing of Hy code.
Errors sub-classing this are not intended to be user-facing, and will,
hopefully, never be seen by users!
"""
def __init__(self, message, *args):
super(HyInternalError, self).__init__(message, *args)
class HyLanguageError(HyError):
"""Errors caused by invalid use of the Hy language.
This, and any errors inheriting from this, are user-facing.
"""
def __init__(self, message, *args):
super(HyLanguageError, self).__init__(message, *args)
class HyCompileError(HyInternalError):
"""Unexpected errors occurring within the compiler."""
class HyTypeError(HyLanguageError, TypeError):
"""TypeErrors occurring during the normal use of Hy."""
def __init__(self, message, filename=None, expression=None, source=None):
"""
Parameters
----------
message: str
The message to display for this error.
filename: str, optional
The filename for the source code generating this error.
expression: HyObject, optional
The Hy expression generating this error.
source: str, optional
The actual source code generating this error.
"""
self.message = message
self.filename = filename
self.expression = expression
self.source = source
super(HyTypeError, self).__init__(message, filename, expression,
source)
def __str__(self):
@ -93,12 +117,92 @@ class HyTypeError(TypeError):
class HyMacroExpansionError(HyTypeError):
pass
"""Errors caused by invalid use of Hy macros.
This, and any errors inheriting from this, are user-facing.
"""
class HyIOError(HyError, IOError):
class HyEvalError(HyLanguageError):
"""Errors occurring during code evaluation at compile-time.
These errors distinguish unexpected errors within the compilation process
(i.e. `HyInternalError`s) from unrelated errors in user code evaluated by
the compiler (e.g. in `eval-and-compile`).
This, and any errors inheriting from this, are user-facing.
"""
Trivial subclass of IOError and HyError, to distinguish between
IOErrors raised by Hy itself as opposed to Hy programs.
class HyIOError(HyInternalError, IOError):
""" Subclass used to distinguish between IOErrors raised by Hy itself as
opposed to Hy programs.
"""
pass
class HySyntaxError(HyLanguageError, SyntaxError):
"""Error during the Lexing of a Hython expression."""
def __init__(self, message, filename=None, lineno=-1, colno=-1,
source=None):
"""
Parameters
----------
message: str
The exception's message.
filename: str, optional
The filename for the source code generating this error.
lineno: int, optional
The line number of the error.
colno: int, optional
The column number of the error.
source: str, optional
The actual source code generating this error.
"""
self.message = message
self.filename = filename
self.lineno = lineno
self.colno = colno
self.source = source
super(HySyntaxError, self).__init__(message,
# The builtin `SyntaxError` needs a
# tuple.
(filename, lineno, colno, source))
@staticmethod
def from_expression(message, expression, filename=None, source=None):
if not source:
# Maybe the expression object has its own source.
source = getattr(expression, 'source', None)
if not filename:
filename = getattr(expression, 'filename', None)
if source:
lineno = expression.start_line
colno = expression.start_column
end_line = getattr(expression, 'end_line', len(source))
lines = source.splitlines()
source = '\n'.join(lines[lineno-1:end_line])
else:
# We could attempt to extract the source given a filename, but we
# don't.
lineno = colno = -1
return HySyntaxError(message, filename, lineno, colno, source)
def __str__(self):
output = traceback.format_exception_only(SyntaxError, self)
output[-1] = colored.yellow(output[-1])
if len(self.source) > 0:
output[-2] = colored.green(output[-2])
for line in output[::-2]:
if line.strip().startswith(
'File "{}", line'.format(self.filename)):
break
output[-3] = colored.red(output[-3])
# Avoid "...expected str instance, ColoredString found"
return reduce(lambda x, y: x + y, output)

View File

@ -17,10 +17,8 @@ import importlib
from functools import partial
from contextlib import contextmanager
from hy.errors import HyTypeError
from hy.compiler import hy_compile, hy_ast_compile_flags
from hy.lex import hy_parse
from hy.lex.exceptions import LexException
from hy._compat import PY3
@ -153,15 +151,9 @@ if PY3:
def _hy_source_to_code(self, data, path, _optimize=-1):
if _could_be_hy_src(path):
source = data.decode("utf-8")
try:
hy_tree = hy_parse(source)
with loader_module_obj(self) as module:
data = hy_compile(hy_tree, module)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = source
e.filename = path
raise
hy_tree = hy_parse(source, filename=path)
with loader_module_obj(self) as module:
data = hy_compile(hy_tree, module)
return _py_source_to_code(self, data, path, _optimize=_optimize)
@ -287,19 +279,15 @@ else:
fullname = self._fix_name(fullname)
if fullname is None:
fullname = self.fullname
try:
hy_source = self.get_source(fullname)
hy_tree = hy_parse(hy_source)
with loader_module_obj(self) as module:
hy_ast = hy_compile(hy_tree, module)
code = compile(hy_ast, self.filename, 'exec',
hy_ast_compile_flags)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = hy_source
e.filename = self.filename
raise
hy_source = self.get_source(fullname)
hy_tree = hy_parse(hy_source, filename=self.filename)
with loader_module_obj(self) as module:
hy_ast = hy_compile(hy_tree, module)
code = compile(hy_ast, self.filename, 'exec',
hy_ast_compile_flags)
if not sys.dont_write_bytecode:
try:
@ -453,7 +441,7 @@ else:
try:
flags = None
if _could_be_hy_src(filename):
hy_tree = hy_parse(source_str)
hy_tree = hy_parse(source_str, filename=filename)
if module is None:
module = inspect.getmodule(inspect.stack()[1][0])
@ -465,9 +453,6 @@ else:
codeobject = compile(source, dfile or filename, 'exec', flags)
except Exception as err:
if isinstance(err, (HyTypeError, LexException)) and err.source is None:
err.source = source_str
err.filename = filename
py_exc = py_compile.PyCompileError(err.__class__, err,
dfile or filename)

View File

@ -8,9 +8,10 @@ import re
import sys
import unicodedata
from hy._compat import str_type, isidentifier, UCS4
from hy._compat import str_type, isidentifier, UCS4, reraise
from hy.lex.exceptions import PrematureEndOfInput, LexException # NOQA
from hy.models import HyExpression, HySymbol
from hy.errors import HySyntaxError
try:
from io import StringIO
@ -18,7 +19,7 @@ except ImportError:
from StringIO import StringIO
def hy_parse(source):
def hy_parse(source, filename='<string>'):
"""Parse a Hy source string.
Parameters
@ -26,31 +27,51 @@ def hy_parse(source):
source: string
Source code to parse.
filename: string, optional
File name corresponding to source. Defaults to "<string>".
Returns
-------
out : instance of `types.CodeType`
out : HyExpression
"""
source = re.sub(r'\A#!.*', '', source)
return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
_source = re.sub(r'\A#!.*', '', source)
try:
res = HyExpression([HySymbol("do")] +
tokenize(_source + "\n",
filename=filename))
res.source = source
res.filename = filename
return res
except HySyntaxError as e:
reraise(type(e), e, None)
def tokenize(buf):
"""
Tokenize a Lisp file or string buffer into internal Hy objects.
class ParserState(object):
def __init__(self, source, filename):
self.source = source
self.filename = filename
def tokenize(source, filename=None):
""" Tokenize a Lisp file or string buffer into internal Hy objects.
Parameters
----------
source: str
The source to tokenize.
filename: str, optional
The filename corresponding to `source`.
"""
from hy.lex.lexer import lexer
from hy.lex.parser import parser
from rply.errors import LexingError
try:
return parser.parse(lexer.lex(buf))
return parser.parse(lexer.lex(source),
state=ParserState(source, filename))
except LexingError as e:
pos = e.getsourcepos()
raise LexException("Could not identify the next token.",
pos.lineno, pos.colno, buf)
except LexException as e:
if e.source is None:
e.source = buf
raise
raise LexException("Could not identify the next token.", filename,
pos.lineno, pos.colno, source)
mangle_delim = 'X'

View File

@ -1,49 +1,34 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
from hy.errors import HyError
from hy.errors import HySyntaxError
class LexException(HyError):
"""Error during the Lexing of a Hython expression."""
def __init__(self, message, lineno, colno, source=None):
super(LexException, self).__init__(message)
self.message = message
self.lineno = lineno
self.colno = colno
self.source = source
self.filename = '<stdin>'
class LexException(HySyntaxError):
def __str__(self):
from hy.errors import colored
@classmethod
def from_lexer(cls, message, state, token):
source_pos = token.getsourcepos()
if token.source_pos:
lineno = source_pos.lineno
colno = source_pos.colno
else:
lineno = -1
colno = -1
line = self.lineno
start = self.colno
if state.source:
lines = state.source.splitlines()
if lines[-1] == '':
del lines[-1]
result = ""
if lineno < 1:
lineno = len(lines)
if colno < 1:
colno = len(lines[-1])
source = self.source.split("\n")
if line > 0 and start > 0:
result += ' File "%s", line %d, column %d\n\n' % (self.filename,
line,
start)
if len(self.source) > 0:
source_line = source[line-1]
else:
source_line = ""
result += ' %s\n' % colored.red(source_line)
result += ' %s%s\n' % (' '*(start-1), colored.green('^'))
result += colored.yellow("LexException: %s\n\n" % self.message)
return result
source = lines[lineno - 1]
return cls(message, state.filename, lineno, colno, source)
class PrematureEndOfInput(LexException):
"""We got a premature end of input"""
def __init__(self, message):
super(PrematureEndOfInput, self).__init__(message, -1, -1)
pass

View File

@ -22,10 +22,10 @@ pg = ParserGenerator([rule.name for rule in lexer.rules] + ['$end'])
def set_boundaries(fun):
@wraps(fun)
def wrapped(p):
def wrapped(state, p):
start = p[0].source_pos
end = p[-1].source_pos
ret = fun(p)
ret = fun(state, p)
ret.start_line = start.lineno
ret.start_column = start.colno
if start is not end:
@ -40,9 +40,9 @@ def set_boundaries(fun):
def set_quote_boundaries(fun):
@wraps(fun)
def wrapped(p):
def wrapped(state, p):
start = p[0].source_pos
ret = fun(p)
ret = fun(state, p)
ret.start_line = start.lineno
ret.start_column = start.colno
ret.end_line = p[-1].end_line
@ -52,54 +52,45 @@ def set_quote_boundaries(fun):
@pg.production("main : list_contents")
def main(p):
def main(state, p):
return p[0]
@pg.production("main : $end")
def main_empty(p):
def main_empty(state, p):
return []
def reject_spurious_dots(*items):
"Reject the spurious dots from items"
for list in items:
for tok in list:
if tok == "." and type(tok) == HySymbol:
raise LexException("Malformed dotted list",
tok.start_line, tok.start_column)
@pg.production("paren : LPAREN list_contents RPAREN")
@set_boundaries
def paren(p):
def paren(state, p):
return HyExpression(p[1])
@pg.production("paren : LPAREN RPAREN")
@set_boundaries
def empty_paren(p):
def empty_paren(state, p):
return HyExpression([])
@pg.production("list_contents : term list_contents")
def list_contents(p):
def list_contents(state, p):
return [p[0]] + p[1]
@pg.production("list_contents : term")
def list_contents_single(p):
def list_contents_single(state, p):
return [p[0]]
@pg.production("list_contents : DISCARD term discarded_list_contents")
def list_contents_empty(p):
def list_contents_empty(state, p):
return []
@pg.production("discarded_list_contents : DISCARD term discarded_list_contents")
@pg.production("discarded_list_contents :")
def discarded_list_contents(p):
def discarded_list_contents(state, p):
pass
@ -109,58 +100,58 @@ def discarded_list_contents(p):
@pg.production("term : list")
@pg.production("term : set")
@pg.production("term : string")
def term(p):
def term(state, p):
return p[0]
@pg.production("term : DISCARD term term")
def term_discard(p):
def term_discard(state, p):
return p[2]
@pg.production("term : QUOTE term")
@set_quote_boundaries
def term_quote(p):
def term_quote(state, p):
return HyExpression([HySymbol("quote"), p[1]])
@pg.production("term : QUASIQUOTE term")
@set_quote_boundaries
def term_quasiquote(p):
def term_quasiquote(state, p):
return HyExpression([HySymbol("quasiquote"), p[1]])
@pg.production("term : UNQUOTE term")
@set_quote_boundaries
def term_unquote(p):
def term_unquote(state, p):
return HyExpression([HySymbol("unquote"), p[1]])
@pg.production("term : UNQUOTESPLICE term")
@set_quote_boundaries
def term_unquote_splice(p):
def term_unquote_splice(state, p):
return HyExpression([HySymbol("unquote-splice"), p[1]])
@pg.production("term : HASHSTARS term")
@set_quote_boundaries
def term_hashstars(p):
def term_hashstars(state, p):
n_stars = len(p[0].getstr()[1:])
if n_stars == 1:
sym = "unpack-iterable"
elif n_stars == 2:
sym = "unpack-mapping"
else:
raise LexException(
raise LexException.from_lexer(
"Too many stars in `#*` construct (if you want to unpack a symbol "
"beginning with a star, separate it with whitespace)",
p[0].source_pos.lineno, p[0].source_pos.colno)
state, p[0])
return HyExpression([HySymbol(sym), p[1]])
@pg.production("term : HASHOTHER term")
@set_quote_boundaries
def hash_other(p):
def hash_other(state, p):
# p == [(Token('HASHOTHER', '#foo'), bar)]
st = p[0].getstr()[1:]
str_object = HyString(st)
@ -170,63 +161,63 @@ def hash_other(p):
@pg.production("set : HLCURLY list_contents RCURLY")
@set_boundaries
def t_set(p):
def t_set(state, p):
return HySet(p[1])
@pg.production("set : HLCURLY RCURLY")
@set_boundaries
def empty_set(p):
def empty_set(state, p):
return HySet([])
@pg.production("dict : LCURLY list_contents RCURLY")
@set_boundaries
def t_dict(p):
def t_dict(state, p):
return HyDict(p[1])
@pg.production("dict : LCURLY RCURLY")
@set_boundaries
def empty_dict(p):
def empty_dict(state, p):
return HyDict([])
@pg.production("list : LBRACKET list_contents RBRACKET")
@set_boundaries
def t_list(p):
def t_list(state, p):
return HyList(p[1])
@pg.production("list : LBRACKET RBRACKET")
@set_boundaries
def t_empty_list(p):
def t_empty_list(state, p):
return HyList([])
@pg.production("string : STRING")
@set_boundaries
def t_string(p):
def t_string(state, p):
# Replace the single double quotes with triple double quotes to allow
# embedded newlines.
try:
s = eval(p[0].value.replace('"', '"""', 1)[:-1] + '"""')
except SyntaxError:
raise LexException("Can't convert {} to a HyString".format(p[0].value),
p[0].source_pos.lineno, p[0].source_pos.colno)
raise LexException.from_lexer("Can't convert {} to a HyString".format(p[0].value),
state, p[0])
return (HyString if isinstance(s, str_type) else HyBytes)(s)
@pg.production("string : PARTIAL_STRING")
def t_partial_string(p):
def t_partial_string(state, p):
# Any unterminated string requires more input
raise PrematureEndOfInput("Premature end of input")
raise PrematureEndOfInput.from_lexer("Partial string literal", state, p[0])
bracket_string_re = next(r.re for r in lexer.rules if r.name == 'BRACKETSTRING')
@pg.production("string : BRACKETSTRING")
@set_boundaries
def t_bracket_string(p):
def t_bracket_string(state, p):
m = bracket_string_re.match(p[0].value)
delim, content = m.groups()
return HyString(content, brackets=delim)
@ -234,7 +225,7 @@ def t_bracket_string(p):
@pg.production("identifier : IDENTIFIER")
@set_boundaries
def t_identifier(p):
def t_identifier(state, p):
obj = p[0].value
val = symbol_like(obj)
@ -243,11 +234,11 @@ def t_identifier(p):
if "." in obj and symbol_like(obj.split(".", 1)[0]) is not None:
# E.g., `5.attr` or `:foo.attr`
raise LexException(
raise LexException.from_lexer(
'Cannot access attribute on anything other than a name (in '
'order to get attributes of expressions, use '
'`(. <expression> <attr>)` or `(.<attr> <expression>)`)',
p[0].source_pos.lineno, p[0].source_pos.colno)
state, p[0])
return HySymbol(obj)
@ -284,14 +275,24 @@ def symbol_like(obj):
@pg.error
def error_handler(token):
def error_handler(state, token):
tokentype = token.gettokentype()
if tokentype == '$end':
raise PrematureEndOfInput("Premature end of input")
source_pos = token.source_pos or token.getsourcepos()
source = state.source
if source_pos:
lineno = source_pos.lineno
colno = source_pos.colno
else:
lineno = -1
colno = -1
raise PrematureEndOfInput.from_lexer("Premature end of input", state,
token)
else:
raise LexException(
"Ran into a %s where it wasn't expected." % tokentype,
token.source_pos.lineno, token.source_pos.colno)
raise LexException.from_lexer(
"Ran into a %s where it wasn't expected." % tokentype, state,
token)
parser = pg.build()

View File

@ -1,14 +1,16 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
import sys
import importlib
import inspect
import pkgutil
from hy._compat import PY3, string_types
from contextlib import contextmanager
from hy._compat import PY3, string_types, reraise
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle
from hy.errors import HyTypeError, HyMacroExpansionError
try:
@ -257,6 +259,32 @@ def make_empty_fn_copy(fn):
return empty_fn
@contextmanager
def macro_exceptions(module, macro_tree, compiler=None):
try:
yield
except Exception as e:
try:
filename = inspect.getsourcefile(module)
source = inspect.getsource(module)
except TypeError:
if compiler:
filename = compiler.filename
source = compiler.source
if not isinstance(e, HyTypeError):
exc_type = HyMacroExpansionError
msg = "expanding `{}': ".format(macro_tree[0])
msg += str(e).replace("<lambda>()", "", 1).strip()
else:
exc_type = HyTypeError
msg = e.message
reraise(exc_type,
exc_type(msg, filename, macro_tree, source),
sys.exc_info()[2].tb_next)
def macroexpand(tree, module, compiler=None, once=False):
"""Expand the toplevel macros for the given Hy AST tree.
@ -324,23 +352,10 @@ def macroexpand(tree, module, compiler=None, once=False):
compiler = HyASTCompiler(module)
opts['compiler'] = compiler
try:
with macro_exceptions(module, tree, compiler):
m_copy = make_empty_fn_copy(m)
m_copy(module.__name__, *tree[1:], **opts)
except TypeError as e:
msg = "expanding `" + str(tree[0]) + "': "
msg += str(e).replace("<lambda>()", "", 1).strip()
raise HyMacroExpansionError(tree, msg)
try:
obj = m(module.__name__, *tree[1:], **opts)
except HyTypeError as e:
if e.expression is None:
e.expression = tree
raise
except Exception as e:
msg = "expanding `" + str(tree[0]) + "': " + repr(e)
raise HyMacroExpansionError(tree, msg)
if isinstance(obj, HyExpression):
obj.module = inspect.getmodule(m)
@ -375,7 +390,8 @@ def tag_macroexpand(tag, tree, module):
None)
if tag_macro is None:
raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag))
raise HyTypeError("`{0}' is not a defined tag macro.".format(tag),
None, tag, None)
expr = tag_macro(tree)

View File

@ -10,7 +10,7 @@ from hy.models import HyObject
from hy.compiler import hy_compile, hy_eval
from hy.errors import HyCompileError, HyTypeError
from hy.lex import hy_parse
from hy.lex.exceptions import LexException
from hy.lex.exceptions import LexException, PrematureEndOfInput
from hy._compat import PY3
import ast
@ -474,7 +474,7 @@ def test_lambda_list_keywords_kwonly():
else:
exception = cant_compile(kwonly_demo)
assert isinstance(exception, HyTypeError)
message, = exception.args
message = exception.args[0]
assert message == "&kwonly parameters require Python 3"
@ -547,7 +547,7 @@ def test_compile_error():
def test_for_compile_error():
"""Ensure we get compile error in tricky 'for' cases"""
with pytest.raises(LexException) as excinfo:
with pytest.raises(PrematureEndOfInput) as excinfo:
can_compile("(fn [] (for)")
assert excinfo.value.message == "Premature end of input"

View File

@ -14,10 +14,10 @@ from fractions import Fraction
import pytest
import hy
from hy.errors import HyTypeError
from hy.lex import hy_parse
from hy.lex.exceptions import LexException
from hy.compiler import hy_compile
from hy.errors import HyLanguageError
from hy.lex.exceptions import PrematureEndOfInput
from hy.compiler import hy_eval, hy_compile
from hy.importer import HyLoader, cache_from_source
try:
@ -57,7 +57,7 @@ def test_runpy():
def test_stringer():
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '__main__')
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), __name__)
assert type(_ast.body[0]) == ast.FunctionDef
@ -79,14 +79,8 @@ def test_imports():
def test_import_error_reporting():
"Make sure that (import) reports errors correctly."
def _import_error_test():
try:
_ = hy_compile(hy_parse("(import \"sys\")"), '__main__')
except HyTypeError:
return "Error reported"
assert _import_error_test() == "Error reported"
assert _import_error_test() is not None
with pytest.raises(HyLanguageError):
hy_compile(hy_parse("(import \"sys\")"), __name__)
def test_import_error_cleanup():
@ -124,7 +118,7 @@ def test_import_autocompiles():
def test_eval():
def eval_str(s):
return hy.eval(hy.read_str(s))
return hy_eval(hy.read_str(s), filename='<string>', source=s)
assert eval_str('[1 2 3]') == [1, 2, 3]
assert eval_str('{"dog" "bark" "cat" "meow"}') == {
@ -205,8 +199,7 @@ def test_reload():
assert mod.a == 11
assert mod.b == 20
# Now cause a `LexException`, and confirm that the good module and its
# contents stick around.
# Now cause a syntax error
unlink(source)
with open(source, "w") as f:
@ -214,7 +207,7 @@ def test_reload():
f.write("(setv a 11")
f.write("(setv b (// 20 1))")
with pytest.raises(LexException):
with pytest.raises(PrematureEndOfInput):
reload(mod)
mod = sys.modules.get(TESTFN)

View File

@ -149,7 +149,7 @@ def test_bin_hy_stdin_unlocatable_hytypeerror():
# inside run_cmd.
_, err = run_cmd("hy", """
(import hy.errors)
(raise (hy.errors.HyTypeError '[] (+ "A" "Z")))""")
(raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""")
assert "AZ" in err

View File

@ -1,13 +1,15 @@
# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
import traceback
import pytest
from math import isnan
from hy.models import (HyExpression, HyInteger, HyFloat, HyComplex, HySymbol,
HyString, HyDict, HyList, HySet, HyKeyword)
from hy.lex import tokenize
from hy.lex.exceptions import LexException, PrematureEndOfInput
import pytest
def peoi(): return pytest.raises(PrematureEndOfInput)
def lexe(): return pytest.raises(LexException)
@ -180,7 +182,23 @@ def test_lex_digit_separators():
def test_lex_bad_attrs():
with lexe(): tokenize("1.foo")
with lexe() as execinfo:
tokenize("1.foo")
expected = [
' File "<string>", line 1\n',
' 1.foo\n',
' ^\n',
('LexException: Cannot access attribute on anything other'
' than a name (in order to get attributes of expressions,'
' use `(. <expression> <attr>)` or `(.<attr> <expression>)`)\n')
]
output = traceback.format_exception_only(execinfo.type, execinfo.value)
assert output[:-1:1] == expected[:-1:1]
# Python 2.7 doesn't give the full exception name, so we compensate.
assert output[-1].endswith(expected[-1])
with lexe(): tokenize("0.foo")
with lexe(): tokenize("1.5.foo")
with lexe(): tokenize("1e3.foo")