syncraft 0.2.5__py3-none-any.whl → 0.2.7__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 CHANGED
@@ -1,32 +1,49 @@
1
1
  from .syntax import (
2
+ Description,
2
3
  Syntax,
4
+ regex,
5
+ literal,
3
6
  choice,
4
7
  lazy,
5
8
  success,
6
9
  fail,
10
+ when,
7
11
  run,
8
12
  )
13
+ from .walker import (
14
+ walk,
15
+ )
16
+ from .algebra import (
17
+ Algebra,
18
+ Error,
19
+ Left,
20
+ Right,
21
+ Either,
22
+ )
9
23
  from .parser import (
24
+ Parser,
10
25
  parse,
11
26
  sqlglot,
12
27
  token,
13
28
  identifier,
14
29
  variable,
15
- literal,
16
30
  number,
17
31
  string,
18
- regex,
19
- until,
20
32
  )
21
33
  from .generator import (
34
+ Generator,
22
35
  generate,
36
+ generate_with,
37
+ validate,
23
38
  )
24
39
  from .finder import (
40
+ Finder,
25
41
  find,
26
42
  matches,
27
43
  anything,
28
44
  )
29
45
  from .constraint import (
46
+ FrozenDict,
30
47
  Constraint,
31
48
  Quantifier,
32
49
  forall,
@@ -34,6 +51,8 @@ from .constraint import (
34
51
  )
35
52
  from .ast import (
36
53
  AST,
54
+ Bimap,
55
+ Biarrow,
37
56
  Token,
38
57
  Then,
39
58
  ThenKind,
@@ -45,15 +64,17 @@ from .ast import (
45
64
  )
46
65
 
47
66
  __all__ = [
67
+ # algebra
68
+ "Algebra", "Error", "Left", "Right", "Either",
48
69
  # syntax & core
49
- "Syntax", "choice", "lazy", "success", "fail", "run",
70
+ "Syntax", "choice", "lazy", "success", "fail", "run", "Description", "when",
50
71
  # parsing/generation helpers
51
- "parse", "sqlglot", "token", "identifier", "variable", "literal", "number", "string", "regex", "until",
52
- "generate",
72
+ "parse", "sqlglot", "token", "identifier", "variable", "literal", "number", "string", "regex", "walk",
73
+ "generate", "generate_with", "validate", "Parser", "Generator",
53
74
  # finder
54
- "find", "matches", "anything",
75
+ "find", "matches", "anything", "Finder",
55
76
  # constraints
56
- "Constraint", "Quantifier", "forall", "exists",
77
+ "Constraint", "Quantifier", "forall", "exists", "FrozenDict",
57
78
  # ast
58
- "AST", "Token", "Then", "ThenKind", "Choice", "ChoiceKind", "Many", "Marked", "Collect",
79
+ "AST", "Token", "Then", "ThenKind", "Choice", "ChoiceKind", "Many", "Marked", "Collect", "Bimap", "Biarrow"
59
80
  ]
syncraft/algebra.py CHANGED
@@ -1,16 +1,14 @@
1
1
  from __future__ import annotations
2
2
  from typing import (
3
3
  Optional, List, Any, TypeVar, Generic, Callable, Tuple, cast,
4
- Dict, Type, ClassVar, Hashable
4
+ Dict, Type, Hashable, Generator
5
5
  )
6
6
 
7
- import traceback
8
7
  from dataclasses import dataclass, replace
9
- from weakref import WeakKeyDictionary
10
- from syncraft.ast import ThenKind, Then, Choice, Many, ChoiceKind, shallow_dict
8
+ from syncraft.ast import ThenKind, Then, Choice, Many, ChoiceKind, shallow_dict, SyncraftError
9
+ from syncraft.cache import Cache
11
10
  from syncraft.constraint import Bindable
12
-
13
-
11
+ from rich import print
14
12
 
15
13
 
16
14
  S = TypeVar('S', bound=Bindable)
@@ -19,7 +17,6 @@ A = TypeVar('A') # Result type
19
17
  B = TypeVar('B') # Mapped result type
20
18
 
21
19
 
22
- InProgress = object() # Marker for in-progress state, used to prevent re-entrance in recursive calls
23
20
  L = TypeVar('L') # Left type for combined results
24
21
  R = TypeVar('R') # Right type for combined results
25
22
 
@@ -38,7 +35,9 @@ class Right(Either[L, R]):
38
35
  value: R
39
36
 
40
37
 
41
-
38
+ @dataclass(frozen=True)
39
+ class Incomplete(Generic[S]):
40
+ state: S
42
41
 
43
42
  @dataclass(frozen=True)
44
43
  class Error:
@@ -47,7 +46,7 @@ class Error:
47
46
  error: Optional[Any] = None
48
47
  state: Optional[Any] = None
49
48
  committed: bool = False
50
- stack: Optional[str] = None
49
+ fatal: bool = False
51
50
  previous: Optional[Error] = None
52
51
 
53
52
  def attach( self,
@@ -76,147 +75,71 @@ class Error:
76
75
  @dataclass(frozen=True)
77
76
  class Algebra(Generic[A, S]):
78
77
  ######################################################## shared among all subclasses ########################################################
79
- run_f: Callable[[S, bool], Either[Any, Tuple[A, S]]]
78
+ run_f: Callable[[S, bool], Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]]
80
79
  name: Hashable
81
- _cache: ClassVar[WeakKeyDictionary[Any, Dict[Any, object | Either[Any, Tuple[Any, Any]]]]] = WeakKeyDictionary()
80
+ cache: Cache[ S, Either[Any, Tuple[A, S]] ]
82
81
 
83
82
  @classmethod
84
- def state(cls, *args:Any, **kwargs:Any)->Optional[S]:
83
+ def state(cls, **kwargs:Any)->Optional[S]:
85
84
  return None
86
85
 
87
86
  def named(self, name: Hashable) -> 'Algebra[A, S]':
88
87
  return replace(self, name=name)
89
-
90
- def __post_init__(self)-> None:
91
- self._cache.setdefault(self.run_f, dict())
92
88
 
93
- def __call__(self, input: S, use_cache: bool) -> Either[Any, Tuple[A, S]]:
89
+ def __call__(self, input: S, use_cache: bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
94
90
  return self.run(input, use_cache=use_cache)
95
91
 
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
- """
128
- cache = self._cache[self.run_f]
129
- assert cache is not None, "Cache should be initialized in __post_init__"
130
- if input in cache:
131
- v = cache.get(input, None)
132
- if v is InProgress:
133
- return Left(
134
- Error(
135
- message="Left-recursion detected in parser",
136
- this=self,
137
- state=input
138
- ))
139
- else:
140
- return cast(Either[Error, Tuple[A, S]], v)
141
- try:
142
- cache[input] = InProgress
143
- result = self.run_f(input, use_cache)
144
- cache[input] = result
145
- if not use_cache:
146
- cache.pop(input, None) # Clear the cache entry if not using cache
147
- if isinstance(result, Left):
148
- if isinstance(result.value, Error):
149
- result = Left(result.value.attach(this=self, state=input))
150
- except Exception as e:
151
- cache.pop(input, None) # Clear the cache entry on exception
152
- # traceback.print_exc()
153
- # print(f"Exception from self.run(S): {e}")
154
- return Left(
155
- Error(
156
- message="Exception from self.run(S): {e}",
157
- this=self,
158
- state=input,
159
- error=e,
160
- stack=traceback.format_exc()
161
- ))
162
- return result
92
+ def run(self, input: S, use_cache: bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
93
+ return (yield from self.cache.gen(self.run_f, input, use_cache))
94
+
163
95
 
164
96
  def as_(self, typ: Type[B])->B:
165
97
  return cast(typ, self) # type: ignore
166
98
 
167
99
  @classmethod
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
- """
180
- def lazy_run(input: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
181
- return thunk().run(input, use_cache)
182
- return cls(lazy_run, name=cls.__name__ + '.lazy')
100
+ def lazy(cls,
101
+ thunk: Callable[[], Algebra[A, S]],
102
+ *,
103
+ cache: Cache) -> Algebra[A, S]:
104
+ def algebra_lazy_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
105
+ alg = thunk()
106
+ # print('--' * 20, "Algebra.lazy.algebra_lazy_run", '--' * 20)
107
+ # print('thunk', thunk, id(thunk))
108
+ # print('input', input, id(input))
109
+ # print('alg', alg, id(alg))
110
+ result = yield from alg.run(input, use_cache)
111
+ return result
112
+ return cls(algebra_lazy_run, name=cls.__name__ + '.lazy', cache=cache)
183
113
 
184
114
  @classmethod
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
- """
194
- def fail_run(input: S, use_cache:bool) -> Either[Any, Tuple[Any, S]]:
115
+ def fail(cls,
116
+ error: Any,
117
+ *,
118
+ cache: Cache) -> Algebra[Any, S]:
119
+ def fail_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
120
+ yield from ()
195
121
  return Left(Error(
196
122
  error=error,
197
123
  this=cls,
198
124
  state=input
199
125
  ))
200
- return cls(fail_run, name=cls.__name__ + '.fail')
126
+ return cls(fail_run, name=cls.__name__ + '.fail', cache=cache)
201
127
 
202
128
  @classmethod
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
- """
214
- def success_run(input: S, use_cache:bool) -> Either[Any, Tuple[Any, S]]:
129
+ def success(cls,
130
+ value: Any,
131
+ *,
132
+ cache: Cache) -> Algebra[Any, S]:
133
+ def success_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
134
+ yield from ()
215
135
  return Right((value, input))
216
- return cls(success_run, name=cls.__name__ + '.success')
136
+ return cls(success_run, name=cls.__name__ + '.success', cache=cache)
217
137
 
218
138
  @classmethod
219
- def factory(cls, name: str, *args: Any, **kwargs: Any) -> Algebra[A, S]:
139
+ def factory(cls,
140
+ name: str,
141
+ *args: Any,
142
+ **kwargs: Any) -> Algebra[A, S]:
220
143
  """Call a named class method to construct an algebra.
221
144
 
222
145
  Args:
@@ -232,10 +155,29 @@ class Algebra(Generic[A, S]):
232
155
  """
233
156
  method = getattr(cls, name, None)
234
157
  if method is None or not callable(method):
235
- raise ValueError(f"Method {name} is not defined in {cls.__name__}")
158
+ raise SyncraftError(f"Method {name} is not defined in {cls.__name__}", offending=method, expect='callable')
236
159
  return cast(Algebra[A, S], method(*args, **kwargs))
237
160
 
161
+ def fatal(self) -> Algebra[A, S]:
162
+ """Commit this branch by marking failures as fatal.
238
163
 
164
+ Converts downstream errors into fatal errors (``fatal=True``),
165
+ which prevents alternatives from being tried in ``or_else``.
166
+
167
+ Returns:
168
+ An algebra that fails fatally on errors produced by this one.
169
+ """
170
+ def fail_all_error(e: Any) -> Error:
171
+ match e:
172
+ case Error():
173
+ return replace(e, fatal=True)
174
+ case _:
175
+ return Error(
176
+ error=e,
177
+ this=self,
178
+ fatal=True
179
+ )
180
+ return self.map_error(fail_all_error)
239
181
 
240
182
  def cut(self) -> Algebra[A, S]:
241
183
  """Commit this branch by marking failures as committed.
@@ -266,8 +208,9 @@ class Algebra(Generic[A, S]):
266
208
  Left[Any, Tuple[A, S]],
267
209
  Any
268
210
  ],
269
- Either[Any, Tuple[B, S]]],
270
- ctx: Optional[Any] = None) -> Algebra[A | B, S]:
211
+ Either[Any, Tuple[B, S]]
212
+ ],
213
+ ctx: Optional[Any] = None) -> Algebra[A | B, S]:
271
214
  """Run a handler only when this algebra fails.
272
215
 
273
216
  Args:
@@ -278,12 +221,13 @@ class Algebra(Generic[A, S]):
278
221
  An algebra that intercepts failures and can recover or transform them.
279
222
  """
280
223
  assert callable(func), "func must be callable"
281
- def fail_run(input: S, use_cache:bool) -> Either[Any, Tuple[A | B, S]]:
282
- result = self.run(input, use_cache)
224
+ def fail_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A|B, S]]]:
225
+ result = yield from self.run(input, use_cache)
283
226
  if isinstance(result, Left):
284
227
  return cast(Either[Any, Tuple[A | B, S]], func(self, input, result, ctx))
285
- return cast(Either[Any, Tuple[A | B, S]], result)
286
- return self.__class__(fail_run, name=self.name) # type: ignore
228
+ else:
229
+ return cast(Either[Any, Tuple[A | B, S]], result)
230
+ return self.__class__(fail_run, name=self.name, cache=self.cache) # type: ignore
287
231
 
288
232
  def on_success(self,
289
233
  func: Callable[
@@ -293,8 +237,9 @@ class Algebra(Generic[A, S]):
293
237
  Right[Any, Tuple[A, S]],
294
238
  Any
295
239
  ],
296
- Either[Any, Tuple[B, S]]],
297
- ctx: Optional[Any] = None) -> Algebra[A | B, S]:
240
+ Either[Any, Tuple[B, S]]
241
+ ],
242
+ ctx: Optional[Any] = None) -> Algebra[A | B, S]:
298
243
  """Run a handler only when this algebra succeeds.
299
244
 
300
245
  Args:
@@ -305,41 +250,14 @@ class Algebra(Generic[A, S]):
305
250
  An algebra that can transform or post-process successes.
306
251
  """
307
252
  assert callable(func), "func must be callable"
308
- def success_run(input: S, use_cache:bool) -> Either[Any, Tuple[A | B, S]]:
309
- result = self.run(input, use_cache)
253
+ def success_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A|B, S]]]:
254
+ result = yield from self.run(input, use_cache)
310
255
  if isinstance(result, Right):
311
256
  return cast(Either[Any, Tuple[A | B, S]], func(self, input, result, ctx))
312
- return cast(Either[Any, Tuple[A | B, S]], result)
313
- return self.__class__(success_run, name=self.name) # type: ignore
314
-
315
- def debug(self,
316
- label: str,
317
- formatter: Optional[Callable[[
318
- Algebra[Any, S],
319
- S,
320
- Either[Any, Tuple[Any, S]]], None]]=None) -> Algebra[A, S]:
321
- def default_formatter(alg: Algebra[Any, S], input: S, result: Either[Any, Tuple[Any, S]]) -> None:
322
- print(f"Debug: {'*' * 40} {alg.name} - State {'*' * 40}")
323
- print(input)
324
- print(f"Debug: {'~' * 40} (Result, State) {'~' * 40}")
325
- print(result)
326
- print()
327
- print()
328
- lazy_self: Algebra[A, S]
329
- def debug_run(input: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
330
- result = self.run(input, use_cache)
331
- try:
332
- if formatter is not None:
333
- formatter(lazy_self, input, result)
334
- else:
335
- default_formatter(lazy_self, input, result)
336
- except Exception as e:
337
- traceback.print_exc()
338
- print(f"Error occurred while formatting debug information: {e}")
339
- finally:
340
- return result
341
- lazy_self = self.__class__(debug_run, name=label)
342
- return lazy_self
257
+ else:
258
+ return cast(Either[Any, Tuple[A | B, S]], result)
259
+ return self.__class__(success_run, name=self.name, cache=self.cache) # type: ignore
260
+
343
261
 
344
262
  ######################################################## map on state ###########################################
345
263
  def map_state(self, f: Callable[[S], S]) -> Algebra[A, S]:
@@ -351,30 +269,12 @@ class Algebra(Generic[A, S]):
351
269
  Returns:
352
270
  An algebra that runs with ``f(state)``.
353
271
  """
354
- def map_state_run(state: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
355
- return self.run(f(state), use_cache)
356
- return self.__class__(map_state_run, name=self.name)
272
+ def map_state_run(state: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
273
+ result = yield from self.run(f(state), use_cache)
274
+ return result
275
+ return self.__class__(map_state_run, name=self.name, cache=self.cache)
357
276
 
358
277
 
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
- """
368
- def map_all_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
369
- match self.run(input, use_cache):
370
- case Right((value, state)):
371
- new_value, new_state = f(value, state)
372
- return Right((new_value, new_state))
373
- case Left(err):
374
- return Left(err)
375
- case x:
376
- raise ValueError(f"Unexpected result from self.run {x}")
377
- return self.__class__(map_all_run, name=self.name) # type: ignore
378
278
  ######################################################## fundamental combinators ############################################
379
279
  def map(self, f: Callable[[A], B]) -> Algebra[B, S]:
380
280
  """Transform the success value, leaving the state unchanged.
@@ -385,13 +285,13 @@ class Algebra(Generic[A, S]):
385
285
  Returns:
386
286
  An algebra that yields ``B`` with the same resulting state.
387
287
  """
388
- def map_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
389
- parsed = self.run(input, use_cache)
288
+ def map_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[B, S]]]:
289
+ parsed = yield from self.run(input, use_cache)
390
290
  if isinstance(parsed, Right):
391
291
  return Right((f(parsed.value[0]), parsed.value[1]))
392
292
  else:
393
293
  return cast(Either[Any, Tuple[B, S]], parsed)
394
- return self.__class__(map_run, name=self.name) # type: ignore
294
+ return self.__class__(map_run, name=self.name, cache=self.cache) # type: ignore
395
295
 
396
296
 
397
297
  def bimap(self, f: Callable[[A], B], i: Callable[[B], A]) -> Algebra[B, S]:
@@ -424,12 +324,13 @@ class Algebra(Generic[A, S]):
424
324
  Returns:
425
325
  An algebra that preserves successes and maps failures.
426
326
  """
427
- def map_error_run(input: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
428
- parsed = self.run(input, use_cache)
327
+ def map_error_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
328
+ parsed = yield from self.run(input, use_cache)
429
329
  if isinstance(parsed, Left):
430
330
  return Left(f(parsed.value))
431
- return parsed
432
- return self.__class__(map_error_run, name=self.name)
331
+ else:
332
+ return parsed
333
+ return self.__class__(map_error_run, name=self.name, cache=self.cache)
433
334
 
434
335
  def flat_map(self, f: Callable[[A], Algebra[B, S]]) -> Algebra[B, S]:
435
336
  """Chain computations where the next algebra depends on the value.
@@ -443,13 +344,31 @@ class Algebra(Generic[A, S]):
443
344
  Returns:
444
345
  An algebra yielding the result of the chained computation.
445
346
  """
446
- def flat_map_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
447
- parsed = self.run(input, use_cache)
347
+ def flat_map_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[B, S]]]:
348
+ parsed = yield from self.run(input, use_cache)
448
349
  if isinstance(parsed, Right):
449
- return f(parsed.value[0]).run(parsed.value[1], use_cache)
350
+ result = yield from f(parsed.value[0]).run(parsed.value[1], use_cache)
351
+ return result
450
352
  else:
451
353
  return cast(Either[Any, Tuple[B, S]], parsed)
452
- return self.__class__(flat_map_run, name=self.name) # type: ignore
354
+ return self.__class__(flat_map_run, name=self.name, cache=self.cache) # type: ignore
355
+
356
+ def map_all(self, f: Callable[[A, S], Tuple[B, S]]) -> Algebra[B, S]:
357
+ """Map both the produced value and the resulting state on success.
358
+
359
+ Args:
360
+ f: Function mapping ``(value, state)`` to ``(new_value, new_state)``.
361
+
362
+ Returns:
363
+ An algebra producing the transformed value and state.
364
+ """
365
+ def map_all_f(a : A) -> Algebra[B, S]:
366
+ def map_all_run_f(input:S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[B, S]]]:
367
+ yield from ()
368
+ return Right(f(a, input))
369
+ return self.__class__(map_all_run_f, name=self.name, cache=self.cache) # type: ignore
370
+ return self.flat_map(map_all_f)
371
+
453
372
 
454
373
 
455
374
  def or_else(self: Algebra[A, S], other: Algebra[B, S]) -> Algebra[Choice[A, B], S]:
@@ -465,21 +384,26 @@ class Algebra(Generic[A, S]):
465
384
  An algebra producing ``Choice.LEFT`` for this success or
466
385
  ``Choice.RIGHT`` for the other's success.
467
386
  """
468
- def or_else_run(input: S, use_cache:bool) -> Either[Any, Tuple[Choice[A, B], S]]:
469
- match self.run(input, use_cache):
387
+ def or_else_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[Choice[A, B], S]]]:
388
+ self_result = yield from self.run(input, use_cache)
389
+ match self_result:
470
390
  case Right((value, state)):
471
391
  return Right((Choice(kind=ChoiceKind.LEFT, value=value), state))
472
392
  case Left(err):
473
- if isinstance(err, Error) and err.committed:
474
- return Left(replace(err, committed=False))
475
- match other.run(input, use_cache):
393
+ if isinstance(err, Error):
394
+ if err.fatal:
395
+ return Left(err)
396
+ elif err.committed:
397
+ return Left(replace(err, committed=False))
398
+ other_result = yield from other.run(input, use_cache)
399
+ match other_result:
476
400
  case Right((other_value, other_state)):
477
401
  return Right((Choice(kind=ChoiceKind.RIGHT, value=other_value), other_state))
478
402
  case Left(other_err):
479
403
  return Left(other_err)
480
- raise TypeError(f"Unexpected result type from {other}")
481
- raise TypeError(f"Unexpected result type from {self}")
482
- return self.__class__(or_else_run, name=f'{self.name} | {other.name}') # type: ignore
404
+ raise SyncraftError(f"Unexpected result type from {other}", offending=other_result, expect=(Left, Right))
405
+ raise SyncraftError(f"Unexpected result type from {self}", offending=self_result, expect=(Left, Right))
406
+ return self.__class__(or_else_run, name=f'{self.name} | {other.name}', cache=self.cache | other.cache) # type: ignore
483
407
 
484
408
  def then_both(self, other: Algebra[B, S]) -> Algebra[Then[A, B], S]:
485
409
  """Sequence two algebras and keep both values.
@@ -496,7 +420,8 @@ class Algebra(Generic[A, S]):
496
420
  def combine(b: B) -> Then[A, B]:
497
421
  return Then(left=a, right=b, kind=ThenKind.BOTH)
498
422
  return other.map(combine)
499
- return self.flat_map(then_both_f).named(f'{self.name} + {other.name}')
423
+ ret = self.flat_map(then_both_f).named(f'{self.name} + {other.name}')
424
+ return replace(ret, cache=self.cache | other.cache)
500
425
 
501
426
  def then_left(self, other: Algebra[B, S]) -> Algebra[Then[A, B], S]:
502
427
  """Sequence two algebras, keep the left value in the result.
@@ -513,7 +438,8 @@ class Algebra(Generic[A, S]):
513
438
  def combine(b: B) -> Then[A, B]:
514
439
  return Then(left=a, right=b, kind=ThenKind.LEFT)
515
440
  return other.map(combine)
516
- return self.flat_map(then_left_f).named(f'{self.name} // {other.name}')
441
+ ret = self.flat_map(then_left_f).named(f'{self.name} // {other.name}')
442
+ return replace(ret, cache=self.cache | other.cache)
517
443
 
518
444
  def then_right(self, other: Algebra[B, S]) -> Algebra[Then[A, B], S]:
519
445
  """Sequence two algebras, keep the right value in the result.
@@ -530,7 +456,8 @@ class Algebra(Generic[A, S]):
530
456
  def combine(b: B) -> Then[A, B]:
531
457
  return Then(left=a, right=b, kind=ThenKind.RIGHT)
532
458
  return other.map(combine)
533
- return self.flat_map(then_right_f).named(f'{self.name} >> {other.name}')
459
+ ret = self.flat_map(then_right_f).named(f'{self.name} >> {other.name}')
460
+ return replace(ret, cache=self.cache | other.cache)
534
461
 
535
462
  def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Many[A], S]:
536
463
  """Repeat this algebra and collect results into ``Many``.
@@ -552,18 +479,20 @@ class Algebra(Generic[A, S]):
552
479
  ``at_most<at_least``).
553
480
  """
554
481
  if at_least <=0 or (at_most is not None and at_most < at_least):
555
- raise ValueError(f"Invalid arguments for many: at_least={at_least}, at_most={at_most}")
556
- def many_run(input: S, use_cache:bool) -> Either[Any, Tuple[Many[A], S]]:
482
+ raise SyncraftError(f"Invalid arguments for many: at_least={at_least}, at_most={at_most}", offending=(at_least, at_most), expect="at_least>0 and (at_most is None or at_most>=at_least)")
483
+ def many_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[Many[A], S]]]:
557
484
  ret: List[A] = []
558
485
  current_input = input
559
486
  while True:
560
- match self.run(current_input, use_cache):
487
+ result = yield from self.run(current_input, use_cache)
488
+ match result:
561
489
  case Left(_):
562
490
  break
563
491
  case Right((value, next_input)):
564
- ret.append(value)
565
492
  if next_input == current_input:
566
493
  break # No progress, stop to avoid infinite loop
494
+ else:
495
+ ret.append(value)
567
496
  current_input = next_input
568
497
  if at_most is not None and len(ret) > at_most:
569
498
  return Left(Error(
@@ -578,7 +507,7 @@ class Algebra(Generic[A, S]):
578
507
  state=current_input
579
508
  ))
580
509
  return Right((Many(value=tuple(ret)), current_input))
581
- return self.__class__(many_run, name=f'*({self.name})') # type: ignore
510
+ return self.__class__(many_run, name=f'*({self.name})', cache=self.cache) # type: ignore
582
511
 
583
512
 
584
513