Repository: hylang/hy

Issue:
A strange operation for "as->" macro.
In the document for [as->](http://docs.hylang.org/en/latest/language/api.html#as), the example code expected following output. 

```hy
=> (as-> 0 it
...      (inc it)
...      (inc it))
2
```

However, I got the following result.

```hy
=> (as-> 0 it
... (inc it)
... (inc it))
(2, 2, 2)
=>
```



Discussion:

Resolving patch:
diff --git a/hy/cmdline.py b/hy/cmdline.py
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -32,14 +32,16 @@
 import ast
 import sys
 import os
+import importlib
 
 import astor.codegen
 
 import hy
 
 from hy.lex import LexException, PrematureEndOfInput, tokenize
-from hy.compiler import hy_compile, HyTypeError
-from hy.importer import (ast_compile, import_buffer_to_module,
+from hy.lex.parser import hy_symbol_mangle
+from hy.compiler import HyTypeError
+from hy.importer import (hy_eval, import_buffer_to_module,
                          import_file_to_ast, import_file_to_hst,
                          import_buffer_to_ast, import_buffer_to_hst)
 from hy.completer import completion
@@ -73,25 +75,37 @@ def __call__(self, code=None):
 builtins.exit = HyQuitter('exit')
 
 
-def print_python_code(_ast):
-    # astor cannot handle ast.Interactive, so disguise it as a module
-    _ast_for_print = ast.Module()
-    _ast_for_print.body = _ast.body
-    print(astor.codegen.to_source(_ast_for_print))
-
-
 class HyREPL(code.InteractiveConsole):
-    def __init__(self, spy=False, locals=None, filename="<input>"):
+    def __init__(self, spy=False, output_fn=None, locals=None,
+                 filename="<input>"):
+
         self.spy = spy
+
+        if output_fn is None:
+            self.output_fn = repr
+        elif callable(output_fn):
+            self.output_fn = output_fn
+        else:
+            f = hy_symbol_mangle(output_fn)
+            if "." in output_fn:
+                module, f = f.rsplit(".", 1)
+                self.output_fn = getattr(importlib.import_module(module), f)
+            else:
+                self.output_fn = __builtins__[f]
+
         code.InteractiveConsole.__init__(self, locals=locals,
                                          filename=filename)
 
     def runsource(self, source, filename='<input>', symbol='single'):
         global SIMPLE_TRACEBACKS
         try:
-            tokens = tokenize(source)
-        except PrematureEndOfInput:
-            return True
+            try:
+                tokens = tokenize(source)
+            except PrematureEndOfInput:
+                return True
+            do = HyExpression([HySymbol('do')] + tokens)
+            do.start_line = do.end_line = do.start_column = do.end_column = 1
+            do.replace(do)
         except LexException as e:
             if e.source is None:
                 e.source = source
@@ -100,10 +114,15 @@ def runsource(self, source, filename='<input>', symbol='single'):
             return False
 
         try:
-            _ast = hy_compile(tokens, "__console__", root=ast.Interactive)
-            if self.spy:
-                print_python_code(_ast)
-            code = ast_compile(_ast, filename, symbol)
+            def ast_callback(main_ast, expr_ast):
+                if self.spy:
+                    # Mush the two AST chunks into a single module for
+                    # conversion into Python.
+                    new_ast = ast.Module(main_ast.body +
+                                         [ast.Expr(expr_ast.body)])
+                    print(astor.to_source(new_ast))
+            value = hy_eval(do, self.locals, "__console__",
+                            ast_callback)
         except HyTypeError as e:
             if e.source is None:
                 e.source = source
@@ -117,7 +136,12 @@ def runsource(self, source, filename='<input>', symbol='single'):
             self.showtraceback()
             return False
 
-        self.runcode(code)
+        if value is not None:
+            # Make the last non-None value available to
+            # the user as `_`.
+            self.locals['_'] = value
+            # Print the value.
+            print(self.output_fn(value))
         return False
 
 
@@ -211,7 +235,7 @@ def run_file(filename):
     return 0
 
 
-def run_repl(hr=None, spy=False):
+def run_repl(hr=None, **kwargs):
     import platform
     sys.ps1 = "=> "
     sys.ps2 = "... "
@@ -221,7 +245,7 @@ def run_repl(hr=None, spy=False):
     with completion(Completer(namespace)):
 
         if not hr:
-            hr = HyREPL(spy, namespace)
+            hr = HyREPL(locals=namespace, **kwargs)
 
         hr.interact("{appname} {version} using "
                     "{py}({build}) {pyversion} on {os}".format(
@@ -236,8 +260,8 @@ def run_repl(hr=None, spy=False):
     return 0
 
 
-def run_icommand(source, spy=False):
-    hr = HyREPL(spy)
+def run_icommand(source, **kwargs):
+    hr = HyREPL(**kwargs)
     if os.path.exists(source):
         with open(source, "r") as f:
             source = f.read()
@@ -272,7 +296,9 @@ def cmdline_handler(scriptname, argv):
         help="program passed in as a string, then stay in REPL")
     parser.add_argument("--spy", action="store_true",
                         help="print equivalent Python code before executing")
-
+    parser.add_argument("--repl-output-fn",
+                        help="function for printing REPL output "
+                             "(e.g., hy.contrib.hy-repr.hy-repr)")
     parser.add_argument("-v", "--version", action="version", version=VERSION)
 
     parser.add_argument("--show-tracebacks", action="store_true",
@@ -315,7 +341,8 @@ def cmdline_handler(scriptname, argv):
 
     if options.icommand:
         # User did "hy -i ..."
-        return run_icommand(options.icommand, spy=options.spy)
+        return run_icommand(options.icommand, spy=options.spy,
+                            output_fn=options.repl_output_fn)
 
     if options.args:
         if options.args[0] == "-":
@@ -332,7 +359,7 @@ def cmdline_handler(scriptname, argv):
                 sys.exit(e.errno)
 
     # User did NOTHING!
-    return run_repl(spy=options.spy)
+    return run_repl(spy=options.spy, output_fn=options.repl_output_fn)
 
 
 # entry point for cmd line script "hy"
diff --git a/hy/importer.py b/hy/importer.py
--- a/hy/importer.py
+++ b/hy/importer.py
@@ -111,7 +111,7 @@ def import_buffer_to_module(module_name, buf):
     return mod
 
 
-def hy_eval(hytree, namespace, module_name):
+def hy_eval(hytree, namespace, module_name, ast_callback=None):
     foo = HyObject()
     foo.start_line = 0
     foo.end_line = 0
@@ -133,6 +133,9 @@ def hy_eval(hytree, namespace, module_name):
         node.lineno = 1
         node.col_offset = 1
 
+    if ast_callback:
+        ast_callback(_ast, expr)
+
     if not isinstance(namespace, dict):
         raise HyTypeError(foo, "Globals must be a dictionary")
 


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -19,6 +19,7 @@
 from .native_tests.contrib.walk import *  # noqa
 from .native_tests.contrib.multi import *  # noqa
 from .native_tests.contrib.sequences import *  # noqa
+from .native_tests.contrib.hy_repr import *  # noqa
 
 if PY3:
     from .native_tests.py3_only_tests import *  # noqa
diff --git a/tests/native_tests/contrib/hy_repr.hy b/tests/native_tests/contrib/hy_repr.hy
new file mode 100644
--- /dev/null
+++ b/tests/native_tests/contrib/hy_repr.hy
@@ -0,0 +1,82 @@
+(import
+  [hy.contrib.hy-repr [hy-repr]])
+
+(defn test-hy-repr-roundtrip-from-value []
+  ; Test that a variety of values round-trip properly.
+  (setv values [
+    None False True
+    5 5.1 '5 '5.1
+    (int 5)
+    1/2
+    5j 5.1j 2+1j 1.2+3.4j
+    "" b""
+    '"" 'b""
+    "apple bloom" b"apple bloom" "⚘"
+    '"apple bloom" 'b"apple bloom" '"⚘"
+    "single ' quotes" b"single ' quotes"
+    "\"double \" quotes\"" b"\"double \" quotes\""
+    'mysymbol :mykeyword
+    [] (,) #{} (frozenset #{})
+    '[] '(,) '#{} '(frozenset #{})
+    '['[]]
+    '(+ 1 2)
+    [1 2 3] (, 1 2 3) #{1 2 3} (frozenset #{1 2 3})
+    '[1 2 3] '(, 1 2 3) '#{1 2 3} '(frozenset #{1 2 3})
+    {"a" 1 "b" 2 "a" 3} '{"a" 1 "b" 2 "a" 3}
+    [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"}]
+    '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"}]])
+  (for [original-val values]
+    (setv evaled (eval (read-str (hy-repr original-val))))
+    (assert (= evaled original-val))
+    (assert (is (type evaled) (type original-val)))))
+
+(defn test-hy-repr-roundtrip-from-str []
+  (setv strs [
+    "[1 2 3]"
+    "'[1 2 3]"
+    "[1 'a 3]"
+    "'[1 a 3]"
+    "'[1 'a 3]"
+    "[1 '[2 3] 4]"
+    "'[1 [2 3] 4]"
+    "'[1 '[2 3] 4]"
+    "'[1 `[2 3] 4]"
+    "'[1 `[~foo ~@bar] 4]"
+    "'[1 `[~(+ 1 2) ~@(+ [1] [2])] 4]"
+    "'[1 `[~(do (print x 'y) 1)] 4]"
+    "{1 20}"
+    "'{1 10 1 20}"
+    "'asymbol"
+    ":akeyword"])
+  (for [original-str strs]
+    (setv rep (hy-repr (eval (read-str original-str))))
+    (assert (= rep original-str))))
+
+(defn test-hy-model-constructors []
+  (import hy)
+  (assert (= (hy-repr (hy.HyInteger 7)) "'7"))
+  (assert (= (hy-repr (hy.HyString "hello")) "'\"hello\""))
+  (assert (= (hy-repr (hy.HyList [1 2 3])) "'[1 2 3]"))
+  (assert (= (hy-repr (hy.HyDict [1 2 3])) "'{1 2 3}")))
+
+(defn test-hy-repr-self-reference []
+
+  (setv x [1 2 3])
+  (setv (get x 1) x)
+  (assert (= (hy-repr x) "[1 [...] 3]"))
+
+  (setv x {1 2  3 [4 5]  6 7})
+  (setv (get x 3 1) x)
+  (assert (in (hy-repr x) (list-comp
+    ; The ordering of a dictionary isn't guaranteed, so we need
+    ; to check for all possible orderings.
+    (+ "{" (.join " " p) "}")
+    [p (permutations ["1 2" "3 [4 {...}]" "6 7"])]))))
+
+(defn test-hy-repr-dunder-method []
+  (defclass C [list] [__hy-repr__ (fn [self] "cuddles")])
+  (assert (= (hy-repr (C)) "cuddles")))
+
+(defn test-hy-repr-fallback []
+  (defclass D [list] [__repr__ (fn [self] "cuddles")])
+  (assert (= (hy-repr (D)) "cuddles")))
diff --git a/tests/test_bin.py b/tests/test_bin.py
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -23,116 +23,164 @@
 # DEALINGS IN THE SOFTWARE.
 import os
 import subprocess
+import re
 from hy._compat import PY3
 
 
 hy_dir = os.environ.get('HY_DIR', '')
 
 
-def run_cmd(cmd, stdin_data=None):
+def hr(s=""):
+    return "hy --repl-output-fn=hy.contrib.hy-repr.hy-repr " + s
+
+
+def run_cmd(cmd, stdin_data=None, expect=0):
     p = subprocess.Popen(os.path.join(hy_dir, cmd),
                          stdin=subprocess.PIPE,
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE,
                          shell=True)
-    stdout = ""
-    stderr = ""
     if stdin_data is not None:
         p.stdin.write(stdin_data.encode('ASCII'))
         p.stdin.flush()
         p.stdin.close()
     # Read stdout and stderr otherwise if the PIPE buffer is full, we might
     # wait for ever…
+    stdout = ""
+    stderr = ""
     while p.poll() is None:
         stdout += p.stdout.read().decode('utf-8')
         stderr += p.stderr.read().decode('utf-8')
-    return p.returncode, stdout, stderr
+    assert p.returncode == expect
+    return stdout, stderr
 
 
 def test_bin_hy():
-    ret = run_cmd("hy", "")
-    assert ret[0] == 0
+    run_cmd("hy", "")
 
 
 def test_bin_hy_stdin():
-    ret = run_cmd("hy", '(koan)')
-    assert ret[0] == 0
-    assert "monk" in ret[1]
+    output, _ = run_cmd("hy", '(koan)')
+    assert "monk" in output
+
+    output, _ = run_cmd("hy --spy", '(koan)')
+    assert "monk" in output
+    assert "\\n  Ummon" in output
+
+    # --spy should work even when an exception is thrown
+    output, _ = run_cmd("hy --spy", '(foof)')
+    assert "foof()" in output
+
+
+def test_bin_hy_stdin_multiline():
+    output, _ = run_cmd("hy", '(+ "a" "b"\n"c" "d")')
+    assert "'abcd'" in output
+
+
+def test_bin_hy_stdin_comments():
+    _, err_empty = run_cmd("hy", '')
+
+    output, err = run_cmd("hy", '(+ "a" "b") ; "c"')
+    assert "'ab'" in output
+    assert err == err_empty
+
+    _, err = run_cmd("hy", '; 1')
+    assert err == err_empty
+
+
+def test_bin_hy_stdin_assignment():
+    # If the last form is an assignment, don't print the value.
+
+    output, _ = run_cmd("hy", '(setv x (+ "A" "Z"))')
+    assert "AZ" not in output
+
+    output, _ = run_cmd("hy", '(setv x (+ "A" "Z")) (+ "B" "Y")')
+    assert "AZ" not in output
+    assert "BY" in output
+
+    output, _ = run_cmd("hy", '(+ "B" "Y") (setv x (+ "A" "Z"))')
+    assert "AZ" not in output
+    assert "BY" not in output
+
+
+def test_bin_hy_stdin_as_arrow():
+    # https://github.com/hylang/hy/issues/1255
+    output, _ = run_cmd("hy", "(as-> 0 it (inc it) (inc it))")
+    assert re.match(r"=>\s+2L?\s+=>", output)
+
+
+def test_bin_hy_stdin_error_underline_alignment():
+    _, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)")
+    assert "\n  (mabcdefghi)\n  ^----------^" in err
+
+
+def test_bin_hy_stdin_hy_repr():
+    output, _ = run_cmd("hy", '(+ [1] [2])')
+    assert "[1, 2]" in output.replace('L', '')
+
+    output, _ = run_cmd(hr(), '(+ [1] [2])')
+    assert "[1 2]" in output
+
+    output, _ = run_cmd(hr("--spy"), '(+ [1] [2])')
+    assert "[1]+[2]" in output.replace('L', '').replace(' ', '')
+    assert "[1 2]" in output
+
+    # --spy should work even when an exception is thrown
+    output, _ = run_cmd(hr("--spy"), '(+ [1] [2] (foof))')
+    assert "[1]+[2]" in output.replace('L', '').replace(' ', '')
 
 
 def test_bin_hy_cmd():
-    ret = run_cmd("hy -c \"(koan)\"")
-    assert ret[0] == 0
-    assert "monk" in ret[1]
+    output, _ = run_cmd("hy -c \"(koan)\"")
+    assert "monk" in output
 
-    ret = run_cmd("hy -c \"(koan\"")
-    assert ret[0] == 1
-    assert "Premature end of input" in ret[2]
+    _, err = run_cmd("hy -c \"(koan\"", expect=1)
+    assert "Premature end of input" in err
 
 
 def test_bin_hy_icmd():
-    ret = run_cmd("hy -i \"(koan)\"", "(ideas)")
-    assert ret[0] == 0
-    output = ret[1]
-
+    output, _ = run_cmd("hy -i \"(koan)\"", "(ideas)")
     assert "monk" in output
     assert "figlet" in output
 
 
 def test_bin_hy_icmd_file():
-    ret = run_cmd("hy -i resources/icmd_test_file.hy", "(ideas)")
-    assert ret[0] == 0
-    output = ret[1]
-
+    output, _ = run_cmd("hy -i resources/icmd_test_file.hy", "(ideas)")
     assert "Hy!" in output
 
 
 def test_bin_hy_icmd_and_spy():
-    ret = run_cmd("hy -i \"(+ [] [])\" --spy", "(+ 1 1)")
-    assert ret[0] == 0
-    output = ret[1]
-
+    output, _ = run_cmd("hy -i \"(+ [] [])\" --spy", "(+ 1 1)")
     assert "([] + [])" in output
 
 
 def test_bin_hy_missing_file():
-    ret = run_cmd("hy foobarbaz")
-    assert ret[0] == 2
-    assert "No such file" in ret[2]
+    _, err = run_cmd("hy foobarbaz", expect=2)
+    assert "No such file" in err
 
 
 def test_bin_hy_file_with_args():
-    ret = run_cmd("hy tests/resources/argparse_ex.hy -h")
-    assert ret[0] == 0
-    assert "usage" in ret[1]
-    ret = run_cmd("hy tests/resources/argparse_ex.hy -c bar")
-    assert ret[0] == 0
-    assert "got c" in ret[1]
-    ret = run_cmd("hy tests/resources/argparse_ex.hy -i foo")
-    assert ret[0] == 0
-    assert "foo" in ret[1]
-    ret = run_cmd("hy tests/resources/argparse_ex.hy -i foo -c bar")
-    assert ret[0] == 0
-    assert "foo" in ret[1]
+    assert "usage" in run_cmd("hy tests/resources/argparse_ex.hy -h")[0]
+    assert "got c" in run_cmd("hy tests/resources/argparse_ex.hy -c bar")[0]
+    assert "foo" in run_cmd("hy tests/resources/argparse_ex.hy -i foo")[0]
+    assert "foo" in run_cmd("hy tests/resources/argparse_ex.hy -i foo -c bar")[0]  # noqa
 
 
 def test_bin_hyc():
-    ret = run_cmd("hyc")
-    assert ret[0] == 2
-    assert "usage" in ret[2]
-    ret = run_cmd("hyc -h")
-    assert ret[0] == 0
-    assert "usage" in ret[1]
-    ret = run_cmd("hyc tests/resources/argparse_ex.hy")
-    assert ret[0] == 0
-    assert "Compiling" in ret[1]
+    _, err = run_cmd("hyc", expect=2)
+    assert "usage" in err
+
+    output, _ = run_cmd("hyc -h")
+    assert "usage" in output
+
+    output, _ = run_cmd("hyc tests/resources/argparse_ex.hy")
+    assert "Compiling" in output
     assert os.path.exists("tests/resources/argparse_ex.pyc")
 
 
 def test_bin_hyc_missing_file():
-    ret = run_cmd("hyc foobarbaz")
-    assert ret[0] == 2
-    assert "[Errno 2]" in ret[2]
+    _, err = run_cmd("hyc foobarbaz", expect=2)
+    assert "[Errno 2]" in err
 
 
 def test_hy2py():
@@ -144,10 +192,10 @@ def test_hy2py():
                     continue
                 else:
                     i += 1
-                    ret = run_cmd("hy2py -s -a " + os.path.join(dirpath, f))
-                    assert ret[0] == 0, f
-                    assert len(ret[1]) > 1, f
-                    assert len(ret[2]) == 0, f
+                    output, err = run_cmd("hy2py -s -a " +
+                                          os.path.join(dirpath, f))
+                    assert len(output) > 1, f
+                    assert len(err) == 0, f
     assert i
 
 
@@ -159,48 +207,40 @@ def test_bin_hy_builtins():
 
 
 def test_bin_hy_main():
-    ret = run_cmd("hy tests/resources/bin/main.hy")
-    assert ret[0] == 0
-    assert "Hello World" in ret[1]
+    output, _ = run_cmd("hy tests/resources/bin/main.hy")
+    assert "Hello World" in output
 
 
 def test_bin_hy_main_args():
-    ret = run_cmd("hy tests/resources/bin/main.hy test 123")
-    assert ret[0] == 0
-    assert "test" in ret[1]
-    assert "123" in ret[1]
+    output, _ = run_cmd("hy tests/resources/bin/main.hy test 123")
+    assert "test" in output
+    assert "123" in output
 
 
 def test_bin_hy_main_exitvalue():
-    ret = run_cmd("hy tests/resources/bin/main.hy exit1")
-    assert ret[0] == 1
+    run_cmd("hy tests/resources/bin/main.hy exit1", expect=1)
 
 
 def test_bin_hy_no_main():
-    ret = run_cmd("hy tests/resources/bin/nomain.hy")
-    assert ret[0] == 0
-    assert "This Should Still Work" in ret[1]
+    output, _ = run_cmd("hy tests/resources/bin/nomain.hy")
+    assert "This Should Still Work" in output
 
 
 def test_bin_hy_module_main():
-    ret = run_cmd("hy -m tests.resources.bin.main")
-    assert ret[0] == 0
-    assert "Hello World" in ret[1]
+    output, _ = run_cmd("hy -m tests.resources.bin.main")
+    assert "Hello World" in output
 
 
 def test_bin_hy_module_main_args():
-    ret = run_cmd("hy -m tests.resources.bin.main test 123")
-    assert ret[0] == 0
-    assert "test" in ret[1]
-    assert "123" in ret[1]
+    output, _ = run_cmd("hy -m tests.resources.bin.main test 123")
+    assert "test" in output
+    assert "123" in output
 
 
 def test_bin_hy_module_main_exitvalue():
-    ret = run_cmd("hy -m tests.resources.bin.main exit1")
-    assert ret[0] == 1
+    run_cmd("hy -m tests.resources.bin.main exit1", expect=1)
 
 
 def test_bin_hy_module_no_main():
-    ret = run_cmd("hy -m tests.resources.bin.nomain")
-    assert ret[0] == 0
-    assert "This Should Still Work" in ret[1]
+    output, _ = run_cmd("hy -m tests.resources.bin.nomain")
+    assert "This Should Still Work" in output