Skip to content

Commit e425d1f

Browse files
committed
fix fvsof
1 parent 9f1748a commit e425d1f

2 files changed

Lines changed: 74 additions & 49 deletions

File tree

effectful/ops/semantics.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ConstructorOperation,
1212
DataclassConstructorOperation,
1313
ObjectInterpretation,
14+
Scoped,
1415
_CustomSingleDispatchCallable,
1516
defop,
1617
implements,
@@ -351,27 +352,40 @@ def typeof[T](term: Expr[T]) -> type[T]:
351352
return typing.cast(type[T], type(type_or_value))
352353

353354

355+
class _FvsAnalysis(typing.NamedTuple):
356+
ops: frozenset[Operation] = frozenset()
357+
fvs: frozenset[Operation] = frozenset()
358+
359+
354360
class _FvsofIntp(ObjectInterpretation):
361+
@staticmethod
362+
def _analysis(value) -> _FvsAnalysis:
363+
if isinstance(value, _FvsAnalysis):
364+
return value
365+
else:
366+
return _FvsAnalysis(Scoped.extract_operations(value))
367+
355368
@implements(ConstructorOperation.__apply__)
356369
def _apply_collection_binders(self, op, *args, **kwargs):
357-
return frozenset().union(
358-
*(x for x in (*args, *kwargs.values()) if isinstance(x, frozenset))
370+
analyses = tuple(self._analysis(x) for x in (*args, *kwargs.values()))
371+
return _FvsAnalysis(
372+
frozenset().union(frozenset(), *(a.ops for a in analyses)),
373+
frozenset().union(frozenset(), *(a.fvs for a in analyses)),
359374
)
360375

361376
@implements(apply)
362377
def _apply_fvs(self, op, *args, **kwargs):
363-
bindings = op.__fvs_rule__(*args, **kwargs)
378+
arg_analyses = tuple(self._analysis(a) for a in args)
379+
kwarg_analyses = {k: self._analysis(v) for k, v in kwargs.items()}
380+
bindings = op.__fvs_rule__(
381+
*(a.ops for a in arg_analyses),
382+
**{k: a.ops for k, a in kwarg_analyses.items()},
383+
)
364384
binders = frozenset().union(*(*bindings.args, *bindings.kwargs.values()))
365-
366385
fvs = frozenset().union(
367-
{op},
368-
*(
369-
x if isinstance(x, frozenset) else frozenset()
370-
for x in (*args, *kwargs.values())
371-
),
386+
{op}, *(a.fvs for a in (*arg_analyses, *kwarg_analyses.values()))
372387
)
373-
fvs -= binders
374-
return fvs
388+
return _FvsAnalysis(fvs=fvs - binders)
375389

376390

377391
_FVSOF_INTP = _FvsofIntp()
@@ -404,4 +418,4 @@ def fvsof[S](term: Expr[S]) -> collections.abc.Set[Operation]:
404418
405419
"""
406420
result = evaluate(term, intp=_FVSOF_INTP)
407-
return frozenset() if not isinstance(result, frozenset) else result
421+
return result.fvs if isinstance(result, _FvsAnalysis) else frozenset()

effectful/ops/syntax.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,41 @@ def infer_annotations(cls, sig: inspect.Signature) -> inspect.Signature:
317317
assert cls._get_root_ordinal(inferred_sig) == root_ordinal != set()
318318
return inferred_sig
319319

320+
@classmethod
321+
def extract_operations(
322+
cls, value, _seen: set[int] | None = None
323+
) -> frozenset[Operation]:
324+
"""Computes the set of :class:`Operation` s appearing directly in ``value`` .
325+
326+
An :class:`Operation` counts when it appears as a value in a collection,
327+
including as the key of a mapping, which is why this cannot be written
328+
in terms of :func:`flatten` . It does not count when it is applied to
329+
arguments, since the resulting :class:`Term` is a use of the operation
330+
rather than a binding occurrence of it.
331+
332+
:param value: The value to traverse.
333+
:returns: The operations that could be bound by a parameter given ``value``.
334+
"""
335+
_seen = set() if _seen is None else _seen
336+
if id(value) in _seen:
337+
return frozenset()
338+
_seen.add(id(value))
339+
340+
if isinstance(value, Operation):
341+
return frozenset({value})
342+
elif isinstance(value, dict):
343+
return frozenset().union(
344+
frozenset(),
345+
*(cls.extract_operations(k, _seen) for k in value.keys()),
346+
*(cls.extract_operations(v, _seen) for v in value.values()),
347+
)
348+
elif isinstance(value, list | set | frozenset | tuple):
349+
return frozenset().union(
350+
frozenset(), *(cls.extract_operations(v, _seen) for v in value)
351+
)
352+
else:
353+
return frozenset()
354+
320355
def analyze(self, bound_sig: inspect.BoundArguments) -> frozenset[Operation]:
321356
"""
322357
Computes a set of bound variables given a signature with bound arguments.
@@ -342,43 +377,19 @@ def analyze(self, bound_sig: inspect.BoundArguments) -> frozenset[Operation]:
342377
param_ordinal = self._get_param_ordinal(param)
343378
if param_ordinal <= self.ordinal and not param_ordinal <= return_ordinal:
344379
param_value = bound_sig.arguments[name]
345-
param_bound_vars = set()
346-
347-
if self._param_is_var(param):
348-
# Handle individual Operation parameters (existing behavior)
349-
if param.kind is inspect.Parameter.VAR_POSITIONAL:
350-
# pre-condition: all bound variables should be distinct
351-
assert len(param_value) == len(set(param_value))
352-
param_bound_vars = set(param_value)
353-
elif param.kind is inspect.Parameter.VAR_KEYWORD:
354-
# pre-condition: all bound variables should be distinct
355-
assert len(param_value.values()) == len(
356-
set(param_value.values())
357-
)
358-
param_bound_vars = set(param_value.values())
359-
else:
360-
param_bound_vars = {param_value}
361-
elif param_ordinal: # Only process if there's a Scoped annotation
362-
# We can't use flatten here because we want to be able
363-
# to see dict keys
364-
def extract_operations(obj, _seen=None):
365-
if _seen is None:
366-
_seen = set()
367-
obj_id = id(obj)
368-
if obj_id in _seen:
369-
return
370-
_seen.add(obj_id)
371-
if isinstance(obj, Operation):
372-
param_bound_vars.add(obj)
373-
elif isinstance(obj, dict):
374-
for k, v in obj.items():
375-
extract_operations(k, _seen)
376-
extract_operations(v, _seen)
377-
elif isinstance(obj, list | set | tuple):
378-
for v in obj:
379-
extract_operations(v, _seen)
380-
381-
extract_operations(param_value)
380+
param_bound_vars: frozenset[Operation] = (
381+
self.extract_operations(param_value)
382+
# only process if the parameter is an Operation or is Scoped
383+
if self._param_is_var(param) or param_ordinal
384+
else frozenset()
385+
)
386+
387+
if self._param_is_var(param) and param.kind in (
388+
inspect.Parameter.VAR_POSITIONAL,
389+
inspect.Parameter.VAR_KEYWORD,
390+
):
391+
# pre-condition: all bound variables should be distinct
392+
assert len(param_bound_vars) == len(param_value)
382393

383394
# pre-condition: all bound variables should be distinct
384395
if param_bound_vars:

0 commit comments

Comments
 (0)