syncraft 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of syncraft might be problematic. Click here for more details.

Files changed (28) hide show
  1. {syncraft-0.2.0 → syncraft-0.2.2}/PKG-INFO +1 -1
  2. {syncraft-0.2.0 → syncraft-0.2.2}/pyproject.toml +1 -1
  3. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/algebra.py +5 -2
  4. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/constraint.py +28 -8
  5. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/generator.py +9 -10
  6. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/parser.py +10 -13
  7. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/syntax.py +15 -49
  8. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft.egg-info/PKG-INFO +1 -1
  9. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft.egg-info/SOURCES.txt +2 -2
  10. {syncraft-0.2.0 → syncraft-0.2.2}/tests/test_bimap.py +58 -45
  11. syncraft-0.2.2/tests/test_constraint.py +53 -0
  12. {syncraft-0.2.0 → syncraft-0.2.2}/tests/test_find.py +1 -1
  13. {syncraft-0.2.0 → syncraft-0.2.2}/tests/test_parse.py +12 -9
  14. {syncraft-0.2.0 → syncraft-0.2.2}/tests/test_to.py +7 -5
  15. {syncraft-0.2.0 → syncraft-0.2.2}/tests/test_until.py +4 -4
  16. syncraft-0.2.0/tests/test_var.py +0 -0
  17. {syncraft-0.2.0 → syncraft-0.2.2}/LICENSE +0 -0
  18. {syncraft-0.2.0 → syncraft-0.2.2}/README.md +0 -0
  19. {syncraft-0.2.0 → syncraft-0.2.2}/setup.cfg +0 -0
  20. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/__init__.py +0 -0
  21. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/ast.py +0 -0
  22. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/diagnostic.py +0 -0
  23. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/finder.py +0 -0
  24. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/py.typed +0 -0
  25. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft/sqlite3.py +0 -0
  26. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft.egg-info/dependency_links.txt +0 -0
  27. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft.egg-info/requires.txt +0 -0
  28. {syncraft-0.2.0 → syncraft-0.2.2}/syncraft.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syncraft"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Parser combinator library"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -7,7 +7,6 @@ from typing import (
7
7
  import traceback
8
8
  from dataclasses import dataclass, replace
9
9
  from weakref import WeakKeyDictionary
10
- from abc import ABC
11
10
  from syncraft.ast import ThenKind, Then, Choice, Many, ChoiceKind, shallow_dict
12
11
  from syncraft.constraint import Bindable
13
12
 
@@ -75,12 +74,16 @@ class Error:
75
74
 
76
75
 
77
76
  @dataclass(frozen=True)
78
- class Algebra(ABC, Generic[A, S]):
77
+ class Algebra(Generic[A, S]):
79
78
  ######################################################## shared among all subclasses ########################################################
80
79
  run_f: Callable[[S, bool], Either[Any, Tuple[A, S]]]
81
80
  name: Hashable
82
81
  _cache: ClassVar[WeakKeyDictionary[Any, Dict[Any, object | Either[Any, Tuple[Any, Any]]]]] = WeakKeyDictionary()
83
82
 
83
+ @classmethod
84
+ def state(cls, *args:Any, **kwargs:Any)->Optional[S]:
85
+ return None
86
+
84
87
  def named(self, name: Hashable) -> 'Algebra[A, S]':
85
88
  return replace(self, name=name)
86
89
 
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field, replace
5
5
  import collections.abc
6
6
  from collections import defaultdict
7
7
  from itertools import product
8
+ from inspect import Signature
8
9
  import inspect
9
10
 
10
11
  K = TypeVar('K')
@@ -122,9 +123,9 @@ class Constraint:
122
123
  def predicate(cls,
123
124
  f: Callable[..., bool],
124
125
  *,
125
- name: Optional[str] = None,
126
- quant: Quantifier = Quantifier.FORALL)->Constraint:
127
- sig = inspect.signature(f)
126
+ sig: Signature,
127
+ name: str,
128
+ quant: Quantifier)->Constraint:
128
129
  pos_params = []
129
130
  kw_params = []
130
131
  for pname, param in sig.parameters.items():
@@ -160,12 +161,31 @@ class Constraint:
160
161
  else:
161
162
  return ConstraintResult(result = all(eval_combo(c) for c in all_combos), unbound=frozenset())
162
163
 
163
- return cls(run_f=run_f, name=name or f.__name__)
164
+ return cls(run_f=run_f, name=name)
165
+
166
+
167
+ def predicate(f: Callable[..., bool],
168
+ *,
169
+ name: Optional[str] = None,
170
+ quant: Quantifier = Quantifier.FORALL,
171
+ bimap: bool = True) -> Constraint:
172
+ name = name or f.__name__
173
+ sig = inspect.signature(f)
174
+ if bimap:
175
+ def wrapper(*args: Any, **kwargs:Any) -> bool:
176
+ mapped_args = [a.bimap()[0] if hasattr(a, "bimap") else a for a in args]
177
+ mapped_kwargs = {k: (v.bimap()[0] if hasattr(v, "bimap") else v) for k,v in kwargs.items()}
178
+ return f(*mapped_args, **mapped_kwargs)
179
+
180
+ return Constraint.predicate(wrapper, sig=sig, name=name, quant=quant)
181
+ else:
182
+ return Constraint.predicate(f, sig=sig, name=name, quant=quant)
164
183
 
165
- def forall(f: Callable[..., bool], name: Optional[str] = None) -> Constraint:
166
- return Constraint.predicate(f, name=name, quant=Quantifier.FORALL)
184
+ def forall(f: Callable[..., bool], name: Optional[str] = None, bimap: bool=True) -> Constraint:
185
+ return predicate(f, name=name, quant=Quantifier.FORALL, bimap=bimap)
167
186
 
168
- def exists(f: Callable[..., bool], name: Optional[str] = None) -> Constraint:
169
- return Constraint.predicate(f, name=name, quant=Quantifier.EXISTS)
187
+ def exists(f: Callable[..., bool], name: Optional[str] = None, bimap:bool = True) -> Constraint:
188
+ return predicate(f, name=name, quant=Quantifier.EXISTS, bimap=bimap)
189
+
170
190
 
171
191
 
@@ -16,14 +16,14 @@ from syncraft.ast import (
16
16
  Choice, Many, ChoiceKind,
17
17
  Then, ThenKind, Marked
18
18
  )
19
-
19
+ from syncraft.constraint import FrozenDict
20
20
  from syncraft.syntax import Syntax
21
21
  from sqlglot import TokenType
22
22
  import re
23
23
  import rstr
24
24
  from functools import lru_cache
25
25
  import random
26
- from rich import print
26
+
27
27
  from syncraft.constraint import Bindable
28
28
 
29
29
  T = TypeVar('T', bound=TokenProtocol)
@@ -144,6 +144,10 @@ class TokenGen(TokenSpec):
144
144
 
145
145
  @dataclass(frozen=True)
146
146
  class Generator(Algebra[ParseResult[T], GenState[T]]):
147
+ @classmethod
148
+ def state(cls, ast: Optional[ParseResult[T]] = None, seed: int = 0, restore_pruned: bool = False)->GenState[T]:
149
+ return GenState.from_ast(ast=ast, seed=seed, restore_pruned=restore_pruned)
150
+
147
151
  def flat_map(self, f: Callable[[ParseResult[T]], Algebra[B, GenState[T]]]) -> Algebra[B, GenState[T]]:
148
152
  def flat_map_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[B, GenState[T]]]:
149
153
  try:
@@ -293,14 +297,9 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
293
297
  def generate(syntax: Syntax[Any, Any],
294
298
  data: Optional[ParseResult[Any]] = None,
295
299
  seed: int = 0,
296
- restore_pruned: bool = False) -> AST | Any:
297
- gen = syntax(Generator)
298
- state = GenState.from_ast(ast=data, seed=seed, restore_pruned=restore_pruned)
299
- result = gen.run(state, use_cache=False)
300
- if isinstance(result, Right):
301
- return result.value[0]
302
- assert isinstance(result, Left), "Generator must return Either[Any, Tuple[Any, Any]]"
303
- return result.value
300
+ restore_pruned: bool = False) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
301
+ from syncraft.syntax import run
302
+ return run(syntax, Generator, False, ast=data, seed=seed, restore_pruned=restore_pruned)
304
303
 
305
304
 
306
305
 
@@ -5,6 +5,7 @@ from typing import (
5
5
  Optional, List, Any, Tuple, TypeVar,
6
6
  Generic
7
7
  )
8
+ from syncraft.constraint import FrozenDict
8
9
  from syncraft.algebra import (
9
10
  Either, Left, Right, Error, Algebra
10
11
  )
@@ -67,6 +68,11 @@ class ParserState(Bindable, Generic[T]):
67
68
 
68
69
  @dataclass(frozen=True)
69
70
  class Parser(Algebra[T, ParserState[T]]):
71
+ @classmethod
72
+ def state(cls, sql: str, dialect: str) -> ParserState[T]:
73
+ tokens = tuple([Token(token_type=token.token_type, text=token.text) for token in tokenize(sql, dialect=dialect)])
74
+ return ParserState.from_tokens(tokens) # type: ignore
75
+
70
76
  @classmethod
71
77
  def token(cls,
72
78
  token_type: Optional[Enum] = None,
@@ -182,21 +188,12 @@ def sqlglot(parser: Syntax[Any, Any],
182
188
  return parser.map(lambda tokens: [e for e in gp.parse(raw_tokens=tokens) if e is not None])
183
189
 
184
190
 
185
- def parse(syntax: Syntax[Any, Any],
186
- sql: str,
187
- dialect: str) -> AST | Any:
188
- parser = syntax(Parser)
189
- input: ParserState[Token] = token_state(sql, dialect=dialect)
190
- result = parser.run(input, True)
191
- if isinstance(result, Right):
192
- return result.value[0]
193
- assert isinstance(result, Left), "Parser must return Either[E, Tuple[A, S]]"
194
- return result.value
191
+ def parse(syntax: Syntax[Any, Any], sql: str, dialect: str) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
192
+ from syncraft.syntax import run
193
+ return run(syntax, Parser, True, sql=sql, dialect=dialect)
194
+
195
195
 
196
196
 
197
- def token_state(sql: str, dialect: str) -> ParserState[Token]:
198
- tokens = tuple([Token(token_type=token.token_type, text=token.text) for token in tokenize(sql, dialect=dialect)])
199
- return ParserState.from_tokens(tokens)
200
197
 
201
198
  def token(token_type: Optional[Enum] = None,
202
199
  text: Optional[str] = None,
@@ -2,17 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  from typing import (
4
4
  Optional, Any, TypeVar, Generic, Callable, Tuple, cast,
5
- Type, Literal, List, overload
5
+ Type, Literal, List
6
6
  )
7
7
  from dataclasses import dataclass, field, replace
8
8
  from functools import reduce
9
- from syncraft.algebra import Algebra, Error, Either, Right
10
- from syncraft.constraint import Bindable
9
+ from syncraft.algebra import Algebra, Error, Either, Right, Left
10
+ from syncraft.constraint import Bindable, FrozenDict
11
11
  from syncraft.ast import Then, ThenKind, Marked, Choice, Many, ChoiceKind, Nothing, Collect, E, Collector
12
12
  from types import MethodType, FunctionType
13
13
  import keyword
14
14
 
15
- from rich import print
15
+
16
16
 
17
17
  def valid_name(name: str) -> bool:
18
18
  return (name.isidentifier()
@@ -319,49 +319,15 @@ def success(value: Any) -> Syntax[Any, Any]:
319
319
  def choice(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
320
320
  return reduce(lambda a, b: a | b, parsers) if len(parsers) > 0 else success(Nothing())
321
321
 
322
-
323
- def all(*parsers: Syntax[Any, S]) -> Syntax[Then[Any, Any], S]:
324
- return reduce(lambda a, b: a + b, parsers) if len(parsers) > 0 else success(Nothing())
325
-
326
- def first(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
327
- return reduce(lambda a, b: a // b, parsers) if len(parsers) > 0 else success(Nothing())
328
-
329
- def last(*parsers: Syntax[Any, S]) -> Syntax[Any, S]:
330
- return reduce(lambda a, b: a >> b, parsers) if len(parsers) > 0 else success(Nothing())
331
-
332
- def bind(* parsers: Syntax[Any, S] | Tuple[str, Syntax[Any, S]]) -> Syntax[Any, S]:
333
- def is_named_parser(x: Any) -> bool:
334
- return isinstance(x, tuple) and len(x) == 2 and isinstance(x[0], str) and isinstance(x[1], Syntax)
335
-
336
- def to_parser(x: Syntax[Any, S] | Tuple[str, Syntax[Any, S]])->Syntax[Any, S]:
337
- if isinstance(x, tuple) and len(x) == 2 and isinstance(x[0], str) and isinstance(x[1], Syntax):
338
- if isinstance(x[0], str):
339
-
340
- return x[1].bind(x[0])
341
- else:
342
- raise ValueError(f"Invalid variable type(must be str | Variable): {x[0]}", x)
343
- elif isinstance(x, Syntax):
344
- return x
345
- else:
346
- raise ValueError(f"Invalid parser or tuple: {x}", x)
347
- ret: Optional[Syntax[Any, S]] = None
348
- has_data = False
349
- for p in parsers:
350
- just_parser = to_parser(p)
351
- if has_data:
352
- if ret is not None:
353
- if is_named_parser(p):
354
- ret = ret + just_parser
355
- else:
356
- ret = ret // just_parser
357
- else:
358
- ret = just_parser
359
- else:
360
- has_data = is_named_parser(p)
361
- if ret is None:
362
- ret = just_parser
363
- else:
364
- ret = ret >> just_parser
365
-
366
- return ret if ret is not None else success(Nothing())
322
+ def run(syntax: Syntax[A, S], alg: Type[Algebra[A, S]], use_cache:bool, *args: Any, **kwargs: Any) -> Tuple[Any, FrozenDict[str, Tuple[Any, ...]]] | Tuple[Any, None]:
323
+ parser = syntax(alg)
324
+ input: Optional[S] = alg.state(*args, **kwargs)
325
+ if input:
326
+ result = parser.run(input, use_cache=use_cache)
327
+ if isinstance(result, Right):
328
+ return result.value[0], result.value[1].binding.bound()
329
+ assert isinstance(result, Left), "Algebra must return Either[E, Tuple[A, S]]"
330
+ return result.value, None
331
+ else:
332
+ return Error(this=None, message="Algebra failed to create initial state"), None
367
333
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
@@ -18,8 +18,8 @@ syncraft.egg-info/dependency_links.txt
18
18
  syncraft.egg-info/requires.txt
19
19
  syncraft.egg-info/top_level.txt
20
20
  tests/test_bimap.py
21
+ tests/test_constraint.py
21
22
  tests/test_find.py
22
23
  tests/test_parse.py
23
24
  tests/test_to.py
24
- tests/test_until.py
25
- tests/test_var.py
25
+ tests/test_until.py
@@ -12,31 +12,33 @@ def test1_simple_then() -> None:
12
12
  A, B, C = literal("a"), literal("b"), literal("c")
13
13
  syntax = A // B // C
14
14
  sql = "a b c"
15
- ast = parse(syntax, sql, dialect="sqlite")
15
+ ast, bound = parse(syntax, sql, dialect="sqlite")
16
16
  print("---" * 40)
17
17
  print(ast)
18
- generated = gen.generate(syntax, ast)
18
+ generated, bound = gen.generate(syntax, ast)
19
19
  print("---" * 40)
20
20
  print(generated)
21
21
  assert ast == generated
22
22
  value, bmap = generated.bimap()
23
23
  print(value)
24
- assert gen.generate(syntax, bmap(value)) == generated
24
+ u, v = gen.generate(syntax, bmap(value))
25
+ assert u == generated
25
26
 
26
27
 
27
28
  def test2_named_results() -> None:
28
29
  A, B = literal("a").mark("x").mark('z'), literal("b").mark("y")
29
30
  syntax = A // B
30
31
  sql = "a b"
31
- ast = parse(syntax, sql, dialect="sqlite")
32
+ ast, bound = parse(syntax, sql, dialect="sqlite")
32
33
  print("---" * 40)
33
34
  print(ast)
34
- generated = gen.generate(syntax, ast)
35
+ generated, bound = gen.generate(syntax, ast)
35
36
  print("---" * 40)
36
37
  print(generated)
37
38
  assert ast == generated
38
39
  value, bmap = generated.bimap()
39
- assert gen.generate(syntax, bmap(value)) == generated
40
+ u,v = gen.generate(syntax, bmap(value))
41
+ assert u == generated
40
42
 
41
43
 
42
44
 
@@ -44,15 +46,16 @@ def test3_many_literals() -> None:
44
46
  A = literal("a")
45
47
  syntax = A.many()
46
48
  sql = "a a a"
47
- ast = parse(syntax, sql, dialect="sqlite")
49
+ ast, bound = parse(syntax, sql, dialect="sqlite")
48
50
  print("---" * 40)
49
51
  print(ast)
50
- generated = gen.generate(syntax, ast)
52
+ generated, bound = gen.generate(syntax, ast)
51
53
  print("---" * 40)
52
54
  print(generated)
53
55
  assert ast == generated
54
56
  value, bmap = generated.bimap()
55
- assert gen.generate(syntax, bmap(value)) == generated
57
+ u, v = gen.generate(syntax, bmap(value))
58
+ assert u == generated
56
59
 
57
60
 
58
61
  def test4_mixed_many_named() -> None:
@@ -60,30 +63,32 @@ def test4_mixed_many_named() -> None:
60
63
  B = literal("b")
61
64
  syntax = (A | B).many()
62
65
  sql = "a b a"
63
- ast = parse(syntax, sql, dialect="sqlite")
66
+ ast, bound = parse(syntax, sql, dialect="sqlite")
64
67
  print("---" * 40)
65
68
  print(ast)
66
- generated = gen.generate(syntax, ast)
69
+ generated, bound = gen.generate(syntax, ast)
67
70
  print("---" * 40)
68
71
  print(generated)
69
72
  assert ast == generated
70
73
  value, bmap = generated.bimap()
71
- assert gen.generate(syntax, bmap(value)) == generated
74
+ u, v = gen.generate(syntax, bmap(value))
75
+ assert u == generated
72
76
 
73
77
 
74
78
  def test5_nested_then_many() -> None:
75
79
  IF, THEN, END = literal("if"), literal("then"), literal("end")
76
80
  syntax = (IF.many() // THEN.many()).many() // END
77
81
  sql = "if if then end"
78
- ast = parse(syntax, sql, dialect="sqlite")
82
+ ast, bound = parse(syntax, sql, dialect="sqlite")
79
83
  print("---" * 40)
80
84
  print(ast)
81
- generated = gen.generate(syntax, ast, restore_pruned=True)
85
+ generated, bound = gen.generate(syntax, ast, restore_pruned=True)
82
86
  print("---" * 40)
83
87
  print(generated)
84
88
  assert ast == generated
85
89
  value, bmap = generated.bimap()
86
- assert gen.generate(syntax, bmap(value), restore_pruned=True) == generated
90
+ u, v = gen.generate(syntax, bmap(value), restore_pruned=True)
91
+ assert u == generated
87
92
 
88
93
 
89
94
 
@@ -91,12 +96,13 @@ def test_then_flatten():
91
96
  A, B, C = literal("a"), literal("b"), literal("c")
92
97
  syntax = A + (B + C)
93
98
  sql = "a b c"
94
- ast = parse(syntax, sql, dialect='sqlite')
99
+ ast, bound = parse(syntax, sql, dialect='sqlite')
95
100
  print(ast)
96
- generated = gen.generate(syntax, ast)
101
+ generated, bound = gen.generate(syntax, ast)
97
102
  assert ast == generated
98
103
  value, bmap = generated.bimap()
99
- assert gen.generate(syntax, bmap(value)) == generated
104
+ u, v = gen.generate(syntax, bmap(value))
105
+ assert u == generated
100
106
 
101
107
 
102
108
 
@@ -106,24 +112,26 @@ def test_named_in_then():
106
112
  C = literal("c").mark("third")
107
113
  syntax = A + B + C
108
114
  sql = "a b c"
109
- ast = parse(syntax, sql, dialect='sqlite')
115
+ ast, bound = parse(syntax, sql, dialect='sqlite')
110
116
  print(ast)
111
- generated = gen.generate(syntax, ast)
117
+ generated, bound = gen.generate(syntax, ast)
112
118
  assert ast == generated
113
119
  value, bmap = generated.bimap()
114
- assert gen.generate(syntax, bmap(value)) == generated
120
+ u, v = gen.generate(syntax, bmap(value))
121
+ assert u == generated
115
122
 
116
123
 
117
124
  def test_named_in_many():
118
125
  A = literal("x").mark("x")
119
126
  syntax = A.many()
120
127
  sql = "x x x"
121
- ast = parse(syntax, sql, dialect='sqlite')
128
+ ast, bound = parse(syntax, sql, dialect='sqlite')
122
129
  print(ast)
123
- generated = gen.generate(syntax, ast)
130
+ generated, bound = gen.generate(syntax, ast)
124
131
  assert ast == generated
125
132
  value, bmap = generated.bimap()
126
- assert gen.generate(syntax, bmap(value)) == generated
133
+ u, v = gen.generate(syntax, bmap(value))
134
+ assert u == generated
127
135
 
128
136
 
129
137
  def test_named_in_or():
@@ -131,12 +139,13 @@ def test_named_in_or():
131
139
  B = literal("b").mark("b")
132
140
  syntax = A | B
133
141
  sql = "b"
134
- ast = parse(syntax, sql, dialect='sqlite')
142
+ ast, bound = parse(syntax, sql, dialect='sqlite')
135
143
  print(ast)
136
- generated = gen.generate(syntax, ast)
144
+ generated, bound = gen.generate(syntax, ast)
137
145
  assert ast == generated
138
146
  value, bmap = generated.bimap()
139
- assert gen.generate(syntax, bmap(value)) == generated
147
+ u, v = gen.generate(syntax, bmap(value))
148
+ assert u == generated
140
149
 
141
150
 
142
151
 
@@ -148,21 +157,22 @@ def test_deep_mix():
148
157
  C = literal("c").mark("c")
149
158
  syntax = ((A + B) | C).many() + B
150
159
  sql = "a b a b c b"
151
- ast = parse(syntax, sql, dialect='sqlite')
160
+ ast, bound = parse(syntax, sql, dialect='sqlite')
152
161
  print(ast)
153
- generated = gen.generate(syntax, ast)
162
+ generated, bound = gen.generate(syntax, ast)
154
163
  print('---' * 40)
155
164
  print(generated)
156
165
  assert ast == generated
157
166
  value, bmap = generated.bimap()
158
- assert gen.generate(syntax, bmap(value)) == generated
167
+ u, v = gen.generate(syntax, bmap(value))
168
+ assert u == generated
159
169
 
160
170
 
161
171
  def test_empty_many() -> None:
162
172
  A = literal("a")
163
173
  syntax = A.many() # This should allow empty matches
164
174
  sql = ""
165
- ast = parse(syntax, sql, dialect="sqlite")
175
+ ast, bound = parse(syntax, sql, dialect="sqlite")
166
176
  assert isinstance(ast, Error)
167
177
 
168
178
 
@@ -171,9 +181,10 @@ def test_backtracking_many() -> None:
171
181
  B = literal("b")
172
182
  syntax = (A.many() + B) # must not eat the final "a" needed for B
173
183
  sql = "a a a a b"
174
- ast = parse(syntax, sql, dialect="sqlite")
184
+ ast, bound = parse(syntax, sql, dialect="sqlite")
175
185
  value, bmap = ast.bimap()
176
- assert gen.generate(syntax, bmap(value)) == ast
186
+ u, v = gen.generate(syntax, bmap(value))
187
+ assert u == ast
177
188
 
178
189
  def test_deep_nesting() -> None:
179
190
  A = literal("a")
@@ -181,7 +192,7 @@ def test_deep_nesting() -> None:
181
192
  for _ in range(100):
182
193
  syntax = syntax + A
183
194
  sql = " " .join("a" for _ in range(101))
184
- ast = parse(syntax, sql, dialect="sqlite")
195
+ ast, bound = parse(syntax, sql, dialect="sqlite")
185
196
  assert ast is not None
186
197
 
187
198
 
@@ -189,7 +200,7 @@ def test_nested_many() -> None:
189
200
  A = literal("a")
190
201
  syntax = (A.many().many()) # groups of groups of "a"
191
202
  sql = "a a a"
192
- ast = parse(syntax, sql, dialect="sqlite")
203
+ ast, bound = parse(syntax, sql, dialect="sqlite")
193
204
  assert isinstance(ast, Many)
194
205
 
195
206
 
@@ -197,9 +208,10 @@ def test_named_many() -> None:
197
208
  A = literal("a").mark("alpha")
198
209
  syntax = A.many()
199
210
  sql = "a a"
200
- ast = parse(syntax, sql, dialect="sqlite")
211
+ ast, bound = parse(syntax, sql, dialect="sqlite")
201
212
  value, bmap = ast.bimap()
202
- assert gen.generate(syntax, bmap(value)) == ast
213
+ u, v = gen.generate(syntax, bmap(value))
214
+ assert u == ast
203
215
 
204
216
 
205
217
  def test_or_named() -> None:
@@ -207,9 +219,10 @@ def test_or_named() -> None:
207
219
  B = literal("b").mark("y")
208
220
  syntax = A | B
209
221
  sql = "b"
210
- ast = parse(syntax, sql, dialect="sqlite")
222
+ ast, bound = parse(syntax, sql, dialect="sqlite")
211
223
  value, bmap = ast.bimap()
212
- assert gen.generate(syntax, bmap(value)) == ast
224
+ u, v = gen.generate(syntax, bmap(value))
225
+ assert u == ast
213
226
 
214
227
 
215
228
  def test_then_associativity() -> None:
@@ -218,7 +231,7 @@ def test_then_associativity() -> None:
218
231
  C = literal("c")
219
232
  syntax = A + B + C
220
233
  sql = "a b c"
221
- ast = parse(syntax, sql, dialect="sqlite")
234
+ ast, bound = parse(syntax, sql, dialect="sqlite")
222
235
  # Should be Then(Then(A,B),C)
223
236
  assert ast == Then(kind=ThenKind.BOTH,
224
237
  left=Then(kind=ThenKind.BOTH,
@@ -232,7 +245,7 @@ def test_ambiguous() -> None:
232
245
  B = literal("a") + literal("b")
233
246
  syntax = A | B
234
247
  sql = "a"
235
- ast = parse(syntax, sql, dialect="sqlite")
248
+ ast, bound = parse(syntax, sql, dialect="sqlite")
236
249
  # Does it prefer A (shorter) or B (fails)? Depends on design.
237
250
  assert ast == Choice[Token, Token](value=TokenGen.from_string("a"), kind=ChoiceKind.LEFT)
238
251
 
@@ -245,19 +258,19 @@ def test_combo() -> None:
245
258
  sql = "a b a b c b"
246
259
  # Should fail, as we discussed earlier
247
260
  # the working syntax should be ((A + B) | C).many() + B
248
- ast = parse(syntax, sql, dialect="sqlite")
261
+ ast, bound = parse(syntax, sql, dialect="sqlite")
249
262
  assert isinstance(ast, Error)
250
- ast = parse(((A + B) | C).many() + B, sql, dialect="sqlite")
263
+ ast, bound = parse(((A + B) | C).many() + B, sql, dialect="sqlite")
251
264
  assert not isinstance(ast, Error)
252
265
 
253
266
 
254
267
  def test_optional():
255
268
  A = literal("a").mark("a")
256
269
  syntax = A.optional()
257
- ast1 = parse(syntax, "", dialect="sqlite")
270
+ ast1, bound = parse(syntax, "", dialect="sqlite")
258
271
  v1, _ = ast1.bimap()
259
272
  assert isinstance(v1, Nothing)
260
- ast2 = parse(syntax, "a", dialect="sqlite")
273
+ ast2, bound = parse(syntax, "a", dialect="sqlite")
261
274
  v2, _ = ast2.bimap()
262
275
  assert v2 == Marked(name='a', value=TokenGen.from_string('a'))
263
276
 
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+ from typing import Any, List, Tuple
3
+ from syncraft.algebra import Either, Left, Right, Error
4
+ from syncraft.ast import Marked, Then, ThenKind, Many, Nothing
5
+ from syncraft.parser import literal, variable, parse, Parser, Token
6
+ from syncraft.generator import TokenGen
7
+ from syncraft.constraint import forall, exists
8
+ from rich import print
9
+ import syncraft.generator as gen
10
+ from dataclasses import dataclass
11
+
12
+
13
+ def test_to() -> None:
14
+ @dataclass
15
+ class IfThenElse:
16
+ condition: Any
17
+ then: Any
18
+ otherwise: Any
19
+
20
+ @dataclass
21
+ class While:
22
+ condition:Any
23
+ body:Any
24
+
25
+ WHILE = literal("while")
26
+ IF = literal("if")
27
+ ELSE = literal("else")
28
+ THEN = literal("then")
29
+ END = literal("end")
30
+ A = literal('a')
31
+ B = literal('b')
32
+ C = literal('c')
33
+ D = literal('d')
34
+ M = literal(',')
35
+ var = A | B | C | D
36
+ condition = var.sep_by(M).mark('condition').bind()
37
+ ifthenelse = (IF >> condition
38
+ // THEN
39
+ + var.sep_by(M).mark('then').bind()
40
+ // ELSE
41
+ + var.sep_by(M).mark('otherwise').bind()
42
+ // END).to(IfThenElse).many()
43
+ syntax = (WHILE >> condition
44
+ + ifthenelse.mark('body').bind()
45
+ // ~END).to(While)
46
+ sql = 'while b if a,b then c,d else a,d end if a,b then c,d else a,d end'
47
+ ast, bound = parse(syntax, sql, dialect='sqlite')
48
+ def p(condition, then, otherwise)->bool:
49
+ print({'condition':condition, 'then':then, 'otherwise':otherwise})
50
+ return True
51
+ if bound is not None:
52
+ forall(p)(bound)
53
+ g, bound = gen.generate(syntax, ast, restore_pruned=True)
@@ -43,5 +43,5 @@ def test_find()->None:
43
43
  + ifthenelse.mark('body')
44
44
  // ~END).to(While)
45
45
  sql = 'while b if a,b then c,d else a,d end if a,b then c,d else a,d end'
46
- ast = parse(syntax, sql, dialect='sqlite')
46
+ ast, bound = parse(syntax, sql, dialect='sqlite')
47
47
  print(ast)
@@ -13,21 +13,23 @@ var = variable()
13
13
  def test_between()->None:
14
14
  sql = "then if then"
15
15
  syntax = IF.between(THEN, THEN)
16
- ast:AST = parse(syntax, sql, dialect='sqlite')
17
- generated = gen.generate(syntax, ast)
16
+ ast, bound = parse(syntax, sql, dialect='sqlite')
17
+ generated, bound = gen.generate(syntax, ast)
18
18
  assert ast == generated, "Parsed and generated results do not match."
19
19
  x, f = generated.bimap()
20
- assert gen.generate(syntax, f(x)) == ast
20
+ u, v = gen.generate(syntax, f(x))
21
+ assert u == ast
21
22
 
22
23
 
23
24
  def test_sep_by()->None:
24
25
  sql = "if then if then if then if"
25
26
  syntax = IF.sep_by(THEN)
26
- ast:AST = parse(syntax, sql, dialect='sqlite')
27
- generated = gen.generate(syntax, ast)
27
+ ast, bound = parse(syntax, sql, dialect='sqlite')
28
+ generated, bound = gen.generate(syntax, ast)
28
29
  assert ast == generated, "Parsed and generated results do not match."
29
30
  x, f = generated.bimap()
30
- assert gen.generate(syntax, f(x)) == ast
31
+ u, v = gen.generate(syntax, f(x))
32
+ assert u == ast
31
33
 
32
34
  def test_many_or()->None:
33
35
  IF = literal("if")
@@ -35,8 +37,9 @@ def test_many_or()->None:
35
37
  END = literal("end")
36
38
  syntax = (IF.many() | THEN.many()).many() // END
37
39
  sql = "if if then end"
38
- ast:AST = parse(syntax, sql, dialect='sqlite')
39
- generated = gen.generate(syntax, ast)
40
+ ast, bound = parse(syntax, sql, dialect='sqlite')
41
+ generated, bound = gen.generate(syntax, ast)
40
42
  assert ast == generated, "Parsed and generated results do not match."
41
43
  x, f = generated.bimap()
42
- assert gen.generate(syntax, f(x)) == ast
44
+ u, v = gen.generate(syntax, f(x))
45
+ assert u == ast
@@ -43,19 +43,21 @@ def test_to() -> None:
43
43
  + ifthenelse.mark('body')
44
44
  // ~END).to(While)
45
45
  sql = 'while b if a,b then c,d else a,d end if a,b then c,d else a,d end'
46
- ast = parse(syntax, sql, dialect='sqlite')
46
+ ast, bound = parse(syntax, sql, dialect='sqlite')
47
47
  print(ast)
48
- g = gen.generate(syntax, ast, restore_pruned=True)
48
+ g, bound = gen.generate(syntax, ast, restore_pruned=True)
49
49
  assert ast == g
50
50
  x, f = g.bimap()
51
51
  print(1, x)
52
- assert gen.generate(syntax, f(x), restore_pruned=True) == ast
52
+ u,v = gen.generate(syntax, f(x), restore_pruned=True)
53
+ assert u == ast
53
54
  x.body.append(x.body[0])
54
55
  print(2, x)
55
56
  print(f(x))
56
- ast2 = gen.generate(syntax, f(x), restore_pruned=True)
57
+ ast2, bound = gen.generate(syntax, f(x), restore_pruned=True)
57
58
  print(ast2)
58
59
  y, fy = ast2.bimap()
59
60
  print(3, y)
60
61
  assert y == x
61
- assert gen.generate(syntax, fy(y), restore_pruned=True) == ast2
62
+ u, v = gen.generate(syntax, fy(y), restore_pruned=True)
63
+ assert u == ast2
@@ -10,7 +10,7 @@ LB, RB = literal("["), literal("]")
10
10
  def test_until_accepts_proper_nesting() -> None:
11
11
  sql = "([])"
12
12
  syntax = until((LP, RP), (LB, RB))
13
- ast: AST | Any = parse(syntax, sql, dialect="sqlite")
13
+ ast, bound = parse(syntax, sql, dialect="sqlite")
14
14
  assert isinstance(ast, tuple), f"Expected AST for proper nesting, got {ast}"
15
15
 
16
16
 
@@ -18,7 +18,7 @@ def test_until_rejects_mismatched_pairs() -> None:
18
18
  # Mismatched: ( ] should fail immediately
19
19
  sql = "(]"
20
20
  syntax = until((LP, RP), (LB, RB))
21
- res = parse(syntax, sql, dialect="sqlite")
21
+ res, bound = parse(syntax, sql, dialect="sqlite")
22
22
  from syncraft.algebra import Error
23
23
  assert isinstance(res, Error), "Mismatched pairs should be rejected with an Error"
24
24
 
@@ -26,7 +26,7 @@ def test_until_rejects_unterminated_group() -> None:
26
26
  # Unterminated: ( ... EOF
27
27
  sql = "("
28
28
  syntax = until((LP, RP))
29
- res = parse(syntax, sql, dialect="sqlite")
29
+ res, bound = parse(syntax, sql, dialect="sqlite")
30
30
  from syncraft.algebra import Error
31
31
  assert isinstance(res, Error), "Unterminated group should be rejected with an Error"
32
32
 
@@ -35,6 +35,6 @@ def test_until_rejects_crossing_pairs() -> None:
35
35
  sql = "([)]"
36
36
  syntax = until((LP, RP), (LB, RB))
37
37
  # Use postgres dialect so [ and ] are tokenized distinctly (not as bracketed identifier)
38
- res = parse(syntax, sql, dialect="postgres")
38
+ res, bound = parse(syntax, sql, dialect="postgres")
39
39
  from syncraft.algebra import Error
40
40
  assert isinstance(res, Error), "Crossing pairs should be rejected with an Error"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes