diff --git a/NEWS.rst b/NEWS.rst index d4255ef..2ec6b16 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -13,6 +13,11 @@ New Features shorthand for `(get obj :key)`, and they accept a default value as a second argument. +Bug Fixes +------------------------------ +* Fixed bugs in the handling of unpacking forms in method calls and + attribute access. + 0.15.0 ============================== diff --git a/hy/compiler.py b/hy/compiler.py index 73395cf..7dee28c 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -6,8 +6,8 @@ from hy.models import (HyObject, HyExpression, HyKeyword, HyInteger, HyComplex, HyString, HyBytes, HySymbol, HyFloat, HyList, HySet, HyDict, HySequence, wrap_value) -from hy.model_patterns import (FORM, SYM, STR, sym, brackets, whole, notpexpr, - dolike, pexpr, times, Tag, tag) +from hy.model_patterns import (FORM, SYM, KEYWORD, STR, sym, brackets, whole, + notpexpr, dolike, pexpr, times, Tag, tag, unpack) from funcparserlib.parser import some, many, oneplus, maybe, NoParseError from hy.errors import HyCompileError, HyTypeError @@ -1550,7 +1550,8 @@ class HyASTCompiler(object): raise HyTypeError( expr, "empty expressions are not allowed at top level") - root = expr[0] + args = list(expr) + root = args.pop(0) func = None if isinstance(root, HySymbol): @@ -1558,11 +1559,12 @@ class HyASTCompiler(object): # First check if `root` is a special operator, unless it has an # `unpack-iterable` in it, since Python's operators (`+`, # 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) if (sroot in _special_form_compilers or sroot in _bad_roots) and ( - sroot == mangle(",") or - not any(is_unpack("iterable", x) for x in expr[1:])): + sroot in (mangle(","), mangle(".")) or + not any(is_unpack("iterable", x) for x in args)): if sroot in _bad_roots: raise HyTypeError( expr, @@ -1571,19 +1573,19 @@ class HyASTCompiler(object): # pattern-match the arguments. build_method, pattern = _special_form_compilers[sroot] try: - parse_tree = pattern.parse(expr[1:]) + parse_tree = pattern.parse(args) except NoParseError as e: raise HyTypeError( expr[min(e.state.pos + 1, len(expr) - 1)], "parse error for special form '{}': {}".format( - expr[0], + root, e.msg.replace("", "end of form"))) return Result() + build_method( self, expr, unmangle(sroot), *parse_tree) if root.startswith("."): # (.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 # in the chain of attributes) @@ -1592,25 +1594,22 @@ class HyASTCompiler(object): # Get the object we're calling the method on # (extracted with the attribute access DSL) - i = 1 - if len(expr) != 2: - # If the expression has only one object, - # always use that as the callee. - # Otherwise, hunt for the first thing that - # isn't a keyword argument or its value. - while i < len(expr): - if isinstance(expr[i], HyKeyword): - # Skip the keyword argument and its value. - i += 1 - else: - # Use expr[i]. - break - i += 1 - else: - raise HyTypeError(expr, - "attribute access requires object") + # Skip past keywords and their arguments. + try: + kws, obj, rest = ( + many(KEYWORD + FORM | unpack("mapping")) + + FORM + + many(FORM)).parse(args) + except NoParseError: + raise HyTypeError( + expr, "attribute access requires object") + # Reconstruct `args` to exclude `obj`. + args = [x for p in kws for x in p] + list(rest) + if is_unpack("iterable", obj): + raise HyTypeError( + obj, "can't call a method on an unpacking form") func = self.compile(HyExpression( - [HySymbol(".").replace(root), expr.pop(i)] + + [HySymbol(".").replace(root), obj] + attrs)) # And get the method @@ -1627,7 +1626,7 @@ class HyASTCompiler(object): with_kwargs = root not in ( "type", "HyKeyword", "keyword", "name", "keyword?", "identity") 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( expr, func=func.expr, args=args, keywords=keywords, diff --git a/hy/model_patterns.py b/hy/model_patterns.py index 1d30ccf..5291768 100644 --- a/hy/model_patterns.py +++ b/hy/model_patterns.py @@ -15,6 +15,7 @@ from math import isinf FORM = some(lambda _: True) SYM = some(lambda x: isinstance(x, HySymbol)) +KEYWORD = some(lambda x: isinstance(x, HyKeyword)) STR = some(lambda x: isinstance(x, HyString)) def sym(wanted): @@ -57,6 +58,14 @@ def notpexpr(*disallowed_heads): isinstance(x[0], HySymbol) and 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): """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.""" diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index 59232f3..322f78c 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -72,6 +72,17 @@ def test_empty_expr(): 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(): "Make sure AST can't compile invalid if*" cant_compile("(if*)")