Merge pull request #1661 from Kodiologist/expr-compile
Fix handling of unpacking in method calls and attribute lookups
This commit is contained in:
commit
0a384e7744
5
NEWS.rst
5
NEWS.rst
@ -13,6 +13,11 @@ New Features
|
|||||||
shorthand for `(get obj :key)`, and they accept a default value
|
shorthand for `(get obj :key)`, and they accept a default value
|
||||||
as a second argument.
|
as a second argument.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
------------------------------
|
||||||
|
* Fixed bugs in the handling of unpacking forms in method calls and
|
||||||
|
attribute access.
|
||||||
|
|
||||||
0.15.0
|
0.15.0
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
@ -458,7 +458,7 @@ arguments constitute the body of the function.
|
|||||||
(defn name [params] bodyform1 bodyform2...)
|
(defn name [params] bodyform1 bodyform2...)
|
||||||
|
|
||||||
If there at least two body forms, and the first of them is a string literal,
|
If there at least two body forms, and the first of them is a string literal,
|
||||||
this string becomes the :ref:`docstring <py:docstring>` of the function.
|
this string becomes the :term:`py:docstring` of the function.
|
||||||
|
|
||||||
Parameters may be prefixed with the following special symbols. If you use more
|
Parameters may be prefixed with the following special symbols. If you use more
|
||||||
than one, they can only appear in the given order (so all `&optional`
|
than one, they can only appear in the given order (so all `&optional`
|
||||||
|
105
hy/compiler.py
105
hy/compiler.py
@ -6,8 +6,8 @@
|
|||||||
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, HySequence, wrap_value)
|
HyDict, HySequence, wrap_value)
|
||||||
from hy.model_patterns import (FORM, SYM, STR, sym, brackets, whole, notpexpr,
|
from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole,
|
||||||
dolike, pexpr, times, Tag, tag)
|
notpexpr, dolike, pexpr, times, Tag, tag, unpack)
|
||||||
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
|
from funcparserlib.parser import some, many, oneplus, maybe, NoParseError
|
||||||
from hy.errors import HyCompileError, HyTypeError
|
from hy.errors import HyCompileError, HyTypeError
|
||||||
|
|
||||||
@ -1539,98 +1539,97 @@ class HyASTCompiler(object):
|
|||||||
else Result())
|
else Result())
|
||||||
|
|
||||||
@builds_model(HyExpression)
|
@builds_model(HyExpression)
|
||||||
def compile_expression(self, expression):
|
def compile_expression(self, expr):
|
||||||
# Perform macro expansions
|
# Perform macro expansions
|
||||||
expression = macroexpand(expression, self)
|
expr = macroexpand(expr, self)
|
||||||
if not isinstance(expression, HyExpression):
|
if not isinstance(expr, HyExpression):
|
||||||
# Go through compile again if the type changed.
|
# Go through compile again if the type changed.
|
||||||
return self.compile(expression)
|
return self.compile(expr)
|
||||||
|
|
||||||
if not expression:
|
if not expr:
|
||||||
raise HyTypeError(
|
raise HyTypeError(
|
||||||
expression, "empty expressions are not allowed at top level")
|
expr, "empty expressions are not allowed at top level")
|
||||||
|
|
||||||
fn = expression[0]
|
args = list(expr)
|
||||||
|
root = args.pop(0)
|
||||||
func = None
|
func = None
|
||||||
|
|
||||||
if isinstance(fn, HySymbol):
|
if isinstance(root, HySymbol):
|
||||||
|
|
||||||
# First check if `fn` is a special operator, unless it has an
|
# First check if `root` is a special operator, unless it has an
|
||||||
# `unpack-iterable` in it, since Python's operators (`+`,
|
# `unpack-iterable` in it, since Python's operators (`+`,
|
||||||
# etc.) can't unpack. An exception to this exception is that
|
# etc.) can't unpack. An exception to this exception is that
|
||||||
# tuple literals (`,`) can unpack.
|
# tuple literals (`,`) can unpack. Finally, we allow unpacking in
|
||||||
sfn = ast_str(fn)
|
# `.` forms here so the user gets a better error message.
|
||||||
if (sfn in _special_form_compilers or sfn in _bad_roots) and (
|
sroot = ast_str(root)
|
||||||
sfn == mangle(",") or
|
if (sroot in _special_form_compilers or sroot in _bad_roots) and (
|
||||||
not any(is_unpack("iterable", x) for x in expression[1:])):
|
sroot in (mangle(","), mangle(".")) or
|
||||||
if sfn in _bad_roots:
|
not any(is_unpack("iterable", x) for x in args)):
|
||||||
|
if sroot in _bad_roots:
|
||||||
raise HyTypeError(
|
raise HyTypeError(
|
||||||
expression,
|
expr,
|
||||||
"The special form '{}' is not allowed here".format(fn))
|
"The special form '{}' is not allowed here".format(root))
|
||||||
# `sfn` is a special operator. Get the build method and
|
# `sroot` is a special operator. Get the build method and
|
||||||
# pattern-match the arguments.
|
# pattern-match the arguments.
|
||||||
build_method, pattern = _special_form_compilers[sfn]
|
build_method, pattern = _special_form_compilers[sroot]
|
||||||
try:
|
try:
|
||||||
parse_tree = pattern.parse(expression[1:])
|
parse_tree = pattern.parse(args)
|
||||||
except NoParseError as e:
|
except NoParseError as e:
|
||||||
raise HyTypeError(
|
raise HyTypeError(
|
||||||
expression[min(e.state.pos + 1, len(expression) - 1)],
|
expr[min(e.state.pos + 1, len(expr) - 1)],
|
||||||
"parse error for special form '{}': {}".format(
|
"parse error for special form '{}': {}".format(
|
||||||
expression[0],
|
root,
|
||||||
e.msg.replace("<EOF>", "end of form")))
|
e.msg.replace("<EOF>", "end of form")))
|
||||||
return Result() + build_method(
|
return Result() + build_method(
|
||||||
self, expression, unmangle(sfn), *parse_tree)
|
self, expr, unmangle(sroot), *parse_tree)
|
||||||
|
|
||||||
if fn.startswith("."):
|
if root.startswith("."):
|
||||||
# (.split "test test") -> "test test".split()
|
# (.split "test test") -> "test test".split()
|
||||||
# (.a.b.c x) -> (.c (. x a b)) -> x.a.b.c()
|
# (.a.b.c x v1 v2) -> (.c (. x a b) v1 v2) -> x.a.b.c(v1, v2)
|
||||||
|
|
||||||
# Get the method name (the last named attribute
|
# Get the method name (the last named attribute
|
||||||
# in the chain of attributes)
|
# in the chain of attributes)
|
||||||
attrs = [HySymbol(a).replace(fn) for a in fn.split(".")[1:]]
|
attrs = [HySymbol(a).replace(root) for a in root.split(".")[1:]]
|
||||||
fn = attrs.pop()
|
root = attrs.pop()
|
||||||
|
|
||||||
# Get the object we're calling the method on
|
# Get the object we're calling the method on
|
||||||
# (extracted with the attribute access DSL)
|
# (extracted with the attribute access DSL)
|
||||||
i = 1
|
# Skip past keywords and their arguments.
|
||||||
if len(expression) != 2:
|
try:
|
||||||
# If the expression has only one object,
|
kws, obj, rest = (
|
||||||
# always use that as the callee.
|
many(KEYWORD + FORM | unpack("mapping")) +
|
||||||
# Otherwise, hunt for the first thing that
|
FORM +
|
||||||
# isn't a keyword argument or its value.
|
many(FORM)).parse(args)
|
||||||
while i < len(expression):
|
except NoParseError:
|
||||||
if isinstance(expression[i], HyKeyword):
|
raise HyTypeError(
|
||||||
# Skip the keyword argument and its value.
|
expr, "attribute access requires object")
|
||||||
i += 1
|
# Reconstruct `args` to exclude `obj`.
|
||||||
else:
|
args = [x for p in kws for x in p] + list(rest)
|
||||||
# Use expression[i].
|
if is_unpack("iterable", obj):
|
||||||
break
|
raise HyTypeError(
|
||||||
i += 1
|
obj, "can't call a method on an unpacking form")
|
||||||
else:
|
|
||||||
raise HyTypeError(expression,
|
|
||||||
"attribute access requires object")
|
|
||||||
func = self.compile(HyExpression(
|
func = self.compile(HyExpression(
|
||||||
[HySymbol(".").replace(fn), expression.pop(i)] +
|
[HySymbol(".").replace(root), obj] +
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
# And get the method
|
# And get the method
|
||||||
func += asty.Attribute(fn,
|
func += asty.Attribute(root,
|
||||||
value=func.force_expr,
|
value=func.force_expr,
|
||||||
attr=ast_str(fn),
|
attr=ast_str(root),
|
||||||
ctx=ast.Load())
|
ctx=ast.Load())
|
||||||
|
|
||||||
if not func:
|
if not func:
|
||||||
func = self.compile(fn)
|
func = self.compile(root)
|
||||||
|
|
||||||
# An exception for pulling together keyword args is if we're doing
|
# An exception for pulling together keyword args is if we're doing
|
||||||
# a typecheck, eg (type :foo)
|
# a typecheck, eg (type :foo)
|
||||||
with_kwargs = fn not in (
|
with_kwargs = root not in (
|
||||||
"type", "HyKeyword", "keyword", "name", "keyword?", "identity")
|
"type", "HyKeyword", "keyword", "name", "keyword?", "identity")
|
||||||
args, ret, keywords, oldpy_star, oldpy_kw = self._compile_collect(
|
args, ret, keywords, oldpy_star, oldpy_kw = self._compile_collect(
|
||||||
expression[1:], with_kwargs, oldpy_unpack=True)
|
args, with_kwargs, oldpy_unpack=True)
|
||||||
|
|
||||||
return func + ret + asty.Call(
|
return func + ret + asty.Call(
|
||||||
expression, func=func.expr, args=args, keywords=keywords,
|
expr, func=func.expr, args=args, keywords=keywords,
|
||||||
starargs=oldpy_star, kwargs=oldpy_kw)
|
starargs=oldpy_star, kwargs=oldpy_kw)
|
||||||
|
|
||||||
@builds_model(HyInteger, HyFloat, HyComplex)
|
@builds_model(HyInteger, HyFloat, HyComplex)
|
||||||
|
@ -15,6 +15,7 @@ from math import isinf
|
|||||||
|
|
||||||
FORM = some(lambda _: True)
|
FORM = some(lambda _: True)
|
||||||
SYM = some(lambda x: isinstance(x, HySymbol))
|
SYM = some(lambda x: isinstance(x, HySymbol))
|
||||||
|
KEYWORD = some(lambda x: isinstance(x, HyKeyword))
|
||||||
STR = some(lambda x: isinstance(x, HyString))
|
STR = some(lambda x: isinstance(x, HyString))
|
||||||
|
|
||||||
def sym(wanted):
|
def sym(wanted):
|
||||||
@ -57,6 +58,14 @@ def notpexpr(*disallowed_heads):
|
|||||||
isinstance(x[0], HySymbol) and
|
isinstance(x[0], HySymbol) and
|
||||||
x[0] in disallowed_heads))
|
x[0] in disallowed_heads))
|
||||||
|
|
||||||
|
def unpack(kind):
|
||||||
|
"Parse an unpacking form, returning it unchanged."
|
||||||
|
return some(lambda x:
|
||||||
|
isinstance(x, HyExpression)
|
||||||
|
and len(x) > 0
|
||||||
|
and isinstance(x[0], HySymbol)
|
||||||
|
and x[0] == "unpack-" + kind)
|
||||||
|
|
||||||
def times(lo, hi, parser):
|
def times(lo, hi, parser):
|
||||||
"""Parse `parser` several times (`lo` to `hi`) in a row. `hi` can be
|
"""Parse `parser` several times (`lo` to `hi`) in a row. `hi` can be
|
||||||
float('inf'). The result is a list no matter the number of instances."""
|
float('inf'). The result is a list no matter the number of instances."""
|
||||||
|
@ -16,7 +16,6 @@ from hy._compat import PY3
|
|||||||
import ast
|
import ast
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def _ast_spotcheck(arg, root, secondary):
|
def _ast_spotcheck(arg, root, secondary):
|
||||||
if "." in arg:
|
if "." in arg:
|
||||||
local, full = arg.split(".", 1)
|
local, full = arg.split(".", 1)
|
||||||
@ -73,6 +72,17 @@ def test_empty_expr():
|
|||||||
can_compile("(print '())")
|
can_compile("(print '())")
|
||||||
|
|
||||||
|
|
||||||
|
def test_dot_unpacking():
|
||||||
|
|
||||||
|
can_compile("(.meth obj #* args az)")
|
||||||
|
cant_compile("(.meth #* args az)")
|
||||||
|
cant_compile("(. foo #* bar baz)")
|
||||||
|
|
||||||
|
can_compile("(.meth obj #** args az)")
|
||||||
|
can_compile("(.meth #** args obj)")
|
||||||
|
cant_compile("(. foo #** bar baz)")
|
||||||
|
|
||||||
|
|
||||||
def test_ast_bad_if():
|
def test_ast_bad_if():
|
||||||
"Make sure AST can't compile invalid if*"
|
"Make sure AST can't compile invalid if*"
|
||||||
cant_compile("(if*)")
|
cant_compile("(if*)")
|
||||||
@ -290,7 +300,7 @@ def test_ast_require():
|
|||||||
|
|
||||||
|
|
||||||
def test_ast_import_require_dotted():
|
def test_ast_import_require_dotted():
|
||||||
"""As in Python, it should be a compile-type error to attempt to
|
"""As in Python, it should be a compile-time error to attempt to
|
||||||
import a dotted name."""
|
import a dotted name."""
|
||||||
cant_compile("(import [spam [foo.bar]])")
|
cant_compile("(import [spam [foo.bar]])")
|
||||||
cant_compile("(require [spam [foo.bar]])")
|
cant_compile("(require [spam [foo.bar]])")
|
||||||
|
Loading…
Reference in New Issue
Block a user