Repository: quantumlib/Cirq

Issue:
_resolve_parameters_ changes are not backwards-compatible
**Description of the issue**

External code that defined a `_resolve_parameters_` magic method for Cirq 0.9 is broken by #3546, which changed the function signature for that method in a non-backwards-compatible way.

**How to reproduce the issue**

Construct a parameterized circuit in Cirq v0.10.0-dev, then attempt to run it in the latest version of qsim.

**Cirq version**

Occurs when upgrading from v0.9.* to v0.10.0-dev


Discussion:
Since `_resolve_parameters_` is a private method (has a leading underscore), we can just make `cirq.resolve_parameters` backwards-compatible. This requires fixing some incorrect behavior in qsim (https://github.com/quantumlib/qsim/pull/278), but is _much_ cleaner to implement.

Resolving patch:
diff --git a/cirq/protocols/resolve_parameters.py b/cirq/protocols/resolve_parameters.py
--- a/cirq/protocols/resolve_parameters.py
+++ b/cirq/protocols/resolve_parameters.py
@@ -142,6 +142,8 @@ def resolve_parameters(
 
     Raises:
         RecursionError if the ParamResolver detects a loop in resolution.
+        ValueError if `recursive=False` is passed to an external
+            _resolve_parameters_ method with no `recursive` parameter.
     """
     if not param_resolver:
         return val
@@ -154,7 +156,19 @@ def resolve_parameters(
         return type(val)(resolve_parameters(e, param_resolver, recursive) for e in val)
 
     getter = getattr(val, '_resolve_parameters_', None)
-    result = NotImplemented if getter is None else getter(param_resolver, recursive)
+    if getter is None:
+        result = NotImplemented
+    # Backwards-compatibility for external _resolve_parameters_ methods.
+    # TODO: remove in Cirq v0.11.0
+    elif 'recursive' in getter.__code__.co_varnames:
+        result = getter(param_resolver, recursive)
+    else:
+        if not recursive:
+            raise ValueError(
+                f'Object type {type(val)} does not support non-recursive parameter resolution.'
+                ' This must be updated before Cirq v0.11.'
+            )
+        result = getter(param_resolver)
 
     if result is not NotImplemented:
         return result


Test patch:
diff --git a/cirq/protocols/resolve_parameters_test.py b/cirq/protocols/resolve_parameters_test.py
--- a/cirq/protocols/resolve_parameters_test.py
+++ b/cirq/protocols/resolve_parameters_test.py
@@ -143,3 +143,22 @@ def test_recursive_resolve():
     assert cirq.resolve_parameters_once(a, resolver) == b
     with pytest.raises(RecursionError):
         _ = cirq.resolve_parameters(a, resolver)
+
+
+# TODO: remove in Cirq v0.11
+def test_backwards_compatible():
+    a, b, c = [sympy.Symbol(l) for l in 'abc']
+    resolver = cirq.ParamResolver({a: b + 3, b: c + 2, c: 1})
+
+    class SymbolSum:
+        def __init__(self, *syms):
+            self.syms = [*syms]
+
+        def _resolve_parameters_(self, pr):
+            return sum([cirq.resolve_parameters(s, pr) for s in self.syms])
+
+    ssum = SymbolSum(a, b, c)
+    assert cirq.resolve_parameters(ssum, resolver) == 10
+    assert cirq.resolve_parameters(ssum, resolver, recursive=True) == 10
+    with pytest.raises(ValueError):
+        _ = cirq.resolve_parameters(ssum, resolver, recursive=False)