syncraft 0.2.5__py3-none-any.whl → 0.2.6__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 +30 -9
- syncraft/algebra.py +143 -214
- syncraft/ast.py +62 -7
- syncraft/cache.py +113 -0
- syncraft/constraint.py +184 -134
- syncraft/dev.py +9 -0
- syncraft/finder.py +17 -12
- syncraft/generator.py +80 -78
- syncraft/lexer.py +131 -0
- syncraft/parser.py +75 -224
- syncraft/syntax.py +187 -100
- syncraft/utils.py +214 -0
- syncraft/walker.py +147 -0
- syncraft-0.2.6.dist-info/METADATA +56 -0
- syncraft-0.2.6.dist-info/RECORD +20 -0
- syncraft/diagnostic.py +0 -70
- syncraft-0.2.5.dist-info/METADATA +0 -113
- syncraft-0.2.5.dist-info/RECORD +0 -16
- {syncraft-0.2.5.dist-info → syncraft-0.2.6.dist-info}/WHEEL +0 -0
- {syncraft-0.2.5.dist-info → syncraft-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {syncraft-0.2.5.dist-info → syncraft-0.2.6.dist-info}/top_level.txt +0 -0
syncraft/utils.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Tuple, Any, Set, Optional
|
|
3
|
+
from sqlglot.expressions import Expression
|
|
4
|
+
from syncraft.syntax import Syntax
|
|
5
|
+
from syncraft.algebra import Left, Right, Error, Either, Algebra
|
|
6
|
+
from syncraft.parser import ParserState, Token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def rich_error(err: Error)->None:
|
|
10
|
+
try:
|
|
11
|
+
from rich import print
|
|
12
|
+
from rich.table import Table as RichTable
|
|
13
|
+
lst = err.to_list()
|
|
14
|
+
leaf = lst[0]
|
|
15
|
+
tbl = RichTable(title="Parser Error", show_lines=True)
|
|
16
|
+
tbl.add_column("Leaf Parser Field", style="blue")
|
|
17
|
+
tbl.add_column("Leaf Parser Value", style="yellow")
|
|
18
|
+
flds: Set[str] = set(leaf.keys())
|
|
19
|
+
for fld in sorted(flds):
|
|
20
|
+
leaf_value = leaf.get(fld, "N/A")
|
|
21
|
+
tbl.add_row(f"{fld}", f"{leaf_value}")
|
|
22
|
+
print(tbl)
|
|
23
|
+
except ImportError:
|
|
24
|
+
print(err)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rich_parser(p: Syntax)-> None:
|
|
28
|
+
try:
|
|
29
|
+
from rich import print
|
|
30
|
+
print("Parser Debug Information:")
|
|
31
|
+
print(repr(p))
|
|
32
|
+
except ImportError:
|
|
33
|
+
print(p)
|
|
34
|
+
|
|
35
|
+
def rich_debug(this: Algebra[Any, ParserState[Any]],
|
|
36
|
+
state: ParserState[Any],
|
|
37
|
+
result: Either[Any, Tuple[Any, ParserState[Any]]])-> None:
|
|
38
|
+
try:
|
|
39
|
+
from rich import print
|
|
40
|
+
from rich.table import Table as RichTable
|
|
41
|
+
def value_to_str(value: Any, prefix:str='') -> str:
|
|
42
|
+
if isinstance(value, (tuple, list)):
|
|
43
|
+
if len(value) == 0:
|
|
44
|
+
return prefix + str(value)
|
|
45
|
+
else:
|
|
46
|
+
return '\n'.join(value_to_str(item, prefix=prefix+' - ') for item in value)
|
|
47
|
+
else:
|
|
48
|
+
if isinstance(value, Expression):
|
|
49
|
+
return prefix + value.sql()
|
|
50
|
+
elif isinstance(value, Token):
|
|
51
|
+
return prefix + f"{value.token_type.name}({value.text})"
|
|
52
|
+
elif isinstance(value, Syntax):
|
|
53
|
+
return prefix + (value.meta.name or 'N/A')
|
|
54
|
+
else:
|
|
55
|
+
return prefix + str(value)
|
|
56
|
+
|
|
57
|
+
tbl = RichTable(title=f"Debug: {this.name}", show_lines=True)
|
|
58
|
+
tbl.add_column("Parser", style="blue")
|
|
59
|
+
tbl.add_column("Old State", style="cyan")
|
|
60
|
+
tbl.add_column("Result", style="magenta")
|
|
61
|
+
tbl.add_column("New State", style="green")
|
|
62
|
+
tbl.add_column("Consumed", style="green")
|
|
63
|
+
if isinstance(result, Left):
|
|
64
|
+
tbl.add_row(value_to_str(this), value_to_str(state), value_to_str(result.value), 'N/A', 'N/A')
|
|
65
|
+
else:
|
|
66
|
+
assert isinstance(result, Right), f"Expected result to be a Right value, got {type(result)}, {result}"
|
|
67
|
+
value, new_state = result.value
|
|
68
|
+
tbl.add_row(value_to_str(this),
|
|
69
|
+
value_to_str(state),
|
|
70
|
+
value_to_str(value),
|
|
71
|
+
value_to_str(new_state))
|
|
72
|
+
|
|
73
|
+
print(tbl)
|
|
74
|
+
except ImportError:
|
|
75
|
+
print(this)
|
|
76
|
+
print(state)
|
|
77
|
+
print(result)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def syntax2svg(syntax: Syntax[Any, Any]) -> Optional[str]:
|
|
83
|
+
try:
|
|
84
|
+
from railroad import Diagram, Terminal, Sequence, Choice, OneOrMore, Comment, Group, Optional as RROptional
|
|
85
|
+
def to_railroad(s: Syntax[Any, Any]):
|
|
86
|
+
meta = s.meta
|
|
87
|
+
if meta is None or meta.name is None:
|
|
88
|
+
return Terminal(str(s))
|
|
89
|
+
children = [to_railroad(p) for p in meta.parameter]
|
|
90
|
+
if meta.name in ('>>', '//', '+'):
|
|
91
|
+
assert len(children) == 2
|
|
92
|
+
return Sequence(children[0], children[1])
|
|
93
|
+
elif meta.name == '|':
|
|
94
|
+
assert len(children) == 2
|
|
95
|
+
return Choice(0, children[0], children[1])
|
|
96
|
+
elif meta.name in ('*',):
|
|
97
|
+
assert len(children) == 1
|
|
98
|
+
return OneOrMore(children[0])
|
|
99
|
+
elif meta.name in ('~',):
|
|
100
|
+
assert len(children) == 1
|
|
101
|
+
return RROptional(children[0])
|
|
102
|
+
elif meta.name.startswith('token'):
|
|
103
|
+
return Terminal(meta.name)
|
|
104
|
+
elif meta.name == "sep_by":
|
|
105
|
+
assert len(children) == 2
|
|
106
|
+
return Sequence(children[0], OneOrMore(Sequence(children[1], children[0])))
|
|
107
|
+
elif meta.name.startswith('to'):
|
|
108
|
+
assert len(children) == 1
|
|
109
|
+
return Sequence(Comment(meta.name), children[0])
|
|
110
|
+
elif meta.name.startswith('bind'):
|
|
111
|
+
assert len(children) == 1
|
|
112
|
+
return Sequence(Comment(meta.name), children[0])
|
|
113
|
+
elif meta.name.startswith('mark'):
|
|
114
|
+
assert len(children) == 1
|
|
115
|
+
return Sequence(Comment(meta.name), children[0])
|
|
116
|
+
elif meta.name.startswith('when'):
|
|
117
|
+
assert len(children) == 2
|
|
118
|
+
return Group(Choice(0, children[0], children[1]),
|
|
119
|
+
label="Conditional on env/config")
|
|
120
|
+
elif meta.name.startswith('success'):
|
|
121
|
+
return Terminal(meta.name)
|
|
122
|
+
elif meta.name.startswith('fail'):
|
|
123
|
+
return Terminal(meta.name)
|
|
124
|
+
elif meta.name.startswith('lazy'):
|
|
125
|
+
return Terminal(meta.name)
|
|
126
|
+
elif meta.name == 'anything':
|
|
127
|
+
return Terminal(meta.name)
|
|
128
|
+
elif meta.name == 'until':
|
|
129
|
+
return Terminal(meta.name)
|
|
130
|
+
else:
|
|
131
|
+
return Sequence(Terminal(meta.name), *(children if children else []))
|
|
132
|
+
|
|
133
|
+
diagram = Diagram(to_railroad(syntax))
|
|
134
|
+
return diagram.writeSvgString()
|
|
135
|
+
except ImportError:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def ast2svg(ast: Any) -> Optional[str]:
|
|
139
|
+
"""
|
|
140
|
+
Generate SVG visualization for a Syncraft AST node using graphviz.
|
|
141
|
+
Returns SVG string or None if graphviz is not available.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
import graphviz
|
|
145
|
+
except ImportError:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def node_label(node):
|
|
149
|
+
from syncraft.ast import Nothing, Marked, Choice, Many, Then, Collect, Custom, Token
|
|
150
|
+
if isinstance(node, Nothing):
|
|
151
|
+
return "Nothing"
|
|
152
|
+
elif isinstance(node, Marked):
|
|
153
|
+
return f"Marked(name={node.name})"
|
|
154
|
+
elif isinstance(node, Choice):
|
|
155
|
+
return f"Choice(kind={getattr(node.kind, 'name', node.kind)})"
|
|
156
|
+
elif isinstance(node, Many):
|
|
157
|
+
return "Many"
|
|
158
|
+
elif isinstance(node, Then):
|
|
159
|
+
return f"Then(kind={node.kind.name})"
|
|
160
|
+
elif isinstance(node, Collect):
|
|
161
|
+
return f"Collect({getattr(node.collector, '__name__', str(node.collector))})"
|
|
162
|
+
elif isinstance(node, Custom):
|
|
163
|
+
return f"Custom(meta={node.meta})"
|
|
164
|
+
elif isinstance(node, Token):
|
|
165
|
+
return f"Token({node.token_type.name}: {node.text})"
|
|
166
|
+
elif hasattr(node, '__class__'):
|
|
167
|
+
return node.__class__.__name__
|
|
168
|
+
else:
|
|
169
|
+
return str(node)
|
|
170
|
+
|
|
171
|
+
def add_nodes_edges(dot, node, parent_id=None, node_id_gen=[0]):
|
|
172
|
+
from syncraft.ast import Nothing, Marked, Choice, Many, Then, Collect, Custom, Token
|
|
173
|
+
node_id = f"n{node_id_gen[0]}"
|
|
174
|
+
node_id_gen[0] += 1
|
|
175
|
+
label = node_label(node)
|
|
176
|
+
dot.node(node_id, label)
|
|
177
|
+
if parent_id is not None:
|
|
178
|
+
dot.edge(parent_id, node_id)
|
|
179
|
+
|
|
180
|
+
# Walk children according to AST type
|
|
181
|
+
if isinstance(node, Nothing):
|
|
182
|
+
return
|
|
183
|
+
elif isinstance(node, Marked):
|
|
184
|
+
add_nodes_edges(dot, node.value, node_id, node_id_gen)
|
|
185
|
+
elif isinstance(node, Choice):
|
|
186
|
+
if node.value is not None:
|
|
187
|
+
add_nodes_edges(dot, node.value, node_id, node_id_gen)
|
|
188
|
+
elif isinstance(node, Many):
|
|
189
|
+
for child in node.value:
|
|
190
|
+
add_nodes_edges(dot, child, node_id, node_id_gen)
|
|
191
|
+
elif isinstance(node, Then):
|
|
192
|
+
add_nodes_edges(dot, node.left, node_id, node_id_gen)
|
|
193
|
+
add_nodes_edges(dot, node.right, node_id, node_id_gen)
|
|
194
|
+
elif isinstance(node, Collect):
|
|
195
|
+
add_nodes_edges(dot, node.value, node_id, node_id_gen)
|
|
196
|
+
elif isinstance(node, Custom):
|
|
197
|
+
add_nodes_edges(dot, node.value, node_id, node_id_gen)
|
|
198
|
+
# Token is a leaf
|
|
199
|
+
# For other types, try to walk __dict__ if they are dataclasses
|
|
200
|
+
elif hasattr(node, '__dataclass_fields__'):
|
|
201
|
+
for f in node.__dataclass_fields__:
|
|
202
|
+
v = getattr(node, f)
|
|
203
|
+
if isinstance(v, (list, tuple)):
|
|
204
|
+
for item in v:
|
|
205
|
+
if hasattr(item, '__class__'):
|
|
206
|
+
add_nodes_edges(dot, item, node_id, node_id_gen)
|
|
207
|
+
elif hasattr(v, '__class__') and v is not node:
|
|
208
|
+
add_nodes_edges(dot, v, node_id, node_id_gen)
|
|
209
|
+
|
|
210
|
+
dot = graphviz.Digraph(format='svg')
|
|
211
|
+
add_nodes_edges(dot, ast)
|
|
212
|
+
return dot.pipe().decode('utf-8')
|
|
213
|
+
|
|
214
|
+
|
syncraft/walker.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
Any, Tuple, Generator as PyGenerator, TypeVar, Generic, Optional, Callable, Hashable
|
|
5
|
+
)
|
|
6
|
+
from dataclasses import dataclass, replace, field
|
|
7
|
+
from syncraft.algebra import (
|
|
8
|
+
Algebra, Either, Right, Incomplete, Left, SyncraftError
|
|
9
|
+
)
|
|
10
|
+
from syncraft.ast import TokenSpec, ThenSpec, ManySpec, ChoiceSpec, LazySpec
|
|
11
|
+
from syncraft.parser import TokenType
|
|
12
|
+
from syncraft.constraint import Bindable, FrozenDict
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from syncraft.syntax import Syntax
|
|
16
|
+
from syncraft.cache import Cache
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
S = TypeVar('S', bound=Bindable)
|
|
20
|
+
A = TypeVar('A')
|
|
21
|
+
B = TypeVar('B')
|
|
22
|
+
SS = TypeVar('SS', bound=Hashable)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class WalkerState(Bindable, Generic[SS]):
|
|
32
|
+
reducer: Optional[Callable[[Any, SS], SS]] = None
|
|
33
|
+
acc: Optional[SS] = None
|
|
34
|
+
visited: frozenset = field(default_factory=frozenset)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def reduce(self, value: Any) -> WalkerState[SS]:
|
|
38
|
+
if self.reducer:
|
|
39
|
+
new_acc = self.reducer(value, self.acc) if self.acc is not None else value
|
|
40
|
+
return replace(self, acc=new_acc)
|
|
41
|
+
else:
|
|
42
|
+
return replace(self, acc=value)
|
|
43
|
+
|
|
44
|
+
def visit(self, key: Hashable) -> WalkerState[SS]:
|
|
45
|
+
return replace(self, visited=self.visited | {key})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Walker(Algebra[SS, WalkerState[SS]]):
|
|
52
|
+
@classmethod
|
|
53
|
+
def state(cls, reducer: Callable[[Any, SS], SS], init: SS )->WalkerState[SS]: # type: ignore
|
|
54
|
+
assert callable(reducer), f"reducer must be a Reducer or None, got {type(reducer)}"
|
|
55
|
+
return WalkerState(reducer=reducer, acc=init)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def lazy(cls, thunk: Callable[[], Algebra[Any, WalkerState[SS]]], cache: Cache) -> Algebra[Any, WalkerState[SS]]:
|
|
60
|
+
def alazy_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
|
|
61
|
+
result = yield from thunk().run(input, use_cache)
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
def lazy_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
|
|
65
|
+
print('thunk', thunk, input.visited)
|
|
66
|
+
if thunk in input.visited:
|
|
67
|
+
return Right((None, input))
|
|
68
|
+
else:
|
|
69
|
+
thunk_result = yield from thunk().run(input, use_cache)
|
|
70
|
+
match thunk_result:
|
|
71
|
+
case Right((value, from_thunk)):
|
|
72
|
+
data = LazySpec(value=value)
|
|
73
|
+
return Right((data, from_thunk.visit(thunk).reduce(data)))
|
|
74
|
+
raise SyncraftError("flat_map should always return a value or an error.", offending=thunk_result, expect=(Left, Right))
|
|
75
|
+
return cls(lazy_run, name=cls.__name__ + '.lazy', cache=cache)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def then_both(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
|
|
79
|
+
def then_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
|
|
80
|
+
self_result = yield from self.run(input, use_cache=use_cache)
|
|
81
|
+
match self_result:
|
|
82
|
+
case Right((value, from_left)):
|
|
83
|
+
other_result = yield from other.run(from_left, use_cache)
|
|
84
|
+
match other_result:
|
|
85
|
+
case Right((result, from_right)):
|
|
86
|
+
data = ThenSpec(left=value, right=result)
|
|
87
|
+
return Right((data, from_right.reduce(data)))
|
|
88
|
+
raise SyncraftError("flat_map should always return a value or an error.", offending=self_result, expect=(Left, Right))
|
|
89
|
+
return self.__class__(then_run, name=self.name, cache=self.cache | other.cache)
|
|
90
|
+
|
|
91
|
+
def then_left(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
|
|
92
|
+
return self.then_both(other) # For simplicity, treat as both
|
|
93
|
+
|
|
94
|
+
def then_right(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
|
|
95
|
+
return self.then_both(other)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Any, WalkerState[SS]]:
|
|
99
|
+
if at_least <=0 or (at_most is not None and at_most < at_least):
|
|
100
|
+
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)")
|
|
101
|
+
def many_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
|
|
102
|
+
self_result = yield from self.run(input, use_cache)
|
|
103
|
+
match self_result:
|
|
104
|
+
case Right((value, from_self)):
|
|
105
|
+
data = ManySpec(value=value, at_least=at_least, at_most=at_most)
|
|
106
|
+
return Right((data, from_self.reduce(data)))
|
|
107
|
+
raise SyncraftError("many should always return a value or an error.", offending=self_result, expect=(Left, Right))
|
|
108
|
+
return self.__class__(many_run, name=f"many({self.name})", cache=self.cache)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def or_else(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
|
|
112
|
+
def or_else_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
|
|
113
|
+
self_result = yield from self.run(input, use_cache=use_cache)
|
|
114
|
+
match self_result:
|
|
115
|
+
case Right((value, from_left)):
|
|
116
|
+
other_result = yield from other.run(from_left, use_cache)
|
|
117
|
+
match other_result:
|
|
118
|
+
case Right((result, from_right)):
|
|
119
|
+
data = ChoiceSpec(left=value, right=result)
|
|
120
|
+
return Right((data, from_right.reduce(data)))
|
|
121
|
+
raise SyncraftError("", offending=self)
|
|
122
|
+
return self.__class__(or_else_run, name=f"or_else({self.name} | {other.name})", cache=self.cache | other.cache)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def token(cls,
|
|
126
|
+
*,
|
|
127
|
+
cache: Cache,
|
|
128
|
+
token_type: Optional[TokenType] = None,
|
|
129
|
+
text: Optional[str] = None,
|
|
130
|
+
case_sensitive: bool = False,
|
|
131
|
+
regex: Optional[re.Pattern[str]] = None
|
|
132
|
+
)-> Algebra[Any, WalkerState[SS]]:
|
|
133
|
+
def token_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
|
|
134
|
+
yield from ()
|
|
135
|
+
data = TokenSpec(token_type=token_type, text=text, regex=regex, case_sensitive=case_sensitive)
|
|
136
|
+
return Right((data, input.reduce(data)))
|
|
137
|
+
return cls(token_run, name=cls.__name__ + f'.token({token_type or text or regex})', cache=cache)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def walk(syntax: Syntax[Any, Any], reducer: Callable[[Any, Any], SS], init: SS)-> Optional[SS]:
|
|
141
|
+
from syncraft.syntax import run
|
|
142
|
+
from rich import print
|
|
143
|
+
v, s = run(syntax=syntax, alg=Walker, use_cache=False, reducer=reducer, init=init)
|
|
144
|
+
if s is not None:
|
|
145
|
+
return s.acc
|
|
146
|
+
else:
|
|
147
|
+
return None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syncraft
|
|
3
|
+
Version: 0.2.6
|
|
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: rstr>=3.2.2
|
|
12
|
+
Requires-Dist: sqlglot>=27.7.0
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# Syncraft
|
|
16
|
+
|
|
17
|
+
Syncraft is a parser/generator combinator library for Python. It helps you
|
|
18
|
+
|
|
19
|
+
- Build grammars
|
|
20
|
+
- Parse SQL statement to AST
|
|
21
|
+
- Search AST by grammar
|
|
22
|
+
- Convert AST to dataclass
|
|
23
|
+
- Check constraints over the AST/dataclass
|
|
24
|
+
- Change dataclass and convert back to AST
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
### pip
|
|
30
|
+
```bash
|
|
31
|
+
pip install syncraft
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### uv
|
|
35
|
+
```bash
|
|
36
|
+
uv add syncraft
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Python 3.10+ is required.
|
|
40
|
+
|
|
41
|
+
### With pip
|
|
42
|
+
```bash
|
|
43
|
+
pip install syncraft[dev]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### With uv
|
|
47
|
+
```bash
|
|
48
|
+
uv sync --group dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
TODO
|
|
52
|
+
- [ ] debug constraint.datalog
|
|
53
|
+
- [ ] debug sqlite3
|
|
54
|
+
- [ ] collect terminal from syntax and build PLY lexer
|
|
55
|
+
- [ ] chunker
|
|
56
|
+
- [ ] unify in find
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
syncraft/__init__.py,sha256=g1Rd3DwPJ-Z7dL0PBNyNkcLBmpkp8bIBDWabkeBCD48,1329
|
|
2
|
+
syncraft/algebra.py,sha256=W6n4YH75VUU960DTW1ZGljmuhvOtLOzkL6_w9DG6fWI,20706
|
|
3
|
+
syncraft/ast.py,sha256=kMpZtox3S41t6srptJzd8G0p7Imu-FX2reC6Zdk47PY,20779
|
|
4
|
+
syncraft/cache.py,sha256=n7YpN6OmkDySVKRP-FWE8Ni6WqzMHz2b4TsSbBPRJtA,3655
|
|
5
|
+
syncraft/constraint.py,sha256=a6j_VafRor8W7FBXh20DoVnIUO9nfn7eIValigCf9lU,16318
|
|
6
|
+
syncraft/dev.py,sha256=v7jdb2aOVCGbio-Jw14tRhO09FkhWc0vrDdIkIKPu2Y,186
|
|
7
|
+
syncraft/finder.py,sha256=Mv9BYrsDjjq62Z4NO3gElZAbkEgSBINhSbqFbKEQW4Q,4347
|
|
8
|
+
syncraft/generator.py,sha256=ybliYMDCC7z_Q3ODUYvYvwaKaMl7lULtJkmtX_c6lWw,21209
|
|
9
|
+
syncraft/lexer.py,sha256=npPHAEEbBRQYcFHPMHtXmiVLbCZAnjpTtj3cnVui4W0,3720
|
|
10
|
+
syncraft/parser.py,sha256=jt34gkHl4OJuImedR4abPGPw0VZtSOPzuTQRsvx8G_k,10094
|
|
11
|
+
syncraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
syncraft/sqlite3.py,sha256=Pq09IHZOwuWg5W82l9D1flzd36QV0TOHQpTJ5U02V8g,34701
|
|
13
|
+
syncraft/syntax.py,sha256=O4HE4IDZ6fclP7kRy5L6rIuIdp_LmnMsr0LRuAIIS8s,25033
|
|
14
|
+
syncraft/utils.py,sha256=2V446Il2q4mR7bwwGX5_qruJPU0WXU2PniXEjl6EOPE,8746
|
|
15
|
+
syncraft/walker.py,sha256=EoTSCCiaIPpBpsd6EcbFCw2_0jlDyNkemAeDoqA4Mus,7118
|
|
16
|
+
syncraft-0.2.6.dist-info/licenses/LICENSE,sha256=wHSV424U5csa3339dy1AZbsz2xsd0hrkMx2QK48CcUk,1062
|
|
17
|
+
syncraft-0.2.6.dist-info/METADATA,sha256=tFUXcscYjk8tgtahL7BPkTstoFN7o3rJgi2QL9z3y-s,1023
|
|
18
|
+
syncraft-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
syncraft-0.2.6.dist-info/top_level.txt,sha256=Kq3t8ESXB2xW1Xt3uPmkENFc-c4f2pamNmaURBk7zc8,9
|
|
20
|
+
syncraft-0.2.6.dist-info/RECORD,,
|
syncraft/diagnostic.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from rich import print
|
|
3
|
-
from rich.table import Table as RichTable
|
|
4
|
-
from typing import Tuple, Any, Set
|
|
5
|
-
from syncraft.syntax import Syntax
|
|
6
|
-
from syncraft.algebra import Left, Right, Error, Either, Algebra
|
|
7
|
-
|
|
8
|
-
from syncraft.parser import ParserState, Token
|
|
9
|
-
from sqlglot.expressions import Expression
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def rich_error(err: Error)->None:
|
|
13
|
-
lst = err.to_list()
|
|
14
|
-
root, leaf = lst[0], lst[-1]
|
|
15
|
-
tbl = RichTable(title="Parser Error", show_lines=True)
|
|
16
|
-
tbl.add_column("Root Parser Field", style="blue")
|
|
17
|
-
tbl.add_column("Root Parser Value", style="green")
|
|
18
|
-
tbl.add_column("...")
|
|
19
|
-
tbl.add_column("Leaf Parser Field", style="blue")
|
|
20
|
-
tbl.add_column("Leaf Parser Value", style="yellow")
|
|
21
|
-
flds: Set[str] = set(root.keys()) | set(leaf.keys())
|
|
22
|
-
for fld in sorted(flds):
|
|
23
|
-
root_value = root.get(fld, "N/A")
|
|
24
|
-
leaf_value = leaf.get(fld, "N/A")
|
|
25
|
-
tbl.add_row(f"{fld}", f"{root_value}", "...", f"{fld}", f"{leaf_value}")
|
|
26
|
-
print(tbl)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def rich_parser(p: Syntax)-> None:
|
|
30
|
-
print("Parser Debug Information:")
|
|
31
|
-
print(p.meta.to_string(lambda _ : True) or repr(p))
|
|
32
|
-
|
|
33
|
-
def rich_debug(this: Algebra[Any, ParserState[Any]],
|
|
34
|
-
state: ParserState[Any],
|
|
35
|
-
result: Either[Any, Tuple[Any, ParserState[Any]]])-> None:
|
|
36
|
-
def value_to_str(value: Any, prefix:str='') -> str:
|
|
37
|
-
if isinstance(value, (tuple, list)):
|
|
38
|
-
if len(value) == 0:
|
|
39
|
-
return prefix + str(value)
|
|
40
|
-
else:
|
|
41
|
-
return '\n'.join(value_to_str(item, prefix=prefix+' - ') for item in value)
|
|
42
|
-
else:
|
|
43
|
-
if isinstance(value, Expression):
|
|
44
|
-
return prefix + value.sql()
|
|
45
|
-
elif isinstance(value, Token):
|
|
46
|
-
return prefix + f"{value.token_type.name}({value.text})"
|
|
47
|
-
elif isinstance(value, Syntax):
|
|
48
|
-
return prefix + (value.meta.to_string(lambda _ : True) or 'N/A')
|
|
49
|
-
else:
|
|
50
|
-
return prefix + str(value)
|
|
51
|
-
|
|
52
|
-
tbl = RichTable(title=f"Debug: {this.name}", show_lines=True)
|
|
53
|
-
tbl.add_column("Parser", style="blue")
|
|
54
|
-
tbl.add_column("Old State", style="cyan")
|
|
55
|
-
tbl.add_column("Result", style="magenta")
|
|
56
|
-
tbl.add_column("New State", style="green")
|
|
57
|
-
tbl.add_column("Consumed", style="green")
|
|
58
|
-
if isinstance(result, Left):
|
|
59
|
-
tbl.add_row(value_to_str(this), value_to_str(state), value_to_str(result.value), 'N/A', 'N/A')
|
|
60
|
-
else:
|
|
61
|
-
assert isinstance(result, Right), f"Expected result to be a Right value, got {type(result)}, {result}"
|
|
62
|
-
value, new_state = result.value
|
|
63
|
-
tbl.add_row(value_to_str(this),
|
|
64
|
-
value_to_str(state),
|
|
65
|
-
value_to_str(value),
|
|
66
|
-
value_to_str(new_state),
|
|
67
|
-
value_to_str(state.delta(new_state)))
|
|
68
|
-
|
|
69
|
-
print(tbl)
|
|
70
|
-
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: syncraft
|
|
3
|
-
Version: 0.2.5
|
|
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.5.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
syncraft/__init__.py,sha256=8LIO0m0XEAsgKd--G8u6uT-lljLDtKMeIequNRNv2dc,894
|
|
2
|
-
syncraft/algebra.py,sha256=zm6HzaE2XeyhWInoKDSJrVtnkjJV6BsD0zPBRz7iDAI,23333
|
|
3
|
-
syncraft/ast.py,sha256=F6aRdxZ6IBNvnk1xe_ugEBvL-BXNXdkN4s1YDIuNw3Y,19134
|
|
4
|
-
syncraft/constraint.py,sha256=qq-DkCH-8NgGqSYrNo4achq5t4gEEycelFTJWg4I_ek,15439
|
|
5
|
-
syncraft/diagnostic.py,sha256=cgwcQnCcgrCRX3h-oGTDb5rcJAtitPV3LfH9eLvO93E,2837
|
|
6
|
-
syncraft/finder.py,sha256=FCOHGdJf2NHG_letl8-IArfBywEolmtZcTJQPOj1iYw,4147
|
|
7
|
-
syncraft/generator.py,sha256=bmXcnExG-M44ChxdS9K_6H0MdCa9gD7erJ8aAu-LpXM,20444
|
|
8
|
-
syncraft/parser.py,sha256=9z9Iq9_Ih3mlXkuz85gsND4xshZARgAqo5-R_DpgA9I,17338
|
|
9
|
-
syncraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
syncraft/sqlite3.py,sha256=Pq09IHZOwuWg5W82l9D1flzd36QV0TOHQpTJ5U02V8g,34701
|
|
11
|
-
syncraft/syntax.py,sha256=nSWE1SHvVDYON0UQM8kgp14kx54hbKBYHV0xB6fgwkk,22366
|
|
12
|
-
syncraft-0.2.5.dist-info/licenses/LICENSE,sha256=wHSV424U5csa3339dy1AZbsz2xsd0hrkMx2QK48CcUk,1062
|
|
13
|
-
syncraft-0.2.5.dist-info/METADATA,sha256=X4WtYQxjCWYSf3JF7UI2zyERU-NJQObI-gwTcUxzHB4,2821
|
|
14
|
-
syncraft-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
-
syncraft-0.2.5.dist-info/top_level.txt,sha256=Kq3t8ESXB2xW1Xt3uPmkENFc-c4f2pamNmaURBk7zc8,9
|
|
16
|
-
syncraft-0.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|