syncraft 0.1.38__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of syncraft might be problematic. Click here for more details.
- {syncraft-0.1.38 → syncraft-0.2.0}/PKG-INFO +1 -1
- {syncraft-0.1.38 → syncraft-0.2.0}/pyproject.toml +1 -1
- syncraft-0.2.0/syncraft/constraint.py +171 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/syntax.py +26 -30
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/PKG-INFO +1 -1
- syncraft-0.1.38/syncraft/constraint.py +0 -198
- {syncraft-0.1.38 → syncraft-0.2.0}/LICENSE +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/README.md +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/setup.cfg +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/__init__.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/algebra.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/ast.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/diagnostic.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/finder.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/generator.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/parser.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/py.typed +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/sqlite3.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/SOURCES.txt +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/dependency_links.txt +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/requires.txt +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/top_level.txt +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_bimap.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_find.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_parse.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_to.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_until.py +0 -0
- {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_var.py +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Callable, Generic, Tuple, TypeVar, Optional, Any, Self
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from dataclasses import dataclass, field, replace
|
|
5
|
+
import collections.abc
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from itertools import product
|
|
8
|
+
import inspect
|
|
9
|
+
|
|
10
|
+
K = TypeVar('K')
|
|
11
|
+
V = TypeVar('V')
|
|
12
|
+
class FrozenDict(collections.abc.Mapping, Generic[K, V]):
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
self._data = dict(*args, **kwargs)
|
|
15
|
+
self._hash = None
|
|
16
|
+
def __getitem__(self, key):
|
|
17
|
+
return self._data[key]
|
|
18
|
+
|
|
19
|
+
def __iter__(self):
|
|
20
|
+
return iter(self._data)
|
|
21
|
+
|
|
22
|
+
def __len__(self):
|
|
23
|
+
return len(self._data)
|
|
24
|
+
|
|
25
|
+
def __hash__(self):
|
|
26
|
+
if self._hash is None:
|
|
27
|
+
self._hash = hash(frozenset(self._data.items()))
|
|
28
|
+
return self._hash
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other):
|
|
31
|
+
if isinstance(other, collections.abc.Mapping):
|
|
32
|
+
return self._data == other
|
|
33
|
+
return NotImplemented
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f"{self.__class__.__name__}({self._data})"
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Binding:
|
|
40
|
+
bindings : frozenset[Tuple[str, Any]] = frozenset()
|
|
41
|
+
def bind(self, name: str, node: Any) -> Binding:
|
|
42
|
+
new_binding = set(self.bindings)
|
|
43
|
+
new_binding.add((name, node))
|
|
44
|
+
return Binding(bindings=frozenset(new_binding))
|
|
45
|
+
|
|
46
|
+
def bound(self)->FrozenDict[str, Tuple[Any, ...]]:
|
|
47
|
+
ret = defaultdict(list)
|
|
48
|
+
for name, node in self.bindings:
|
|
49
|
+
ret[name].append(node)
|
|
50
|
+
return FrozenDict({k: tuple(vs) for k, vs in ret.items()})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Bindable:
|
|
56
|
+
binding: Binding = field(default_factory=Binding)
|
|
57
|
+
|
|
58
|
+
def map(self, f: Callable[[Any], Any])->Self:
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def bind(self, name: str, node:Any)->Self:
|
|
62
|
+
return replace(self, binding=self.binding.bind(name, node))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Quantifier(Enum):
|
|
66
|
+
FORALL = "forall"
|
|
67
|
+
EXISTS = "exists"
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ConstraintResult:
|
|
71
|
+
result: bool
|
|
72
|
+
unbound: frozenset[str] = frozenset()
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class Constraint:
|
|
75
|
+
run_f: Callable[[FrozenDict[str, Tuple[Any, ...]]], ConstraintResult]
|
|
76
|
+
name: str = ""
|
|
77
|
+
def __call__(self, bound: FrozenDict[str, Tuple[Any, ...]])->ConstraintResult:
|
|
78
|
+
return self.run_f(bound)
|
|
79
|
+
def __and__(self, other: Constraint) -> Constraint:
|
|
80
|
+
def and_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
81
|
+
res1 = self(bound)
|
|
82
|
+
res2 = other(bound)
|
|
83
|
+
combined_result = res1.result and res2.result
|
|
84
|
+
combined_unbound = res1.unbound.union(res2.unbound)
|
|
85
|
+
return ConstraintResult(result=combined_result, unbound=combined_unbound)
|
|
86
|
+
return Constraint(
|
|
87
|
+
run_f=and_run,
|
|
88
|
+
name=f"({self.name} && {other.name})"
|
|
89
|
+
)
|
|
90
|
+
def __or__(self, other: Constraint) -> Constraint:
|
|
91
|
+
def or_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
92
|
+
res1 = self(bound)
|
|
93
|
+
res2 = other(bound)
|
|
94
|
+
combined_result = res1.result or res2.result
|
|
95
|
+
combined_unbound = res1.unbound.union(res2.unbound)
|
|
96
|
+
return ConstraintResult(result=combined_result, unbound=combined_unbound)
|
|
97
|
+
return Constraint(
|
|
98
|
+
run_f=or_run,
|
|
99
|
+
name=f"({self.name} || {other.name})"
|
|
100
|
+
)
|
|
101
|
+
def __xor__(self, other: Constraint) -> Constraint:
|
|
102
|
+
def xor_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
103
|
+
res1 = self(bound)
|
|
104
|
+
res2 = other(bound)
|
|
105
|
+
combined_result = res1.result ^ res2.result
|
|
106
|
+
combined_unbound = res1.unbound.union(res2.unbound)
|
|
107
|
+
return ConstraintResult(result=combined_result, unbound=combined_unbound)
|
|
108
|
+
return Constraint(
|
|
109
|
+
run_f=xor_run,
|
|
110
|
+
name=f"({self.name} ^ {other.name})"
|
|
111
|
+
)
|
|
112
|
+
def __invert__(self) -> Constraint:
|
|
113
|
+
def invert_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
114
|
+
res = self(bound)
|
|
115
|
+
return ConstraintResult(result=not res.result, unbound=res.unbound)
|
|
116
|
+
return Constraint(
|
|
117
|
+
run_f=invert_run,
|
|
118
|
+
name=f"!({self.name})"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def predicate(cls,
|
|
123
|
+
f: Callable[..., bool],
|
|
124
|
+
*,
|
|
125
|
+
name: Optional[str] = None,
|
|
126
|
+
quant: Quantifier = Quantifier.FORALL)->Constraint:
|
|
127
|
+
sig = inspect.signature(f)
|
|
128
|
+
pos_params = []
|
|
129
|
+
kw_params = []
|
|
130
|
+
for pname, param in sig.parameters.items():
|
|
131
|
+
if param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
|
132
|
+
pos_params.append(pname)
|
|
133
|
+
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
134
|
+
kw_params.append(pname)
|
|
135
|
+
else:
|
|
136
|
+
raise TypeError(f"Unsupported parameter kind: {param.kind}")
|
|
137
|
+
def run_f(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
138
|
+
# positional argument values
|
|
139
|
+
pos_values = [bound.get(pname, ()) for pname in pos_params]
|
|
140
|
+
# keyword argument values
|
|
141
|
+
kw_values = [bound.get(pname, ()) for pname in kw_params]
|
|
142
|
+
|
|
143
|
+
# If any param is unbound, fail
|
|
144
|
+
all_params = pos_params + kw_params
|
|
145
|
+
all_values = pos_values + kw_values
|
|
146
|
+
unbound_args = [p for p, vs in zip(all_params, all_values) if not vs]
|
|
147
|
+
if unbound_args:
|
|
148
|
+
return ConstraintResult(result=quant is Quantifier.FORALL, unbound=frozenset(unbound_args))
|
|
149
|
+
|
|
150
|
+
# Cartesian product
|
|
151
|
+
all_combos = product(*pos_values, *kw_values)
|
|
152
|
+
|
|
153
|
+
def eval_combo(combo):
|
|
154
|
+
pos_args = combo[: len(pos_values)]
|
|
155
|
+
kw_args = dict(zip(kw_params, combo[len(pos_values) :]))
|
|
156
|
+
return f(*pos_args, **kw_args)
|
|
157
|
+
|
|
158
|
+
if quant is Quantifier.EXISTS:
|
|
159
|
+
return ConstraintResult(result = any(eval_combo(c) for c in all_combos), unbound=frozenset())
|
|
160
|
+
else:
|
|
161
|
+
return ConstraintResult(result = all(eval_combo(c) for c in all_combos), unbound=frozenset())
|
|
162
|
+
|
|
163
|
+
return cls(run_f=run_f, name=name or f.__name__)
|
|
164
|
+
|
|
165
|
+
def forall(f: Callable[..., bool], name: Optional[str] = None) -> Constraint:
|
|
166
|
+
return Constraint.predicate(f, name=name, quant=Quantifier.FORALL)
|
|
167
|
+
|
|
168
|
+
def exists(f: Callable[..., bool], name: Optional[str] = None) -> Constraint:
|
|
169
|
+
return Constraint.predicate(f, name=name, quant=Quantifier.EXISTS)
|
|
170
|
+
|
|
171
|
+
|
|
@@ -7,12 +7,17 @@ from typing import (
|
|
|
7
7
|
from dataclasses import dataclass, field, replace
|
|
8
8
|
from functools import reduce
|
|
9
9
|
from syncraft.algebra import Algebra, Error, Either, Right
|
|
10
|
-
from syncraft.constraint import
|
|
10
|
+
from syncraft.constraint import Bindable
|
|
11
11
|
from syncraft.ast import Then, ThenKind, Marked, Choice, Many, ChoiceKind, Nothing, Collect, E, Collector
|
|
12
12
|
from types import MethodType, FunctionType
|
|
13
|
+
import keyword
|
|
13
14
|
|
|
14
15
|
from rich import print
|
|
15
16
|
|
|
17
|
+
def valid_name(name: str) -> bool:
|
|
18
|
+
return (name.isidentifier()
|
|
19
|
+
and not keyword.iskeyword(name)
|
|
20
|
+
and not (name.startswith('__') and name.endswith('__')))
|
|
16
21
|
|
|
17
22
|
A = TypeVar('A') # Result type
|
|
18
23
|
B = TypeVar('B') # Result type for mapping
|
|
@@ -249,27 +254,18 @@ class Syntax(Generic[A, S]):
|
|
|
249
254
|
return self.optional()
|
|
250
255
|
|
|
251
256
|
|
|
252
|
-
######################################################################## data processing combinators #########################################################
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
collector: None = None)-> Syntax[A | Marked[A], S]: ...
|
|
257
|
-
|
|
258
|
-
@overload
|
|
259
|
-
def bind(self,
|
|
260
|
-
var: Variable,
|
|
261
|
-
collector: Type[E]) -> Syntax[Collect[A, E] | Marked[Collect[A, E]], S]: ...
|
|
262
|
-
|
|
263
|
-
def bind(self,
|
|
264
|
-
var: Variable,
|
|
265
|
-
collector: Optional[Type[E]]=None) -> Syntax[Any, S]:
|
|
257
|
+
######################################################################## data processing combinators #########################################################
|
|
258
|
+
def bind(self, name: Optional[str] = None) -> Syntax[A, S]:
|
|
259
|
+
if name:
|
|
260
|
+
assert valid_name(name), f"Invalid mark name: {name}"
|
|
266
261
|
def bind_v(v: Any, s: S)->Tuple[Any, S]:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
262
|
+
if name:
|
|
263
|
+
return v, s.bind(name, v)
|
|
264
|
+
elif isinstance(v, Marked):
|
|
265
|
+
return v.value, s.bind(v.name, v.value)
|
|
266
|
+
else:
|
|
267
|
+
return v, s
|
|
268
|
+
return self.map_all(bind_v).describe(name=f'bind({name})', fixity='postfix', parameter=(self,))
|
|
273
269
|
|
|
274
270
|
def to(self, f: Collector[E])-> Syntax[Collect[A, E], S]:
|
|
275
271
|
def to_f(v: A) -> Collect[A, E]:
|
|
@@ -283,15 +279,16 @@ class Syntax(Generic[A, S]):
|
|
|
283
279
|
|
|
284
280
|
|
|
285
281
|
def mark(self, name: str) -> Syntax[Marked[A], S]:
|
|
286
|
-
|
|
282
|
+
assert valid_name(name), f"Invalid mark name: {name}"
|
|
283
|
+
def mark_s(value: A) -> Marked[A]:
|
|
287
284
|
if isinstance(value, Marked):
|
|
288
285
|
return replace(value, name=name)
|
|
289
286
|
else:
|
|
290
287
|
return Marked(name=name, value=value)
|
|
291
|
-
def
|
|
288
|
+
def imark_s(m : Marked[A]) -> A:
|
|
292
289
|
return m.value if isinstance(m, Marked) else m
|
|
293
290
|
|
|
294
|
-
return self.bimap(
|
|
291
|
+
return self.bimap(mark_s, imark_s).describe(name=f'mark("{name}")', fixity='postfix', parameter=(self,))
|
|
295
292
|
|
|
296
293
|
|
|
297
294
|
|
|
@@ -332,15 +329,14 @@ def first(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
|
|
|
332
329
|
def last(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
|
|
333
330
|
return reduce(lambda a, b: a >> b, parsers) if len(parsers) > 0 else success(Nothing())
|
|
334
331
|
|
|
335
|
-
def
|
|
332
|
+
def bind(* parsers: Syntax[Any, S] | Tuple[str, Syntax[Any, S]]) -> Syntax[Any, S]:
|
|
336
333
|
def is_named_parser(x: Any) -> bool:
|
|
337
|
-
return isinstance(x, tuple) and len(x) == 2 and isinstance(x[0],
|
|
334
|
+
return isinstance(x, tuple) and len(x) == 2 and isinstance(x[0], str) and isinstance(x[1], Syntax)
|
|
338
335
|
|
|
339
|
-
def to_parser(x: Syntax[Any, S] | Tuple[str
|
|
340
|
-
if isinstance(x, tuple) and len(x) == 2 and isinstance(x[0],
|
|
336
|
+
def to_parser(x: Syntax[Any, S] | Tuple[str, Syntax[Any, S]])->Syntax[Any, S]:
|
|
337
|
+
if isinstance(x, tuple) and len(x) == 2 and isinstance(x[0], str) and isinstance(x[1], Syntax):
|
|
341
338
|
if isinstance(x[0], str):
|
|
342
|
-
|
|
343
|
-
elif isinstance(x[0], Variable):
|
|
339
|
+
|
|
344
340
|
return x[1].bind(x[0])
|
|
345
341
|
else:
|
|
346
342
|
raise ValueError(f"Invalid variable type(must be str | Variable): {x[0]}", x)
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from typing import Callable, Generic, Tuple, TypeVar, Optional, Any, Self
|
|
3
|
-
from enum import Enum
|
|
4
|
-
from dataclasses import dataclass, field, replace
|
|
5
|
-
import collections.abc
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
from itertools import product
|
|
8
|
-
|
|
9
|
-
K = TypeVar('K')
|
|
10
|
-
V = TypeVar('V')
|
|
11
|
-
class FrozenDict(collections.abc.Mapping, Generic[K, V]):
|
|
12
|
-
def __init__(self, *args, **kwargs):
|
|
13
|
-
self._data = dict(*args, **kwargs)
|
|
14
|
-
self._hash = None
|
|
15
|
-
def __getitem__(self, key):
|
|
16
|
-
return self._data[key]
|
|
17
|
-
|
|
18
|
-
def __iter__(self):
|
|
19
|
-
return iter(self._data)
|
|
20
|
-
|
|
21
|
-
def __len__(self):
|
|
22
|
-
return len(self._data)
|
|
23
|
-
|
|
24
|
-
def __hash__(self):
|
|
25
|
-
if self._hash is None:
|
|
26
|
-
self._hash = hash(frozenset(self._data.items()))
|
|
27
|
-
return self._hash
|
|
28
|
-
|
|
29
|
-
def __eq__(self, other):
|
|
30
|
-
if isinstance(other, collections.abc.Mapping):
|
|
31
|
-
return self._data == other
|
|
32
|
-
return NotImplemented
|
|
33
|
-
|
|
34
|
-
def __repr__(self):
|
|
35
|
-
return f"{self.__class__.__name__}({self._data})"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@dataclass(frozen=True)
|
|
41
|
-
class Expr:
|
|
42
|
-
left: Any
|
|
43
|
-
op: str
|
|
44
|
-
right: Any
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@dataclass(frozen=True)
|
|
48
|
-
class Variable:
|
|
49
|
-
name: Optional[str] = None
|
|
50
|
-
_root: Optional[Variable] = field(default=None, compare=False, repr=False)
|
|
51
|
-
_mapf: Optional[Callable[[Any], Any]] = field(default=None, compare=False, repr=False)
|
|
52
|
-
|
|
53
|
-
def __post_init__(self):
|
|
54
|
-
if self._root is None:
|
|
55
|
-
object.__setattr__(self, '_root', self)
|
|
56
|
-
|
|
57
|
-
def raw(self, b:'BoundVar') -> Tuple[Any, ...]:
|
|
58
|
-
if self._root is None:
|
|
59
|
-
raise ValueError("_rawf can not be None")
|
|
60
|
-
return b.get(self._root, ())
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def map(self, f: Callable[[Any], Any]) -> "Variable":
|
|
64
|
-
if self._mapf is None:
|
|
65
|
-
return replace(self, _mapf=f)
|
|
66
|
-
else:
|
|
67
|
-
oldf = self._mapf
|
|
68
|
-
return replace(self, _mapf=lambda a: f(oldf(a)))
|
|
69
|
-
|
|
70
|
-
def get(self, b: 'BoundVar') -> Tuple[Any, ...]:
|
|
71
|
-
vals = self.raw(b)
|
|
72
|
-
if self._mapf is not None:
|
|
73
|
-
return tuple(self._mapf(v) for v in vals)
|
|
74
|
-
else:
|
|
75
|
-
return vals
|
|
76
|
-
|
|
77
|
-
def __call__(self, b:'BoundVar', raw:bool=False) -> Any:
|
|
78
|
-
if raw:
|
|
79
|
-
return self.raw(b)
|
|
80
|
-
else:
|
|
81
|
-
return self.get(b)
|
|
82
|
-
|
|
83
|
-
def __eq__(self, other):
|
|
84
|
-
return Expr(self, '==', other)
|
|
85
|
-
def __ne__(self, other):
|
|
86
|
-
return Expr(self, '!=', other)
|
|
87
|
-
def __lt__(self, other):
|
|
88
|
-
return Expr(self, '<', other)
|
|
89
|
-
def __le__(self, other):
|
|
90
|
-
return Expr(self, '<=', other)
|
|
91
|
-
def __gt__(self, other):
|
|
92
|
-
return Expr(self, '>', other)
|
|
93
|
-
def __ge__(self, other):
|
|
94
|
-
return Expr(self, '>=', other)
|
|
95
|
-
|
|
96
|
-
BoundVar = FrozenDict[Variable, Tuple[Any, ...]]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@dataclass(frozen=True)
|
|
100
|
-
class Binding:
|
|
101
|
-
bindings : frozenset[Tuple[Variable, Any]] = frozenset()
|
|
102
|
-
def bind(self, var: Variable, node: Any) -> Binding:
|
|
103
|
-
new_binding = set(self.bindings)
|
|
104
|
-
new_binding.add((var, node))
|
|
105
|
-
return Binding(bindings=frozenset(new_binding))
|
|
106
|
-
|
|
107
|
-
def to_dict(self)->BoundVar:
|
|
108
|
-
ret = defaultdict(list)
|
|
109
|
-
for var, node in self.bindings:
|
|
110
|
-
ret[var].append(node)
|
|
111
|
-
return FrozenDict({k: tuple(vs) for k, vs in ret.items()})
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
A = TypeVar('A')
|
|
115
|
-
@dataclass(frozen=True)
|
|
116
|
-
class Bindable:
|
|
117
|
-
binding: Binding = field(default_factory=Binding)
|
|
118
|
-
|
|
119
|
-
def map(self, f: Callable[[Any], Any])->Self:
|
|
120
|
-
return self
|
|
121
|
-
|
|
122
|
-
def bind(self, var: Variable, node:Any)->Self:
|
|
123
|
-
return replace(self, binding=self.binding.bind(var, node))
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class Quantifier(Enum):
|
|
127
|
-
FORALL = "forall"
|
|
128
|
-
EXISTS = "exists"
|
|
129
|
-
|
|
130
|
-
@dataclass(frozen=True)
|
|
131
|
-
class Constraint:
|
|
132
|
-
run_f: Callable[[BoundVar], bool]
|
|
133
|
-
name: str = ""
|
|
134
|
-
def __call__(self, bound: BoundVar)->bool:
|
|
135
|
-
return self.run_f(bound)
|
|
136
|
-
def __and__(self, other: Constraint) -> Constraint:
|
|
137
|
-
return Constraint(
|
|
138
|
-
run_f=lambda bound: self(bound) and other(bound),
|
|
139
|
-
name=f"({self.name} && {other.name})"
|
|
140
|
-
)
|
|
141
|
-
def __or__(self, other: Constraint) -> Constraint:
|
|
142
|
-
return Constraint(
|
|
143
|
-
run_f=lambda bound: self(bound) or other(bound),
|
|
144
|
-
name=f"({self.name} || {other.name})"
|
|
145
|
-
)
|
|
146
|
-
def __xor__(self, other: Constraint) -> Constraint:
|
|
147
|
-
return Constraint(
|
|
148
|
-
run_f=lambda bound: self(bound) ^ other(bound),
|
|
149
|
-
name=f"({self.name} ^ {other.name})"
|
|
150
|
-
)
|
|
151
|
-
def __invert__(self) -> Constraint:
|
|
152
|
-
return Constraint(
|
|
153
|
-
run_f=lambda bound: not self(bound),
|
|
154
|
-
name=f"!({self.name})"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
@classmethod
|
|
158
|
-
def predicate(cls, f: Callable[..., bool],*, name: Optional[str] = None, quant: Quantifier = Quantifier.FORALL)->Callable[..., Constraint]:
|
|
159
|
-
def wrapper(*args: Any, **kwargs:Any) -> Constraint:
|
|
160
|
-
arg_list = list(args)
|
|
161
|
-
kw_list = [(k, v) for k, v in kwargs.items()]
|
|
162
|
-
def run_f(bound: BoundVar) -> bool:
|
|
163
|
-
# positional argument values
|
|
164
|
-
pos_values = [
|
|
165
|
-
arg.get(bound) if isinstance(arg, Variable) else (arg,)
|
|
166
|
-
for arg in arg_list
|
|
167
|
-
]
|
|
168
|
-
# keyword argument values
|
|
169
|
-
kw_keys, kw_values = zip(*[
|
|
170
|
-
(k, v.get(bound) if isinstance(v, Variable) else (v,))
|
|
171
|
-
for k, v in kw_list
|
|
172
|
-
]) if kw_list else ([], [])
|
|
173
|
-
|
|
174
|
-
# Cartesian product over all argument values
|
|
175
|
-
all_combos = product(*pos_values, *kw_values)
|
|
176
|
-
|
|
177
|
-
# evaluate predicate on each combination
|
|
178
|
-
def eval_combo(combo):
|
|
179
|
-
pos_args = combo[:len(pos_values)]
|
|
180
|
-
kw_args = dict(zip(kw_keys, combo[len(pos_values):]))
|
|
181
|
-
return f(*pos_args, **kw_args)
|
|
182
|
-
|
|
183
|
-
if quant is Quantifier.EXISTS:
|
|
184
|
-
return any(eval_combo(c) for c in all_combos)
|
|
185
|
-
else:
|
|
186
|
-
return all(eval_combo(c) for c in all_combos)
|
|
187
|
-
return cls(run_f=run_f, name = name or f.__name__)
|
|
188
|
-
return wrapper
|
|
189
|
-
|
|
190
|
-
@classmethod
|
|
191
|
-
def forall(cls, f: Callable[..., bool], name: Optional[str] = None) -> Callable[..., Constraint]:
|
|
192
|
-
return cls.predicate(f, name=name, quant=Quantifier.FORALL)
|
|
193
|
-
|
|
194
|
-
@classmethod
|
|
195
|
-
def exists(cls, f: Callable[..., bool], name: Optional[str] = None):
|
|
196
|
-
return cls.predicate(f, name=name, quant=Quantifier.EXISTS)
|
|
197
|
-
|
|
198
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|