Skip to content

Commit b68a742

Browse files
authored
Add unify rule for Operation with Callable (#679)
* Unify Operation with Callable * grab fix from 656
1 parent 6b0fa80 commit b68a742

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

effectful/internals/unification.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,17 @@ def _unify_generic(typ, subtyp, subs: Substitutions) -> Substitutions:
539539
typing.get_origin(typ), collections.abc.Generator
540540
):
541541
return unify(typing.get_args(typ)[0], typing.get_args(subtyp)[0], subs)
542+
elif typing.get_origin(subtyp) is effectful.ops.types.Operation and not (
543+
isinstance(typing.get_origin(typ), type)
544+
and issubclass(typing.get_origin(typ), effectful.ops.types.Operation)
545+
):
546+
# An Operation[P, R] is a Callable[P, R] (gh #669): unify the pattern
547+
# against the operation's parameter/return signature. ``Operation``'s
548+
# args are (params, return) just like ``Callable``'s, except params is
549+
# a tuple (or ``...``) rather than a list.
550+
op_params, op_ret = typing.get_args(subtyp)
551+
callable_params = op_params if op_params is ... else list(op_params)
552+
return unify(typ, collections.abc.Callable[callable_params, op_ret], subs) # type: ignore
542553
elif typing.get_origin(typ) == typing.get_origin(subtyp):
543554
return unify(typing.get_args(typ), typing.get_args(subtyp), subs)
544555
elif types.get_original_bases(typing.get_origin(subtyp)):
@@ -556,6 +567,17 @@ def _unify_generic(typ, subtyp, subs: Substitutions) -> Substitutions:
556567
and issubclass(subtyp, typing.get_origin(typ))
557568
):
558569
return subs # implicit expansion to subtyp[Any]
570+
elif isinstance(typ, GenericAlias):
571+
# Special case for treating arrays as iterables of arrays
572+
try:
573+
import jax
574+
575+
if typing.get_origin(typ) is collections.abc.Iterable and issubclass(
576+
subtyp, jax.Array
577+
):
578+
return unify(typing.get_args(typ)[0], jax.Array, subs)
579+
except ImportError:
580+
pass
559581
raise TypeError(f"Cannot unify generic type {typ} with {subtyp} given {subs}.")
560582

561583

tests/test_internals_unification.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,3 +1967,69 @@ class Info(typing.TypedDict):
19671967

19681968
subs = unify(collections.abc.Mapping, Info)
19691969
assert subs == {}
1970+
1971+
1972+
def test_unify_jax_array_iterable():
1973+
import jax
1974+
1975+
subs = unify(collections.abc.Iterable[T], jax.Array)
1976+
assert subs == {T: jax.Array}
1977+
1978+
1979+
def test_unify_operation_callable():
1980+
"""An ``Operation[P, R]`` unifies as a ``Callable[P, R]`` (gh #669)."""
1981+
from effectful.ops.types import Operation
1982+
1983+
# TypeVar params bind to the operation's parameter/return types
1984+
assert unify(collections.abc.Callable[[T], V], Operation[[int], int]) == {
1985+
T: int,
1986+
V: int,
1987+
}
1988+
# a repeated TypeVar binds consistently
1989+
assert unify(collections.abc.Callable[[T], T], Operation[[int], int]) == {T: int}
1990+
# multiple parameters
1991+
assert unify(collections.abc.Callable[[T, U], V], Operation[[int, str], bool]) == {
1992+
T: int,
1993+
U: str,
1994+
V: bool,
1995+
}
1996+
# ``...`` parameters in the pattern ignore the operation's parameter types
1997+
assert unify(collections.abc.Callable[..., V], Operation[[int], int]) == {V: int}
1998+
# fully concrete: nothing to bind
1999+
assert unify(collections.abc.Callable[[int], int], Operation[[int], int]) == {}
2000+
# nested: an operation-valued argument
2001+
assert unify(
2002+
collections.abc.Callable[[T], list[V]], Operation[[int], list[str]]
2003+
) == {T: int, V: str}
2004+
2005+
2006+
def test_unify_operation_callable_failure():
2007+
"""An arity mismatch between the Callable pattern and the Operation fails."""
2008+
from effectful.ops.types import Operation
2009+
2010+
with pytest.raises(TypeError):
2011+
unify(collections.abc.Callable[[T, U], V], Operation[[int], int])
2012+
with pytest.raises(TypeError):
2013+
unify(collections.abc.Callable[[T], V], Operation[[int, str], bool])
2014+
2015+
2016+
def test_operation_unifies_with_callable_param_gh669():
2017+
"""An Operation passed where a ``Callable`` is expected infers correctly.
2018+
2019+
Regression test for gh #669: calling an operation whose parameter is typed
2020+
``Callable[[S], T]`` with another operation should unify and infer the return
2021+
type, rather than raising ``Cannot unify generic type ...``.
2022+
"""
2023+
from effectful.ops.semantics import typeof
2024+
from effectful.ops.types import NotHandled, Operation
2025+
2026+
@Operation.define
2027+
def f(x: int) -> int:
2028+
raise NotHandled
2029+
2030+
@Operation.define
2031+
def g[S, R](x: collections.abc.Callable[[S], R]) -> R:
2032+
raise NotHandled
2033+
2034+
term = g(f)
2035+
assert typeof(term) is int

0 commit comments

Comments
 (0)