Merge pull request #1661 from Kodiologist/expr-compile

Fix handling of unpacking in method calls and attribute lookups
This commit is contained in:
Kodi Arfer 2018-07-30 10:24:57 -07:00 committed by GitHub
commit 0a384e7744
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 56 deletions

View File

@ -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
============================== ==============================

View File

@ -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`

View File

@ -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)

View File

@ -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."""

View File

@ -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]])")