syncraft 0.1.9__py3-none-any.whl → 0.1.11__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/ast.py ADDED
@@ -0,0 +1,202 @@
1
+
2
+
3
+ from __future__ import annotations
4
+ import re
5
+ from typing import (
6
+ Optional, Any, TypeVar, Tuple, runtime_checkable, Dict,
7
+ Protocol, Generic, Callable, Union
8
+ )
9
+ from syncraft.algebra import (
10
+ NamedResult, OrResult,ThenResult, ManyResult, ThenKind,
11
+ Lens
12
+ )
13
+ from dataclasses import dataclass, field, replace, is_dataclass, asdict
14
+ from enum import Enum
15
+ from functools import cached_property
16
+
17
+ @runtime_checkable
18
+ class TokenProtocol(Protocol):
19
+ @property
20
+ def token_type(self) -> Enum: ...
21
+ @property
22
+ def text(self) -> str: ...
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Token:
27
+ token_type: Enum
28
+ text: str
29
+ def __str__(self) -> str:
30
+ return f"{self.token_type.name}({self.text})"
31
+
32
+ def __repr__(self) -> str:
33
+ return self.__str__()
34
+
35
+ @dataclass(frozen=True)
36
+ class TokenSpec:
37
+ token_type: Optional[Enum] = None
38
+ text: Optional[str] = None
39
+ case_sensitive: bool = False
40
+ regex: Optional[re.Pattern[str]] = None
41
+
42
+ def is_valid(self, token: TokenProtocol) -> bool:
43
+ type_match = self.token_type is None or token.token_type == self.token_type
44
+ value_match = self.text is None or (token.text.strip() == self.text.strip() if self.case_sensitive else
45
+ token.text.strip().upper() == self.text.strip().upper())
46
+ value_match = value_match or (self.regex is not None and self.regex.fullmatch(token.text) is not None)
47
+ return type_match and value_match
48
+
49
+
50
+
51
+
52
+ T = TypeVar('T', bound=TokenProtocol)
53
+
54
+
55
+ ParseResult = Union[
56
+ ThenResult['ParseResult[T]', 'ParseResult[T]'],
57
+ NamedResult['ParseResult[T]', Any],
58
+ ManyResult['ParseResult[T]'],
59
+ OrResult['ParseResult[T]'],
60
+ Tuple[T, ...],
61
+ T,
62
+ ]
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class NamedRecord:
72
+ lens: Lens[Any, Any]
73
+ value: Any
74
+
75
+ @dataclass(frozen=True)
76
+ class Walker:
77
+ lens: Optional[Lens[Any, Any]] = None
78
+ def get(self, root: ParseResult[Any]) -> Dict[str, NamedRecord]:
79
+ match root:
80
+ case ManyResult(value=children):
81
+ new_named: Dict[str, NamedRecord] = {}
82
+ for i, child in enumerate(children):
83
+ new_walker = replace(self, lens=(self.lens / ManyResult.lens(i)) if self.lens else ManyResult.lens(i))
84
+ new_named |= new_walker.get(child)
85
+ return new_named
86
+ case OrResult(value=value):
87
+ new_walker = replace(self, lens=(self.lens / OrResult.lens()) if self.lens else OrResult.lens())
88
+ return new_walker.get(value)
89
+ case ThenResult(left=left,
90
+ right=right,
91
+ kind=kind):
92
+ new_walker = replace(self, lens=(self.lens / ThenResult.lens(kind)) if self.lens else ThenResult.lens(kind))
93
+ return new_walker.get(left) | new_walker.get(right)
94
+ case NamedResult(name=name,
95
+ value=value,
96
+ forward_map=forward_map,
97
+ backward_map=backward_map,
98
+ aggregator=aggregator):
99
+ this_lens = (self.lens / NamedResult.lens()) if self.lens else NamedResult.lens()
100
+ if callable(forward_map) and callable(backward_map):
101
+ this_lens = this_lens.bimap(forward_map, backward_map)
102
+ elif callable(forward_map):
103
+ this_lens = this_lens.bimap(forward_map, lambda _: value)
104
+ elif callable(backward_map):
105
+ raise ValueError("backward_map provided without forward_map")
106
+ new_walker = replace(self, lens=this_lens)
107
+ child_named = new_walker.get(value)
108
+ if aggregator is not None:
109
+ return child_named | {name: NamedRecord(lens=this_lens,
110
+ value=aggregator(child_named))}
111
+ else:
112
+ return child_named
113
+ return {}
114
+
115
+ def set(self, root: ParseResult[Any], updated_values: Dict[str, Any]) -> ParseResult[Any]:
116
+ named_records = self.get(root)
117
+ def apply_update(name: str, value: Any, root: ParseResult[Any]) -> ParseResult[Any]:
118
+ if name not in named_records:
119
+ # Skip unknown names safely
120
+ return root
121
+ record = named_records[name]
122
+ target_named: NamedResult[Any, Any] = record.lens.get(root)
123
+ assert isinstance(target_named, NamedResult)
124
+
125
+ if target_named.aggregator is not None:
126
+ # Break apart dataclass/dict into child fields
127
+ if isinstance(value, dict):
128
+ child_updates = value
129
+ elif is_dataclass(value) and not isinstance(value, type):
130
+ child_updates = asdict(value)
131
+ else:
132
+ raise TypeError(f"Unsupported aggregator value for '{name}': {type(value)}")
133
+
134
+ # Recursively apply each child update
135
+ for child_name, child_value in child_updates.items():
136
+ root = apply_update(child_name, child_value, root)
137
+ return root
138
+
139
+ else:
140
+ # Leaf: just replace the value
141
+ updated_named = replace(target_named, value=value)
142
+ return record.lens.set(root, updated_named)
143
+
144
+ for name, value in updated_values.items():
145
+ root = apply_update(name, value, root)
146
+
147
+ return root
148
+
149
+ @dataclass(frozen=True)
150
+ class AST(Generic[T]):
151
+ focus: ParseResult[T]
152
+ pruned: bool = False
153
+ parent: Optional[AST[T]] = None
154
+
155
+ def up(self)->Optional[AST[T]]:
156
+ return self.parent
157
+
158
+ def left(self) -> Optional[AST[T]]:
159
+ match self.focus:
160
+ case ThenResult(left=left, kind=kind):
161
+ return replace(self, focus=left, parent=self, pruned = self.pruned or kind == ThenKind.RIGHT)
162
+ case _:
163
+ raise TypeError(f"Invalid focus type({self.focus}) for left traversal")
164
+
165
+ def right(self) -> Optional[AST[T]]:
166
+ match self.focus:
167
+ case ThenResult(right=right, kind=kind):
168
+ return replace(self, focus=right, parent=self, pruned = self.pruned or kind == ThenKind.LEFT)
169
+ case _:
170
+ raise TypeError(f"Invalid focus type({self.focus}) for right traversal")
171
+
172
+
173
+ def down(self, index: int) -> Optional[AST[T]]:
174
+ match self.focus:
175
+ case ManyResult(value=children):
176
+ if 0 <= index < len(children):
177
+ return replace(self, focus=children[index], parent=self, pruned=self.pruned)
178
+ else:
179
+ raise IndexError(f"Index {index} out of bounds for ManyResult with {len(children)} children")
180
+ case OrResult(value=value):
181
+ if index == 0:
182
+ return replace(self, focus=value, parent=self, pruned=self.pruned)
183
+ else:
184
+ raise IndexError(f"Index {index} out of bounds for OrResult")
185
+ case _:
186
+ raise TypeError(f"Invalid focus type({self.focus}) for down traversal")
187
+
188
+ def how_many(self)->int:
189
+ match self.focus:
190
+ case ManyResult(value=children):
191
+ return len(children)
192
+ case _:
193
+ raise TypeError(f"Invalid focus type({self.focus}) for how_many")
194
+
195
+
196
+
197
+ @cached_property
198
+ def root(self) -> AST[T]:
199
+ while self.parent is not None:
200
+ self = self.parent
201
+ return self
202
+
syncraft/dsl.py CHANGED
@@ -156,7 +156,7 @@ class DSL(Generic[A, S], Insptectable):
156
156
  def sep_by(self, sep: DSL[Any, S]) -> DSL[ThenResult[A, ManyResult[ThenResult[None, A]]], S]:
157
157
  return (self + (sep >> self).many()).describe(
158
158
  name='sep_by',
159
- fixity='infix',
159
+ fixity='prefix',
160
160
  parameter=[self, sep]
161
161
  )
162
162
 
syncraft/generator.py CHANGED
@@ -4,22 +4,19 @@ from typing import (
4
4
  Any, TypeVar, Tuple, Optional, Callable, Generic, Union, Iterable, Hashable,
5
5
  cast, List
6
6
  )
7
-
7
+ from functools import cached_property
8
8
  from dataclasses import dataclass, replace
9
9
  from syncraft.algebra import (
10
10
  Algebra, ThenResult, Either, Left, Right, Error, Insptectable,
11
- NamedResult, OrResult, ManyResult
11
+ NamedResult, OrResult, ManyResult, ThenKind
12
12
  )
13
- from syncraft.parser import TokenProtocol, ParseResult, AST, Token, TokenSpec, Crumb
13
+ from syncraft.ast import TokenProtocol, ParseResult, AST, Token, TokenSpec
14
14
  from sqlglot import TokenType
15
15
  import re
16
- from rich import print
17
16
  import rstr
18
17
  from functools import lru_cache
19
18
  import random
20
19
 
21
-
22
- A = TypeVar('A')
23
20
  B = TypeVar('B')
24
21
  T = TypeVar('T', bound=TokenProtocol)
25
22
 
@@ -31,12 +28,6 @@ GenResult = Union[
31
28
  T
32
29
  ]
33
30
 
34
-
35
-
36
-
37
- E = TypeVar('E', bound=Hashable)
38
- F = TypeVar('F', bound=Hashable)
39
-
40
31
  @dataclass(frozen=True)
41
32
  class GenState(Generic[T], Insptectable):
42
33
  ast: Optional[AST[T]]
@@ -51,15 +42,9 @@ class GenState(Generic[T], Insptectable):
51
42
  def to_string(self, interested: Callable[[Any], bool]) -> str | None:
52
43
  return f"GenState(current={self.focus})"
53
44
 
54
- @property
55
- def is_freeform(self) -> bool:
56
- if self.ast is None:
57
- return True
58
- return self.ast.is_pruned
59
-
60
- @property
61
- def ended(self) -> bool:
62
- return self.ast is None
45
+ @cached_property
46
+ def pruned(self)->bool:
47
+ return self.ast is None or self.ast.pruned
63
48
 
64
49
 
65
50
  @property
@@ -69,65 +54,35 @@ class GenState(Generic[T], Insptectable):
69
54
  return self.ast.focus
70
55
 
71
56
 
72
- def leftmost(self)-> GenState[T]:
57
+ def left(self)-> GenState[T]:
73
58
  if self.ast is None:
74
59
  return self
75
- return replace(self, ast=self.ast.leftmost)
60
+ return replace(self, ast=self.ast.left())
76
61
 
77
- def down_left(self) -> GenState[T]:
78
- if self.ast is None:
79
- return self
80
- return replace(self, ast=self.ast.down_left())
81
-
82
62
  def right(self) -> GenState[T]:
83
63
  if self.ast is None:
84
- return self
64
+ return self
85
65
  return replace(self, ast=self.ast.right())
86
-
87
- def advance(self, filter: Callable[[Any], bool]) -> GenState[T]:
88
- if self.ast is None:
89
- return self
90
- def filtered(z: GenState[T]) -> GenState[T]:
91
- return z if z.ast is None or filter(z.ast.focus) else z.advance(filter=filter)
92
- z = self.down_left()
93
- if z.ast is not None:
94
- return filtered(z)
95
- z = self.right()
96
- if z.ast is not None:
97
- return filtered(z)
98
-
99
- zs = self.ast
100
- while tmp_z := zs.up():
101
- if next_z := tmp_z.right():
102
- return filtered(replace(self, ast=next_z))
103
- zs = tmp_z
104
- return filtered(replace(self, ast=None))
105
-
106
-
107
-
108
- @staticmethod
109
- def only_terminal(node: Any) -> bool:
110
- return not isinstance(node, (ManyResult, ThenResult, NamedResult, OrResult))
111
66
 
112
- def copy(self) -> GenState[T]:
113
- return self.__class__(ast=self.ast, seed=self.seed)
114
-
115
- def delta(self, new_state: GenState[T]) -> Tuple[T, ...]:
116
- return tuple()
117
-
118
- def scoped(self) -> GenState[T]:
119
- return ScopedState(ast=self.ast,
120
- seed=self.seed,
121
- scope=self.ast.breadcrumbs[-1] if self.ast and self.ast.breadcrumbs else None)
122
-
123
- def freeform(self) -> GenState[T]:
124
- return FreeformState(ast=None, seed=self.seed)
67
+ def up(self)->GenState[T]:
68
+ if self.ast is None:
69
+ return self
70
+ return replace(self, ast=self.ast.up())
125
71
 
72
+ def down(self, index: int) -> GenState[T]:
73
+ if self.ast is None:
74
+ return self
75
+ return replace(self, ast=self.ast.down(index))
126
76
 
77
+ @cached_property
78
+ def how_many(self) -> int:
79
+ if self.ast is None:
80
+ return 0
81
+ return self.ast.how_many()
82
+
127
83
  @classmethod
128
84
  def from_ast(cls, ast: Optional[AST[T]], seed: int = 0) -> GenState[T]:
129
- ret = cls(ast=ast, seed=seed)
130
- return ret if ast is not None else ret.freeform()
85
+ return cls(ast=ast, seed=seed)
131
86
 
132
87
 
133
88
  @classmethod
@@ -135,32 +90,7 @@ class GenState(Generic[T], Insptectable):
135
90
  return cls.from_ast(AST(parse_result) if parse_result else None, seed)
136
91
 
137
92
 
138
- @dataclass(frozen=True)
139
- class ScopedState(GenState[T]):
140
- scope: None | Crumb[T]
141
- @property
142
- def ended(self) -> bool:
143
- return self.ast is None or self.scope in self.ast.closed
144
-
145
- def right(self)-> GenState[T]:
146
- ret: ScopedState[T] = cast(ScopedState[T], super().right())
147
- if ret.ast is not None and self.scope is not None:
148
- if self.scope not in ret.ast.closed and self.scope not in ret.ast.breadcrumbs:
149
- return replace(ret, scope=ret.ast.breadcrumbs[-1] if ret.ast.breadcrumbs else None)
150
- return ret
151
-
152
-
153
- @dataclass(frozen=True)
154
- class FreeformState(GenState[T]):
155
- @property
156
- def ended(self) -> bool:
157
- return False
158
-
159
- def scoped(self) -> GenState[T]:
160
- return self
161
93
 
162
- def advance(self, filter: Callable[[Any], bool]) -> GenState[T]:
163
- return self
164
94
 
165
95
 
166
96
  @lru_cache(maxsize=None)
@@ -212,81 +142,68 @@ class TokenGen(TokenSpec):
212
142
  class Generator(Algebra[GenResult[T], GenState[T]]):
213
143
  def flat_map(self, f: Callable[[GenResult[T]], Algebra[B, GenState[T]]]) -> Algebra[B, GenState[T]]:
214
144
  def flat_map_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[B, GenState[T]]]:
215
- left = input.down_left()
216
- s = left.scoped()
217
- match self.run(s, use_cache=use_cache):
145
+ match self.run(input.left(), use_cache=use_cache):
218
146
  case Left(error):
219
147
  return Left(error)
220
148
  case Right((value, next_input)):
221
- return f(value).run(left.right(), use_cache)
149
+ r = input.right()
150
+ return f(value).run(r, use_cache)
222
151
  raise ValueError("flat_map should always return a value or an error.")
223
152
  return Generator(run_f = flat_map_run, name=self.name) # type: ignore
224
-
225
- def gen(self,
226
- freeform: Algebra[Any, GenState[T]],
227
- default: Algebra[Any, GenState[T]]
228
- ) -> Algebra[Any, GenState[T]]:
229
- def gen_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Any, GenState[T]]]:
230
- if input.ended:
231
- return Left(Error(this=self,
232
- message=f"{input.__class__.__name__} has ended, cannot run many.",
233
- state=input))
234
- elif input.is_freeform:
235
- return freeform.run(input, use_cache)
236
- else:
237
- return default.run(input, use_cache)
238
- return self.__class__(gen_run, name=default.name)
239
-
240
- def gen_many(self,
241
- at_least: int,
242
- at_most: Optional[int] = None
243
- ) -> Algebra[ManyResult[GenResult[T]], GenState[T]]:
244
- def gen_many_run(input: GenState[T],
245
- use_cache:bool
246
- ) -> Either[Any, Tuple[ManyResult[GenResult[T]], GenState[T]]]:
247
- upper = at_most if at_most is not None else at_least + 2
248
- count = input.rng("many").randint(at_least, upper)
249
- ret: List[Any] = []
250
- current_input: GenState[T] = input.freeform()
251
- for _ in range(count):
252
- forked_input = current_input.fork(tag=len(ret))
253
- match self.run(forked_input, use_cache):
254
- case Right((value, next_input)):
255
- current_input = next_input
256
- ret.append(value)
257
- case Left(_):
258
- break
259
- return Right((ManyResult(tuple(ret)), input))
260
- return self.__class__(run_f=gen_many_run, name=f"free_many({self.name})") # type: ignore
153
+
261
154
 
262
155
 
263
156
  def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[ManyResult[GenResult[T]], GenState[T]]:
264
157
  assert at_least > 0, "at_least must be greater than 0"
265
158
  assert at_most is None or at_least <= at_most, "at_least must be less than or equal to at_most"
266
- return self.gen(freeform=self.gen_many(at_least, at_most),
267
- default=super().many(at_least=at_least, at_most=at_most))
159
+ def many_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[ManyResult[GenResult[T]], GenState[T]]]:
160
+ if input.pruned:
161
+ upper = at_most if at_most is not None else at_least + 2
162
+ count = input.rng("many").randint(at_least, upper)
163
+ ret: List[Any] = []
164
+ for i in range(count):
165
+ forked_input = input.down(0).fork(tag=len(ret))
166
+ match self.run(forked_input, use_cache):
167
+ case Right((value, next_input)):
168
+ ret.append(value)
169
+ case Left(_):
170
+ pass
171
+ return Right((ManyResult(tuple(ret)), input))
172
+ else:
173
+ ret = []
174
+ for index in range(input.how_many):
175
+ match self.run(input.down(index), use_cache):
176
+ case Right((value, next_input)):
177
+ ret.append(value)
178
+ case Left(_):
179
+ pass
180
+ return Right((ManyResult(tuple(ret)), input))
181
+ return self.__class__(many_run, name=f"many({self.name})") # type: ignore
268
182
 
269
- def gen_or_else(self,
270
- other: Algebra[GenResult[T], GenState[T]]) -> Algebra[OrResult[GenResult[T]], GenState[T]]:
271
- def gen_or_else_run(input: GenState[T],
272
- use_cache:bool
273
- )->Either[Any, Tuple[OrResult[GenResult[T]], GenState[T]]]:
274
- forked_input = input.fork(tag="or_else")
275
- match forked_input.rng("or_else").choice((self, other)).run(forked_input.freeform(), use_cache):
276
- case Right((value, next_input)):
277
- return Right((OrResult(value), next_input))
278
- case Left(error):
279
- return Left(error)
280
- raise TypeError(f"Unexpected result type from {self}")
281
- return self.__class__(gen_or_else_run, name=f"free_or({self.name} | {other.name})") # type: ignore
282
183
 
283
-
284
184
  def or_else(self, # type: ignore
285
185
  other: Algebra[GenResult[T], GenState[T]]
286
186
  ) -> Algebra[OrResult[GenResult[T]], GenState[T]]:
287
- return self.gen(freeform=self.gen_or_else(other),
288
- default=super().or_else(other))
289
-
187
+ def or_else_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[OrResult[GenResult[T]], GenState[T]]]:
188
+ if input.pruned:
189
+ forked_input = input.fork(tag="or_else")
190
+ match forked_input.rng("or_else").choice((self, other)).run(forked_input, use_cache):
191
+ case Right((value, next_input)):
192
+ return Right((OrResult(value), next_input))
193
+ case Left(error):
194
+ return Left(error)
195
+ else:
196
+ match self.run(input.down(0), use_cache):
197
+ case Right((value, next_input)):
198
+ return Right((OrResult(value), next_input))
199
+ case Left(error):
200
+ match other.run(input.down(0), use_cache):
201
+ case Right((value, next_input)):
202
+ return Right((OrResult(value), next_input))
203
+ case Left(error):
204
+ return Left(error)
205
+ raise ValueError("or_else should always return a value or an error.")
206
+ return self.__class__(or_else_run, name=f"free_or({self.name} | {other.name})") # type: ignore
290
207
 
291
208
  @classmethod
292
209
  def token(cls,
@@ -298,21 +215,15 @@ class Generator(Algebra[GenResult[T], GenState[T]]):
298
215
  gen = TokenGen(token_type=token_type, text=text, case_sensitive=case_sensitive, regex=regex)
299
216
  lazy_self: Algebra[GenResult[T], GenState[T]]
300
217
  def token_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[GenResult[Token], GenState[T]]]:
301
- print('token', gen, input)
302
- if input.ended:
303
- return Left(Error(None,
304
- message=f"{input.__class__.__name__} has ended, cannot run token.",
305
- state=input))
306
- elif input.is_freeform:
307
- return Right((gen.gen(), input.advance(GenState.only_terminal)))
218
+ if input.pruned:
219
+ return Right((gen.gen(), input))
308
220
  else:
309
- input = input.leftmost()
310
221
  current = input.focus
311
222
  if not isinstance(current, Token) or not gen.is_valid(current):
312
223
  return Left(Error(None,
313
224
  message=f"Expected a Token, but got {type(current)}.",
314
225
  state=input))
315
- return Right((current, input.advance(GenState.only_terminal)))
226
+ return Right((current, input))
316
227
  lazy_self = cls(token_run, name=cls.__name__ + f'.token({token_type or text or regex})') # type: ignore
317
228
  return lazy_self
318
229
 
@@ -323,6 +234,6 @@ def generate(gen: Algebra[Any, Any], data: Optional[AST[Any]] = None, seed: int
323
234
  result = gen.run(state, use_cache=False)
324
235
  if isinstance(result, Right):
325
236
  return AST(result.value[0])
326
- assert isinstance(result, Left), "Parser must return Either[E, Tuple[A, S]]"
237
+ assert isinstance(result, Left), "Generator must return Either[Any, Tuple[Any, Any]]"
327
238
  return result.value
328
239
 
syncraft/parser.py CHANGED
@@ -2,329 +2,23 @@ from __future__ import annotations
2
2
  import re
3
3
  from sqlglot import tokenize, TokenType, Parser as GlotParser, exp
4
4
  from typing import (
5
- Optional, List, Any, TypeVar, Tuple, runtime_checkable, Dict,
6
- Protocol, Generic, Callable, Union
5
+ Optional, List, Any, Tuple,
6
+ Generic, Callable
7
7
  )
8
8
  from syncraft.algebra import (
9
- Either, Left, Right, Error, Insptectable, Algebra, NamedResult, OrResult,ThenResult, ManyResult, ThenKind,
10
- Lens
9
+ Either, Left, Right, Error, Insptectable, Algebra
11
10
  )
12
- from dataclasses import dataclass, field, replace, is_dataclass, asdict
11
+ from dataclasses import dataclass, field, replace
13
12
  from enum import Enum
14
- from functools import reduce, cached_property
13
+ from functools import reduce
15
14
  from syncraft.dsl import DSL
16
15
 
16
+ from syncraft.ast import Token, TokenSpec, AST, T
17
17
 
18
18
 
19
19
 
20
20
 
21
21
 
22
- @runtime_checkable
23
- class TokenProtocol(Protocol):
24
- @property
25
- def token_type(self) -> TokenType: ...
26
- @property
27
- def text(self) -> str: ...
28
-
29
-
30
- @dataclass(frozen=True)
31
- class Token:
32
- token_type: TokenType
33
- text: str
34
- def __str__(self) -> str:
35
- return f"{self.token_type.name}({self.text})"
36
-
37
- def __repr__(self) -> str:
38
- return self.__str__()
39
-
40
- @dataclass(frozen=True)
41
- class TokenSpec:
42
- token_type: Optional[TokenType] = None
43
- text: Optional[str] = None
44
- case_sensitive: bool = False
45
- regex: Optional[re.Pattern[str]] = None
46
-
47
- def is_valid(self, token: TokenProtocol) -> bool:
48
- type_match = self.token_type is None or token.token_type == self.token_type
49
- value_match = self.text is None or (token.text.strip() == self.text.strip() if self.case_sensitive else
50
- token.text.strip().upper() == self.text.strip().upper())
51
- value_match = value_match or (self.regex is not None and self.regex.fullmatch(token.text) is not None)
52
- return type_match and value_match
53
-
54
-
55
-
56
-
57
- T = TypeVar('T', bound=TokenProtocol)
58
-
59
-
60
- ParseResult = Union[
61
- ThenResult['ParseResult[T]', 'ParseResult[T]'],
62
- NamedResult['ParseResult[T]', Any],
63
- ManyResult['ParseResult[T]'],
64
- OrResult['ParseResult[T]'],
65
- Tuple[T, ...],
66
- T,
67
- ]
68
-
69
-
70
-
71
- @dataclass(frozen=True)
72
- class Crumb(Generic[T]):
73
- entrance: str
74
- id: int = field(init=False)
75
- def __post_init__(self)-> None:
76
- object.__setattr__(self, 'id', id(self))
77
-
78
-
79
-
80
- @dataclass(frozen=True)
81
- class LeftCrumb(Crumb[T]):
82
- right: ParseResult[T]
83
- kind: ThenKind
84
-
85
-
86
- @dataclass(frozen=True)
87
- class OrCrumb(Crumb[T]):
88
- pass
89
- @dataclass(frozen=True)
90
- class RightCrumb(Crumb[T]):
91
- left: ParseResult[T]
92
- kind: ThenKind
93
-
94
-
95
- @dataclass(frozen=True)
96
- class NamedCrumb(Crumb[T]):
97
- name: str
98
- forward_map: Callable[[Any], Any] | None
99
- backward_map: Callable[[Any], Any] | None
100
- aggregator: Callable[..., Any] | None
101
-
102
-
103
- @dataclass(frozen=True)
104
- class ManyCrumb(Crumb[T]):
105
- before: Tuple[ParseResult[T], ...]
106
- after: Tuple[ParseResult[T], ...]
107
-
108
-
109
- @dataclass(frozen=True)
110
- class NamedRecord:
111
- lens: Lens[Any, Any]
112
- value: Any
113
-
114
- @dataclass(frozen=True)
115
- class Walker:
116
- lens: Optional[Lens[Any, Any]] = None
117
- def get(self, root: ParseResult[Any]) -> Dict[str, NamedRecord]:
118
- match root:
119
- case ManyResult(value=children):
120
- new_named: Dict[str, NamedRecord] = {}
121
- for i, child in enumerate(children):
122
- new_walker = replace(self, lens=(self.lens / ManyResult.lens(i)) if self.lens else ManyResult.lens(i))
123
- new_named |= new_walker.get(child)
124
- return new_named
125
- case OrResult(value=value):
126
- new_walker = replace(self, lens=(self.lens / OrResult.lens()) if self.lens else OrResult.lens())
127
- return new_walker.get(value)
128
- case ThenResult(left=left,
129
- right=right,
130
- kind=kind):
131
- new_walker = replace(self, lens=(self.lens / ThenResult.lens(kind)) if self.lens else ThenResult.lens(kind))
132
- return new_walker.get(left) | new_walker.get(right)
133
- case NamedResult(name=name,
134
- value=value,
135
- forward_map=forward_map,
136
- backward_map=backward_map,
137
- aggregator=aggregator):
138
- this_lens = (self.lens / NamedResult.lens()) if self.lens else NamedResult.lens()
139
- if callable(forward_map) and callable(backward_map):
140
- this_lens = this_lens.bimap(forward_map, backward_map)
141
- elif callable(forward_map):
142
- this_lens = this_lens.bimap(forward_map, lambda _: value)
143
- elif callable(backward_map):
144
- raise ValueError("backward_map provided without forward_map")
145
- new_walker = replace(self, lens=this_lens)
146
- child_named = new_walker.get(value)
147
- if aggregator is not None:
148
- return child_named | {name: NamedRecord(lens=this_lens,
149
- value=aggregator(child_named))}
150
- else:
151
- return child_named
152
- return {}
153
-
154
- def set(self, root: ParseResult[Any], updated_values: Dict[str, Any]) -> ParseResult[Any]:
155
- named_records = self.get(root)
156
- def apply_update(name: str, value: Any, root: ParseResult[Any]) -> ParseResult[Any]:
157
- if name not in named_records:
158
- # Skip unknown names safely
159
- return root
160
- record = named_records[name]
161
- target_named: NamedResult[Any, Any] = record.lens.get(root)
162
- assert isinstance(target_named, NamedResult)
163
-
164
- if target_named.aggregator is not None:
165
- # Break apart dataclass/dict into child fields
166
- if isinstance(value, dict):
167
- child_updates = value
168
- elif is_dataclass(value) and not isinstance(value, type):
169
- child_updates = asdict(value)
170
- else:
171
- raise TypeError(f"Unsupported aggregator value for '{name}': {type(value)}")
172
-
173
- # Recursively apply each child update
174
- for child_name, child_value in child_updates.items():
175
- root = apply_update(child_name, child_value, root)
176
- return root
177
-
178
- else:
179
- # Leaf: just replace the value
180
- updated_named = replace(target_named, value=value)
181
- return record.lens.set(root, updated_named)
182
-
183
- for name, value in updated_values.items():
184
- root = apply_update(name, value, root)
185
-
186
- return root
187
-
188
- @dataclass(frozen=True)
189
- class AST(Generic[T]):
190
- focus: ParseResult[T]
191
- breadcrumbs: Tuple[Crumb[T], ...] = field(default_factory=tuple)
192
- closed: frozenset[Crumb[T]] = field(default_factory=frozenset)
193
-
194
- @cached_property
195
- def is_pruned(self) -> bool:
196
- for crumb in self.breadcrumbs:
197
- match crumb:
198
- case LeftCrumb(kind=ThenKind.RIGHT):
199
- return True # you're in a left child of a then_right
200
- case RightCrumb(kind=ThenKind.LEFT):
201
- return True # you're in a right child of a then_left
202
- return False
203
-
204
-
205
- def up(self) -> Optional[AST[T]]:
206
- if not self.breadcrumbs:
207
- return None
208
- *rest, last = self.breadcrumbs
209
-
210
- match last:
211
- case LeftCrumb(right=right, kind=kind):
212
- parent: ParseResult[T] = ThenResult(kind=kind, left=self.focus, right=right)
213
- case RightCrumb(left=left, kind=kind):
214
- parent = ThenResult(kind=kind, left=left, right=self.focus)
215
- case NamedCrumb(name=name, forward_map=forward_map, backward_map=backward_map, aggregator=aggregator):
216
- parent = NamedResult(name=name, value=self.focus, forward_map=forward_map, backward_map=backward_map, aggregator=aggregator)
217
- case ManyCrumb(before=before, after=after):
218
- parent = ManyResult(value=before + (self.focus,) + after)
219
- case OrCrumb():
220
- parent = OrResult(value=self.focus)
221
- case _:
222
- raise ValueError(f"Unexpected crumb type: {last}")
223
- return AST(focus=parent, breadcrumbs=tuple(rest), closed=frozenset(self.closed | {last}))
224
-
225
- def down_left(self) -> Optional[AST[T]]:
226
- match self.focus:
227
- case ThenResult(left=left, right=_, kind=kind):
228
- return AST(focus=left,
229
- breadcrumbs=self.breadcrumbs + (LeftCrumb("\u2199", right=self.focus.right, kind=kind),),
230
- closed=self.closed)
231
- case NamedResult(name=name,
232
- value=inner,
233
-
234
- forward_map=forward_map,
235
- backward_map=backward_map,
236
- aggregator=aggregator):
237
- return AST(focus=inner,
238
- breadcrumbs=self.breadcrumbs + (NamedCrumb("\u2199",
239
- name=name,
240
-
241
- forward_map=forward_map,
242
- backward_map=backward_map,
243
- aggregator=aggregator),),
244
- closed=self.closed)
245
- case ManyResult(value=()):
246
- return None
247
- case ManyResult(value=(head, *tail)):
248
- return AST(focus=head,
249
- breadcrumbs=self.breadcrumbs + (ManyCrumb("\u2199", before=(), after=tuple(tail)),),
250
- closed=self.closed)
251
- case OrResult(value=value):
252
- return AST(focus=value,
253
- breadcrumbs=self.breadcrumbs + (OrCrumb("\u2199"),),
254
- closed=self.closed)
255
- case _:
256
- return None
257
-
258
-
259
-
260
-
261
- def right(self) -> Optional[AST[T]]:
262
- if not self.breadcrumbs:
263
- return None
264
- *rest, last = self.breadcrumbs
265
- match last:
266
- case ManyCrumb(before=before, after=(next_, *after)):
267
- # If inside a ManyResult, and there are elements in after, return the next sibling and update before to include current focus
268
- new_last = ManyCrumb("\u2192", before=before + (self.focus,), after=tuple(after))
269
- return AST(focus=next_,
270
- breadcrumbs=tuple(rest) + (new_last,),
271
- # don't add the ManyCrumb(last) to closed, because we only close one of its children
272
- # and the whole ManyCrumb can not be considered closed
273
- # so we only add the current focus to closed if it is a Crumb.
274
- # if the client code hold MnayCrumb(last) as a scope, it should check
275
- # if the scope is in closed, and update the scope to the new ManyCrumb
276
- closed=frozenset(self.closed | {self.focus}) if isinstance(self.focus, Crumb) else frozenset(self.closed)
277
- )
278
- case LeftCrumb(right=right, kind=kind):
279
- return AST(focus=right,
280
- breadcrumbs=tuple(rest) + (RightCrumb("\u2192", self.focus, kind),),
281
- closed=frozenset(self.closed | {last}))
282
- case _:
283
- return None
284
-
285
-
286
- def replace(self, new_focus: ParseResult[T]) -> AST[T]:
287
- focus = new_focus
288
- for crumb in reversed(self.breadcrumbs):
289
- match crumb:
290
- case LeftCrumb(right=right, kind=kind):
291
- focus = ThenResult(left=focus, right=right, kind=kind)
292
- case RightCrumb(left=left, kind=kind):
293
- focus = ThenResult(left=left, right=focus, kind=kind)
294
- case NamedCrumb(name=name,
295
-
296
- forward_map=forward_map,
297
- backward_map=backward_map,
298
- aggregator=aggregator):
299
- focus = NamedResult(name=name,
300
- value=focus,
301
-
302
- forward_map=forward_map,
303
- backward_map=backward_map,
304
- aggregator=aggregator)
305
- case ManyCrumb(before=before, after=after):
306
- focus = ManyResult(value=before + (focus,) + after)
307
- case OrCrumb():
308
- focus = OrResult(value=focus)
309
- return AST(focus=focus)
310
-
311
- @cached_property
312
- def root(self) -> AST[T]:
313
- z = self
314
- while z.breadcrumbs:
315
- z = z.up() # type: ignore
316
- assert z is not None, "Zipper should not be None when breadcrumbs are present"
317
- return z
318
-
319
- @cached_property
320
- def leftmost(self) -> AST[T]:
321
- z = self
322
- while True:
323
- next_z = z.down_left()
324
- if next_z is None:
325
- return z
326
- z = next_z
327
-
328
22
 
329
23
 
330
24
 
@@ -386,7 +80,7 @@ class ParserState(Generic[T], Insptectable):
386
80
  class Parser(Algebra[Tuple[T,...] | T, ParserState[T]]):
387
81
  @classmethod
388
82
  def token(cls,
389
- token_type: Optional[TokenType] = None,
83
+ token_type: Optional[Enum] = None,
390
84
  text: Optional[str] = None,
391
85
  case_sensitive: bool = False,
392
86
  regex: Optional[re.Pattern[str]] = None
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
7
7
  Keywords: parser,combinator,sql,sqlite,generator,printer
8
- Requires-Python: >=3.9
8
+ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: rich>=14.1.0
@@ -0,0 +1,15 @@
1
+ syncraft/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ syncraft/algebra.py,sha256=jiqdo1GbXVwi_7Uoh1zBLe-TparNaHp8jW2IFt8PzBc,24494
3
+ syncraft/ast.py,sha256=y48DKYO-rSzmrObxsLXq2k_DfAplMnXjJBURY4Gv30U,7582
4
+ syncraft/cmd.py,sha256=DzXgU8QLVOg0YTFKpmOyzbf02LKPphQ4EeKSLzqpJ_s,2012
5
+ syncraft/diagnostic.py,sha256=_kLbl1LlU4Y2PtsLLZFA_OLqeZqq1f2rjT_LRXLnDaI,2813
6
+ syncraft/dsl.py,sha256=T89zkcE6xKvQSP4PJDHaWp9TsygXCTAzgx6r91KysFc,16504
7
+ syncraft/generator.py,sha256=56LbqoasPk7zFlGqYAKaXkZND0MfA-8V-F0xTrWKaUA,9261
8
+ syncraft/parser.py,sha256=rk3YF8B0G8PG3JKYpeMQsYmHC5JxqrkCMXZAPfaqgME,10210
9
+ syncraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ syncraft/sqlite3.py,sha256=2JNGDymSJti3mdc7hdeXIF78S6SsSLpKJwe6AzivskQ,34757
11
+ syncraft-0.1.11.dist-info/licenses/LICENSE,sha256=wHSV424U5csa3339dy1AZbsz2xsd0hrkMx2QK48CcUk,1062
12
+ syncraft-0.1.11.dist-info/METADATA,sha256=AII8S8yjjEW45-3YeCsK9bhWH1EJSJKqe5byW3AkHNM,946
13
+ syncraft-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ syncraft-0.1.11.dist-info/top_level.txt,sha256=Kq3t8ESXB2xW1Xt3uPmkENFc-c4f2pamNmaURBk7zc8,9
15
+ syncraft-0.1.11.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- syncraft/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- syncraft/algebra.py,sha256=jiqdo1GbXVwi_7Uoh1zBLe-TparNaHp8jW2IFt8PzBc,24494
3
- syncraft/cmd.py,sha256=DzXgU8QLVOg0YTFKpmOyzbf02LKPphQ4EeKSLzqpJ_s,2012
4
- syncraft/diagnostic.py,sha256=_kLbl1LlU4Y2PtsLLZFA_OLqeZqq1f2rjT_LRXLnDaI,2813
5
- syncraft/dsl.py,sha256=cUBBB8upivOJDfZteTgwppoRg8Z_e5Bo4urDgdUG_QU,16503
6
- syncraft/generator.py,sha256=Ik1dKRw--LBJYZY5QM9TgGHRl16o2zWwQRTDZPK8Ve0,12318
7
- syncraft/parser.py,sha256=wDiRJuNLsBsTBv0HZlGUGtkKMbDpIA8sTfsV3tA4BU0,22835
8
- syncraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- syncraft/sqlite3.py,sha256=2JNGDymSJti3mdc7hdeXIF78S6SsSLpKJwe6AzivskQ,34757
10
- syncraft-0.1.9.dist-info/licenses/LICENSE,sha256=wHSV424U5csa3339dy1AZbsz2xsd0hrkMx2QK48CcUk,1062
11
- syncraft-0.1.9.dist-info/METADATA,sha256=AcunHK5NzuLAbE7dsEEUGlFr_XOeH6Wy99RFDZKnCrY,944
12
- syncraft-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- syncraft-0.1.9.dist-info/top_level.txt,sha256=Kq3t8ESXB2xW1Xt3uPmkENFc-c4f2pamNmaURBk7zc8,9
14
- syncraft-0.1.9.dist-info/RECORD,,