diff --git a/NEWS.rst b/NEWS.rst
index d4255efa..2ec6b165 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/docs/language/api.rst b/docs/language/api.rst
index cb048a35..ab0c0418 100644
--- a/docs/language/api.rst
+++ b/docs/language/api.rst
@@ -458,7 +458,7 @@ arguments constitute the body of the function.
     (defn name [params] bodyform1 bodyform2...)
 
 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
 than one, they can only appear in the given order (so all `&optional`
diff --git a/hy/compiler.py b/hy/compiler.py
index 4f4f694e..7dee28c8 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
 
@@ -1539,98 +1539,97 @@ class HyASTCompiler(object):
                 else Result())
 
     @builds_model(HyExpression)
-    def compile_expression(self, expression):
+    def compile_expression(self, expr):
         # Perform macro expansions
-        expression = macroexpand(expression, self)
-        if not isinstance(expression, HyExpression):
+        expr = macroexpand(expr, self)
+        if not isinstance(expr, HyExpression):
             # Go through compile again if the type changed.
-            return self.compile(expression)
+            return self.compile(expr)
 
-        if not expression:
+        if not expr:
             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
 
-        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 (`+`,
             # etc.) can't unpack. An exception to this exception is that
-            # tuple literals (`,`) can unpack.
-            sfn = ast_str(fn)
-            if (sfn in _special_form_compilers or sfn in _bad_roots) and (
-                    sfn == mangle(",") or
-                    not any(is_unpack("iterable", x) for x in expression[1:])):
-                if sfn in _bad_roots:
+            # 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 in (mangle(","), mangle(".")) or
+                    not any(is_unpack("iterable", x) for x in args)):
+                if sroot in _bad_roots:
                     raise HyTypeError(
-                        expression,
-                        "The special form '{}' is not allowed here".format(fn))
-                # `sfn` is a special operator. Get the build method and
+                        expr,
+                        "The special form '{}' is not allowed here".format(root))
+                # `sroot` is a special operator. Get the build method and
                 # pattern-match the arguments.
-                build_method, pattern = _special_form_compilers[sfn]
+                build_method, pattern = _special_form_compilers[sroot]
                 try:
-                    parse_tree = pattern.parse(expression[1:])
+                    parse_tree = pattern.parse(args)
                 except NoParseError as e:
                     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(
-                            expression[0],
+                            root,
                             e.msg.replace("<EOF>", "end of form")))
                 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()
-                # (.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)
-                attrs = [HySymbol(a).replace(fn) for a in fn.split(".")[1:]]
-                fn = attrs.pop()
+                attrs = [HySymbol(a).replace(root) for a in root.split(".")[1:]]
+                root = attrs.pop()
 
                 # Get the object we're calling the method on
                 # (extracted with the attribute access DSL)
-                i = 1
-                if len(expression) != 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(expression):
-                        if isinstance(expression[i], HyKeyword):
-                            # Skip the keyword argument and its value.
-                            i += 1
-                        else:
-                            # Use expression[i].
-                            break
-                        i += 1
-                    else:
-                        raise HyTypeError(expression,
-                                          "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(fn), expression.pop(i)] +
+                    [HySymbol(".").replace(root), obj] +
                     attrs))
 
                 # And get the method
-                func += asty.Attribute(fn,
+                func += asty.Attribute(root,
                                        value=func.force_expr,
-                                       attr=ast_str(fn),
+                                       attr=ast_str(root),
                                        ctx=ast.Load())
 
         if not func:
-            func = self.compile(fn)
+            func = self.compile(root)
 
         # An exception for pulling together keyword args is if we're doing
         # a typecheck, eg (type :foo)
-        with_kwargs = fn not in (
+        with_kwargs = root not in (
             "type", "HyKeyword", "keyword", "name", "keyword?", "identity")
         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(
-            expression, func=func.expr, args=args, keywords=keywords,
+            expr, func=func.expr, args=args, keywords=keywords,
             starargs=oldpy_star, kwargs=oldpy_kw)
 
     @builds_model(HyInteger, HyFloat, HyComplex)
diff --git a/hy/model_patterns.py b/hy/model_patterns.py
index 1d30ccfa..52917683 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."""

`(.foo #* bar)` raises a compile-time RecursionError
Maybe we'll decide not to implement this construct. Even then, we should avoid the infinite recursion.
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
index a5f42af1..322f78c4 100644
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -16,7 +16,6 @@ from hy._compat import PY3
 import ast
 import pytest
 
-
 def _ast_spotcheck(arg, root, secondary):
     if "." in arg:
         local, full = arg.split(".", 1)
@@ -73,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*)")
@@ -290,7 +300,7 @@ def test_ast_require():
 
 
 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."""
     cant_compile("(import [spam [foo.bar]])")
     cant_compile("(require [spam [foo.bar]])")

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.7
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
appdirs==1.4.4
args==0.1.0
astor==0.8.1
Babel==2.14.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
clint==0.5.1
coverage==7.2.7
distlib==0.3.9
docutils==0.17.1
exceptiongroup==1.2.2
filelock==3.12.2
flake8==5.0.4
funcparserlib==1.0.1
-e git+https://github.com/hylang/hy.git@5bfd4592f6c5df063afbcd9c67ea40877f4ed52e#egg=hy
idna==3.10
imagesize==1.4.1
importlib-metadata==4.2.0
iniconfig==2.0.0
Jinja2==3.1.6
MarkupSafe==2.1.5
mccabe==0.7.0
packaging==24.0
platformdirs==2.6.2
pluggy==1.2.0
py==1.11.0
pycodestyle==2.9.1
pyflakes==2.5.0
Pygments==2.17.2
pytest==7.4.4
pytz==2025.2
requests==2.31.0
rply==0.7.8
six==1.17.0
snowballstemmer==2.2.0
Sphinx==4.3.2
sphinx-rtd-theme==1.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==2.0.1
tox==3.28.0
typing_extensions==4.7.1
urllib3==2.0.7
virtualenv==20.16.2
zipp==3.15.0

name: hy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - appdirs==1.4.4
      - args==0.1.0
      - astor==0.8.1
      - babel==2.14.0
      - charset-normalizer==3.4.1
      - clint==0.5.1
      - coverage==7.2.7
      - distlib==0.3.9
      - docutils==0.17.1
      - exceptiongroup==1.2.2
      - filelock==3.12.2
      - flake8==5.0.4
      - funcparserlib==1.0.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - jinja2==3.1.6
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - packaging==24.0
      - platformdirs==2.6.2
      - pluggy==1.2.0
      - py==1.11.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pytest==7.4.4
      - pytz==2025.2
      - requests==2.31.0
      - rply==0.7.8
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==4.3.2
      - sphinx-rtd-theme==1.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==2.0.1
      - tox==3.28.0
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - virtualenv==20.16.2
      - zipp==3.15.0
prefix: /opt/conda/envs/hy

tests/compilers/test_ast.py::test_dot_unpacking
tests/compilers/test_ast.py::test_ast_bad_type tests/compilers/test_ast.py::test_empty_expr tests/compilers/test_ast.py::test_ast_bad_if tests/compilers/test_ast.py::test_ast_valid_if tests/compilers/test_ast.py::test_ast_valid_unary_op tests/compilers/test_ast.py::test_ast_invalid_unary_op tests/compilers/test_ast.py::test_ast_bad_while tests/compilers/test_ast.py::test_ast_good_do tests/compilers/test_ast.py::test_ast_good_raise tests/compilers/test_ast.py::test_ast_raise_from tests/compilers/test_ast.py::test_ast_bad_raise tests/compilers/test_ast.py::test_ast_good_try tests/compilers/test_ast.py::test_ast_bad_try tests/compilers/test_ast.py::test_ast_good_except tests/compilers/test_ast.py::test_ast_bad_except tests/compilers/test_ast.py::test_ast_good_assert tests/compilers/test_ast.py::test_ast_bad_assert tests/compilers/test_ast.py::test_ast_good_global tests/compilers/test_ast.py::test_ast_bad_global tests/compilers/test_ast.py::test_ast_good_nonlocal tests/compilers/test_ast.py::test_ast_bad_nonlocal tests/compilers/test_ast.py::test_ast_good_defclass tests/compilers/test_ast.py::test_ast_good_defclass_with_metaclass tests/compilers/test_ast.py::test_ast_bad_defclass tests/compilers/test_ast.py::test_ast_good_lambda tests/compilers/test_ast.py::test_ast_bad_lambda tests/compilers/test_ast.py::test_ast_good_yield tests/compilers/test_ast.py::test_ast_bad_yield tests/compilers/test_ast.py::test_ast_import_mangle_dotted tests/compilers/test_ast.py::test_ast_good_import_from tests/compilers/test_ast.py::test_ast_require tests/compilers/test_ast.py::test_ast_import_require_dotted tests/compilers/test_ast.py::test_ast_no_pointless_imports tests/compilers/test_ast.py::test_ast_good_get tests/compilers/test_ast.py::test_ast_bad_get tests/compilers/test_ast.py::test_ast_good_cut tests/compilers/test_ast.py::test_ast_bad_cut tests/compilers/test_ast.py::test_ast_good_take tests/compilers/test_ast.py::test_ast_good_drop tests/compilers/test_ast.py::test_ast_good_assoc tests/compilers/test_ast.py::test_ast_bad_assoc tests/compilers/test_ast.py::test_ast_bad_with tests/compilers/test_ast.py::test_ast_valid_while tests/compilers/test_ast.py::test_ast_valid_for tests/compilers/test_ast.py::test_nullary_break_continue tests/compilers/test_ast.py::test_ast_expression_basics tests/compilers/test_ast.py::test_ast_anon_fns_basics tests/compilers/test_ast.py::test_ast_non_decoratable tests/compilers/test_ast.py::test_ast_lambda_lists tests/compilers/test_ast.py::test_ast_print tests/compilers/test_ast.py::test_ast_tuple tests/compilers/test_ast.py::test_lambda_list_keywords_rest tests/compilers/test_ast.py::test_lambda_list_keywords_kwargs tests/compilers/test_ast.py::test_lambda_list_keywords_kwonly tests/compilers/test_ast.py::test_lambda_list_keywords_mixed tests/compilers/test_ast.py::test_missing_keyword_argument_value tests/compilers/test_ast.py::test_ast_unicode_strings tests/compilers/test_ast.py::test_ast_unicode_vs_bytes tests/compilers/test_ast.py::test_ast_bracket_string tests/compilers/test_ast.py::test_compile_error tests/compilers/test_ast.py::test_for_compile_error tests/compilers/test_ast.py::test_attribute_access tests/compilers/test_ast.py::test_attribute_empty tests/compilers/test_ast.py::test_bad_setv tests/compilers/test_ast.py::test_defn tests/compilers/test_ast.py::test_setv_builtins tests/compilers/test_ast.py::test_top_level_unquote tests/compilers/test_ast.py::test_lots_of_comment_lines tests/compilers/test_ast.py::test_exec_star tests/compilers/test_ast.py::test_compiler_macro_tag_try tests/compilers/test_ast.py::test_ast_good_yield_from tests/compilers/test_ast.py::test_ast_bad_yield_from tests/compilers/test_ast.py::test_eval_generator_with_return
hy/compiler.py NEWS.rst docs/language/api.rst hy/model_patterns.py
hy/compiler.py NEWS.rst docs/language/api.rst hy/model_patterns.py