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.
- syncraft-0.2.4/PKG-INFO +113 -0
- syncraft-0.2.4/README.md +98 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/pyproject.toml +6 -1
- syncraft-0.2.4/syncraft/__init__.py +59 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/algebra.py +230 -25
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/ast.py +101 -4
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/constraint.py +41 -0
- syncraft-0.2.4/syncraft/finder.py +115 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/generator.py +181 -4
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/parser.py +162 -0
- syncraft-0.2.4/syncraft/syntax.py +567 -0
- syncraft-0.2.4/syncraft.egg-info/PKG-INFO +113 -0
- syncraft-0.2.2/PKG-INFO +0 -34
- syncraft-0.2.2/README.md +0 -19
- syncraft-0.2.2/syncraft/__init__.py +0 -0
- syncraft-0.2.2/syncraft/finder.py +0 -58
- syncraft-0.2.2/syncraft/syntax.py +0 -333
- syncraft-0.2.2/syncraft.egg-info/PKG-INFO +0 -34
- {syncraft-0.2.2 → syncraft-0.2.4}/LICENSE +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/setup.cfg +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/diagnostic.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/py.typed +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft/sqlite3.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/SOURCES.txt +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/dependency_links.txt +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/requires.txt +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/syncraft.egg-info/top_level.txt +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_bimap.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_constraint.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_find.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_parse.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_to.py +0 -0
- {syncraft-0.2.2 → syncraft-0.2.4}/tests/test_until.py +0 -0
syncraft-0.2.4/PKG-INFO
ADDED
|
@@ -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
|
syncraft-0.2.4/README.md
ADDED
|
@@ -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.
|
|
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
|
|
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]]:
|