From 080b86303f0bc8002c67fd7af1f5ee3092a4e62e Mon Sep 17 00:00:00 2001 From: Eli Date: Wed, 13 Aug 2025 21:16:03 -0400 Subject: [PATCH 1/7] Remove tree dependency from core --- effectful/ops/semantics.py | 42 ++++++++++++++++-------- effectful/ops/syntax.py | 67 ++++++++++++++++++++------------------ tests/test_ops_syntax.py | 12 ++----- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/effectful/ops/semantics.py b/effectful/ops/semantics.py index 9fea27d6..b2e5d167 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -1,12 +1,12 @@ +import collections.abc import contextlib +import dataclasses import functools import types import typing from collections.abc import Callable from typing import Any -import tree - from effectful.ops.syntax import deffn, defop from effectful.ops.types import Expr, Interpretation, Operation, Term @@ -68,7 +68,7 @@ def call[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: } with handler(subs): return evaluate(body) - elif not any(isinstance(a, Term) for a in tree.flatten((fn, args, kwargs))): + elif not fvsof((fn, args, kwargs)): return fn(*args, **kwargs) else: raise NotImplementedError @@ -246,20 +246,36 @@ def evaluate[T](expr: Expr[T], *, intp: Interpretation | None = None) -> Expr[T] 6 """ - if intp is None: - from effectful.internals.runtime import get_interpretation + from effectful.internals.runtime import get_interpretation, interpreter - intp = get_interpretation() + if intp is not None: + return interpreter(intp)(evaluate)(expr) if isinstance(expr, Term): - (args, kwargs) = tree.map_structure( - functools.partial(evaluate, intp=intp), (expr.args, expr.kwargs) - ) - return apply.__default_rule__(intp, expr.op, *args, **kwargs) - elif tree.is_nested(expr): - return tree.map_structure(functools.partial(evaluate, intp=intp), expr) + args = tuple(evaluate(arg) for arg in expr.args) + kwargs = {k: evaluate(v) for k, v in expr.kwargs.items()} + return expr.op(*args, **kwargs) + elif isinstance(expr, Operation): + op_intp = get_interpretation().get(expr, expr) + return op_intp if isinstance(op_intp, Operation) else expr # type: ignore + elif isinstance(expr, collections.abc.Mapping): + if isinstance(expr, collections.defaultdict): + return type(expr)(expr.default_factory, evaluate(tuple(expr.items()))) # type: ignore + elif isinstance(expr, types.MappingProxyType): + return type(expr)(dict(evaluate(tuple(expr.items())))) # type: ignore + else: + return type(expr)(evaluate(tuple(expr.items()))) # type: ignore + elif isinstance(expr, collections.abc.Sequence | collections.abc.Set): + if isinstance(expr, str | bytes): + return expr # type: ignore + elif isinstance(expr, collections.abc.MappingView): + return [evaluate(item) for item in expr] # type: ignore + else: + return type(expr)(evaluate(item) for item in expr) # type: ignore + elif dataclasses.is_dataclass(expr) and not isinstance(expr, type): + return dataclasses.replace(expr, **evaluate(dataclasses.asdict(expr))) # type: ignore else: - return expr + return expr # type: ignore def typeof[T](term: Expr[T]) -> type[T]: diff --git a/effectful/ops/syntax.py b/effectful/ops/syntax.py index 8a39eb9e..0063624b 100644 --- a/effectful/ops/syntax.py +++ b/effectful/ops/syntax.py @@ -8,8 +8,6 @@ from collections.abc import Callable, Iterable, Mapping from typing import Annotated, Concatenate -import tree - from effectful.ops.types import Annotation, Expr, Operation, Term @@ -355,7 +353,7 @@ def analyze(self, bound_sig: inspect.BoundArguments) -> frozenset[Operation]: else: param_bound_vars = {param_value} elif param_ordinal: # Only process if there's a Scoped annotation - # We can't use tree.flatten here because we want to be able + # We can't use flatten here because we want to be able # to see dict keys def extract_operations(obj): if isinstance(obj, Operation): @@ -535,6 +533,10 @@ def __init__( self._freshening = freshening or [] self.__signature__ = inspect.signature(default) + @property + def __isabstractmethod__(self) -> bool: + return False + def __eq__(self, other): if not isinstance(other, Operation): return NotImplemented @@ -662,7 +664,9 @@ def func() -> t: # type: ignore def _[**P, T](t: Callable[P, T], *, name: str | None = None) -> Operation[P, T]: @functools.wraps(t) def func(*args, **kwargs): - if not any(isinstance(a, Term) for a in tree.flatten((args, kwargs))): + from effectful.ops.semantics import fvsof + + if not fvsof((args, kwargs)): return t(*args, **kwargs) else: raise NotImplementedError @@ -872,18 +876,6 @@ def defterm[T](__dispatch: Callable[[type], Callable[[T], Expr[T]]], value: T): return __dispatch(type(value))(value) -def _map_structure_and_keys(func, structure): - def _map_value(value): - if isinstance(value, dict): - return {func(k): v for k, v in value.items()} - elif not tree.is_nested(value): - return func(value) - else: - return value - - return tree.traverse(_map_value, structure, top_down=False) - - @_CustomSingleDispatchCallable def defdata[T]( __dispatch: Callable[[type], Callable[..., Expr[T]]], @@ -960,9 +952,6 @@ def _(op, *args, **kwargs): *{k: (v, kwarg_ctxs[k]) for k, v in kwargs.items()}.items(), ): if c: - v = _map_structure_and_keys( - lambda a: renaming.get(a, a) if isinstance(a, Operation) else a, v - ) res = evaluate( v, intp={ @@ -1133,21 +1122,37 @@ def syntactic_eq[T](x: Expr[T], other: Expr[T]) -> bool: if isinstance(x, Term) and isinstance(other, Term): op, args, kwargs = x.op, x.args, x.kwargs op2, args2, kwargs2 = other.op, other.args, other.kwargs - try: - tree.assert_same_structure( - (op, args, kwargs), (op2, args2, kwargs2), check_types=True - ) - except (TypeError, ValueError): - return False - return all( - tree.flatten( - tree.map_structure( - syntactic_eq, (op, args, kwargs), (op2, args2, kwargs2) - ) - ) + return ( + op == op2 + and len(args) == len(args2) + and set(kwargs) == set(kwargs2) + and all(syntactic_eq(a, b) for a, b in zip(args, args2)) + and all(syntactic_eq(kwargs[k], kwargs2[k]) for k in kwargs) ) elif isinstance(x, Term) or isinstance(other, Term): return False + elif isinstance(x, collections.abc.Mapping) and isinstance( + other, collections.abc.Mapping + ): + return all( + k in x and k in other and syntactic_eq(x[k], other[k]) + for k in set(x) | set(other) + ) + elif isinstance(x, collections.abc.Sequence) and isinstance( + other, collections.abc.Sequence + ): + return len(x) == len(other) and all( + syntactic_eq(a, b) for a, b in zip(x, other) + ) + elif ( + dataclasses.is_dataclass(x) + and not isinstance(x, type) + and dataclasses.is_dataclass(other) + and not isinstance(other, type) + ): + return type(x) == type(other) and syntactic_eq( + dataclasses.asdict(x), dataclasses.asdict(other) + ) else: return x == other diff --git a/tests/test_ops_syntax.py b/tests/test_ops_syntax.py index d4e06de1..897f83de 100644 --- a/tests/test_ops_syntax.py +++ b/tests/test_ops_syntax.py @@ -10,7 +10,6 @@ from effectful.ops.syntax import ( Scoped, _CustomSingleDispatchCallable, - _map_structure_and_keys, deffn, defop, defstream, @@ -111,13 +110,6 @@ def f(x): assert f_op != ff_op -def test_map_structure_and_keys(): - s = {1: 2, 3: [4, 5, (6, {7: 8})]} - expected = {2: 3, 4: [5, 6, (7, {8: 9})]} - actual = _map_structure_and_keys(lambda x: x + 1, s) - assert actual == expected - - def test_scoped_collections(): """Test that Scoped annotations work with tree-structured collections containing Operations.""" @@ -151,13 +143,13 @@ def let_many[S, T, A, B]( # Test with nested collections @defop def let_nested[S, T, A, B]( - bindings: Annotated[list[tuple[Operation[[], T], T]], Scoped[A]], + bindings: Annotated[tuple[tuple[Operation[[], T], T], ...], Scoped[A]], body: Annotated[S, Scoped[A | B]], ) -> Annotated[S, Scoped[B]]: raise NotImplementedError w = defop(int, name="w") - nested_bindings = [(x, 1), (y, 2)] + nested_bindings = ((x, 1), (y, 2)) term2 = let_nested(nested_bindings, x() + y() + w()) free_vars2 = fvsof(term2) From 45c0d99abe18e7901cbaf898ac3418d33718ef81 Mon Sep 17 00:00:00 2001 From: Eli Date: Wed, 13 Aug 2025 21:16:50 -0400 Subject: [PATCH 2/7] remove dataclass --- effectful/ops/semantics.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/effectful/ops/semantics.py b/effectful/ops/semantics.py index b2e5d167..62a2c35f 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -1,6 +1,5 @@ import collections.abc import contextlib -import dataclasses import functools import types import typing @@ -272,8 +271,6 @@ def evaluate[T](expr: Expr[T], *, intp: Interpretation | None = None) -> Expr[T] return [evaluate(item) for item in expr] # type: ignore else: return type(expr)(evaluate(item) for item in expr) # type: ignore - elif dataclasses.is_dataclass(expr) and not isinstance(expr, type): - return dataclasses.replace(expr, **evaluate(dataclasses.asdict(expr))) # type: ignore else: return expr # type: ignore From 5a3127127de51a47fdeb5d862aa5dfd322679391 Mon Sep 17 00:00:00 2001 From: Eli Date: Wed, 13 Aug 2025 21:18:53 -0400 Subject: [PATCH 3/7] syntactic_eq dataclass case removed --- effectful/ops/syntax.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/effectful/ops/syntax.py b/effectful/ops/syntax.py index 0063624b..130d9303 100644 --- a/effectful/ops/syntax.py +++ b/effectful/ops/syntax.py @@ -1144,15 +1144,6 @@ def syntactic_eq[T](x: Expr[T], other: Expr[T]) -> bool: return len(x) == len(other) and all( syntactic_eq(a, b) for a, b in zip(x, other) ) - elif ( - dataclasses.is_dataclass(x) - and not isinstance(x, type) - and dataclasses.is_dataclass(other) - and not isinstance(other, type) - ): - return type(x) == type(other) and syntactic_eq( - dataclasses.asdict(x), dataclasses.asdict(other) - ) else: return x == other From ce6f48fe962ed567e16d44a5480f709a09b17d0c Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 14 Aug 2025 10:22:17 -0400 Subject: [PATCH 4/7] lint and revert --- effectful/ops/semantics.py | 2 +- tests/test_ops_syntax.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/effectful/ops/semantics.py b/effectful/ops/semantics.py index 62a2c35f..09848c20 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -272,7 +272,7 @@ def evaluate[T](expr: Expr[T], *, intp: Interpretation | None = None) -> Expr[T] else: return type(expr)(evaluate(item) for item in expr) # type: ignore else: - return expr # type: ignore + return expr def typeof[T](term: Expr[T]) -> type[T]: diff --git a/tests/test_ops_syntax.py b/tests/test_ops_syntax.py index 897f83de..2d6fb215 100644 --- a/tests/test_ops_syntax.py +++ b/tests/test_ops_syntax.py @@ -143,13 +143,13 @@ def let_many[S, T, A, B]( # Test with nested collections @defop def let_nested[S, T, A, B]( - bindings: Annotated[tuple[tuple[Operation[[], T], T], ...], Scoped[A]], + bindings: Annotated[list[tuple[Operation[[], T], T]], Scoped[A]], body: Annotated[S, Scoped[A | B]], ) -> Annotated[S, Scoped[B]]: raise NotImplementedError w = defop(int, name="w") - nested_bindings = ((x, 1), (y, 2)) + nested_bindings = [(x, 1), (y, 2)] term2 = let_nested(nested_bindings, x() + y() + w()) free_vars2 = fvsof(term2) From e4e89ad5a5cff573c7316f1598ffe8d4d405cf9f Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 14 Aug 2025 10:31:52 -0400 Subject: [PATCH 5/7] cases --- effectful/ops/semantics.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/effectful/ops/semantics.py b/effectful/ops/semantics.py index 09848c20..780e453f 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -264,13 +264,18 @@ def evaluate[T](expr: Expr[T], *, intp: Interpretation | None = None) -> Expr[T] return type(expr)(dict(evaluate(tuple(expr.items())))) # type: ignore else: return type(expr)(evaluate(tuple(expr.items()))) # type: ignore - elif isinstance(expr, collections.abc.Sequence | collections.abc.Set): + elif isinstance(expr, collections.abc.Sequence): if isinstance(expr, str | bytes): return expr # type: ignore - elif isinstance(expr, collections.abc.MappingView): - return [evaluate(item) for item in expr] # type: ignore else: return type(expr)(evaluate(item) for item in expr) # type: ignore + elif isinstance(expr, collections.abc.Set): + if isinstance(expr, collections.abc.ItemsView | collections.abc.KeysView): + return {evaluate(item) for item in expr} # type: ignore + else: + return type(expr)(evaluate(item) for item in expr) # type: ignore + elif isinstance(expr, collections.abc.ValuesView): + return [evaluate(item) for item in expr] # type: ignore else: return expr From 6f4b8b1c714838b5df85bf695d942abf0f087340 Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 14 Aug 2025 10:38:06 -0400 Subject: [PATCH 6/7] lint --- effectful/ops/semantics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/effectful/ops/semantics.py b/effectful/ops/semantics.py index 780e453f..16154bb7 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -266,7 +266,7 @@ def evaluate[T](expr: Expr[T], *, intp: Interpretation | None = None) -> Expr[T] return type(expr)(evaluate(tuple(expr.items()))) # type: ignore elif isinstance(expr, collections.abc.Sequence): if isinstance(expr, str | bytes): - return expr # type: ignore + return typing.cast(T, expr) # mypy doesnt like ignore here, so we use cast else: return type(expr)(evaluate(item) for item in expr) # type: ignore elif isinstance(expr, collections.abc.Set): From 770dc8761a4b1ffb201bedabc33d971de627ec9f Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 14 Aug 2025 11:19:13 -0400 Subject: [PATCH 7/7] remove abstractmethod --- effectful/ops/syntax.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/effectful/ops/syntax.py b/effectful/ops/syntax.py index 130d9303..e97f1816 100644 --- a/effectful/ops/syntax.py +++ b/effectful/ops/syntax.py @@ -533,10 +533,6 @@ def __init__( self._freshening = freshening or [] self.__signature__ = inspect.signature(default) - @property - def __isabstractmethod__(self) -> bool: - return False - def __eq__(self, other): if not isinstance(other, Operation): return NotImplemented