syncraft 0.2.2__tar.gz → 0.2.4__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 (33) hide show
  1. syncraft-0.2.4/PKG-INFO +113 -0
  2. syncraft-0.2.4/README.md +98 -0
  3. {syncraft-0.2.2 → syncraft-0.2.4}/pyproject.toml +6 -1
  4. syncraft-0.2.4/syncraft/__init__.py +59 -0
  5. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/algebra.py +230 -25
  6. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/ast.py +101 -4
  7. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/constraint.py +41 -0
  8. syncraft-0.2.4/syncraft/finder.py +115 -0
  9. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/generator.py +181 -4
  10. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/parser.py +162 -0
  11. syncraft-0.2.4/syncraft/syntax.py +567 -0
  12. syncraft-0.2.4/syncraft.egg-info/PKG-INFO +113 -0
  13. syncraft-0.2.2/PKG-INFO +0 -34
  14. syncraft-0.2.2/README.md +0 -19
  15. syncraft-0.2.2/syncraft/__init__.py +0 -0
  16. syncraft-0.2.2/syncraft/finder.py +0 -58
  17. syncraft-0.2.2/syncraft/syntax.py +0 -333
  18. syncraft-0.2.2/syncraft.egg-info/PKG-INFO +0 -34
  19. {syncraft-0.2.2 → syncraft-0.2.4}/LICENSE +0 -0
  20. {syncraft-0.2.2 → syncraft-0.2.4}/setup.cfg +0 -0
  21. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/diagnostic.py +0 -0
  22. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/py.typed +0 -0
  23. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/sqlite3.py +0 -0
  24. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/SOURCES.txt +0 -0
  25. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/dependency_links.txt +0 -0
  26. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/requires.txt +0 -0
  27. {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/top_level.txt +0 -0
  28. {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_bimap.py +0 -0
  29. {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_constraint.py +0 -0
  30. {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_find.py +0 -0
  31. {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_parse.py +0 -0
  32. {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_to.py +0 -0
  33. {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_until.py +0 -0
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncraft
3
+ Version: 0.2.4
4
+ Summary: Parser combinator library
5
+ Author-email: Michael Afmokt <michael@esacca.com>
6
+ License-Expression: MIT
7
+ Keywords: parser,combinator,sql,sqlite,generator,printer
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: rich>=14.1.0
12
+ Requires-Dist: rstr>=3.2.2
13
+ Requires-Dist: sqlglot>=27.7.0
14
+ Dynamic: license-file
15
+
16
+ # Syncraft
17
+
18
+ Syncraft is a parser/generator combinator library for Python. It helps you
19
+
20
+ - Build grammars
21
+ - Parse SQL statement to AST
22
+ - Search AST by grammar
23
+ - Convert AST to dataclass
24
+ - Check constraints over the AST/dataclass
25
+ - Change dataclass and convert back to AST
26
+
27
+
28
+ ## Installation
29
+
30
+ ### pip
31
+ ```bash
32
+ pip install syncraft
33
+ ```
34
+
35
+ ### uv
36
+ ```bash
37
+ uv add syncraft
38
+ ```
39
+
40
+ Python 3.10+ is required.
41
+
42
+ ## Quickstart
43
+
44
+ !. Define grammar
45
+
46
+ ```python
47
+ from dataclasses import dataclass
48
+ from syncraft import literal, parse, generate
49
+
50
+ A = literal("a")
51
+ B = literal("b")
52
+ syntax = A + B # sequence
53
+
54
+ ast, _ = parse(syntax, "a b", dialect="sqlite")
55
+ gen, _ = generate(syntax, ast)
56
+ assert ast == gen
57
+ ```
58
+
59
+ Collect parsed pieces into dataclasses using marks and `.to()`:
60
+
61
+ ```python
62
+ from dataclasses import dataclass
63
+ from syncraft import literal
64
+
65
+ @dataclass
66
+ class Pair:
67
+ first: any
68
+ second: any
69
+
70
+ A = literal("a").mark("first")
71
+ B = literal("b").mark("second")
72
+ syntax = (A + B).to(Pair)
73
+
74
+ ast, _ = parse(syntax, "a b", dialect="sqlite")
75
+ value, invert = ast.bimap()
76
+ # value is Pair(first=VAR(a), second=VAR(b))
77
+ round_tripped, _ = generate(syntax, invert(value))
78
+ assert round_tripped == ast
79
+ ```
80
+
81
+ Use the built‑in SQLite grammar snippets to parse statements:
82
+
83
+ ```python
84
+ from syncraft import parse
85
+ from syncraft.sqlite3 import select_stmt
86
+
87
+ ast, _ = parse(select_stmt, "select a from t where a > 1", dialect="sqlite")
88
+ ```
89
+
90
+ ## Core ideas
91
+
92
+ - Syntax describes structure and transforms values; Algebra executes it.
93
+ - AST types: Then, Choice, Many, Marked, Collect, Nothing, Token.
94
+ - Operators: `+` (both), `>>` (keep right), `//` (keep left), `|` (choice), `~` (optional), `many()`, `sep_by()`, `between()`.
95
+ - Error model supports backtracking and commit (`cut()`).
96
+
97
+ ## Documentation
98
+
99
+ - Tutorials and API reference are built with MkDocs. Local preview:
100
+ 1) install dev deps (see `pyproject.toml` dev group)
101
+ 2) activate your venv and run `mkdocs serve`
102
+
103
+ - Version injection: pages can use `{{ version }}`. It is provided by mkdocs-macros via `docs/main.py`, which resolves the version in this order:
104
+ - `[project].version` from `pyproject.toml`
105
+ - installed package metadata (`importlib.metadata.version('syncraft')`)
106
+ - fallback `"0.0.0"`
107
+
108
+ The macros plugin is configured in `mkdocs.yml` with `module_name: docs/main`.
109
+
110
+ ## Contributing / Roadmap
111
+
112
+ - Improve performance and add benchmarks
113
+ - Expand tutorials and SQLite coverage examples
@@ -0,0 +1,98 @@
1
+ # Syncraft
2
+
3
+ Syncraft is a parser/generator combinator library for Python. It helps you
4
+
5
+ - Build grammars
6
+ - Parse SQL statement to AST
7
+ - Search AST by grammar
8
+ - Convert AST to dataclass
9
+ - Check constraints over the AST/dataclass
10
+ - Change dataclass and convert back to AST
11
+
12
+
13
+ ## Installation
14
+
15
+ ### pip
16
+ ```bash
17
+ pip install syncraft
18
+ ```
19
+
20
+ ### uv
21
+ ```bash
22
+ uv add syncraft
23
+ ```
24
+
25
+ Python 3.10+ is required.
26
+
27
+ ## Quickstart
28
+
29
+ !. Define grammar
30
+
31
+ ```python
32
+ from dataclasses import dataclass
33
+ from syncraft import literal, parse, generate
34
+
35
+ A = literal("a")
36
+ B = literal("b")
37
+ syntax = A + B # sequence
38
+
39
+ ast, _ = parse(syntax, "a b", dialect="sqlite")
40
+ gen, _ = generate(syntax, ast)
41
+ assert ast == gen
42
+ ```
43
+
44
+ Collect parsed pieces into dataclasses using marks and `.to()`:
45
+
46
+ ```python
47
+ from dataclasses import dataclass
48
+ from syncraft import literal
49
+
50
+ @dataclass
51
+ class Pair:
52
+ first: any
53
+ second: any
54
+
55
+ A = literal("a").mark("first")
56
+ B = literal("b").mark("second")
57
+ syntax = (A + B).to(Pair)
58
+
59
+ ast, _ = parse(syntax, "a b", dialect="sqlite")
60
+ value, invert = ast.bimap()
61
+ # value is Pair(first=VAR(a), second=VAR(b))
62
+ round_tripped, _ = generate(syntax, invert(value))
63
+ assert round_tripped == ast
64
+ ```
65
+
66
+ Use the built‑in SQLite grammar snippets to parse statements:
67
+
68
+ ```python
69
+ from syncraft import parse
70
+ from syncraft.sqlite3 import select_stmt
71
+
72
+ ast, _ = parse(select_stmt, "select a from t where a > 1", dialect="sqlite")
73
+ ```
74
+
75
+ ## Core ideas
76
+
77
+ - Syntax describes structure and transforms values; Algebra executes it.
78
+ - AST types: Then, Choice, Many, Marked, Collect, Nothing, Token.
79
+ - Operators: `+` (both), `>>` (keep right), `//` (keep left), `|` (choice), `~` (optional), `many()`, `sep_by()`, `between()`.
80
+ - Error model supports backtracking and commit (`cut()`).
81
+
82
+ ## Documentation
83
+
84
+ - Tutorials and API reference are built with MkDocs. Local preview:
85
+ 1) install dev deps (see `pyproject.toml` dev group)
86
+ 2) activate your venv and run `mkdocs serve`
87
+
88
+ - Version injection: pages can use `{{ version }}`. It is provided by mkdocs-macros via `docs/main.py`, which resolves the version in this order:
89
+ - `[project].version` from `pyproject.toml`
90
+ - installed package metadata (`importlib.metadata.version('syncraft')`)
91
+ - fallback `"0.0.0"`
92
+
93
+ The macros plugin is configured in `mkdocs.yml` with `module_name: docs/main`.
94
+
95
+ ## Contributing / Roadmap
96
+
97
+ - Improve performance and add benchmarks
98
+ - Expand tutorials and SQLite coverage examples
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syncraft"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Parser combinator library"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -18,6 +18,11 @@ dependencies = [
18
18
  [dependency-groups]
19
19
  dev = [
20
20
  "pytest>=8.4.1",
21
+ "mkdocs>=1.6.0",
22
+ "mkdocs-material>=9.5.0",
23
+ "mkdocstrings>=0.25.0",
24
+ "mkdocstrings-python>=1.10.0",
25
+ "mkdocs-macros-plugin>=1.0.5",
21
26
  ]
22
27
 
23
28
 
@@ -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
+ ]
@@ -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 post_state(self, f: Callable[[S], S]) -> Algebra[A, S]:
248
- def post_state_run(input: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
249
- match self.run(input, use_cache):
250
- case Right((value, state)):
251
- return Right((value, f(state)))
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
- def pre_state(self, f: Callable[[S], S]) -> Algebra[A, S]:
259
- def pre_state_run(state: S, use_cache:bool) -> Either[Any, Tuple[A, S]]:
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__(pre_state_run, name=self.name)
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 fmap(self, f: Callable[[A], B]) -> Algebra[B, S]:
277
- def fmap_run(input: S, use_cache:bool) -> Either[Any, Tuple[B, S]]:
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__(fmap_run, name=self.name) # type: ignore
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
- return self.fmap(f).pre_state(lambda s: s.map(i))
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.fmap(combine)
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.fmap(combine)
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.fmap(combine)
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]]: