syncraft 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl
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/__init__.py +59 -0
- syncraft/algebra.py +230 -25
- syncraft/ast.py +101 -4
- syncraft/constraint.py +41 -0
- syncraft/finder.py +71 -14
- syncraft/generator.py +181 -4
- syncraft/parser.py +162 -0
- syncraft/syntax.py +339 -105
- syncraft-0.2.3.dist-info/METADATA +113 -0
- syncraft-0.2.3.dist-info/RECORD +16 -0
- syncraft-0.2.2.dist-info/METADATA +0 -34
- syncraft-0.2.2.dist-info/RECORD +0 -16
- {syncraft-0.2.2.dist-info → syncraft-0.2.3.dist-info}/WHEEL +0 -0
- {syncraft-0.2.2.dist-info → syncraft-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {syncraft-0.2.2.dist-info → syncraft-0.2.3.dist-info}/top_level.txt +0 -0
syncraft/constraint.py
CHANGED
|
@@ -11,6 +11,12 @@ import inspect
|
|
|
11
11
|
K = TypeVar('K')
|
|
12
12
|
V = TypeVar('V')
|
|
13
13
|
class FrozenDict(collections.abc.Mapping, Generic[K, V]):
|
|
14
|
+
"""An immutable, hashable mapping.
|
|
15
|
+
|
|
16
|
+
Behaves like a read-only dict and caches its hash, making it suitable as a
|
|
17
|
+
key in other dictionaries or for set membership. Equality compares the
|
|
18
|
+
underlying mapping to any other Mapping.
|
|
19
|
+
"""
|
|
14
20
|
def __init__(self, *args, **kwargs):
|
|
15
21
|
self._data = dict(*args, **kwargs)
|
|
16
22
|
self._hash = None
|
|
@@ -54,12 +60,19 @@ class Binding:
|
|
|
54
60
|
|
|
55
61
|
@dataclass(frozen=True)
|
|
56
62
|
class Bindable:
|
|
63
|
+
"""Mixin that carries named bindings produced during evaluation.
|
|
64
|
+
|
|
65
|
+
Instances accumulate bindings of name->node pairs. Subclasses should return
|
|
66
|
+
a new instance from ``bind`` to preserve immutability.
|
|
67
|
+
"""
|
|
57
68
|
binding: Binding = field(default_factory=Binding)
|
|
58
69
|
|
|
59
70
|
def map(self, f: Callable[[Any], Any])->Self:
|
|
71
|
+
"""Optionally transform the underlying value (no-op by default)."""
|
|
60
72
|
return self
|
|
61
73
|
|
|
62
74
|
def bind(self, name: str, node:Any)->Self:
|
|
75
|
+
"""Return a copy with ``node`` recorded under ``name`` in bindings."""
|
|
63
76
|
return replace(self, binding=self.binding.bind(name, node))
|
|
64
77
|
|
|
65
78
|
|
|
@@ -73,11 +86,19 @@ class ConstraintResult:
|
|
|
73
86
|
unbound: frozenset[str] = frozenset()
|
|
74
87
|
@dataclass(frozen=True)
|
|
75
88
|
class Constraint:
|
|
89
|
+
"""A composable boolean check over a set of bound values.
|
|
90
|
+
|
|
91
|
+
The check is a function from a mapping of names to tuples of values to a
|
|
92
|
+
``ConstraintResult`` with a boolean outcome and any unbound requirements.
|
|
93
|
+
Constraints compose with logical operators (``&``, ``|``, ``^``, ``~``).
|
|
94
|
+
"""
|
|
76
95
|
run_f: Callable[[FrozenDict[str, Tuple[Any, ...]]], ConstraintResult]
|
|
77
96
|
name: str = ""
|
|
78
97
|
def __call__(self, bound: FrozenDict[str, Tuple[Any, ...]])->ConstraintResult:
|
|
98
|
+
"""Evaluate this constraint against the provided bindings."""
|
|
79
99
|
return self.run_f(bound)
|
|
80
100
|
def __and__(self, other: Constraint) -> Constraint:
|
|
101
|
+
"""Logical AND composition of two constraints."""
|
|
81
102
|
def and_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
82
103
|
res1 = self(bound)
|
|
83
104
|
res2 = other(bound)
|
|
@@ -89,6 +110,7 @@ class Constraint:
|
|
|
89
110
|
name=f"({self.name} && {other.name})"
|
|
90
111
|
)
|
|
91
112
|
def __or__(self, other: Constraint) -> Constraint:
|
|
113
|
+
"""Logical OR composition of two constraints."""
|
|
92
114
|
def or_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
93
115
|
res1 = self(bound)
|
|
94
116
|
res2 = other(bound)
|
|
@@ -100,6 +122,7 @@ class Constraint:
|
|
|
100
122
|
name=f"({self.name} || {other.name})"
|
|
101
123
|
)
|
|
102
124
|
def __xor__(self, other: Constraint) -> Constraint:
|
|
125
|
+
"""Logical XOR composition of two constraints."""
|
|
103
126
|
def xor_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
104
127
|
res1 = self(bound)
|
|
105
128
|
res2 = other(bound)
|
|
@@ -111,6 +134,7 @@ class Constraint:
|
|
|
111
134
|
name=f"({self.name} ^ {other.name})"
|
|
112
135
|
)
|
|
113
136
|
def __invert__(self) -> Constraint:
|
|
137
|
+
"""Logical NOT of this constraint."""
|
|
114
138
|
def invert_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
|
|
115
139
|
res = self(bound)
|
|
116
140
|
return ConstraintResult(result=not res.result, unbound=res.unbound)
|
|
@@ -169,6 +193,21 @@ def predicate(f: Callable[..., bool],
|
|
|
169
193
|
name: Optional[str] = None,
|
|
170
194
|
quant: Quantifier = Quantifier.FORALL,
|
|
171
195
|
bimap: bool = True) -> Constraint:
|
|
196
|
+
"""Create a constraint from a Python predicate function.
|
|
197
|
+
|
|
198
|
+
The predicate's parameters define the required bindings. When ``bimap`` is
|
|
199
|
+
true, arguments with a ``bimap()`` method are mapped to their forward value
|
|
200
|
+
before evaluation, making it convenient to write predicates over AST values.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
f: The boolean function to wrap as a constraint.
|
|
204
|
+
name: Optional human-friendly name; defaults to ``f.__name__``.
|
|
205
|
+
quant: Quantification over bound values (forall or exists).
|
|
206
|
+
bimap: Whether to call ``bimap()`` on arguments before evaluation.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Constraint: A composable constraint.
|
|
210
|
+
"""
|
|
172
211
|
name = name or f.__name__
|
|
173
212
|
sig = inspect.signature(f)
|
|
174
213
|
if bimap:
|
|
@@ -182,9 +221,11 @@ def predicate(f: Callable[..., bool],
|
|
|
182
221
|
return Constraint.predicate(f, sig=sig, name=name, quant=quant)
|
|
183
222
|
|
|
184
223
|
def forall(f: Callable[..., bool], name: Optional[str] = None, bimap: bool=True) -> Constraint:
|
|
224
|
+
"""``forall`` wrapper around ``predicate`` (all combinations must satisfy)."""
|
|
185
225
|
return predicate(f, name=name, quant=Quantifier.FORALL, bimap=bimap)
|
|
186
226
|
|
|
187
227
|
def exists(f: Callable[..., bool], name: Optional[str] = None, bimap:bool = True) -> Constraint:
|
|
228
|
+
"""``exists`` wrapper around ``predicate`` (at least one combination)."""
|
|
188
229
|
return predicate(f, name=name, quant=Quantifier.EXISTS, bimap=bimap)
|
|
189
230
|
|
|
190
231
|
|
syncraft/finder.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import (
|
|
4
|
-
Any, Tuple, Generator as YieldGen, TypeVar
|
|
4
|
+
Any, Tuple, Generator as YieldGen, TypeVar, Generic
|
|
5
5
|
)
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from syncraft.algebra import (
|
|
@@ -16,43 +16,100 @@ from syncraft.syntax import Syntax
|
|
|
16
16
|
|
|
17
17
|
T=TypeVar('T', bound=TokenProtocol)
|
|
18
18
|
@dataclass(frozen=True)
|
|
19
|
-
class Finder(Generator[T]):
|
|
19
|
+
class Finder(Generator[T], Generic[T]):
|
|
20
|
+
"""Generator backend used to search/inspect parse trees.
|
|
21
|
+
|
|
22
|
+
This class is passed to a ``Syntax`` to obtain an ``Algebra`` that can be
|
|
23
|
+
run against a ``GenState``. In this module it's used to implement tree-wide search utilities
|
|
24
|
+
such as ``matches`` and ``find``.
|
|
25
|
+
"""
|
|
20
26
|
@classmethod
|
|
21
27
|
def anything(cls)->Algebra[Any, GenState[T]]:
|
|
28
|
+
"""Match any node and return it unchanged.
|
|
29
|
+
|
|
30
|
+
Succeeds on any input ``GenState`` and returns the current AST node as
|
|
31
|
+
the value, leaving the state untouched. Useful as a catch‑all predicate
|
|
32
|
+
when searching a tree.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Algebra[Any, GenState[T]]: An algebra that always succeeds with the
|
|
36
|
+
tuple ``(input.ast, input)``.
|
|
37
|
+
"""
|
|
22
38
|
def anything_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Any, GenState[T]]]:
|
|
23
39
|
return Right((input.ast, input))
|
|
24
40
|
return cls(anything_run, name=cls.__name__ + '.anything')
|
|
25
41
|
|
|
26
42
|
|
|
27
43
|
|
|
44
|
+
#: A ``Syntax`` that matches any node and returns it as the result without
|
|
45
|
+
#: consuming or modifying state.
|
|
28
46
|
anything = Syntax(lambda cls: cls.factory('anything')).describe(name="Anything", fixity='infix')
|
|
29
47
|
|
|
30
|
-
def
|
|
31
|
-
gen = syntax(Finder)
|
|
48
|
+
def _matches(alg: Algebra[Any, GenState[Any]], data: ParseResult[Any])-> bool:
|
|
32
49
|
state = GenState[Any].from_ast(ast = data, restore_pruned=True)
|
|
33
|
-
result =
|
|
50
|
+
result = alg.run(state, use_cache=True)
|
|
34
51
|
return isinstance(result, Right)
|
|
35
52
|
|
|
36
53
|
|
|
37
|
-
def
|
|
38
|
-
if not isinstance(data, Marked):
|
|
39
|
-
if
|
|
54
|
+
def _find(alg: Algebra[Any, GenState[Any]], data: ParseResult[Any]) -> YieldGen[ParseResult[Any], None, None]:
|
|
55
|
+
if not isinstance(data, (Marked, Collect)):
|
|
56
|
+
if _matches(alg, data):
|
|
40
57
|
yield data
|
|
41
58
|
match data:
|
|
42
59
|
case Then(left=left, right=right):
|
|
43
60
|
if left is not None:
|
|
44
|
-
yield from
|
|
61
|
+
yield from _find(alg, left)
|
|
45
62
|
if right is not None:
|
|
46
|
-
yield from
|
|
63
|
+
yield from _find(alg, right)
|
|
47
64
|
case Many(value = value):
|
|
48
65
|
for e in value:
|
|
49
|
-
yield from
|
|
66
|
+
yield from _find(alg, e)
|
|
50
67
|
case Marked(value=value):
|
|
51
|
-
yield from
|
|
68
|
+
yield from _find(alg, value)
|
|
52
69
|
case Choice(value=value):
|
|
53
70
|
if value is not None:
|
|
54
|
-
yield from
|
|
71
|
+
yield from _find(alg, value)
|
|
55
72
|
case Collect(value=value):
|
|
56
|
-
yield from
|
|
73
|
+
yield from _find(alg, value)
|
|
57
74
|
case _:
|
|
58
75
|
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def matches(syntax: Syntax[Any, Any], data: ParseResult[Any])-> bool:
|
|
79
|
+
"""Check whether a syntax matches a specific node.
|
|
80
|
+
|
|
81
|
+
Runs the given ``syntax`` (compiled with ``Finder``) against ``data`` only;
|
|
82
|
+
it does not traverse the tree. ``Marked`` and ``Collect`` node are treated as transparent.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
syntax: The ``Syntax`` to run.
|
|
86
|
+
data: The AST node (``ParseResult``) to test.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: ``True`` if the syntax succeeds on ``data``, ``False`` otherwise.
|
|
90
|
+
"""
|
|
91
|
+
gen = syntax(Finder)
|
|
92
|
+
if isinstance(data, (Marked, Collect)):
|
|
93
|
+
return _matches(gen, data.value)
|
|
94
|
+
else:
|
|
95
|
+
return _matches(gen, data)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find(syntax: Syntax[Any, Any], data: ParseResult[Any]) -> YieldGen[ParseResult[Any], None, None]:
|
|
99
|
+
"""Yield all subtrees that match a syntax.
|
|
100
|
+
|
|
101
|
+
Performs a depth‑first traversal of ``data`` and yields each node where the
|
|
102
|
+
provided ``syntax`` (compiled with ``Finder``) succeeds. Wrapper nodes like
|
|
103
|
+
``Marked`` and ``Collect`` are treated as transparent for matching and are
|
|
104
|
+
not yielded themselves.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
syntax: The ``Syntax`` predicate to apply at each node.
|
|
108
|
+
data: The root ``ParseResult`` to search.
|
|
109
|
+
|
|
110
|
+
Yields:
|
|
111
|
+
ParseResult[Any]: Each node that satisfies ``syntax`` (pre‑order: the
|
|
112
|
+
current node is tested before visiting its children).
|
|
113
|
+
"""
|
|
114
|
+
gen = syntax(Finder)
|
|
115
|
+
yield from _find(gen, data)
|
syncraft/generator.py
CHANGED
|
@@ -37,28 +37,90 @@ B = TypeVar('B')
|
|
|
37
37
|
|
|
38
38
|
@dataclass(frozen=True)
|
|
39
39
|
class GenState(Bindable, Generic[T]):
|
|
40
|
+
"""Lightweight state passed between generator combinators.
|
|
41
|
+
|
|
42
|
+
Holds the current AST focus (or ``None`` when pruned), a flag controlling
|
|
43
|
+
whether traversals are allowed to access pruned branches, and a deterministic
|
|
44
|
+
seed for randomized generation paths.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
ast: The current AST node or ``None`` if the branch is pruned.
|
|
48
|
+
restore_pruned: When true, allows navigation into branches that would
|
|
49
|
+
normally be considered pruned by the AST structure.
|
|
50
|
+
seed: Integer seed used to derive reproducible random choices.
|
|
51
|
+
"""
|
|
40
52
|
ast: Optional[ParseResult[T]] = None
|
|
41
53
|
restore_pruned: bool = False
|
|
42
54
|
seed: int = 0
|
|
43
55
|
def map(self, f: Callable[[Any], Any]) -> GenState[T]:
|
|
56
|
+
"""Return a copy with ``ast`` replaced by ``f(ast)``.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
f: Mapping function applied to the current ``ast``.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
GenState[T]: A new state with the mapped ``ast``.
|
|
63
|
+
"""
|
|
44
64
|
return replace(self, ast=f(self.ast))
|
|
45
65
|
|
|
46
66
|
def inject(self, a: Any) -> GenState[T]:
|
|
67
|
+
"""Return a copy with ``ast`` set to ``a``.
|
|
68
|
+
|
|
69
|
+
Shorthand for ``map(lambda _: a)``.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
a: The value to place into ``ast``.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
GenState[T]: A new state with ``ast`` equal to ``a``.
|
|
76
|
+
"""
|
|
47
77
|
return self.map(lambda _: a)
|
|
48
78
|
|
|
49
79
|
def fork(self, tag: Any) -> GenState[T]:
|
|
80
|
+
"""Create a deterministic fork of the state using ``tag``.
|
|
81
|
+
|
|
82
|
+
The new ``seed`` is derived from the current ``seed`` and ``tag`` so
|
|
83
|
+
that repeated forks with the same inputs are reproducible.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tag: Any value used to derive the child seed.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
GenState[T]: A new state with a forked ``seed``.
|
|
90
|
+
"""
|
|
50
91
|
return replace(self, seed=hash((self.seed, tag)))
|
|
51
92
|
|
|
52
93
|
def rng(self, tag: Any = None) -> random.Random:
|
|
94
|
+
"""Get a deterministic RNG for this state.
|
|
95
|
+
|
|
96
|
+
If ``tag`` is provided, the RNG seed is derived from ``(seed, tag)``;
|
|
97
|
+
otherwise the state's ``seed`` is used.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
tag: Optional label to derive a sub-seed.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
random.Random: A RNG instance seeded deterministically.
|
|
104
|
+
"""
|
|
53
105
|
return random.Random(self.seed if tag is None else hash((self.seed, tag)))
|
|
54
106
|
|
|
55
107
|
|
|
56
108
|
|
|
57
109
|
@cached_property
|
|
58
110
|
def pruned(self)->bool:
|
|
111
|
+
"""Whether the current branch is pruned (``ast`` is ``None``)."""
|
|
59
112
|
return self.ast is None
|
|
60
113
|
|
|
61
114
|
def left(self)-> GenState[T]:
|
|
115
|
+
"""Focus on the left side of a ``Then`` node or prune.
|
|
116
|
+
|
|
117
|
+
When ``restore_pruned`` is true, traversal is allowed even if the
|
|
118
|
+
``Then`` is marked as coming from the right branch.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
GenState[T]: State focused on the left child or pruned when not
|
|
122
|
+
applicable.
|
|
123
|
+
"""
|
|
62
124
|
if self.ast is None:
|
|
63
125
|
return self
|
|
64
126
|
if isinstance(self.ast, Then) and (self.ast.kind != ThenKind.RIGHT or self.restore_pruned):
|
|
@@ -66,6 +128,15 @@ class GenState(Bindable, Generic[T]):
|
|
|
66
128
|
return replace(self, ast=None)
|
|
67
129
|
|
|
68
130
|
def right(self) -> GenState[T]:
|
|
131
|
+
"""Focus on the right side of a ``Then`` node or prune.
|
|
132
|
+
|
|
133
|
+
When ``restore_pruned`` is true, traversal is allowed even if the
|
|
134
|
+
``Then`` is marked as coming from the left branch.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
GenState[T]: State focused on the right child or pruned when not
|
|
138
|
+
applicable.
|
|
139
|
+
"""
|
|
69
140
|
if self.ast is None:
|
|
70
141
|
return self
|
|
71
142
|
if isinstance(self.ast, Then) and (self.ast.kind != ThenKind.LEFT or self.restore_pruned):
|
|
@@ -73,6 +144,18 @@ class GenState(Bindable, Generic[T]):
|
|
|
73
144
|
return replace(self, ast=None)
|
|
74
145
|
|
|
75
146
|
def down(self, index: int) -> GenState[T]:
|
|
147
|
+
"""Descend through wrapper nodes to reach the contained value.
|
|
148
|
+
|
|
149
|
+
Currently unwraps ``Marked`` nodes. Raises ``TypeError`` for other
|
|
150
|
+
node types.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
index: Placeholder for a future multi-child descent API.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
GenState[T]: State focused on the unwrapped child or unchanged when
|
|
157
|
+
pruned.
|
|
158
|
+
"""
|
|
76
159
|
if self.ast is None:
|
|
77
160
|
return self
|
|
78
161
|
match self.ast:
|
|
@@ -121,13 +204,22 @@ class TokenGen(TokenSpec):
|
|
|
121
204
|
return self.__str__()
|
|
122
205
|
|
|
123
206
|
def gen(self) -> Token:
|
|
207
|
+
"""Generate a token consistent with this specification.
|
|
208
|
+
|
|
209
|
+
Resolution order is: exact text, regex pattern, token type value, and
|
|
210
|
+
finally a generic placeholder literal.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Token: A token whose ``token_type`` is derived from the generated
|
|
214
|
+
text when necessary.
|
|
215
|
+
"""
|
|
124
216
|
text: str
|
|
125
217
|
if self.text is not None:
|
|
126
218
|
text = self.text
|
|
127
219
|
elif self.regex is not None:
|
|
128
220
|
try:
|
|
129
221
|
text = rstr.xeger(self.regex)
|
|
130
|
-
except Exception
|
|
222
|
+
except Exception:
|
|
131
223
|
# If the regex is invalid or generation fails
|
|
132
224
|
text = self.regex.pattern # fallback to pattern string
|
|
133
225
|
elif self.token_type is not None:
|
|
@@ -146,9 +238,31 @@ class TokenGen(TokenSpec):
|
|
|
146
238
|
class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
147
239
|
@classmethod
|
|
148
240
|
def state(cls, ast: Optional[ParseResult[T]] = None, seed: int = 0, restore_pruned: bool = False)->GenState[T]:
|
|
241
|
+
"""Create an initial ``GenState`` for generation or checking.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
ast: Optional root AST to validate/generate against.
|
|
245
|
+
seed: Seed for deterministic random generation.
|
|
246
|
+
restore_pruned: Allow traversing pruned branches.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
GenState[T]: The constructed initial state.
|
|
250
|
+
"""
|
|
149
251
|
return GenState.from_ast(ast=ast, seed=seed, restore_pruned=restore_pruned)
|
|
150
252
|
|
|
151
253
|
def flat_map(self, f: Callable[[ParseResult[T]], Algebra[B, GenState[T]]]) -> Algebra[B, GenState[T]]:
|
|
254
|
+
"""Sequence a dependent generator using the left child value.
|
|
255
|
+
|
|
256
|
+
Expects the input AST to be a ``Then`` node; applies ``self`` to the
|
|
257
|
+
left side, then passes the produced value to ``f`` and applies the
|
|
258
|
+
resulting algebra to the right side.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
f: Function mapping the left value to the next algebra.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Algebra[B, GenState[T]]: An algebra yielding the final result.
|
|
265
|
+
"""
|
|
152
266
|
def flat_map_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[B, GenState[T]]]:
|
|
153
267
|
try:
|
|
154
268
|
if not isinstance(input.ast, Then) or isinstance(input.ast, Nothing):
|
|
@@ -178,6 +292,24 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
|
178
292
|
|
|
179
293
|
|
|
180
294
|
def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Many[ParseResult[T]], GenState[T]]:
|
|
295
|
+
"""Apply ``self`` repeatedly with cardinality constraints.
|
|
296
|
+
|
|
297
|
+
In pruned mode, generates a random number of items in the inclusive
|
|
298
|
+
range ``[at_least, at_most or at_least+2]`` and attempts each
|
|
299
|
+
independently. Otherwise, validates an existing ``Many`` node and
|
|
300
|
+
applies ``self`` to each element.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
at_least: Minimum number of successful applications required.
|
|
304
|
+
at_most: Optional maximum number allowed.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Algebra[Many[ParseResult[T]], GenState[T]]: An algebra that yields a
|
|
308
|
+
``Many`` of results.
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
ValueError: If bounds are invalid.
|
|
312
|
+
"""
|
|
181
313
|
if at_least <=0 or (at_most is not None and at_most < at_least):
|
|
182
314
|
raise ValueError(f"Invalid arguments for many: at_least={at_least}, at_most={at_most}")
|
|
183
315
|
def many_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Many[ParseResult[T]], GenState[T]]]:
|
|
@@ -188,7 +320,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
|
188
320
|
for i in range(count):
|
|
189
321
|
forked_input = input.fork(tag=len(ret))
|
|
190
322
|
match self.run(forked_input, use_cache):
|
|
191
|
-
case Right((value,
|
|
323
|
+
case Right((value, _)):
|
|
192
324
|
ret.append(value)
|
|
193
325
|
case Left(_):
|
|
194
326
|
pass
|
|
@@ -201,7 +333,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
|
201
333
|
ret = []
|
|
202
334
|
for x in input.ast.value:
|
|
203
335
|
match self.run(input.inject(x), use_cache):
|
|
204
|
-
case Right((value,
|
|
336
|
+
case Right((value, _)):
|
|
205
337
|
ret.append(value)
|
|
206
338
|
if at_most is not None and len(ret) > at_most:
|
|
207
339
|
return Left(Error(
|
|
@@ -209,7 +341,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
|
209
341
|
this=self,
|
|
210
342
|
state=input.inject(x)
|
|
211
343
|
))
|
|
212
|
-
case Left(
|
|
344
|
+
case Left(_):
|
|
213
345
|
pass
|
|
214
346
|
if len(ret) < at_least:
|
|
215
347
|
return Left(Error(
|
|
@@ -224,6 +356,18 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
|
224
356
|
def or_else(self, # type: ignore
|
|
225
357
|
other: Algebra[ParseResult[T], GenState[T]]
|
|
226
358
|
) -> Algebra[Choice[ParseResult[T], ParseResult[T]], GenState[T]]:
|
|
359
|
+
"""Try ``self``; if it fails without commitment, try ``other``.
|
|
360
|
+
|
|
361
|
+
In pruned mode, deterministically chooses a branch using a forked RNG.
|
|
362
|
+
With an existing ``Choice`` AST, it executes the indicated branch.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
other: Fallback algebra to try when ``self`` is not committed.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Algebra[Choice[ParseResult[T], ParseResult[T]], GenState[T]]: An
|
|
369
|
+
algebra yielding which branch succeeded and its value.
|
|
370
|
+
"""
|
|
227
371
|
def or_else_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Choice[ParseResult[T], ParseResult[T]], GenState[T]]]:
|
|
228
372
|
def exec(kind: ChoiceKind | None,
|
|
229
373
|
left: GenState[T],
|
|
@@ -277,6 +421,22 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
|
|
|
277
421
|
case_sensitive: bool = False,
|
|
278
422
|
regex: Optional[re.Pattern[str]] = None
|
|
279
423
|
)-> Algebra[ParseResult[T], GenState[T]]:
|
|
424
|
+
"""Match or synthesize a single token.
|
|
425
|
+
|
|
426
|
+
When validating, succeeds if the current AST node is a ``Token`` that
|
|
427
|
+
satisfies this spec. When generating (pruned), produces a token based on
|
|
428
|
+
``text``, ``regex``, or ``token_type``.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
token_type: Expected token type.
|
|
432
|
+
text: Exact text to match or produce.
|
|
433
|
+
case_sensitive: Whether text matching respects case.
|
|
434
|
+
regex: Regular expression to synthesize text from when generating.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Algebra[ParseResult[T], GenState[T]]: An algebra producing a Token
|
|
438
|
+
node or validating the current one.
|
|
439
|
+
"""
|
|
280
440
|
gen = TokenGen(token_type=token_type, text=text, case_sensitive=case_sensitive, regex=regex)
|
|
281
441
|
lazy_self: Algebra[ParseResult[T], GenState[T]]
|
|
282
442
|
def token_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[ParseResult[Token], GenState[T]]]:
|
|
@@ -298,6 +458,23 @@ def generate(syntax: Syntax[Any, Any],
|
|
|
298
458
|
data: Optional[ParseResult[Any]] = None,
|
|
299
459
|
seed: int = 0,
|
|
300
460
|
restore_pruned: bool = False) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
|
|
461
|
+
"""Run a ``Syntax`` with the ``Generator`` backend.
|
|
462
|
+
|
|
463
|
+
In validation mode (``data`` provided), walks the structure and returns the
|
|
464
|
+
original AST and collected marks. In generation mode (``data`` is ``None``),
|
|
465
|
+
synthesizes an AST from the syntax using the given seed.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
syntax: The syntax to execute.
|
|
469
|
+
data: Optional root AST to validate against.
|
|
470
|
+
seed: Seed for deterministic random generation.
|
|
471
|
+
restore_pruned: Allow traversing pruned branches when validating.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]: The
|
|
475
|
+
resulting AST with marks when validating, or a synthesized AST when
|
|
476
|
+
generating.
|
|
477
|
+
"""
|
|
301
478
|
from syncraft.syntax import run
|
|
302
479
|
return run(syntax, Generator, False, ast=data, seed=seed, restore_pruned=restore_pruned)
|
|
303
480
|
|