Merge pull request #1314 from Kodiologist/compiler-autobox

Auto-promote values to HyObjects in the compiler
This commit is contained in:
Tuukka Turto 2017-07-10 09:04:08 +03:00 committed by GitHub
commit 7c53a07b93
12 changed files with 164 additions and 109 deletions

4
NEWS
View File

@ -4,6 +4,10 @@ Changes from 0.13.0
* Single-character "sharp macros" changed to "tag macros", which can have * Single-character "sharp macros" changed to "tag macros", which can have
longer names longer names
* Periods are no longer allowed in keywords * Periods are no longer allowed in keywords
* `eval` is now a function instead of a special form
* The compiler now automatically promotes values to Hy model objects
as necessary, so you can write ``(eval `(+ 1 ~n))`` instead of
``(eval `(+ 1 ~(HyInteger n)))``
[ Bug Fixes ] [ Bug Fixes ]
* Numeric literals are no longer parsed as symbols when followed by a dot * Numeric literals are no longer parsed as symbols when followed by a dot

View File

@ -863,27 +863,6 @@ doto
=> collection => collection
[2 1] [2 1]
eval
----
``eval`` evaluates a quoted expression and returns the value. The optional
second and third arguments specify the dictionary of globals to use and the
module name. The globals dictionary defaults to ``(local)`` and the module name
defaults to the name of the current module.
.. code-block:: clj
=> (eval '(print "Hello World"))
"Hello World"
If you want to evaluate a string, use ``read-str`` to convert it to a
form first:
.. code-block:: clj
=> (eval (read-str "(+ 1 1)"))
2
eval-and-compile eval-and-compile
---------------- ----------------

View File

@ -230,6 +230,30 @@ Returns ``True`` if *coll* is empty. Equivalent to ``(= 0 (len coll))``.
False False
.. _eval-fn:
eval
----
``eval`` evaluates a quoted expression and returns the value. The optional
second and third arguments specify the dictionary of globals to use and the
module name. The globals dictionary defaults to ``(local)`` and the module name
defaults to the name of the current module.
.. code-block:: clj
=> (eval '(print "Hello World"))
"Hello World"
If you want to evaluate a string, use ``read-str`` to convert it to a
form first:
.. code-block:: clj
=> (eval (read-str "(+ 1 1)"))
2
.. _every?-fn: .. _every?-fn:
every? every?

View File

@ -15,7 +15,7 @@ import astor.codegen
import hy import hy
from hy.lex import LexException, PrematureEndOfInput, tokenize from hy.lex import LexException, PrematureEndOfInput
from hy.lex.parser import hy_symbol_mangle from hy.lex.parser import hy_symbol_mangle
from hy.compiler import HyTypeError from hy.compiler import HyTypeError
from hy.importer import (hy_eval, import_buffer_to_module, from hy.importer import (hy_eval, import_buffer_to_module,
@ -77,12 +77,9 @@ class HyREPL(code.InteractiveConsole):
global SIMPLE_TRACEBACKS global SIMPLE_TRACEBACKS
try: try:
try: try:
tokens = tokenize(source) do = import_buffer_to_hst(source)
except PrematureEndOfInput: except PrematureEndOfInput:
return True 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: except LexException as e:
if e.source is None: if e.source is None:
e.source = source e.source = source

View File

@ -5,14 +5,15 @@
from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex, from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex,
HyString, HyBytes, HySymbol, HyFloat, HyList, HySet, HyString, HyBytes, HySymbol, HyFloat, HyList, HySet,
HyDict, HyCons) HyDict, HyCons, wrap_value)
from hy.errors import HyCompileError, HyTypeError from hy.errors import HyCompileError, HyTypeError
from hy.lex.parser import hy_symbol_mangle from hy.lex.parser import hy_symbol_mangle
import hy.macros import hy.macros
from hy._compat import ( from hy._compat import (
str_type, bytes_type, long_type, PY3, PY34, PY35, raise_empty) str_type, string_types, bytes_type, long_type, PY3, PY34, PY35,
raise_empty)
from hy.macros import require, macroexpand, tag_macroexpand from hy.macros import require, macroexpand, tag_macroexpand
import hy.importer import hy.importer
@ -110,6 +111,19 @@ def builds_if(_type, condition):
return lambda fn: fn return lambda fn: fn
def spoof_positions(obj):
if not isinstance(obj, HyObject) or isinstance(obj, HyCons):
return
if not hasattr(obj, "start_column"):
obj.start_column = 0
if not hasattr(obj, "start_line"):
obj.start_line = 0
if (hasattr(obj, "__iter__") and
not isinstance(obj, (string_types, bytes_type))):
for x in obj:
spoof_positions(x)
class Result(object): class Result(object):
""" """
Smart representation of the result of a hy->AST compilation Smart representation of the result of a hy->AST compilation
@ -378,23 +392,23 @@ class HyASTCompiler(object):
ret = Result() ret = Result()
for module, names in self.imports.items(): for module, names in self.imports.items():
if None in names: if None in names:
ret += self.compile([ e = HyExpression([
HyExpression([
HySymbol("import"), HySymbol("import"),
HySymbol(module), HySymbol(module),
]).replace(expr) ]).replace(expr)
]) spoof_positions(e)
ret += self.compile(e)
names = sorted(name for name in names if name) names = sorted(name for name in names if name)
if names: if names:
ret += self.compile([ e = HyExpression([
HyExpression([
HySymbol("import"), HySymbol("import"),
HyList([ HyList([
HySymbol(module), HySymbol(module),
HyList([HySymbol(name) for name in names]) HyList([HySymbol(name) for name in names])
]) ])
]).replace(expr) ]).replace(expr)
]) spoof_positions(e)
ret += self.compile(e)
self.imports = defaultdict(set) self.imports = defaultdict(set)
return ret.stmts return ret.stmts
@ -404,6 +418,11 @@ class HyASTCompiler(object):
if not isinstance(ret, Result): if not isinstance(ret, Result):
ret = Result() + ret ret = Result() + ret
return ret return ret
if not isinstance(atom, HyObject):
atom = wrap_value(atom)
if isinstance(atom, HyObject):
spoof_positions(atom)
return self.compile_atom(type(atom), atom)
def compile(self, tree): def compile(self, tree):
try: try:
@ -602,12 +621,6 @@ class HyASTCompiler(object):
ast.copy_location(new_name, name) ast.copy_location(new_name, name)
return new_name return new_name
@builds(list)
def compile_raw_list(self, entries):
ret = self._compile_branch(entries)
ret += ret.expr_as_stmt()
return ret
def _render_quoted_form(self, form, level): def _render_quoted_form(self, form, level):
""" """
Render a quoted form as a new HyExpression. Render a quoted form as a new HyExpression.
@ -706,31 +719,6 @@ class HyASTCompiler(object):
raise HyTypeError(expr, raise HyTypeError(expr,
"`%s' can't be used at the top-level" % expr[0]) "`%s' can't be used at the top-level" % expr[0])
@builds("eval")
@checkargs(min=1, max=3)
def compile_eval(self, expr):
expr.pop(0)
if not isinstance(expr[0], (HyExpression, HySymbol)):
raise HyTypeError(expr, "expression expected as first argument")
elist = [HySymbol("hy_eval")] + [expr[0]]
if len(expr) >= 2:
elist.append(expr[1])
else:
elist.append(HyExpression([HySymbol("locals")]))
if len(expr) == 3:
elist.append(expr[2])
else:
elist.append(HyString(self.module_name))
ret = self.compile(HyExpression(elist).replace(expr))
ret.add_imports("hy.importer", ["hy_eval"])
return ret
@builds("do") @builds("do")
def compile_do(self, expression): def compile_do(self, expression):
expression.pop(0) expression.pop(0)
@ -766,6 +754,7 @@ class HyASTCompiler(object):
return ret return ret
@builds("try") @builds("try")
@checkargs(min=2)
def compile_try_expression(self, expr): def compile_try_expression(self, expr):
expr.pop(0) # try expr.pop(0) # try
@ -1858,6 +1847,7 @@ class HyASTCompiler(object):
op = ops[expression.pop(0)] op = ops[expression.pop(0)]
right_associative = op == ast.Pow right_associative = op == ast.Pow
lineno, col_offset = expression.start_line, expression.start_column
if right_associative: if right_associative:
expression = expression[::-1] expression = expression[::-1]
ret = self.compile(expression.pop(0)) ret = self.compile(expression.pop(0))
@ -1870,8 +1860,8 @@ class HyASTCompiler(object):
ret += ast.BinOp(left=left_expr, ret += ast.BinOp(left=left_expr,
op=op(), op=op(),
right=right_expr, right=right_expr,
lineno=child.start_line, lineno=lineno,
col_offset=child.start_column) col_offset=col_offset)
return ret return ret
@builds("**") @builds("**")
@ -2619,21 +2609,21 @@ def hy_compile(tree, module_name, root=ast.Module, get_expr=False):
body = [] body = []
expr = None expr = None
if not (isinstance(tree, HyObject) or type(tree) is list): if not isinstance(tree, HyObject):
raise HyCompileError("tree must be a HyObject or a list") tree = wrap_value(tree)
if not isinstance(tree, HyObject):
raise HyCompileError("`tree` must be a HyObject or capable of "
"being promoted to one")
spoof_positions(tree)
if isinstance(tree, HyObject) or tree: compiler = HyASTCompiler(module_name)
compiler = HyASTCompiler(module_name) result = compiler.compile(tree)
result = compiler.compile(tree) expr = result.force_expr
expr = result.force_expr
if not get_expr: if not get_expr:
result += result.expr_as_stmt() result += result.expr_as_stmt()
# We need to test that the type is *exactly* `list` because we don't body = compiler.imports_as_stmts(tree) + result.stmts
# want to do `tree[0]` on HyList or such.
spoof_tree = tree[0] if type(tree) is list else tree
body = compiler.imports_as_stmts(spoof_tree) + result.stmts
ret = root(body=body) ret = root(body=body)

View File

@ -18,7 +18,8 @@
(import [hy._compat [long-type]]) ; long for python2, int for python3 (import [hy._compat [long-type]]) ; long for python2, int for python3
(import [hy.models [HyCons HySymbol HyKeyword]]) (import [hy.models [HyCons HySymbol HyKeyword]])
(import [hy.lex [LexException PrematureEndOfInput tokenize]]) (import [hy.lex [LexException PrematureEndOfInput tokenize]])
(import [hy.compiler [HyASTCompiler]]) (import [hy.compiler [HyASTCompiler spoof-positions]])
(import [hy.importer [hy-eval :as eval]])
(defn butlast [coll] (defn butlast [coll]
"Returns coll except of last element." "Returns coll except of last element."
@ -74,8 +75,8 @@
(import astor) (import astor)
(import hy.compiler) (import hy.compiler)
(fake-source-positions tree) (spoof-positions tree)
(setv compiled (hy.compiler.hy_compile tree (calling-module-name))) (setv compiled (hy.compiler.hy-compile tree (calling-module-name)))
((if codegen ((if codegen
astor.codegen.to_source astor.codegen.to_source
astor.dump) astor.dump)
@ -174,15 +175,6 @@
"Return true if (pred x) is logical true for every x in coll, else false" "Return true if (pred x) is logical true for every x in coll, else false"
(all (map pred coll))) (all (map pred coll)))
(defn fake-source-positions [tree]
"Fake the source positions for a given tree"
(if (coll? tree)
(for* [subtree tree]
(fake-source-positions subtree)))
(for* [attr '[start-line end-line start-column end-column]]
(if (not (hasattr tree attr))
(setattr tree attr 1))))
(defn flatten [coll] (defn flatten [coll]
"Return a single flat list expanding all members of coll" "Return a single flat list expanding all members of coll"
(if (coll? coll) (if (coll? coll)
@ -469,7 +461,7 @@
(def *exports* (def *exports*
'[*map accumulate butlast calling-module-name chain coll? combinations '[*map accumulate butlast calling-module-name chain coll? combinations
comp complement compress cons cons? constantly count cycle dec distinct comp complement compress cons cons? constantly count cycle dec distinct
disassemble drop drop-last drop-while empty? even? every? first filter disassemble drop drop-last drop-while empty? eval even? every? first filter
flatten float? fraction gensym group-by identity inc input instance? flatten float? fraction gensym group-by identity inc input instance?
integer integer? integer-char? interleave interpose islice iterable? integer integer? integer-char? interleave interpose islice iterable?
iterate iterator? juxt keyword keyword? last list* macroexpand iterate iterator? juxt keyword keyword? last list* macroexpand

View File

@ -3,7 +3,7 @@
# license. See the LICENSE. # license. See the LICENSE.
from hy.compiler import hy_compile, HyTypeError from hy.compiler import hy_compile, HyTypeError
from hy.models import HyObject, replace_hy_obj from hy.models import HyObject, HyExpression, HySymbol, replace_hy_obj
from hy.lex import tokenize, LexException from hy.lex import tokenize, LexException
from hy.errors import HyIOError from hy.errors import HyIOError
@ -14,6 +14,7 @@ import struct
import imp import imp
import sys import sys
import ast import ast
import inspect
import os import os
import __future__ import __future__
@ -31,7 +32,7 @@ def ast_compile(ast, filename, mode):
def import_buffer_to_hst(buf): def import_buffer_to_hst(buf):
"""Import content from buf and return a Hy AST.""" """Import content from buf and return a Hy AST."""
return tokenize(buf + "\n") return HyExpression([HySymbol("do")] + tokenize(buf + "\n"))
def import_file_to_hst(fpath): def import_file_to_hst(fpath):
@ -142,7 +143,27 @@ def import_buffer_to_module(module_name, buf):
return mod return mod
def hy_eval(hytree, namespace, module_name, ast_callback=None): def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
"""``eval`` evaluates a quoted expression and returns the value. The optional
second and third arguments specify the dictionary of globals to use and the
module name. The globals dictionary defaults to ``(local)`` and the module
name defaults to the name of the current module.
=> (eval '(print "Hello World"))
"Hello World"
If you want to evaluate a string, use ``read-str`` to convert it to a
form first:
=> (eval (read-str "(+ 1 1)"))
2"""
if namespace is None:
frame = inspect.stack()[1][0]
namespace = inspect.getargvalues(frame).locals
if module_name is None:
m = inspect.getmodule(inspect.stack()[1][0])
module_name = '__eval__' if m is None else m.__name__
foo = HyObject() foo = HyObject()
foo.start_line = 0 foo.start_line = 0
foo.end_line = 0 foo.end_line = 0

View File

@ -3,7 +3,7 @@
# license. See the LICENSE. # license. See the LICENSE.
from inspect import getargspec, formatargspec from inspect import getargspec, formatargspec
from hy.models import replace_hy_obj, wrap_value, HyExpression, HyString from hy.models import replace_hy_obj, HyExpression, HyString
from hy.errors import HyTypeError, HyMacroExpansionError from hy.errors import HyTypeError, HyMacroExpansionError
@ -196,7 +196,7 @@ def macroexpand_1(tree, compiler):
raise HyMacroExpansionError(tree, msg) raise HyMacroExpansionError(tree, msg)
try: try:
obj = wrap_value(m(*ntree[1:], **opts)) obj = m(*ntree[1:], **opts)
except HyTypeError as e: except HyTypeError as e:
if e.expression is None: if e.expression is None:
e.expression = tree e.expression = tree
@ -225,4 +225,4 @@ def tag_macroexpand(tag, tree, compiler):
) )
expr = tag_macro(tree) expr = tag_macro(tree)
return replace_hy_obj(wrap_value(expr), tree) return replace_hy_obj(expr, tree)

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from hy._compat import PY3, str_type, bytes_type, long_type, string_types from hy._compat import PY3, str_type, bytes_type, long_type, string_types
from fractions import Fraction
class HyObject(object): class HyObject(object):
@ -229,6 +230,8 @@ class HyExpression(HyList):
return "(%s)" % (" ".join([repr(x) for x in self])) return "(%s)" % (" ".join([repr(x) for x in self]))
_wrappers[HyExpression] = lambda e: HyExpression(wrap_value(x) for x in e) _wrappers[HyExpression] = lambda e: HyExpression(wrap_value(x) for x in e)
_wrappers[Fraction] = lambda e: HyExpression(
[HySymbol("fraction"), wrap_value(e.numerator), wrap_value(e.denominator)])
class HySet(HyList): class HySet(HyList):

View File

@ -7,9 +7,9 @@ from __future__ import unicode_literals
from hy import HyString from hy import HyString
from hy.models import HyObject from hy.models import HyObject
from hy.compiler import hy_compile from hy.compiler import hy_compile
from hy.importer import import_buffer_to_hst
from hy.errors import HyCompileError, HyTypeError from hy.errors import HyCompileError, HyTypeError
from hy.lex.exceptions import LexException from hy.lex.exceptions import LexException
from hy.lex import tokenize
from hy._compat import PY3 from hy._compat import PY3
import ast import ast
@ -25,12 +25,12 @@ def _ast_spotcheck(arg, root, secondary):
def can_compile(expr): def can_compile(expr):
return hy_compile(tokenize(expr), "__main__") return hy_compile(import_buffer_to_hst(expr), "__main__")
def cant_compile(expr): def cant_compile(expr):
try: try:
hy_compile(tokenize(expr), "__main__") hy_compile(import_buffer_to_hst(expr), "__main__")
assert False assert False
except HyTypeError as e: except HyTypeError as e:
# Anything that can't be compiled should raise a user friendly # Anything that can't be compiled should raise a user friendly
@ -48,8 +48,10 @@ def cant_compile(expr):
def test_ast_bad_type(): def test_ast_bad_type():
"Make sure AST breakage can happen" "Make sure AST breakage can happen"
class C:
pass
try: try:
hy_compile("foo", "__main__") hy_compile(C(), "__main__")
assert True is False assert True is False
except HyCompileError: except HyCompileError:
pass pass
@ -252,7 +254,7 @@ def test_ast_require():
def test_ast_no_pointless_imports(): def test_ast_no_pointless_imports():
def contains_import_from(code): def contains_import_from(code):
return any([isinstance(node, ast.ImportFrom) return any([isinstance(node, ast.ImportFrom)
for node in hy_compile(tokenize(code), "__main__").body]) for node in can_compile(code).body])
# `reduce` is a builtin in Python 2, but not Python 3. # `reduce` is a builtin in Python 2, but not Python 3.
# The version of `map` that returns an iterator is a builtin in # The version of `map` that returns an iterator is a builtin in
# Python 3, but not Python 2. # Python 3, but not Python 2.
@ -460,7 +462,7 @@ def test_ast_unicode_strings():
hy_s.start_line = hy_s.end_line = 0 hy_s.start_line = hy_s.end_line = 0
hy_s.start_column = hy_s.end_column = 0 hy_s.start_column = hy_s.end_column = 0
code = hy_compile([hy_s], "__main__") code = hy_compile(hy_s, "__main__")
# code == ast.Module(body=[ast.Expr(value=ast.Str(s=xxx))]) # code == ast.Module(body=[ast.Expr(value=ast.Str(s=xxx))])
return code.body[0].value.s return code.body[0].value.s
@ -471,7 +473,7 @@ def test_ast_unicode_strings():
def test_ast_unicode_vs_bytes(): def test_ast_unicode_vs_bytes():
def f(x): return hy_compile(tokenize(x), "__main__").body[0].value.s def f(x): return can_compile(x).body[0].value.s
assert f('"hello"') == u"hello" assert f('"hello"') == u"hello"
assert type(f('"hello"')) is (str if PY3 else unicode) # noqa assert type(f('"hello"')) is (str if PY3 else unicode) # noqa
assert f('b"hello"') == (eval('b"hello"') if PY3 else "hello") assert f('b"hello"') == (eval('b"hello"') if PY3 else "hello")

View File

@ -5,6 +5,7 @@
(import [tests.resources [kwtest function-with-a-dash]] (import [tests.resources [kwtest function-with-a-dash]]
[os.path [exists isdir isfile]] [os.path [exists isdir isfile]]
[sys :as systest] [sys :as systest]
re
[operator [or_]] [operator [or_]]
[hy.errors [HyTypeError]] [hy.errors [HyTypeError]]
pytest) pytest)
@ -1087,10 +1088,42 @@
(defn test-eval-failure [] (defn test-eval-failure []
"NATIVE: test eval failure modes" "NATIVE: test eval failure modes"
; yo dawg ; yo dawg
(try (eval '(eval)) (except [e HyTypeError]) (else (assert False))) (try (eval '(eval)) (except [e TypeError]) (else (assert False)))
(try (eval '(eval "snafu")) (except [e HyTypeError]) (else (assert False))) (defclass C)
(try (eval (C)) (except [e TypeError]) (else (assert False)))
(try (eval 'False []) (except [e HyTypeError]) (else (assert False))) (try (eval 'False []) (except [e HyTypeError]) (else (assert False)))
(try (eval 'False {} 1) (except [e HyTypeError]) (else (assert False)))) (try (eval 'False {} 1) (except [e TypeError]) (else (assert False))))
(defn test-eval-quasiquote []
; https://github.com/hylang/hy/issues/1174
(for [x [
None False True
5 5.1
1/2
5j 5.1j 2+1j 1.2+3.4j
"" b""
"apple bloom" b"apple bloom" "⚘" b"\x00"
:mykeyword
[] #{} {}
[1 2 3] #{1 2 3} {"a" 1 "b" 2}]]
(assert (= (eval `(identity ~x)) x))
(assert (= (eval x) x)))
; Tuples wrap to HyLists, not HyExpressions.
(assert (= (eval (,)) []))
(assert (= (eval (, 1 2 3)) [1 2 3]))
(assert (= (eval `(+ "a" ~(+ "b" "c"))) "abc"))
(setv l ["a" "b"])
(setv n 1)
(assert (= (eval `(get ~l ~n) "b")))
(setv d {"a" 1 "b" 2})
(setv k "b")
(assert (= (eval `(get ~d ~k)) 2)))
(defn test-import-syntax [] (defn test-import-syntax []
@ -1366,7 +1399,9 @@
(assert (= (disassemble '(do (leaky) (leaky) (macros))) (assert (= (disassemble '(do (leaky) (leaky) (macros)))
"Module(\n body=[\n Expr(value=Call(func=Name(id='leaky'), args=[], keywords=[], starargs=None, kwargs=None)),\n Expr(value=Call(func=Name(id='leaky'), args=[], keywords=[], starargs=None, kwargs=None)),\n Expr(value=Call(func=Name(id='macros'), args=[], keywords=[], starargs=None, kwargs=None))])"))) "Module(\n body=[\n Expr(value=Call(func=Name(id='leaky'), args=[], keywords=[], starargs=None, kwargs=None)),\n Expr(value=Call(func=Name(id='leaky'), args=[], keywords=[], starargs=None, kwargs=None)),\n Expr(value=Call(func=Name(id='macros'), args=[], keywords=[], starargs=None, kwargs=None))])")))
(assert (= (disassemble '(do (leaky) (leaky) (macros)) True) (assert (= (disassemble '(do (leaky) (leaky) (macros)) True)
"leaky()\nleaky()\nmacros()"))) "leaky()\nleaky()\nmacros()"))
(assert (= (re.sub r"[L() ]" "" (disassemble `(+ ~(+ 1 1) 40) True))
"2+40")))
(defn test-attribute-access [] (defn test-attribute-access []

View File

@ -84,6 +84,14 @@
"NATIVE: test macro calling a plain function" "NATIVE: test macro calling a plain function"
(assert (= 3 (bar 1 2)))) (assert (= 3 (bar 1 2))))
(defn test-optional-and-apply-in-macro []
; https://github.com/hylang/hy/issues/1154
(defn f [&rest args]
(+ "f:" (repr args)))
(defmacro mac [&optional x]
`(apply f [~x]))
(assert (= (mac) "f:(None,)")))
(defn test-midtree-yield [] (defn test-midtree-yield []
"NATIVE: test yielding with a returnable" "NATIVE: test yielding with a returnable"
(defn kruft [] (yield) (+ 1 1))) (defn kruft [] (yield) (+ 1 1)))