Fix handling of unpacking in method calls and attribute lookups

This commit is contained in:
Kodi Arfer 2018-07-24 09:45:00 -07:00
parent 0f85331c81
commit 081a710b0f
4 changed files with 52 additions and 28 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

@ -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
@ -1550,7 +1550,8 @@ class HyASTCompiler(object):
raise HyTypeError( raise HyTypeError(
expr, "empty expressions are not allowed at top level") expr, "empty expressions are not allowed at top level")
root = expr[0] args = list(expr)
root = args.pop(0)
func = None func = None
if isinstance(root, HySymbol): if isinstance(root, HySymbol):
@ -1558,11 +1559,12 @@ class HyASTCompiler(object):
# First check if `root` 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
# `.` forms here so the user gets a better error message.
sroot = ast_str(root) sroot = ast_str(root)
if (sroot in _special_form_compilers or sroot in _bad_roots) and ( if (sroot in _special_form_compilers or sroot in _bad_roots) and (
sroot == mangle(",") or sroot in (mangle(","), mangle(".")) or
not any(is_unpack("iterable", x) for x in expr[1:])): not any(is_unpack("iterable", x) for x in args)):
if sroot in _bad_roots: if sroot in _bad_roots:
raise HyTypeError( raise HyTypeError(
expr, expr,
@ -1571,19 +1573,19 @@ class HyASTCompiler(object):
# pattern-match the arguments. # pattern-match the arguments.
build_method, pattern = _special_form_compilers[sroot] build_method, pattern = _special_form_compilers[sroot]
try: try:
parse_tree = pattern.parse(expr[1:]) parse_tree = pattern.parse(args)
except NoParseError as e: except NoParseError as e:
raise HyTypeError( raise HyTypeError(
expr[min(e.state.pos + 1, len(expr) - 1)], expr[min(e.state.pos + 1, len(expr) - 1)],
"parse error for special form '{}': {}".format( "parse error for special form '{}': {}".format(
expr[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, expr, unmangle(sroot), *parse_tree) self, expr, unmangle(sroot), *parse_tree)
if root.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)
@ -1592,25 +1594,22 @@ class HyASTCompiler(object):
# 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(expr) != 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(expr): except NoParseError:
if isinstance(expr[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 expr[i]. if is_unpack("iterable", obj):
break raise HyTypeError(
i += 1 obj, "can't call a method on an unpacking form")
else:
raise HyTypeError(expr,
"attribute access requires object")
func = self.compile(HyExpression( func = self.compile(HyExpression(
[HySymbol(".").replace(root), expr.pop(i)] + [HySymbol(".").replace(root), obj] +
attrs)) attrs))
# And get the method # And get the method
@ -1627,7 +1626,7 @@ class HyASTCompiler(object):
with_kwargs = root 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(
expr[1:], with_kwargs, oldpy_unpack=True) args, with_kwargs, oldpy_unpack=True)
return func + ret + asty.Call( return func + ret + asty.Call(
expr, func=func.expr, args=args, keywords=keywords, expr, func=func.expr, args=args, keywords=keywords,

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

@ -72,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*)")