Skip to content

Commit 1edd657

Browse files
committed
fix bug
1 parent bb48c04 commit 1edd657

2 files changed

Lines changed: 48 additions & 10 deletions

File tree

effectful/ops/semantics.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,28 @@ def _evaluate_iterator(expr, **kwargs):
302302
except (TypeError, AttributeError):
303303
return expr # un-reducible iterators are opaque, like any object we can't recurse into
304304

305-
if ctor is iter:
306-
result = ctor(*evaluate(args))
307-
else:
308-
from effectful.internals.unification import nested_type
305+
from effectful.internals.unification import nested_type
309306

310-
ExprType = nested_type(expr).value
307+
ExprType = nested_type(expr).value
311308

312-
@Operation.define
313-
def ctor_op(*args) -> ExprType:
314-
return ctor(*args)
309+
@Operation.define
310+
def ctor_op(*args) -> ExprType:
311+
return ctor(*args)
315312

316-
result = ctor_op(*evaluate(args))
313+
# Reify through ``ctor_op`` rather than calling the live constructor: when
314+
# the evaluated args contain a ``Term`` the result is a structural ``Term``
315+
# node whose source iterables stay traversable (so ``fvsof`` finds their
316+
# free variables and term-reconstruction interpretations can rewrite them).
317+
# For fully concrete args ``ctor_op``'s default rule rebuilds the live
318+
# iterator, preserving laziness.
319+
result = ctor_op(*evaluate(args))
317320

318-
if state and state[0] is not None and hasattr(result, "__setstate__"):
321+
if (
322+
not isinstance(result, Term)
323+
and state
324+
and state[0] is not None
325+
and hasattr(result, "__setstate__")
326+
):
319327
result.__setstate__(
320328
evaluate(state[0])
321329
) # preserve position so advanced iterators don't reset

tests/test_ops_syntax.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,36 @@ def cons_iterable(*args: int) -> Iterable[int]:
525525
assert list(tm.args) == [1, 2, 3]
526526

527527

528+
def test_defdata_preserves_free_vars_in_iterator_arg():
529+
"""Free variables inside a builtin iterator argument must survive term
530+
construction.
531+
532+
A ``map`` object is a lazy iterator that can close over free variables --
533+
here ``x`` via the element ``x()``. When such an iterator is passed as an
534+
argument to an operation, ``defdata`` reconstructs the term and should
535+
preserve those free variables. Currently the iterator is reconstructed as an
536+
opaque iterator that no longer references ``x``, so ``x`` is silently dropped
537+
from the constructed term's free variables (and can no longer be
538+
substituted).
539+
"""
540+
541+
@defop
542+
def g(xs: Iterable[int]) -> int:
543+
raise NotHandled
544+
545+
x = defop(int, name="x")
546+
547+
def keep(v: int) -> int:
548+
return v
549+
550+
# Sanity: the raw map closes over ``x``.
551+
assert x in fvsof(map(keep, [x()]))
552+
553+
# Passing it to ``g`` must not drop ``x`` from the term's free variables.
554+
term = g(map(keep, [x()]))
555+
assert x in fvsof(term)
556+
557+
528558
def test_defstream_1():
529559
x = defop(int, name="x")
530560
y = defop(int, name="y")

0 commit comments

Comments
 (0)