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.

Files changed (28) hide show
  1. {syncraft-0.1.38 → syncraft-0.2.0}/PKG-INFO +1 -1
  2. {syncraft-0.1.38 → syncraft-0.2.0}/pyproject.toml +1 -1
  3. syncraft-0.2.0/syncraft/constraint.py +171 -0
  4. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/syntax.py +26 -30
  5. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/PKG-INFO +1 -1
  6. syncraft-0.1.38/syncraft/constraint.py +0 -198
  7. {syncraft-0.1.38 → syncraft-0.2.0}/LICENSE +0 -0
  8. {syncraft-0.1.38 → syncraft-0.2.0}/README.md +0 -0
  9. {syncraft-0.1.38 → syncraft-0.2.0}/setup.cfg +0 -0
  10. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/__init__.py +0 -0
  11. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/algebra.py +0 -0
  12. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/ast.py +0 -0
  13. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/diagnostic.py +0 -0
  14. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/finder.py +0 -0
  15. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/generator.py +0 -0
  16. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/parser.py +0 -0
  17. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/py.typed +0 -0
  18. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft/sqlite3.py +0 -0
  19. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/SOURCES.txt +0 -0
  20. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/dependency_links.txt +0 -0
  21. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/requires.txt +0 -0
  22. {syncraft-0.1.38 → syncraft-0.2.0}/syncraft.egg-info/top_level.txt +0 -0
  23. {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_bimap.py +0 -0
  24. {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_find.py +0 -0
  25. {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_parse.py +0 -0
  26. {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_to.py +0 -0
  27. {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_until.py +0 -0
  28. {syncraft-0.1.38 → syncraft-0.2.0}/tests/test_var.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.1.38
3
+ Version: 0.2.0
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syncraft"
3
- version = "0.1.38"
3
+ version = "0.2.0"
4
4
  description = "Parser combinator library"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -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 Variable, Bindable
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
- @overload
254
- def bind(self,
255
- var: Variable,
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
- return v, s.bind(var, v)
268
- if callable(collector):
269
- ret = self.to(collector).mark(var.name).map_all(bind_v) if var.name else self.to(collector).map_all(bind_v)
270
- else:
271
- ret = self.mark(var.name).map_all(bind_v) if var.name else self.map_all(bind_v)
272
- return ret.describe(name=f'bind({var.name})', fixity='postfix', parameter=(self,))
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
- def bind_s(value: A) -> Marked[A]:
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 ibind_s(m : Marked[A]) -> A:
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(bind_s, ibind_s).describe(name=f'bind("{name}")', fixity='postfix', parameter=(self,))
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 bound(* parsers: Syntax[Any, S] | Tuple[str|Variable, Syntax[Any, S]]) -> Syntax[Any, S]:
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], (str, Variable)) and isinstance(x[1], Syntax)
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|Variable, Syntax[Any, S]])->Syntax[Any, S]:
340
- if isinstance(x, tuple) and len(x) == 2 and isinstance(x[0], (str, Variable)) and isinstance(x[1], Syntax):
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
- return x[1].mark(x[0])
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.1.38
3
+ Version: 0.2.0
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
@@ -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