Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate more concise syntax and compiler errors #1687

Merged
merged 8 commits into from
Feb 7, 2019

Conversation

brandonwillard
Copy link
Member

@brandonwillard brandonwillard commented Oct 11, 2018

This PR refactors the exception/error classes and their handling, keeps Hy source strings and their corresponding file information (if any) closer to the origin of an exception (so that calling code isn't responsible for annotating exceptions), and provides minimally intrusive traceback print-out filtering via a context manager that temporarily alters sys.excepthook (this is automatically enabled for Hy command-line sessions).

New environment and package variables have been introduced, as well:

  • HY_COLORED_ERRORS, and package variable, hy.errors._hy_colored_errors, that enables/disables manual error coloring, and
  • HY_COLORED_AST_OBJECTS, and package variable, hy.models._hy_colored_ast_objects, that enables/disables AST object coloring.

Also, source caching has been added to the Hy interpreter. It's modeled after the approach used by IPython via linecache. Now, for example, traces will show the relevant source lines for functions, objects, etc., defined exclusively in the REPL (see the example below).

Notes

As mentioned above, the sys.excepthook provided here only hides internal frames from the traceback print-out, and not the actual traceback object, so, for instance, it does not affect the stack in pdb.

There are other, more powerful and direct approaches that involve altering and/or creating low-level traceback objects (e.g. Jinja's). These allow for a better debugging experience (e.g. without any compiler or parser frames), but they're also more complicated and distribution dependent. In the long run, I think it's better to have custom tracebacks, so I'll keep an eye on good/better ways to do this; for example, this approach is interesting.

Example

Before

hy 0.15.0+39.gd2319dc using CPython(default) 3.6.6 on Linux
=> (with [file (open "a.hy" "w")] (file.write "(print 'hi\""))
11
=> (import a)
Traceback (most recent call last):
  File "/home/bwillard/projects/code/python/hy-master/hy/importer.py", line 138, in hy_eval
    eval(ast_compile(_ast, "<eval_body>", "exec"), namespace)
  File "<eval_body>", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 674, in exec_module
  File "<frozen importlib._bootstrap_external>", line 781, in get_code
  File "/home/bwillard/projects/code/python/hy-master/hy/importer.py", line 228, in _hy_source_to_code
    hy_tree = hy_parse(source)
  File "/home/bwillard/projects/code/python/hy-master/hy/importer.py", line 65, in hy_parse
    return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
  File "/home/bwillard/projects/code/python/hy-master/hy/lex/__init__.py", line 19, in tokenize
    return parser.parse(lexer.lex(buf))
  File "/home/bwillard/apps/anaconda3/envs/hy-master-cpython36/lib/python3.6/site-packages/rply/parser.py", line 23, in parse
    t, symstack, statestack, state
  File "/home/bwillard/apps/anaconda3/envs/hy-master-cpython36/lib/python3.6/site-packages/rply/parser.py", line 80, in _reduce_production
    value = p.func(targ)
  File "/home/bwillard/projects/code/python/hy-master/hy/lex/parser.py", line 223, in t_partial_string
    raise PrematureEndOfInput("Premature end of input")
hy.lex.exceptions.PrematureEndOfInput: LexException: Premature end of input

=> (require blah)
Traceback (most recent call last):
  File "/home/bwillard/projects/code/python/hy-master/hy/importer.py", line 120, in hy_eval
    _ast, expr = hy_compile(hytree, module_name, get_expr=True)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 1716, in hy_compile
    result = compiler.compile(tree)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 350, in compile
    ret = self.compile_atom(tree)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 344, in compile_atom
    return Result() + _model_compilers[type(atom)](self, atom)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 1584, in compile_expression
    self, expr, unmangle(sroot), *parse_tree)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 573, in compile_do
    return self._compile_branch(body)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 444, in _compile_branch
    ret += self.compile(exprs[-1])
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 361, in compile
    raise_empty(HyCompileError, e, sys.exc_info()[2])
  File "<string>", line 1, in raise_empty
hy.errors.HyCompileError: Internal Compiler Bug 😱
⤷ ModuleNotFoundError: No module named 'blah'
Compilation traceback:
File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 350, in compile
    ret = self.compile_atom(tree)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 344, in compile_atom
    return Result() + _model_compilers[type(atom)](self, atom)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 1584, in compile_expression
    self, expr, unmangle(sroot), *parse_tree)
  File "/home/bwillard/projects/code/python/hy-master/hy/compiler.py", line 1154, in compile_import_or_require
    importlib.import_module(module)
  File "/home/bwillard/apps/anaconda3/envs/hy-master-cpython36/lib/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked
=> (defn test [x]
... (print(+ 1 x))
... )
=> (test 'a)
Traceback (most recent call last):
  File "/home/bwillard/projects/code/python/hy-master/hy/importer.py", line 141, in hy_eval
    return eval(ast_compile(expr, "<eval>", "eval"), namespace)
  File "<eval>", line 1, in <module>
  File "<eval_body>", line 1, in test
TypeError: unsupported operand type(s) for +: 'int' and 'HySymbol'
=> (defmacro blah [x] `(print ~@z))
<function <lambda> at 0x7f1351095730>
=> (defmacro bleh [q] `(print (blah ~q)))
<function <lambda> at 0x7f1351095840>
=> (bleh y)
  File "<input>", unknown location
HyMacroExpansionError: expanding `blah': NameError("name 'z' is not defined",)

=> (defn foo []
... (setv bar 42)
... (import pdb) (pdb.set-trace))
=> (foo)
--Return--
> <eval_body>(1)foo()->None
(Pdb) l
[EOF]

After

hy 0.15.0+45.ga8d6a48 using CPython(default) 3.6.6 on Linux
=> (with [file (open "a.hy" "w")] (file.write "(print 'hi\""))
11
=> (import a)
Traceback (most recent call last):
  File "stdin-99c0d4eb813cb56c622500ab4696f5bbb3cb3660", line 1, in <module>
    (import a)
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 674, in exec_module
  File "<frozen importlib._bootstrap_external>", line 781, in get_code
  File "/tmp/a.hy", line 1
    (print 'hi"
              ^
hy.lex.exceptions.PrematureEndOfInput: Partial string literal
=> (require blah)
Traceback (most recent call last):
  File "stdin-97db2eacebe610e14f46e56e428d68ec1c466f2f", line 1, in <module>
    (require blah)
hy.errors.HyRequireError: No module named 'blah'
=> (defn test [x]
... (print (+ 1 x))
... )
None
=> (test 'a)
Traceback (most recent call last):
  File "stdin-e199a43c50b388bff5e7901a7a1f02f5525d862d", line 1, in <module>
    (test 'a)
  File "stdin-71d0b2f8a864d8760f5a5eab4efa209694ee2714", line 2, in test
    (print (+ 1 x))
TypeError: unsupported operand type(s) for +: 'int' and 'HySymbol'
=> (defmacro blah [x] `(print ~@z))
<function <lambda> at 0x7fd6741319d8>
=> (defmacro bleh [q] `(print (blah ~q)))
<function <lambda> at 0x7fd674131268>
=> (bleh y)
Traceback (most recent call last):
  File "stdin-ff397ee5ec5c34f03703a2acccc9f62ed9b3a9ae", line 1, in <module>
    (bleh y)
  File "stdin-a0f7e3bfef673f0bae54abd3721c76c25db16c65", line 1, in <lambda>
    (defmacro blah [x] `(print ~@z))
hy.errors.HyMacroExpansionError: expanding macro blah
  NameError: name 'z' is not defined
=> (defn foo []
... (setv bar 42)
... (import pdb) (pdb.set-trace))
None
=> (foo)
--Return--
> /home/bwillard/stdin-2741f77da2a8503d126c87cd48d527a65903cc14(3)foo()->None
(Pdb) l
  1  	(defn foo []
  2  	(setv bar 42)
  3  ->	(import pdb) (pdb.set-trace))
[EOF]

@Kodiologist
Copy link
Member

Kodiologist commented Oct 11, 2018

It looks like a PrematureEndOfInput exception will display the whole file, or at least, a lot of it. Also, what's the arrow pointing at?

$ perl -E 'say foreach "a".."z"; say "("' >/tmp/test.hy && hy /tmp/test.hy
Traceback (most recent call last):
  File "/home/hippo/Desktop/hyenv/bin/hy", line 12, in <module>
    sys.exit(hy_main())
  File "/home/hippo/Desktop/hyenv/hy/hy/cmdline.py", line 368, in hy_main
    sys.exit(cmdline_handler("hy", sys.argv))
  File "/home/hippo/Desktop/hyenv/hy/hy/cmdline.py", line 354, in cmdline_handler
    runhy.run_path(filename, run_name='__main__')
  File "/usr/local/lib/python3.7/runpy.py", line 261, in run_path
    code, fname = _get_code_from_file(run_name, path_name)
  File "/home/hippo/Desktop/hyenv/hy/hy/importer.py", line 221, in _get_code_from_file
    code = _hy_code_from_file(fname, loader_type=HyLoader)
  File "/home/hippo/Desktop/hyenv/hy/hy/importer.py", line 200, in _hy_code_from_file
    code = loader.get_code(modname)
  File "<frozen importlib._bootstrap_external>", line 860, in get_code
  File "/home/hippo/Desktop/hyenv/hy/hy/importer.py", line 246, in _hy_source_to_code
    hy_tree = hy_parse(source, filename=path)
  File "/home/hippo/Desktop/hyenv/hy/hy/importer.py", line 76, in hy_parse
    reraise(type(e), e.args, None)
  File "/home/hippo/Desktop/hyenv/hy/hy/_compat.py", line 5, in reraise
    try:
  File "/tmp/test.hy", line 1
    a
b
c
[ and so on ]
y
z
(

    ^
hy.lex.exceptions.PrematureEndOfInput: Premature end of input

@brandonwillard
Copy link
Member Author

Looks like it's pointing at the end of the file, where the paren isn't closed, but the indenting in the print-out stops after a.

@brandonwillard
Copy link
Member Author

brandonwillard commented Oct 11, 2018

Just pushed a fix that should take care of that print-out.

> hy test.hy
Traceback (most recent call last):
  File "/home/bwillard/apps/anaconda3/envs/hy-dev/bin/hy", line 11, in <module>
    load_entry_point('hy', 'console_scripts', 'hy')()
  File "/home/bwillard/apps/anaconda3/envs/hy-dev/lib/python3.6/runpy.py", line 261, in run_path
    code, fname = _get_code_from_file(run_name, path_name)
  File "<frozen importlib._bootstrap_external>", line 781, in get_code
  File "/tmp/test.hy", line 27
    (
    ^
hy.lex.exceptions.PrematureEndOfInput: Premature end of input

Copy link
Member

@Kodiologist Kodiologist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a huge commit. Can you group your changes into logical units instead?

hy/cmdline.py Outdated Show resolved Hide resolved
hy/cmdline.py Show resolved Hide resolved
hy/compiler.py Outdated Show resolved Hide resolved
hy/compiler.py Outdated Show resolved Hide resolved
hy/compiler.py Outdated Show resolved Hide resolved
hy/errors.py Show resolved Hide resolved
hy/lex/parser.py Outdated Show resolved Hide resolved
@brandonwillard brandonwillard force-pushed the clean-up-exceptions branch 6 times, most recently from 5e5778e to 31a630d Compare October 17, 2018 18:01
@Kodiologist
Copy link
Member

Would you please:

  • Try to split the big commits into smaller logical units.
  • Separate out refactoring and other code changes unrelated to the purpose of a commit (e.g., adding object to the base classes of HyREPL) into other commits.
  • Generally speaking, don't make a commit that undoes or alters changes that were introduced by a previous commit in the same PR. Instead, change the previous commit itself.
  • Avoid intermediate variables that are used only once (e.g., instead of x = a + b; z = x + y, write z = a + b + y).
  • Remove comments with XXX, TODO, FIXME, or questions, or any other comments that do something other than describe the current state and usage of the code. If you wish, make issues out of these removed comments or post them to this PR.

Let me know when the PR is ready for review.

@brandonwillard brandonwillard force-pushed the clean-up-exceptions branch 3 times, most recently from 8477d1e to a8d6a48 Compare October 29, 2018 04:27
@brandonwillard
Copy link
Member Author

All right, I further separated those commits by their functionality. Unfortunately, the first one is necessarily large-ish, due to how much code it affects; however, most of the changes are simple (e.g. adding new arguments for source and filename).

@Kodiologist
Copy link
Member

It's okay. There will at least be fewer added lines once you introduce a helper for HyTypeError's new parameters.

@brandonwillard
Copy link
Member Author

All right, added some helpers for the relevant errors.

@Kodiologist
Copy link
Member

Kodiologist commented Oct 30, 2018

You mean like this?

raise HySyntaxError.from_expression(
    "Pythons < 3.5 allow only one "
    "`unpack-mapping` per call",
    expr, self.filename, self.source)

Why not something like this?

raise self.error(
    "Pythons < 3.5 allow only one "
    "`unpack-mapping` per call",
    expr)

Then you don't have to pass in self.filename and self.source every time.

@brandonwillard
Copy link
Member Author

No, the helper functions are the additional class methods in HySyntaxError and LexException.

Sure, a method in the compiler could be added, too.

@brandonwillard
Copy link
Member Author

Added a HyASTCompiler._syntax_error method.

@Kodiologist
Copy link
Member

Much nicer.

@Kodiologist
Copy link
Member

The commit message for "Make colored output configurable" mentions HY_FILTER_INTERNAL_ERRORS where I think you meant HY_COLORED_ERRORS (right?).

It seems that HY_FILTER_INTERNAL_ERRORS only has an effect when set to the empty string. I think it's counterintuitive that a binary environment variable would distinguish between an empty string and absence. How about inverting the variable and calling it HY_SHOW_INTERNAL_ERRORS or something?

At least at the tip of the PR, there's a regression such that a return value of None is no longer silenced at the REPL.

@Kodiologist Kodiologist mentioned this pull request Dec 17, 2018
@Kodiologist
Copy link
Member

I'm sorry, Brandon, I've only just noticed that you updated this. It seems that GitHub doesn't send a notification for force-pushes. I'll continue review soon.

@brandonwillard
Copy link
Member Author

No problem; I try to respond with a message after force-pushes, since I also don't recall ever getting such notifications.

I believe I've addressed the matters in your last comment, except for the REPL return value. Is it by design that it doesn't display None?

The Python REPL doesn't either, but, if I recall, Clojure and some other Lisps do. The only immediate advantage— that I can think of right now—involves code that reads from the REPL output (e.g. comint in Emacs); it's often easier to handle situations in which output is guaranteed/consistent.

Regardless, it's easy to change.

@Kodiologist
Copy link
Member

Is it by design that it doesn't display None?

Yeah, the non-display of None has to be implemented separately, since None doesn't str or repr to the empty string (and then there's the matter of the newline). I think it helps keep the output neater.

@Kodiologist
Copy link
Member

So do that, if you don't mind, and then I'll look again.

@brandonwillard brandonwillard force-pushed the clean-up-exceptions branch 2 times, most recently from 51ec427 to a4c084f Compare January 17, 2019 05:26
@brandonwillard
Copy link
Member Author

All right, now it doesn't print None expression values in the console

@Kodiologist
Copy link
Member

Thanks. I just reviewed more of these commits and made more changes and I'm getting a "Permission denied" error when I try to push. Do you have the option to accept changes from reviewers switched off?

@brandonwillard
Copy link
Member Author

Yes

@Kodiologist
Copy link
Member

Can you turn it back on? Otherwise, I don't think there's a way for me to cleanly rewrite history. You can make a copy first with git branch clean-up-exceptions--backup or so.

@brandonwillard
Copy link
Member Author

I'm fine with one reasonably sized set of final reviewer changes—which is what I thought the last one was—but a review process consisting of rebases is too confusing and time consuming.

Please describe the changes you think are necessary to merge this PR.

@Kodiologist
Copy link
Member

I could save up all my edits till I'm done reviewing the entire PR, but I thought you'd rather get them as I make them, rather than get a giant stack of them all at once after a long time.

I've pushed my current version of clean-up-exceptions to my own fork. If you'd prefer to wait till I've finished reviewing the whole PR (which hopefully won't be that much longer), I can let you know when I'm done.

@Kodiologist
Copy link
Member

So far, at any rate, I've reviewed up to and including "Improve correspondence with Python errors and console behavior", and made the following notable changes:

  • Replaced usages of future_builtins.filter with generator expressions (and removed the import of future_builtins.map since it seemed to have no effect).

  • Moved the introduction of _initialize_env_var back to the first commit where it's useful, and moved it from compat.py (since it doesn't have anything to do with portability) to __init__.py.

  • Shortened the implementation of test_bin_hy_macro_require.

  • Removed the following comment:

    def can_eval(expr):
    # XXX: filename and source are needed to avoid ridiculous pytest
    # issues.
    return hy_eval(hy_parse(expr))

@Kodiologist
Copy link
Member

Okay, all done. Sorry for the long delay. I had a busy week and I expected there'd be a lot more work to do here, but I made only minor changes to the last two commits.

@Kodiologist
Copy link
Member

If all looks good to you, replace your version of the branch with mine and we should be good to merge (pending the final Travis check).

@brandonwillard
Copy link
Member Author

Finally had a chance to look at your branch; give it a push.

@Kodiologist
Copy link
Member

Okay, I'll do it this afternoon. There will probably be some merge conflicts, but if those can be smoothed out without major issues and Travis has no surprises in store, I'll merge.

Kodiologist and others added 7 commits February 7, 2019 13:43
And standardize the indentation of these calls.
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#657.
These changes make the Hy REPL more closely follow `code.InteractiveConsole`'s
class interface and provide minimally intrusive traceback print-out filtering
via a context manager that temporarily alters `sys.excepthook`.  In other words,
exception messages from the REPL will no longer show Hy internal
code (e.g. importer, compiler and parsing functions).

The boolean variable `hy.errors._hy_filter_internal_errors` dynamically
enables/disables trace filtering, and the env variable
`HY_FILTER_INTERNAL_ERRORS` can be used as the initial value.
Colored exception output is now disabled by default and configurable through
`hy.errors._hy_colored_errors` and the environment variable
`HY_COLORED_ERRORS`.

Likewise, Hy model/AST color printing is now configurable and disabled by
default.  The corresponding variables are `hy.models._hy_colored_ast_objects`
and `HY_COLORED_AST_OBJECTS`.

Closes hylang#1429, closes hylang#1510.
Compiler and command-line error messages now reflect their Python counterparts.
E.g. where Python emits a `SyntaxError`, so does Hy; same for `TypeError`s.
Multiple tests have been added that check the format and type of raised
exceptions over varying command-line invocations (e.g. interactive and not).

A new exception type for `require` errors was added so that they can be treated
like normal run-time errors and not compiler errors.

The Hy REPL has been further refactored to better match the class-structured
API.  Now, different error types are handled separately and leverage more base
class-provided functionality.

Closes hylang#1486.
Source entered interactively can now be displayed in traceback output.  Also,
the REPL object is now available in its namespace, so that, for instance,
display options--like `spy`--can be turned on and off interactively.

Closes hylang#1397.
@Kodiologist Kodiologist merged commit e2d6640 into hylang:master Feb 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants