syncraft 0.1.37__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.37 → syncraft-0.2.0}/PKG-INFO +1 -1
- {syncraft-0.1.37 → syncraft-0.2.0}/pyproject.toml +1 -1
- syncraft-0.2.0/syncraft/constraint.py +171 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/syntax.py +28 -26
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft.egg-info/PKG-INFO +1 -1
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft.egg-info/SOURCES.txt +2 -1
- syncraft-0.2.0/tests/test_var.py +0 -0
- syncraft-0.1.37/syncraft/constraint.py +0 -198
- {syncraft-0.1.37 → syncraft-0.2.0}/LICENSE +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/README.md +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/setup.cfg +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/__init__.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/algebra.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/ast.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/diagnostic.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/finder.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/generator.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/parser.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/py.typed +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft/sqlite3.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft.egg-info/dependency_links.txt +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft.egg-info/requires.txt +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/syncraft.egg-info/top_level.txt +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/tests/test_bimap.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/tests/test_find.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/tests/test_parse.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/tests/test_to.py +0 -0
- {syncraft-0.1.37 → syncraft-0.2.0}/tests/test_until.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
|
+
|
|
@@ -2,17 +2,22 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import (
|
|
4
4
|
Optional, Any, TypeVar, Generic, Callable, Tuple, cast,
|
|
5
|
-
Type, Literal, List
|
|
5
|
+
Type, Literal, List, overload
|
|
6
6
|
)
|
|
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,21 +254,18 @@ class Syntax(Generic[A, S]):
|
|
|
249
254
|
return self.optional()
|
|
250
255
|
|
|
251
256
|
|
|
252
|
-
######################################################################## data processing combinators #########################################################
|
|
253
|
-
def bind(self,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
else:
|
|
265
|
-
ret = self.mark(var.name).map_all(bind_v) if var.name else self.map_all(bind_v)
|
|
266
|
-
return ret.describe(name=f'bind({var.name})', fixity='postfix', parameter=(self,))
|
|
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}"
|
|
261
|
+
def bind_v(v: Any, s: S)->Tuple[Any, S]:
|
|
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,))
|
|
267
269
|
|
|
268
270
|
def to(self, f: Collector[E])-> Syntax[Collect[A, E], S]:
|
|
269
271
|
def to_f(v: A) -> Collect[A, E]:
|
|
@@ -277,15 +279,16 @@ class Syntax(Generic[A, S]):
|
|
|
277
279
|
|
|
278
280
|
|
|
279
281
|
def mark(self, name: str) -> Syntax[Marked[A], S]:
|
|
280
|
-
|
|
282
|
+
assert valid_name(name), f"Invalid mark name: {name}"
|
|
283
|
+
def mark_s(value: A) -> Marked[A]:
|
|
281
284
|
if isinstance(value, Marked):
|
|
282
285
|
return replace(value, name=name)
|
|
283
286
|
else:
|
|
284
287
|
return Marked(name=name, value=value)
|
|
285
|
-
def
|
|
288
|
+
def imark_s(m : Marked[A]) -> A:
|
|
286
289
|
return m.value if isinstance(m, Marked) else m
|
|
287
290
|
|
|
288
|
-
return self.bimap(
|
|
291
|
+
return self.bimap(mark_s, imark_s).describe(name=f'mark("{name}")', fixity='postfix', parameter=(self,))
|
|
289
292
|
|
|
290
293
|
|
|
291
294
|
|
|
@@ -326,15 +329,14 @@ def first(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
|
|
|
326
329
|
def last(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
|
|
327
330
|
return reduce(lambda a, b: a >> b, parsers) if len(parsers) > 0 else success(Nothing())
|
|
328
331
|
|
|
329
|
-
def
|
|
332
|
+
def bind(* parsers: Syntax[Any, S] | Tuple[str, Syntax[Any, S]]) -> Syntax[Any, S]:
|
|
330
333
|
def is_named_parser(x: Any) -> bool:
|
|
331
|
-
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)
|
|
332
335
|
|
|
333
|
-
def to_parser(x: Syntax[Any, S] | Tuple[str
|
|
334
|
-
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):
|
|
335
338
|
if isinstance(x[0], str):
|
|
336
|
-
|
|
337
|
-
elif isinstance(x[0], Variable):
|
|
339
|
+
|
|
338
340
|
return x[1].bind(x[0])
|
|
339
341
|
else:
|
|
340
342
|
raise ValueError(f"Invalid variable type(must be str | Variable): {x[0]}", x)
|
|
File without changes
|
|
@@ -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
|