syncraft 0.2.2__py3-none-any.whl → 0.2.4__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.
- 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.4.dist-info/METADATA +113 -0
- syncraft-0.2.4.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.4.dist-info}/WHEEL +0 -0
- {syncraft-0.2.2.dist-info → syncraft-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {syncraft-0.2.2.dist-info → syncraft-0.2.4.dist-info}/top_level.txt +0 -0
syncraft/__init__.py
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from .syntax import (
|
|
2
|
+
Syntax,
|
|
3
|
+
choice,
|
|
4
|
+
lazy,
|
|
5
|
+
success,
|
|
6
|
+
fail,
|
|
7
|
+
run,
|
|
8
|
+
)
|
|
9
|
+
from .parser import (
|
|
10
|
+
parse,
|
|
11
|
+
sqlglot,
|
|
12
|
+
token,
|
|
13
|
+
identifier,
|
|
14
|
+
variable,
|
|
15
|
+
literal,
|
|
16
|
+
number,
|
|
17
|
+
string,
|
|
18
|
+
regex,
|
|
19
|
+
until,
|
|
20
|
+
)
|
|
21
|
+
from .generator import (
|
|
22
|
+
generate,
|
|
23
|
+
)
|
|
24
|
+
from .finder import (
|
|
25
|
+
find,
|
|
26
|
+
matches,
|
|
27
|
+
anything,
|
|
28
|
+
)
|
|
29
|
+
from .constraint import (
|
|
30
|
+
Constraint,
|
|
31
|
+
Quantifier,
|
|
32
|
+
forall,
|
|
33
|
+
exists,
|
|
34
|
+
)
|
|
35
|
+
from .ast import (
|
|
36
|
+
AST,
|
|
37
|
+
Token,
|
|
38
|
+
Then,
|
|
39
|
+
ThenKind,
|
|
40
|
+
Choice,
|
|
41
|
+
ChoiceKind,
|
|
42
|
+
Many,
|
|
43
|
+
Marked,
|
|
44
|
+
Collect,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# syntax & core
|
|
49
|
+
"Syntax", "choice", "lazy", "success", "fail", "run",
|
|
50
|
+
# parsing/generation helpers
|
|
51
|
+
"parse", "sqlglot", "token", "identifier", "variable", "literal", "number", "string", "regex", "until",
|
|
52
|
+
"generate",
|
|
53
|
+
# finder
|
|
54
|
+
"find", "matches", "anything",
|
|
55
|
+
# constraints
|
|
56
|
+
"Constraint", "Quantifier", "forall", "exists",
|
|
57
|
+
# ast
|
|
58
|
+
"AST", "Token", "Then", "ThenKind", "Choice", "ChoiceKind", "Many", "Marked", "Collect",
|
|
59
|
+
]
|
syncraft/algebra.py
CHANGED
|
@@ -93,8 +93,38 @@ class Algebra(Generic[A, S]):
|
|
|
93
93
|
def __call__(self, input: S, use_cache: bool) -> Either[Any, Tuple[A, S]]:
|
|
94
94
|
return self.run(input, use_cache=use_cache)
|
|
95
95
|
|
|
96
|
-
|
|
97
96
|
def run(self, input: S, use_cache: bool) -> Either[Any, Tuple[A, S]]:
|
|
97
|
+
"""Execute this algebra on the given state with optional memoization.
|
|
98
|
+
|
|
99
|
+
This is the core evaluation entry point used by all combinators. It
|
|
100
|
+
supports per-"parser" memoization and protects against infinite
|
|
101
|
+
recursion by detecting left-recursive re-entrance.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
input: The initial state to run against. Must be hashable as it's
|
|
105
|
+
used as a cache key.
|
|
106
|
+
use_cache: When True, memoize results for ``(self.run_f, input)``
|
|
107
|
+
so repeated calls short-circuit. When False, the cache entry is
|
|
108
|
+
cleared after the run to effectively disable the cache.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Either[Error, Tuple[A, S]]: On success, ``Right((value, next_state))``.
|
|
112
|
+
On failure, ``Left(Error)``. Errors produced downstream are
|
|
113
|
+
automatically enriched with ``this=self`` and ``state=input`` to
|
|
114
|
+
preserve context. If an exception escapes the user code, it's
|
|
115
|
+
captured and returned as ``Left(Error)`` with a traceback in
|
|
116
|
+
``stack``.
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
- Left recursion: if a re-entrant call is observed on the same
|
|
120
|
+
``input`` while an evaluation is in progress, a ``Left(Error)``
|
|
121
|
+
is returned indicating left-recursion was detected.
|
|
122
|
+
- Memoization scope: results are cached per ``run_f`` (the concrete
|
|
123
|
+
compiled function of this algebra) and keyed by the input state.
|
|
124
|
+
- Commitment: downstream combinators (e.g. ``cut``) may set the
|
|
125
|
+
``committed`` flag in ``Error``; ``run`` preserves that flag but
|
|
126
|
+
does not set it itself.
|
|
127
|
+
"""
|
|
98
128
|
cache = self._cache[self.run_f]
|
|
99
129
|
assert cache is not None, "Cache should be initialized in __post_init__"
|
|
100
130
|
if input in cache:
|
|
@@ -136,12 +166,31 @@ class Algebra(Generic[A, S]):
|
|
|
136
166
|
|
|
137
167
|
@classmethod
|
|
138
168
|
def lazy(cls, thunk: Callable[[], Algebra[A, S]]) -> Algebra[A, S]:
|
|
169
|
+
"""Lazily construct an algebra at run time.
|
|
170
|
+
|
|
171
|
+
Useful for recursive definitions. The thunk is evaluated when this
|
|
172
|
+
algebra runs, and the resulting algebra is executed.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
thunk: Zero-argument function returning the underlying algebra.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
An algebra that defers to the thunk-provided algebra.
|
|
179
|
+
"""
|
|
139
180
|
def lazy_run(input: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
|
|
140
181
|
return thunk().run(input, use_cache)
|
|
141
182
|
return cls(lazy_run, name=cls.__name__ + '.lazy')
|
|
142
183
|
|
|
143
184
|
@classmethod
|
|
144
185
|
def fail(cls, error: Any) -> Algebra[Any, S]:
|
|
186
|
+
"""Return an algebra that always fails with ``error``.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
error: The error payload to wrap in ``Left``.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
An algebra producing ``Left(Error(...))`` without consuming input.
|
|
193
|
+
"""
|
|
145
194
|
def fail_run(input: S, use_cache:bool) -> Either[Any, Tuple[Any, S]]:
|
|
146
195
|
return Left(Error(
|
|
147
196
|
error=error,
|
|
@@ -152,12 +201,35 @@ class Algebra(Generic[A, S]):
|
|
|
152
201
|
|
|
153
202
|
@classmethod
|
|
154
203
|
def success(cls, value: Any) -> Algebra[Any, S]:
|
|
204
|
+
"""Return an algebra that always succeeds with ``value``.
|
|
205
|
+
|
|
206
|
+
The input state is passed through unchanged.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
value: The constant value to return.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
``Right((value, input))`` for any input state.
|
|
213
|
+
"""
|
|
155
214
|
def success_run(input: S, use_cache:bool) -> Either[Any, Tuple[Any, S]]:
|
|
156
215
|
return Right((value, input))
|
|
157
216
|
return cls(success_run, name=cls.__name__ + '.success')
|
|
158
217
|
|
|
159
218
|
@classmethod
|
|
160
219
|
def factory(cls, name: str, *args: Any, **kwargs: Any) -> Algebra[A, S]:
|
|
220
|
+
"""Call a named class method to construct an algebra.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
name: Name of a classmethod/staticmethod on this class.
|
|
224
|
+
*args: Positional args passed to the method.
|
|
225
|
+
**kwargs: Keyword args passed to the method.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The algebra returned by the method.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ValueError: If the method is missing or not callable.
|
|
232
|
+
"""
|
|
161
233
|
method = getattr(cls, name, None)
|
|
162
234
|
if method is None or not callable(method):
|
|
163
235
|
raise ValueError(f"Method {name} is not defined in {cls.__name__}")
|
|
@@ -166,6 +238,14 @@ class Algebra(Generic[A, S]):
|
|
|
166
238
|
|
|
167
239
|
|
|
168
240
|
def cut(self) -> Algebra[A, S]:
|
|
241
|
+
"""Commit this branch by marking failures as committed.
|
|
242
|
+
|
|
243
|
+
Converts downstream errors into committed errors (``committed=True``),
|
|
244
|
+
which prevents alternatives from being tried in ``or_else``.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
An algebra that commits errors produced by this one.
|
|
248
|
+
"""
|
|
169
249
|
def commit_error(e: Any) -> Error:
|
|
170
250
|
match e:
|
|
171
251
|
case Error():
|
|
@@ -188,6 +268,15 @@ class Algebra(Generic[A, S]):
|
|
|
188
268
|
],
|
|
189
269
|
Either[Any, Tuple[B, S]]],
|
|
190
270
|
ctx: Optional[Any] = None) -> Algebra[A | B, S]:
|
|
271
|
+
"""Run a handler only when this algebra fails.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
func: Callback ``(alg, input, left, ctx) -> Either`` executed on failure.
|
|
275
|
+
ctx: Optional context object passed to the callback.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
An algebra that intercepts failures and can recover or transform them.
|
|
279
|
+
"""
|
|
191
280
|
assert callable(func), "func must be callable"
|
|
192
281
|
def fail_run(input: S, use_cache:bool) -> Either[Any, Tuple[A | B, S]]:
|
|
193
282
|
result = self.run(input, use_cache)
|
|
@@ -206,6 +295,15 @@ class Algebra(Generic[A, S]):
|
|
|
206
295
|
],
|
|
207
296
|
Either[Any, Tuple[B, S]]],
|
|
208
297
|
ctx: Optional[Any] = None) -> Algebra[A | B, S]:
|
|
298
|
+
"""Run a handler only when this algebra succeeds.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
func: Callback ``(alg, input, right, ctx) -> Either`` executed on success.
|
|
302
|
+
ctx: Optional context object passed to the callback.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
An algebra that can transform or post-process successes.
|
|
306
|
+
"""
|
|
209
307
|
assert callable(func), "func must be callable"
|
|
210
308
|
def success_run(input: S, use_cache:bool) -> Either[Any, Tuple[A | B, S]]:
|
|
211
309
|
result = self.run(input, use_cache)
|
|
@@ -244,24 +342,29 @@ class Algebra(Generic[A, S]):
|
|
|
244
342
|
return lazy_self
|
|
245
343
|
|
|
246
344
|
######################################################## map on state ###########################################
|
|
247
|
-
def
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
case Left(err):
|
|
253
|
-
return Left(err)
|
|
254
|
-
case x:
|
|
255
|
-
raise ValueError(f"Unexpected result from self.run {x}")
|
|
256
|
-
return self.__class__(post_state_run, name=self.name)
|
|
345
|
+
def map_state(self, f: Callable[[S], S]) -> Algebra[A, S]:
|
|
346
|
+
"""Map the input state before running this algebra.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
f: ``S -> S`` function applied to the state prior to running.
|
|
257
350
|
|
|
258
|
-
|
|
259
|
-
|
|
351
|
+
Returns:
|
|
352
|
+
An algebra that runs with ``f(state)``.
|
|
353
|
+
"""
|
|
354
|
+
def map_state_run(state: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
|
|
260
355
|
return self.run(f(state), use_cache)
|
|
261
|
-
return self.__class__(
|
|
356
|
+
return self.__class__(map_state_run, name=self.name)
|
|
262
357
|
|
|
263
358
|
|
|
264
359
|
def map_all(self, f: Callable[[A, S], Tuple[B, S]]) -> Algebra[B, S]:
|
|
360
|
+
"""Map both the produced value and the resulting state on success.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
f: Function mapping ``(value, state)`` to ``(new_value, new_state)``.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
An algebra producing the transformed value and state.
|
|
367
|
+
"""
|
|
265
368
|
def map_all_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
|
|
266
369
|
match self.run(input, use_cache):
|
|
267
370
|
case Right((value, state)):
|
|
@@ -273,23 +376,54 @@ class Algebra(Generic[A, S]):
|
|
|
273
376
|
raise ValueError(f"Unexpected result from self.run {x}")
|
|
274
377
|
return self.__class__(map_all_run, name=self.name) # type: ignore
|
|
275
378
|
######################################################## fundamental combinators ############################################
|
|
276
|
-
def
|
|
277
|
-
|
|
379
|
+
def map(self, f: Callable[[A], B]) -> Algebra[B, S]:
|
|
380
|
+
"""Transform the success value, leaving the state unchanged.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
f: Mapper from ``A`` to ``B``.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
An algebra that yields ``B`` with the same resulting state.
|
|
387
|
+
"""
|
|
388
|
+
def map_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
|
|
278
389
|
parsed = self.run(input, use_cache)
|
|
279
390
|
if isinstance(parsed, Right):
|
|
280
391
|
return Right((f(parsed.value[0]), parsed.value[1]))
|
|
281
392
|
else:
|
|
282
393
|
return cast(Either[Any, Tuple[B, S]], parsed)
|
|
283
|
-
return self.__class__(
|
|
394
|
+
return self.__class__(map_run, name=self.name) # type: ignore
|
|
284
395
|
|
|
285
|
-
|
|
286
|
-
def map(self, f: Callable[[A], B]) -> Algebra[B, S]:
|
|
287
|
-
return self.fmap(f)
|
|
288
|
-
|
|
396
|
+
|
|
289
397
|
def bimap(self, f: Callable[[A], B], i: Callable[[B], A]) -> Algebra[B, S]:
|
|
290
|
-
|
|
398
|
+
"""Bidirectionally map values with an inverse, updating the state.
|
|
399
|
+
|
|
400
|
+
Applies ``f`` to the success value. The state is pre-mapped with the
|
|
401
|
+
inverse ``i`` via the state's ``map`` method to preserve round-trips.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
f: Forward mapping ``A -> B``.
|
|
405
|
+
i: Inverse mapping ``B -> A`` applied to the state.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
An algebra producing ``B`` while keeping value/state alignment.
|
|
409
|
+
|
|
410
|
+
Note:
|
|
411
|
+
Different subclass of Algebra can override state.map method to change
|
|
412
|
+
the behavior of bimap. For example, ParserState.map will return the
|
|
413
|
+
state unchanged, and GenState.map will apply the inverse map and update
|
|
414
|
+
the next AST node for generation.
|
|
415
|
+
"""
|
|
416
|
+
return self.map(f).map_state(lambda s: s.map(i))
|
|
291
417
|
|
|
292
418
|
def map_error(self, f: Callable[[Optional[Any]], Any]) -> Algebra[A, S]:
|
|
419
|
+
"""Transform the error payload when this algebra fails.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
f: Function applied to the error payload inside ``Left``.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
An algebra that preserves successes and maps failures.
|
|
426
|
+
"""
|
|
293
427
|
def map_error_run(input: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
|
|
294
428
|
parsed = self.run(input, use_cache)
|
|
295
429
|
if isinstance(parsed, Left):
|
|
@@ -298,6 +432,17 @@ class Algebra(Generic[A, S]):
|
|
|
298
432
|
return self.__class__(map_error_run, name=self.name)
|
|
299
433
|
|
|
300
434
|
def flat_map(self, f: Callable[[A], Algebra[B, S]]) -> Algebra[B, S]:
|
|
435
|
+
"""Chain computations where the next algebra depends on the value.
|
|
436
|
+
|
|
437
|
+
On success, passes the produced value to ``f`` to obtain the next
|
|
438
|
+
algebra, then runs it with the resulting state.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
f: Mapper from a value to the next algebra.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
An algebra yielding the result of the chained computation.
|
|
445
|
+
"""
|
|
301
446
|
def flat_map_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
|
|
302
447
|
parsed = self.run(input, use_cache)
|
|
303
448
|
if isinstance(parsed, Right):
|
|
@@ -308,6 +453,18 @@ class Algebra(Generic[A, S]):
|
|
|
308
453
|
|
|
309
454
|
|
|
310
455
|
def or_else(self: Algebra[A, S], other: Algebra[B, S]) -> Algebra[Choice[A, B], S]:
|
|
456
|
+
"""Try this algebra; if it fails uncommitted, try ``other``.
|
|
457
|
+
|
|
458
|
+
If the failure is committed (``committed=True``), the alternative is
|
|
459
|
+
not attempted and the error is propagated.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
other: Fallback algebra to try from the same input state.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
An algebra producing ``Choice.LEFT`` for this success or
|
|
466
|
+
``Choice.RIGHT`` for the other's success.
|
|
467
|
+
"""
|
|
311
468
|
def or_else_run(input: S, use_cache:bool) -> Either[Any, Tuple[Choice[A, B], S]]:
|
|
312
469
|
match self.run(input, use_cache):
|
|
313
470
|
case Right((value, state)):
|
|
@@ -325,27 +482,75 @@ class Algebra(Generic[A, S]):
|
|
|
325
482
|
return self.__class__(or_else_run, name=f'{self.name} | {other.name}') # type: ignore
|
|
326
483
|
|
|
327
484
|
def then_both(self, other: Algebra[B, S]) -> Algebra[Then[A, B], S]:
|
|
485
|
+
"""Sequence two algebras and keep both values.
|
|
486
|
+
|
|
487
|
+
Returns a ``Then(kind=BOTH)`` holding the left and right values.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
other: The algebra to run after this one.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
An algebra producing ``Then(left, right, kind=BOTH)``.
|
|
494
|
+
"""
|
|
328
495
|
def then_both_f(a: A) -> Algebra[Then[A, B], S]:
|
|
329
496
|
def combine(b: B) -> Then[A, B]:
|
|
330
497
|
return Then(left=a, right=b, kind=ThenKind.BOTH)
|
|
331
|
-
return other.
|
|
498
|
+
return other.map(combine)
|
|
332
499
|
return self.flat_map(then_both_f).named(f'{self.name} + {other.name}')
|
|
333
500
|
|
|
334
501
|
def then_left(self, other: Algebra[B, S]) -> Algebra[Then[A, B], S]:
|
|
502
|
+
"""Sequence two algebras, keep the left value in the result.
|
|
503
|
+
|
|
504
|
+
Produces ``Then(kind=LEFT)`` with both values attached.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
other: The algebra to run after this one.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
An algebra producing ``Then(left, right, kind=LEFT)``.
|
|
511
|
+
"""
|
|
335
512
|
def then_left_f(a: A) -> Algebra[Then[A, B], S]:
|
|
336
513
|
def combine(b: B) -> Then[A, B]:
|
|
337
514
|
return Then(left=a, right=b, kind=ThenKind.LEFT)
|
|
338
|
-
return other.
|
|
515
|
+
return other.map(combine)
|
|
339
516
|
return self.flat_map(then_left_f).named(f'{self.name} // {other.name}')
|
|
340
517
|
|
|
341
518
|
def then_right(self, other: Algebra[B, S]) -> Algebra[Then[A, B], S]:
|
|
519
|
+
"""Sequence two algebras, keep the right value in the result.
|
|
520
|
+
|
|
521
|
+
Produces ``Then(kind=RIGHT)`` with both values attached.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
other: The algebra to run after this one.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
An algebra producing ``Then(left, right, kind=RIGHT)``.
|
|
528
|
+
"""
|
|
342
529
|
def then_right_f(a: A) -> Algebra[Then[A, B], S]:
|
|
343
530
|
def combine(b: B) -> Then[A, B]:
|
|
344
531
|
return Then(left=a, right=b, kind=ThenKind.RIGHT)
|
|
345
|
-
return other.
|
|
532
|
+
return other.map(combine)
|
|
346
533
|
return self.flat_map(then_right_f).named(f'{self.name} >> {other.name}')
|
|
347
534
|
|
|
348
535
|
def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Many[A], S]:
|
|
536
|
+
"""Repeat this algebra and collect results into ``Many``.
|
|
537
|
+
|
|
538
|
+
Repeats greedily until failure or no progress. Enforces cardinality
|
|
539
|
+
constraints. If ``at_most`` is ``None``, there is no upper bound.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
at_least: Minimum number of matches required (>= 1).
|
|
543
|
+
at_most: Optional maximum number of matches.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
On success, ``Right((Many(values), state))``.
|
|
547
|
+
Note:
|
|
548
|
+
at_most, if given, is enforced strictly, more than at_most matches
|
|
549
|
+
is treated as an error.
|
|
550
|
+
Raises:
|
|
551
|
+
ValueError: If bounds are invalid (e.g., ``at_least<=0`` or
|
|
552
|
+
``at_most<at_least``).
|
|
553
|
+
"""
|
|
349
554
|
if at_least <=0 or (at_most is not None and at_most < at_least):
|
|
350
555
|
raise ValueError(f"Invalid arguments for many: at_least={at_least}, at_most={at_most}")
|
|
351
556
|
def many_run(input: S, use_cache:bool) -> Either[Any, Tuple[Many[A], S]]:
|
syncraft/ast.py
CHANGED
|
@@ -90,10 +90,28 @@ class Lens(Generic[C, A]):
|
|
|
90
90
|
|
|
91
91
|
@dataclass(frozen=True)
|
|
92
92
|
class Bimap(Generic[A, B]):
|
|
93
|
+
"""A reversible mapping that returns both a forward value and an inverse function.
|
|
94
|
+
|
|
95
|
+
``Bimap`` is like a function ``A -> B`` paired with a way to map a value
|
|
96
|
+
of type ``B`` back into an ``A``. It composes with other ``Bimap``s or a
|
|
97
|
+
``Biarrow`` using ``>>`` and ``<<``-style operations, preserving an
|
|
98
|
+
automatically derived inverse.
|
|
99
|
+
"""
|
|
93
100
|
run_f: Callable[[A], Tuple[B, Callable[[B], A]]]
|
|
94
101
|
def __call__(self, a: A) -> Tuple[B, Callable[[B], A]]:
|
|
102
|
+
"""Apply the mapping to ``a``.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
tuple: ``(forward_value, inverse)`` where ``inverse`` maps
|
|
106
|
+
a compatible ``B`` back into an ``A``.
|
|
107
|
+
"""
|
|
95
108
|
return self.run_f(a)
|
|
96
109
|
def __rshift__(self, other: Bimap[B, C] | Biarrow[B, C]) -> Bimap[A, C]:
|
|
110
|
+
"""Compose this mapping with another mapping/arrow.
|
|
111
|
+
|
|
112
|
+
``self >> other`` first applies ``self``, then ``other``. The produced
|
|
113
|
+
inverse runs ``other``'s inverse followed by ``self``'s inverse.
|
|
114
|
+
"""
|
|
97
115
|
if isinstance(other, Biarrow):
|
|
98
116
|
def biarrow_then_run(a: A) -> Tuple[C, Callable[[C], A]]:
|
|
99
117
|
b, inv1 = self(a)
|
|
@@ -114,6 +132,7 @@ class Bimap(Generic[A, B]):
|
|
|
114
132
|
else:
|
|
115
133
|
raise TypeError(f"Unsupported type for Bimap >>: {type(other)}")
|
|
116
134
|
def __rrshift__(self, other: Bimap[C, A] | Biarrow[C, A]) -> Bimap[C, B]:
|
|
135
|
+
"""Right-composition so arrows or bimaps can be on the left of ``>>``."""
|
|
117
136
|
if isinstance(other, Biarrow):
|
|
118
137
|
def biarrow_then_run(c: C) -> Tuple[B, Callable[[B], C]]:
|
|
119
138
|
a = other.forward(c)
|
|
@@ -137,18 +156,28 @@ class Bimap(Generic[A, B]):
|
|
|
137
156
|
|
|
138
157
|
|
|
139
158
|
@staticmethod
|
|
140
|
-
def const(a: B)->Bimap[B, B]:
|
|
159
|
+
def const(a: B) -> Bimap[B, B]:
|
|
160
|
+
"""Return a bimap that ignores input and always yields ``a``.
|
|
161
|
+
|
|
162
|
+
The inverse is identity for the output type.
|
|
163
|
+
"""
|
|
141
164
|
return Bimap(lambda _: (a, lambda b: b))
|
|
142
165
|
|
|
143
166
|
@staticmethod
|
|
144
|
-
def identity()->Bimap[A, A]:
|
|
167
|
+
def identity() -> Bimap[A, A]:
|
|
168
|
+
"""The identity bimap where forward and inverse are no-ops."""
|
|
145
169
|
return Bimap(lambda a: (a, lambda b: b))
|
|
146
170
|
|
|
147
171
|
@staticmethod
|
|
148
172
|
def when(cond: Callable[[A], bool],
|
|
149
173
|
then: Bimap[A, B],
|
|
150
174
|
otherwise: Optional[Bimap[A, C]] = None) -> Bimap[A, A | B | C]:
|
|
151
|
-
|
|
175
|
+
"""Choose a mapping depending on the input value.
|
|
176
|
+
|
|
177
|
+
Applies ``then`` when ``cond(a)`` is true; otherwise applies
|
|
178
|
+
``otherwise`` if provided, or ``identity``.
|
|
179
|
+
"""
|
|
180
|
+
def when_run(a: A) -> Tuple[A | B | C, Callable[[A | B | C], A]]:
|
|
152
181
|
bimap = then if cond(a) else (otherwise if otherwise is not None else Bimap.identity())
|
|
153
182
|
abc, inv = bimap(a)
|
|
154
183
|
def inv_f(b: Any) -> A:
|
|
@@ -184,11 +213,22 @@ class Reducer(Generic[A, S]):
|
|
|
184
213
|
|
|
185
214
|
@dataclass(frozen=True)
|
|
186
215
|
class AST:
|
|
216
|
+
"""Base class for all Syncraft AST nodes.
|
|
217
|
+
|
|
218
|
+
Nodes implement ``bimap`` to transform contained values while providing an
|
|
219
|
+
inverse that can reconstruct the original node from transformed output.
|
|
220
|
+
"""
|
|
187
221
|
def bimap(self, r: Bimap[Any, Any]=Bimap.identity()) -> Tuple[Any, Callable[[Any], Any]]:
|
|
222
|
+
"""Apply a bimap to this node, returning a value and an inverse.
|
|
223
|
+
|
|
224
|
+
The default behavior defers to the provided mapping ``r`` with the
|
|
225
|
+
node itself as input. The ``r`` only applies to the leaf node of AST tree.
|
|
226
|
+
"""
|
|
188
227
|
return r(self)
|
|
189
228
|
|
|
190
229
|
@dataclass(frozen=True)
|
|
191
230
|
class Nothing(AST):
|
|
231
|
+
"""Singleton sentinel representing the absence of a value in the AST."""
|
|
192
232
|
_instance = None
|
|
193
233
|
def __new__(cls):
|
|
194
234
|
if cls._instance is None:
|
|
@@ -202,9 +242,19 @@ class Nothing(AST):
|
|
|
202
242
|
|
|
203
243
|
@dataclass(frozen=True)
|
|
204
244
|
class Marked(Generic[A], AST):
|
|
245
|
+
"""Annotate a AST node with a name.
|
|
246
|
+
|
|
247
|
+
Used to tag subtrees so they can be collected by name later (e.g., in
|
|
248
|
+
collectors) without altering the structural shape.
|
|
249
|
+
"""
|
|
205
250
|
name: str
|
|
206
251
|
value: A
|
|
207
252
|
def bimap(self, r: Bimap[A, B]=Bimap.identity()) -> Tuple[Marked[B], Callable[[Marked[B]], Marked[A]]]:
|
|
253
|
+
"""Transform the inner value while preserving the mark name.
|
|
254
|
+
|
|
255
|
+
Returns a new ``Marked`` with transformed value and an inverse that
|
|
256
|
+
expects a ``Marked`` to recover the original.
|
|
257
|
+
"""
|
|
208
258
|
v, inner_f = self.value.bimap(r) if isinstance(self.value, AST) else r(self.value)
|
|
209
259
|
return Marked(name=self.name, value=v), lambda b: Marked(name = b.name, value=inner_f(b.value))
|
|
210
260
|
|
|
@@ -214,9 +264,19 @@ class ChoiceKind(Enum):
|
|
|
214
264
|
|
|
215
265
|
@dataclass(frozen=True)
|
|
216
266
|
class Choice(Generic[A, B], AST):
|
|
267
|
+
"""Represent a binary alternative between left and right values.
|
|
268
|
+
|
|
269
|
+
``kind`` indicates which branch was taken, or ``None`` when unknown.
|
|
270
|
+
"""
|
|
217
271
|
kind: Optional[ChoiceKind]
|
|
218
272
|
value: Optional[A | B] = None
|
|
219
273
|
def bimap(self, r: Bimap[A | B, C]=Bimap.identity()) -> Tuple[Optional[C], Callable[[Optional[C]], Choice[A, B]]]:
|
|
274
|
+
"""Map over the held value if present; propagate ``None`` otherwise.
|
|
275
|
+
|
|
276
|
+
The inverse resets ``kind`` to ``None`` to avoid biasing the result.
|
|
277
|
+
When user edit the data we cannot assume which branch the data should go
|
|
278
|
+
back to. Set ``kind`` to ``None`` to indicate this situation.
|
|
279
|
+
"""
|
|
220
280
|
if self.value is None:
|
|
221
281
|
return None, lambda c: replace(self, value=None, kind=None)
|
|
222
282
|
else:
|
|
@@ -225,8 +285,15 @@ class Choice(Generic[A, B], AST):
|
|
|
225
285
|
|
|
226
286
|
@dataclass(frozen=True)
|
|
227
287
|
class Many(Generic[A], AST):
|
|
288
|
+
"""A finite sequence of values within the AST."""
|
|
228
289
|
value: Tuple[A, ...]
|
|
229
290
|
def bimap(self, r: Bimap[A, B]=Bimap.identity()) -> Tuple[List[B], Callable[[List[B]], Many[A]]]:
|
|
291
|
+
"""Map each element to a list and provide an inverse.
|
|
292
|
+
|
|
293
|
+
The inverse accepts a list of transformed elements. If the provided
|
|
294
|
+
list is shorter than the original, only the prefix is used. If longer,
|
|
295
|
+
the extra values are inverted using the last element's inverse.
|
|
296
|
+
"""
|
|
230
297
|
ret = [v.bimap(r) if isinstance(v, AST) else r(v) for v in self.value]
|
|
231
298
|
def inv(bs: List[B]) -> Many[A]:
|
|
232
299
|
if len(bs) <= len(ret):
|
|
@@ -244,6 +311,12 @@ class ThenKind(Enum):
|
|
|
244
311
|
|
|
245
312
|
@dataclass(eq=True, frozen=True)
|
|
246
313
|
class Then(Generic[A, B], AST):
|
|
314
|
+
"""Pair two values with a composition kind (both, left, or right).
|
|
315
|
+
|
|
316
|
+
The ``kind`` determines how values are combined.
|
|
317
|
+
``LEFT``/``RIGHT`` indicate single-sided results; ``BOTH`` flattens both
|
|
318
|
+
sides.
|
|
319
|
+
"""
|
|
247
320
|
kind: ThenKind
|
|
248
321
|
left: A
|
|
249
322
|
right: B
|
|
@@ -259,7 +332,15 @@ class Then(Generic[A, B], AST):
|
|
|
259
332
|
else:
|
|
260
333
|
return 1
|
|
261
334
|
|
|
262
|
-
def bimap(self, r: Bimap[A|B, Any]=Bimap.identity()) -> Tuple[Any | Tuple[Any, ...], Callable[[Any | Tuple[Any, ...]], Then[A, B]]]:
|
|
335
|
+
def bimap(self, r: Bimap[A | B, Any] = Bimap.identity()) -> Tuple[Any | Tuple[Any, ...], Callable[[Any | Tuple[Any, ...]], Then[A, B]]]:
|
|
336
|
+
"""Transform the left/right values according to ``kind``.
|
|
337
|
+
|
|
338
|
+
- ``LEFT``: map and return the left value; inverse sets only ``left``.
|
|
339
|
+
- ``RIGHT``: map and return the right value; inverse sets only ``right``.
|
|
340
|
+
- ``BOTH``: return a flattened tuple of mapped left values followed by
|
|
341
|
+
mapped right values. The inverse expects a tuple whose length equals
|
|
342
|
+
``left.arity() + right.arity()`` and reconstructs the structure.
|
|
343
|
+
"""
|
|
263
344
|
def need_wrap(x: Any) -> bool:
|
|
264
345
|
return not (isinstance(x, Then) and x.kind == ThenKind.BOTH)
|
|
265
346
|
match self.kind:
|
|
@@ -296,9 +377,23 @@ E = TypeVar("E", bound=DataclassInstance)
|
|
|
296
377
|
Collector = Type[E] | Callable[..., E]
|
|
297
378
|
@dataclass(frozen=True)
|
|
298
379
|
class Collect(Generic[A, E], AST):
|
|
380
|
+
"""Apply a collector to a value to build a dataclass-like instance.
|
|
381
|
+
|
|
382
|
+
When the inner value is a ``Then`` and the forward result is a tuple, any
|
|
383
|
+
``Marked`` elements become named arguments to the collector; the remainder
|
|
384
|
+
are passed positionally. The inverse breaks the produced instance back into
|
|
385
|
+
a structure compatible with the original ``Then``.
|
|
386
|
+
"""
|
|
299
387
|
collector: Collector
|
|
300
388
|
value: A
|
|
301
389
|
def bimap(self, r: Bimap[A, B]=Bimap.identity()) -> Tuple[B | E, Callable[[B | E], Collect[A, E]]]:
|
|
390
|
+
"""Map the inner value, collect it, and supply a matching inverse.
|
|
391
|
+
|
|
392
|
+
For multi-field tuples derived from ``Then``, the inverse rebuilds the
|
|
393
|
+
appropriate mix of ``Marked`` and positional elements using the
|
|
394
|
+
collector's dataclass fields. For single-argument collectors, the first
|
|
395
|
+
field of the dataclass is used.
|
|
396
|
+
"""
|
|
302
397
|
|
|
303
398
|
def inv_one_positional(e: E) -> B:
|
|
304
399
|
if not is_dataclass(e):
|
|
@@ -341,6 +436,7 @@ class Collect(Generic[A, E], AST):
|
|
|
341
436
|
#########################################################################################################################
|
|
342
437
|
@dataclass(frozen=True)
|
|
343
438
|
class Token(AST):
|
|
439
|
+
"""Leaf node representing a single token with type and text."""
|
|
344
440
|
token_type: Enum
|
|
345
441
|
text: str
|
|
346
442
|
def __str__(self) -> str:
|
|
@@ -374,6 +470,7 @@ class TokenSpec:
|
|
|
374
470
|
return type_match and value_match
|
|
375
471
|
|
|
376
472
|
|
|
473
|
+
#: Union-like type describing the shape of AST parse results across nodes.
|
|
377
474
|
ParseResult = Union[
|
|
378
475
|
Then['ParseResult[T]', 'ParseResult[T]'],
|
|
379
476
|
Marked['ParseResult[T]'],
|