From f04704bf3ddd690e0d5dd2a911e24beb48d494ad Mon Sep 17 00:00:00 2001 From: Jack Feser Date: Tue, 21 Apr 2026 17:20:43 -0400 Subject: [PATCH 1/2] add an op for dataclass construction --- effectful/ops/semantics.py | 32 ++++++++++++++++++++++---------- tests/test_ops_semantics.py | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/effectful/ops/semantics.py b/effectful/ops/semantics.py index f7678fd2..f38ea4cd 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -211,19 +211,31 @@ def evaluate[T]( @evaluate.register(bytes) def _evaluate_object[T](expr: T, **kwargs) -> T: if dataclasses.is_dataclass(expr) and not isinstance(expr, type): - return typing.cast( - T, - dataclasses.replace( - expr, - **{ - field.name: evaluate(getattr(expr, field.name)) - for field in dataclasses.fields(expr) - }, - ), - ) + return _evaluate_dataclass(expr, **kwargs) return expr +def _get_dataclass_constr_op(typ): + if hasattr(typ, "constr_op"): + return typ.constr_op + + @Operation.define + def constr_op(*args, **kwargs) -> typ: + return typ(*args, **kwargs) + + typ.constr_op = constr_op + return constr_op + + +def _evaluate_dataclass[T](expr: T, **kwargs) -> T: + dataclass_op = _get_dataclass_constr_op(type(expr)) + subst = { + field.name: evaluate(getattr(expr, field.name)) + for field in dataclasses.fields(expr) + } + return typing.cast(T, dataclass_op(**subst)) + + @evaluate.register(Term) def _evaluate_term(expr: Term, **kwargs): args = tuple(evaluate(arg) for arg in expr.args) diff --git a/tests/test_ops_semantics.py b/tests/test_ops_semantics.py index 287c8176..85526c9c 100644 --- a/tests/test_ops_semantics.py +++ b/tests/test_ops_semantics.py @@ -877,3 +877,24 @@ def __init__(self, x: int): v = Operation.define(int) assert fvsof(A(v())) == {v} + + +def test_defdata_dataclass_init_effects() -> None: + @Operation.define + def f(x: int): + raise NotHandled + + @dataclasses.dataclass + class A: + x: int + + def __init__(self, x: int): + self.x = f(x) + + @Operation.define + def g(a: A): + raise NotHandled + + v = Operation.define(int) + t = g(A(v())) + assert isinstance(t.args[0].x, Term) From a8bdc172067dfe81ca1fd07beaded5929efac71a Mon Sep 17 00:00:00 2001 From: Jack Feser Date: Tue, 21 Apr 2026 17:25:51 -0400 Subject: [PATCH 2/2] 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 f38ea4cd..8059b538 100644 --- a/effectful/ops/semantics.py +++ b/effectful/ops/semantics.py @@ -231,7 +231,7 @@ def _evaluate_dataclass[T](expr: T, **kwargs) -> T: dataclass_op = _get_dataclass_constr_op(type(expr)) subst = { field.name: evaluate(getattr(expr, field.name)) - for field in dataclasses.fields(expr) + for field in dataclasses.fields(expr) # type: ignore[arg-type] } return typing.cast(T, dataclass_op(**subst))