syncraft 0.2.6__tar.gz → 0.2.8__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 (34) hide show
  1. {syncraft-0.2.6/syncraft.egg-info → syncraft-0.2.8}/PKG-INFO +1 -1
  2. {syncraft-0.2.6 → syncraft-0.2.8}/pyproject.toml +1 -1
  3. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/algebra.py +5 -5
  4. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/ast.py +1 -0
  5. syncraft-0.2.8/syncraft/cache.py +142 -0
  6. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/generator.py +1 -1
  7. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/parser.py +0 -27
  8. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/syntax.py +13 -22
  9. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/walker.py +39 -43
  10. {syncraft-0.2.6 → syncraft-0.2.8/syncraft.egg-info}/PKG-INFO +1 -1
  11. syncraft-0.2.8/tests/test_lazy.py +107 -0
  12. {syncraft-0.2.6 → syncraft-0.2.8}/tests/test_walk.py +4 -4
  13. syncraft-0.2.6/syncraft/cache.py +0 -113
  14. syncraft-0.2.6/tests/test_lazy.py +0 -50
  15. {syncraft-0.2.6 → syncraft-0.2.8}/LICENSE +0 -0
  16. {syncraft-0.2.6 → syncraft-0.2.8}/README.md +0 -0
  17. {syncraft-0.2.6 → syncraft-0.2.8}/setup.cfg +0 -0
  18. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/__init__.py +0 -0
  19. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/constraint.py +0 -0
  20. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/dev.py +0 -0
  21. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/finder.py +0 -0
  22. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/lexer.py +0 -0
  23. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/py.typed +0 -0
  24. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/sqlite3.py +0 -0
  25. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft/utils.py +0 -0
  26. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft.egg-info/SOURCES.txt +0 -0
  27. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft.egg-info/dependency_links.txt +0 -0
  28. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft.egg-info/requires.txt +0 -0
  29. {syncraft-0.2.6 → syncraft-0.2.8}/syncraft.egg-info/top_level.txt +0 -0
  30. {syncraft-0.2.6 → syncraft-0.2.8}/tests/test_bimap.py +0 -0
  31. {syncraft-0.2.6 → syncraft-0.2.8}/tests/test_constraint.py +0 -0
  32. {syncraft-0.2.6 → syncraft-0.2.8}/tests/test_find.py +0 -0
  33. {syncraft-0.2.6 → syncraft-0.2.8}/tests/test_parse.py +0 -0
  34. {syncraft-0.2.6 → syncraft-0.2.8}/tests/test_to.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.2.6
3
+ Version: 0.2.8
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.6"
3
+ version = "0.2.8"
4
4
  description = "Parser combinator library"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -77,7 +77,7 @@ class Algebra(Generic[A, S]):
77
77
  ######################################################## shared among all subclasses ########################################################
78
78
  run_f: Callable[[S, bool], Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]]
79
79
  name: Hashable
80
- cache: Cache[ S, Either[Any, Tuple[A, S]] ]
80
+ cache: Cache[Either[Any, Tuple[A, S]]]
81
81
 
82
82
  @classmethod
83
83
  def state(cls, **kwargs:Any)->Optional[S]:
@@ -103,10 +103,10 @@ class Algebra(Generic[A, S]):
103
103
  cache: Cache) -> Algebra[A, S]:
104
104
  def algebra_lazy_run(input: S, use_cache:bool) -> Generator[Incomplete[S], S, Either[Any, Tuple[A, S]]]:
105
105
  alg = thunk()
106
- print('--' * 20, "Algebra.lazy.algebra_lazy_run", '--' * 20)
107
- print('thunk', thunk, id(thunk))
108
- print('input', input, id(input))
109
- print('alg', alg, id(alg))
106
+ # print('--' * 20, "Algebra.lazy.algebra_lazy_run", '--' * 20)
107
+ # print('thunk', thunk, id(thunk))
108
+ # print('input', input, id(input))
109
+ # print('alg', alg, id(alg))
110
110
  result = yield from alg.run(input, use_cache)
111
111
  return result
112
112
  return cls(algebra_lazy_run, name=cls.__name__ + '.lazy', cache=cache)
@@ -491,6 +491,7 @@ class LazySpec(SyntaxSpec, Generic[A]):
491
491
  value: A
492
492
  @dataclass(frozen=True)
493
493
  class ThenSpec(SyntaxSpec, Generic[A, B]):
494
+ kind: ThenKind
494
495
  left: A
495
496
  right: B
496
497
 
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, TypeVar, Hashable, Generic, Callable, Any, Generator, overload, Literal
5
+ from weakref import WeakKeyDictionary
6
+ from syncraft.ast import SyncraftError
7
+
8
+
9
+ class RecursionError(SyncraftError):
10
+ def __init__(self, message: str, offending: Any, expect: Any = None, **kwargs: Any) -> None:
11
+ super().__init__(message, offending, expect, **kwargs)
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class InProgress:
16
+ _instance = None
17
+ def __new__(cls):
18
+ if cls._instance is None:
19
+ cls._instance = super(InProgress, cls).__new__(cls)
20
+ return cls._instance
21
+ def __str__(self)->str:
22
+ return self.__class__.__name__
23
+ def __repr__(self)->str:
24
+ return self.__str__()
25
+
26
+
27
+
28
+
29
+ Args = TypeVar('Args', bound=Hashable)
30
+ Ret = TypeVar('Ret')
31
+
32
+ @dataclass
33
+ class Cache(Generic[Ret]):
34
+ cache: WeakKeyDictionary[Callable[..., Any], Dict[Hashable, Ret | InProgress]] = field(default_factory=WeakKeyDictionary)
35
+
36
+ def __contains__(self, f: Callable[..., Any]) -> bool:
37
+ return f in self.cache
38
+
39
+ def __repr__(self) -> str:
40
+ return f"Cache({({f.__name__: list(c.keys()) for f, c in self.cache.items()})})"
41
+
42
+
43
+ def __or__(self, other: Cache[Any]) -> Cache[Any]:
44
+ assert self.cache is other.cache, "There should be only one global cache"
45
+ return self
46
+
47
+ @overload
48
+ def _execute(self,
49
+ f: Callable[..., Ret],
50
+ *args:Any,
51
+ is_gen: Literal[False],
52
+ **kwargs:Any) -> Ret: ...
53
+ @overload
54
+ def _execute(self,
55
+ f: Callable[..., Generator[Any, Any, Ret]],
56
+ *args: Any,
57
+ is_gen: Literal[True],
58
+ **kwargs: Any) -> Generator[Any, Any, Ret]: ...
59
+
60
+
61
+ def _execute(self,
62
+ f: Callable[..., Any],
63
+ *args: Any,
64
+ is_gen: bool,
65
+ **kwargs: Any) -> Ret | Generator[Any, Any, Ret]:
66
+ if f not in self.cache:
67
+ self.cache.setdefault(f, dict())
68
+ c: Dict[Hashable, Ret | InProgress] = self.cache[f]
69
+ key = (args, tuple(sorted(kwargs.items())))
70
+ if key in c:
71
+ v = c[key]
72
+ if isinstance(v, InProgress):
73
+ raise RecursionError("Left-recursion detected in parser", offending=f, state=args)
74
+ else:
75
+ return v
76
+ try:
77
+ c[key] = InProgress()
78
+ if is_gen:
79
+ result = yield from f(*args, **kwargs)
80
+ else:
81
+ result = f(*args, **kwargs)
82
+ c[key] = result
83
+ if kwargs.get('use_cache', True) is False:
84
+ c.pop(key, None)
85
+ return result
86
+ except Exception as e:
87
+ c.pop(key, None)
88
+ raise e
89
+
90
+ def gen(self,
91
+ f: Callable[..., Generator[Any, Any, Ret]],
92
+ *args: Any,
93
+ **kwargs: Any) -> Generator[Any, Any, Ret]:
94
+ if f not in self.cache:
95
+ self.cache.setdefault(f, dict())
96
+ c: Dict[Hashable, Ret | InProgress] = self.cache[f]
97
+ key = (tuple(filter(lambda x: not isinstance(x, Cache), args)), tuple(sorted(filter(lambda item: not isinstance(item[1], Cache), kwargs.items()))))
98
+ if key in c:
99
+ v = c[key]
100
+ if isinstance(v, InProgress):
101
+ raise RecursionError("Left-recursion detected in parser", offending=f, state=args)
102
+ else:
103
+ return v
104
+ try:
105
+ c[key] = InProgress()
106
+ result = yield from f(*args, **kwargs)
107
+ c[key] = result
108
+ if kwargs.get('use_cache', True) is False:
109
+ c.pop(key, None)
110
+ return result
111
+ except Exception as e:
112
+ c.pop(key, None)
113
+ raise e
114
+
115
+
116
+ def call(self,
117
+ f: Callable[..., Ret],
118
+ *args:Any,
119
+ **kwargs:Any) -> Ret:
120
+ if f not in self.cache:
121
+ self.cache.setdefault(f, dict())
122
+ c: Dict[Hashable, Ret | InProgress] = self.cache[f]
123
+ key = (tuple(filter(lambda x: not isinstance(x, Cache), args)), tuple(sorted(filter(lambda item: not isinstance(item[1], Cache), kwargs.items()))))
124
+ if key in c:
125
+ v = c[key]
126
+ if isinstance(v, InProgress):
127
+ raise RecursionError("Left-recursion detected in parser", offending=f, state=args)
128
+ else:
129
+ return v
130
+ try:
131
+ c[key] = InProgress()
132
+ result = f(*args, **kwargs)
133
+ c[key] = result
134
+ if kwargs.get('use_cache', True) is False:
135
+ c.pop(key, None)
136
+ return result
137
+ except Exception as e:
138
+ c.pop(key, None)
139
+ raise e
140
+
141
+
142
+
@@ -245,7 +245,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
245
245
  Algebra[B, GenState[T]]: An algebra yielding the final result.
246
246
  """
247
247
  def flat_map_run(input: GenState[T], use_cache:bool) -> PyGenerator[Incomplete[GenState[T]], GenState[T], Either[Any, Tuple[B, GenState[T]]]]:
248
- if not isinstance(input.ast, Then) or isinstance(input.ast, Nothing):
248
+ if not input.pruned and (not isinstance(input.ast, Then) or isinstance(input.ast, Nothing)):
249
249
  return Left(Error(this=self,
250
250
  message=f"Expect Then got {input.ast}",
251
251
  state=input))
@@ -122,18 +122,6 @@ class ParserState(Bindable, Generic[T]):
122
122
  class Parser(Algebra[T, ParserState[T]]):
123
123
  @classmethod
124
124
  def state(cls, sql: str, dialect: str) -> ParserState[T]: # type: ignore
125
- """Tokenize SQL text into an initial ``ParserState``.
126
-
127
- Uses ``sqlglot.tokenize`` for the given dialect and wraps tokens into
128
- the project's ``Token`` type.
129
-
130
- Args:
131
- sql: The SQL text to tokenize.
132
- dialect: The sqlglot dialect name (e.g. "sqlite", "duckdb").
133
-
134
- Returns:
135
- ParserState[T]: Initial parser state at index 0.
136
- """
137
125
  tokens = tuple([Token(token_type=token.token_type, text=token.text) for token in tokenize(sql, dialect=dialect)])
138
126
  return ParserState.from_tokens(tokens) # type: ignore
139
127
 
@@ -146,21 +134,6 @@ class Parser(Algebra[T, ParserState[T]]):
146
134
  case_sensitive: bool = False,
147
135
  regex: Optional[re.Pattern[str]] = None
148
136
  )-> Algebra[T, ParserState[T]]:
149
- """Match a single token according to a specification.
150
-
151
- Succeeds when the current token satisfies the provided
152
- ``TokenSpec`` (by type, exact text, or regex). On failure,
153
- an informative ``Error`` is produced with location context.
154
-
155
- Args:
156
- token_type: Expected enum type of the token.
157
- text: Exact token text to match.
158
- case_sensitive: Whether text matching is case sensitive.
159
- regex: Regular expression pattern to match token text.
160
-
161
- Returns:
162
- Algebra[T, ParserState[T]]: An algebra yielding the matched token.
163
- """
164
137
  spec = TokenSpec(token_type=token_type, text=text, case_sensitive=case_sensitive, regex=regex)
165
138
  def token_run(state: ParserState[T], use_cache:bool) -> Generator[Incomplete[ParserState[T]],ParserState[T], Either[Any, Tuple[T, ParserState[T]]]]:
166
139
  while True:
@@ -54,7 +54,7 @@ class Syntax(Generic[A, S]):
54
54
  """
55
55
  The core signature of Syntax is take an Algebra Class and return an Algebra Instance.
56
56
  """
57
- alg: Callable[[Type[Algebra[Any, Any]], Cache[Any, Any]], Algebra[A, S]]
57
+ alg: Callable[[Type[Algebra[Any, Any]], Cache[Any]], Algebra[A, S]]
58
58
  meta: Description = field(default_factory=Description, repr=False)
59
59
 
60
60
  def algebra(self, name: str | MethodType | FunctionType, *args: Any, **kwargs: Any) -> Syntax[A, S]:
@@ -71,7 +71,7 @@ class Syntax(Generic[A, S]):
71
71
  Returns:
72
72
  A new Syntax reflecting the transformed algebra.
73
73
  """
74
- def algebra_run(cls: Type[Algebra[Any, S]], cache: Cache[Any, Any]) -> Algebra[Any, S]:
74
+ def algebra_run(cls: Type[Algebra[Any, S]], cache: Cache[Any]) -> Algebra[Any, S]:
75
75
  a = self(cls, cache)
76
76
  if isinstance(name, str):
77
77
  attr = getattr(a, name, None) or getattr(cls, name, None)
@@ -97,7 +97,7 @@ class Syntax(Generic[A, S]):
97
97
  def as_(self, typ: Type[B]) -> B:
98
98
  return cast(typ, self) # type: ignore
99
99
 
100
- def __call__(self, alg: Type[Algebra[Any, Any]], cache: Cache[Any, Any]) -> Algebra[A, S]:
100
+ def __call__(self, alg: Type[Algebra[Any, Any]], cache: Cache[Any]) -> Algebra[A, S]:
101
101
  return self.alg(alg, cache)
102
102
 
103
103
 
@@ -562,37 +562,28 @@ def run(*,
562
562
 
563
563
 
564
564
  def lazy(thunk: Callable[[], Syntax[A, S]]) -> Syntax[A, S]:
565
- syntax: Optional[Syntax[A, S]] = None
566
565
  algebra: Optional[Algebra[A, S]] = None
566
+ syntax: Optional[Syntax[A, S]] = None
567
+ previous_cls: Optional[Type[Algebra[Any, S]]] = None
567
568
  def syntax_lazy_run(cls: Type[Algebra[Any, S]], cache: Cache) -> Algebra[A, S]:
568
- nonlocal syntax, algebra
569
- print('==' * 20, 'Syntax.lazy.syntax_lazy_run', '==' * 20)
570
- print('thunk', thunk, id(thunk))
571
- print('syntax', syntax, id(syntax))
572
- print('algebra', algebra, id(algebra))
569
+ nonlocal algebra, syntax, previous_cls
570
+ # print('==' * 20, 'Syntax.lazy.syntax_lazy_run', '==' * 20)
571
+ # print('thunk', thunk, id(thunk))
572
+ # print('syntax', syntax, id(syntax))
573
+ # print('algebra', algebra, id(algebra))
573
574
  if syntax is None:
574
575
  syntax = thunk()
575
576
  def algebra_lazy_f():
576
577
  if syntax is None:
577
578
  raise SyncraftError("Lazy thunk did not resolve to a Syntax", offending=thunk, expect="a Syntax")
578
579
  return syntax(cls, cache)
579
- if algebra is None:
580
- algebra = cls.lazy(algebra_lazy_f, cache=cache)
580
+ if algebra is None or (previous_cls is not None and previous_cls is not cls):
581
+ algebra = cls.lazy(algebra_lazy_f, cache=cache)
582
+ previous_cls = cls
581
583
  return algebra
582
584
  return Syntax(syntax_lazy_run).describe(name='lazy(?)', fixity='postfix')
583
585
 
584
586
 
585
- # def lazy(thunk: Callable[[], Syntax[A, S]]) -> Syntax[A, S]:
586
- # resolved: Optional[Syntax[A, S]] = None
587
-
588
- # def run_lazy(cls: Type[Algebra[Any, S]], cache: Cache) -> Algebra[A, S]:
589
- # nonlocal resolved
590
- # if resolved is None:
591
- # resolved = thunk()
592
- # return resolved(cls, cache) # reuse the same Algebra instance
593
- # return Syntax(run_lazy)
594
-
595
-
596
587
 
597
588
 
598
589
  def token(*,
@@ -7,13 +7,14 @@ from dataclasses import dataclass, replace, field
7
7
  from syncraft.algebra import (
8
8
  Algebra, Either, Right, Incomplete, Left, SyncraftError
9
9
  )
10
- from syncraft.ast import TokenSpec, ThenSpec, ManySpec, ChoiceSpec, LazySpec
10
+ from syncraft.ast import TokenSpec, ThenSpec, ManySpec, ChoiceSpec, LazySpec, ThenKind
11
11
  from syncraft.parser import TokenType
12
12
  from syncraft.constraint import Bindable, FrozenDict
13
13
 
14
14
  import re
15
15
  from syncraft.syntax import Syntax
16
16
  from syncraft.cache import Cache
17
+ from rich import print
17
18
 
18
19
 
19
20
  S = TypeVar('S', bound=Bindable)
@@ -31,7 +32,6 @@ SS = TypeVar('SS', bound=Hashable)
31
32
  class WalkerState(Bindable, Generic[SS]):
32
33
  reducer: Optional[Callable[[Any, SS], SS]] = None
33
34
  acc: Optional[SS] = None
34
- visited: frozenset = field(default_factory=frozenset)
35
35
 
36
36
 
37
37
  def reduce(self, value: Any) -> WalkerState[SS]:
@@ -41,8 +41,6 @@ class WalkerState(Bindable, Generic[SS]):
41
41
  else:
42
42
  return replace(self, acc=value)
43
43
 
44
- def visit(self, key: Hashable) -> WalkerState[SS]:
45
- return replace(self, visited=self.visited | {key})
46
44
 
47
45
 
48
46
 
@@ -55,24 +53,40 @@ class Walker(Algebra[SS, WalkerState[SS]]):
55
53
  return WalkerState(reducer=reducer, acc=init)
56
54
 
57
55
 
56
+
57
+ @classmethod
58
+ def token(cls,
59
+ *,
60
+ cache: Cache,
61
+ token_type: Optional[TokenType] = None,
62
+ text: Optional[str] = None,
63
+ case_sensitive: bool = False,
64
+ regex: Optional[re.Pattern[str]] = None
65
+ )-> Algebra[Any, WalkerState[SS]]:
66
+ def token_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
67
+ yield from ()
68
+ data = TokenSpec(token_type=token_type, text=text, regex=regex, case_sensitive=case_sensitive)
69
+ return Right((data, input.reduce(data)))
70
+ return cls(token_run, name=cls.__name__ + f'.token({token_type or text or regex})', cache=cache)
71
+
58
72
  @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)))
73
+ def lazy(cls,
74
+ thunk: Callable[[], Algebra[Any, WalkerState[SS]]],
75
+ cache: Cache) -> Algebra[Any, WalkerState[SS]]:
76
+ def algebra_lazy_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
77
+ alg = thunk()
78
+ print('--' * 20, "Walker.lazy.algebra_lazy_run", '--' * 20)
79
+ print('thunk', thunk, id(thunk))
80
+ print('input', input, id(input))
81
+ print('alg', alg, id(alg))
82
+ thunk_result = yield from alg.run(input, use_cache)
83
+ match thunk_result:
84
+ case Right((value, from_thunk)):
85
+ data = LazySpec(value=value)
86
+ return Right((data, from_thunk.reduce(data)))
74
87
  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)
88
+ return cls(algebra_lazy_run, name=cls.__name__ + '.lazy', cache=cache)
89
+
76
90
 
77
91
 
78
92
  def then_both(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
@@ -83,16 +97,16 @@ class Walker(Algebra[SS, WalkerState[SS]]):
83
97
  other_result = yield from other.run(from_left, use_cache)
84
98
  match other_result:
85
99
  case Right((result, from_right)):
86
- data = ThenSpec(left=value, right=result)
100
+ data = ThenSpec(kind=ThenKind.BOTH, left=value, right=result)
87
101
  return Right((data, from_right.reduce(data)))
88
102
  raise SyncraftError("flat_map should always return a value or an error.", offending=self_result, expect=(Left, Right))
89
103
  return self.__class__(then_run, name=self.name, cache=self.cache | other.cache)
90
104
 
91
105
  def then_left(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
92
- return self.then_both(other) # For simplicity, treat as both
106
+ return self.then_both(other).map(lambda t: replace(t, kind=ThenKind.LEFT))
93
107
 
94
108
  def then_right(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
95
- return self.then_both(other)
109
+ return self.then_both(other).map(lambda t: replace(t, kind=ThenKind.RIGHT))
96
110
 
97
111
 
98
112
  def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Any, WalkerState[SS]]:
@@ -121,27 +135,9 @@ class Walker(Algebra[SS, WalkerState[SS]]):
121
135
  raise SyncraftError("", offending=self)
122
136
  return self.__class__(or_else_run, name=f"or_else({self.name} | {other.name})", cache=self.cache | other.cache)
123
137
 
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
138
 
139
139
 
140
- def walk(syntax: Syntax[Any, Any], reducer: Callable[[Any, Any], SS], init: SS)-> Optional[SS]:
140
+ def walk(syntax: Syntax[Any, Any], reducer: Callable[[Any, Any], SS], init: SS)-> Tuple[Any, None | SS]:
141
141
  from syncraft.syntax import run
142
- from rich import print
143
142
  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
143
+ return v, s
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+ from syncraft.parser import token
3
+ import pytest
4
+ from syncraft.walker import walk
5
+ from syncraft.ast import Nothing
6
+ from syncraft.syntax import lazy, literal, regex
7
+ from syncraft.parser import parse
8
+ from syncraft.generator import TokenGen
9
+ from syncraft.cache import RecursionError
10
+ from rich import print
11
+
12
+ def test_simple_recursion()->None:
13
+ A = lazy(lambda: literal('a') + ~A | literal('a'))
14
+ v, s = parse(A, 'a a a', dialect='sqlite')
15
+ print(v)
16
+ ast1, inv = v.bimap()
17
+ print(ast1)
18
+ assert ast1 == (
19
+ TokenGen.from_string('a'),
20
+ (
21
+ TokenGen.from_string('a'),
22
+ (
23
+ TokenGen.from_string('a'),
24
+ Nothing()
25
+ )
26
+ )
27
+ )
28
+
29
+ def test_direct_recursion()->None:
30
+ Expr1 = lazy(lambda: literal('a') + ~Expr1)
31
+ v, s = parse(Expr1, 'a a a', dialect='sqlite')
32
+ x, _ = v.bimap()
33
+ assert x == (
34
+ TokenGen.from_string('a'),
35
+ (
36
+ TokenGen.from_string('a'),
37
+ (
38
+ TokenGen.from_string('a'),
39
+ Nothing()
40
+ )
41
+ )
42
+ )
43
+
44
+
45
+ def test_mutual_recursion()->None:
46
+ A = lazy(lambda: literal('a') + B)
47
+ B = lazy(lambda: (literal('b') + A) | (literal('c')))
48
+ v, s = parse(A, 'a b a b a c', dialect='sqlite')
49
+ print('--' * 20, "test_mutual_recursion", '--' * 20)
50
+ print(v)
51
+ ast1, inv = v.bimap()
52
+ print(ast1)
53
+ assert ast1 == (
54
+ TokenGen.from_string('a'),
55
+ (
56
+ TokenGen.from_string('b'),
57
+ TokenGen.from_string('a'),
58
+ (
59
+ TokenGen.from_string('b'),
60
+ TokenGen.from_string('a'),
61
+ TokenGen.from_string('c')
62
+ )
63
+ )
64
+ )
65
+
66
+
67
+
68
+ def test_recursion() -> None:
69
+ A = literal('a')
70
+ B = literal('b')
71
+ L = lazy(lambda: literal("if") >> (A | B) // literal('then'))
72
+
73
+ def parens():
74
+ return A + ~lazy(parens) + B
75
+ p_code = 'a a b b'
76
+ LL = parens() | L
77
+
78
+ v, s = parse(LL, p_code, dialect='sqlite')
79
+ ast1, inv = v.bimap()
80
+ print(v)
81
+ print(ast1)
82
+ assert ast1 == (
83
+ TokenGen.from_string('a'),
84
+ (
85
+ TokenGen.from_string('a'),
86
+ Nothing(),
87
+ TokenGen.from_string('b')
88
+ ),
89
+ TokenGen.from_string('b')
90
+ )
91
+
92
+ def test_direct_left_recursion()->None:
93
+ Term = literal('n')
94
+ Expr = lazy(lambda: Expr + literal('+') + Term | Term)
95
+ with pytest.raises(RecursionError):
96
+ v, s = parse(Expr, 'n+n+n', dialect='sqlite')
97
+
98
+
99
+
100
+ def test_indirect_left_recursion()->None:
101
+ NUMBER = regex(r'\d+').map(int)
102
+ PLUS = token(text='+')
103
+ STAR = token(text='*')
104
+ A = lazy(lambda: (B >> PLUS >> A) | B)
105
+ B = lazy(lambda: (A >> STAR >> NUMBER) | NUMBER)
106
+ with pytest.raises(RecursionError):
107
+ v, s = parse(A, '1 + 2 * 3', dialect='sqlite')
@@ -6,17 +6,17 @@ from syncraft.ast import TokenSpec
6
6
 
7
7
  def test_walk() -> None:
8
8
  syntax = literal("test")
9
- result = walk(syntax, lambda a, s: s + (a,), ())
10
- assert result == (TokenSpec.create(text='test', case_sensitive=True),)
9
+ result, s = walk(syntax, lambda a, s: s + (a,), ())
10
+ assert s and s.acc == (TokenSpec.create(text='test', case_sensitive=True),)
11
11
 
12
12
 
13
13
  def test_walk_case_insensitive() -> None:
14
14
  A = literal('a').many()
15
15
  B = literal('b').many()
16
16
  syntax = literal("if") >> (A | B) + literal('then')
17
- result = walk(syntax, lambda a, s: s + (a,) if isinstance(a, TokenSpec) else s, ())
17
+ result, s = walk(syntax, lambda a, s: s + (a,) if isinstance(a, TokenSpec) else s, ())
18
18
  print(result)
19
- assert result == (
19
+ assert s and s.acc == (
20
20
  TokenSpec.create(text='if', case_sensitive=True),
21
21
  TokenSpec.create(text='a', case_sensitive=True),
22
22
  TokenSpec.create(text='b', case_sensitive=True),
@@ -1,113 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import Dict, TypeVar, Hashable, Generic, Callable, Any, Generator, overload, Literal
5
- from weakref import WeakKeyDictionary
6
- from syncraft.ast import SyncraftError
7
-
8
-
9
- class RecursionError(SyncraftError):
10
- def __init__(self, message: str, offending: Any, expect: Any = None, **kwargs: Any) -> None:
11
- super().__init__(message, offending, expect, **kwargs)
12
-
13
-
14
- @dataclass(frozen=True)
15
- class InProgress:
16
- _instance = None
17
- def __new__(cls):
18
- if cls._instance is None:
19
- cls._instance = super(InProgress, cls).__new__(cls)
20
- return cls._instance
21
- def __str__(self)->str:
22
- return self.__class__.__name__
23
- def __repr__(self)->str:
24
- return self.__str__()
25
-
26
-
27
-
28
-
29
- Args = TypeVar('Args', bound=Hashable)
30
- Ret = TypeVar('Ret')
31
-
32
- @dataclass
33
- class Cache(Generic[Args, Ret]):
34
- cache: WeakKeyDictionary[Callable[..., Any], Dict[Args, Ret | InProgress]] = field(default_factory=WeakKeyDictionary)
35
-
36
- def __contains__(self, f: Callable[..., Any]) -> bool:
37
- return f in self.cache
38
-
39
- def __repr__(self) -> str:
40
- return f"Cache({({f.__name__: list(c.keys()) for f, c in self.cache.items()})})"
41
-
42
-
43
- def __or__(self, other: Cache[Args, Any]) -> Cache[Args, Any]:
44
- assert self.cache is other.cache, "There should be only one global cache"
45
- if self.cache is other.cache:
46
- return self
47
- elif len(self.cache) == 0:
48
- return other
49
- elif len(other.cache) == 0:
50
- return self
51
- merged = Cache[Args, Ret]()
52
- for f, c in self.cache.items():
53
- merged.cache[f] = c.copy()
54
- for f, c in other.cache.items():
55
- merged.cache.setdefault(f, {}).update(c)
56
- return merged
57
-
58
- @overload
59
- def _execute(self,
60
- f: Callable[[Args, bool], Ret],
61
- args: Args,
62
- use_cache: bool,
63
- is_gen: Literal[False]) -> Ret: ...
64
- @overload
65
- def _execute(self,
66
- f: Callable[[Args, bool], Generator[Any, Any, Ret]],
67
- args: Args,
68
- use_cache: bool,
69
- is_gen: Literal[True]) -> Generator[Any, Any, Ret]: ...
70
-
71
-
72
- def _execute(self,
73
- f: Callable[[Args, bool], Any],
74
- args: Args,
75
- use_cache:bool,
76
- is_gen: bool
77
- ) -> Ret | Generator[Any, Any, Ret]:
78
- if f not in self.cache:
79
- self.cache.setdefault(f, dict())
80
- c: Dict[Args, Ret | InProgress] = self.cache[f]
81
- if args in c:
82
- v = c[args]
83
- if isinstance(v, InProgress):
84
- raise RecursionError("Left-recursion detected in parser", offending=f, state=args)
85
- else:
86
- return v
87
- try:
88
- c[args] = InProgress()
89
- if is_gen:
90
- result = yield from f(args, use_cache)
91
- else:
92
- result = f(args, use_cache)
93
- c[args] = result
94
- if not use_cache:
95
- c.pop(args, None)
96
- return result
97
- except Exception as e:
98
- c.pop(args, None)
99
- raise e
100
-
101
- def gen(self,
102
- f: Callable[[Args, bool], Generator[Any, Any, Ret]],
103
- args: Args,
104
- use_cache:bool) -> Generator[Any, Any, Ret]:
105
- return (yield from self._execute(f, args, use_cache, is_gen=True))
106
-
107
- def call(self,
108
- f: Callable[[Args, bool], Ret],
109
- args: Args,
110
- use_cache:bool) -> Ret:
111
- return self._execute(f, args, use_cache, is_gen=False)
112
-
113
-
@@ -1,50 +0,0 @@
1
- from __future__ import annotations
2
- from syncraft.parser import token
3
-
4
- from syncraft.walker import walk
5
- from syncraft.ast import Nothing
6
- from syncraft.syntax import lazy, literal, regex
7
- from syncraft.parser import parse
8
- from syncraft.generator import TokenGen
9
-
10
- from rich import print
11
-
12
-
13
- def test_recursion() -> None:
14
- A = literal('a')
15
- B = literal('b')
16
- L = lazy(lambda: literal("if") >> (A | B) // literal('then'))
17
-
18
- def parens():
19
- return A + ~lazy(parens) + B
20
- p_code = 'a a b b'
21
- LL = parens() | L
22
-
23
- v, s = parse(LL, p_code, dialect='sqlite')
24
- ast1, inv = v.bimap()
25
- print(v)
26
- print(ast1)
27
- assert ast1 == (
28
- TokenGen.from_string('a'),
29
- (
30
- TokenGen.from_string('a'),
31
- Nothing(),
32
- TokenGen.from_string('b')
33
- ),
34
- TokenGen.from_string('b')
35
- )
36
-
37
- def test_direct_left_recursion()->None:
38
- Term = literal('n')
39
- Expr = lazy(lambda: Expr + literal('+') + Term | Term)
40
- v, s = parse(Expr, 'n+n+n', dialect='sqlite')
41
-
42
-
43
-
44
- def test_indirect_left_recursion()->None:
45
- NUMBER = regex(r'\d+').map(int)
46
- PLUS = token(text='+')
47
- STAR = token(text='*')
48
- A = lazy(lambda: (B >> PLUS >> A) | B)
49
- B = lazy(lambda: (A >> STAR >> NUMBER) | NUMBER)
50
- v, s = parse(A, '1 + 2 * 3', dialect='sqlite')
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
File without changes
File without changes
File without changes
File without changes
File without changes