syncraft 0.2.2__py3-none-any.whl → 0.2.3__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/constraint.py CHANGED
@@ -11,6 +11,12 @@ import inspect
11
11
  K = TypeVar('K')
12
12
  V = TypeVar('V')
13
13
  class FrozenDict(collections.abc.Mapping, Generic[K, V]):
14
+ """An immutable, hashable mapping.
15
+
16
+ Behaves like a read-only dict and caches its hash, making it suitable as a
17
+ key in other dictionaries or for set membership. Equality compares the
18
+ underlying mapping to any other Mapping.
19
+ """
14
20
  def __init__(self, *args, **kwargs):
15
21
  self._data = dict(*args, **kwargs)
16
22
  self._hash = None
@@ -54,12 +60,19 @@ class Binding:
54
60
 
55
61
  @dataclass(frozen=True)
56
62
  class Bindable:
63
+ """Mixin that carries named bindings produced during evaluation.
64
+
65
+ Instances accumulate bindings of name->node pairs. Subclasses should return
66
+ a new instance from ``bind`` to preserve immutability.
67
+ """
57
68
  binding: Binding = field(default_factory=Binding)
58
69
 
59
70
  def map(self, f: Callable[[Any], Any])->Self:
71
+ """Optionally transform the underlying value (no-op by default)."""
60
72
  return self
61
73
 
62
74
  def bind(self, name: str, node:Any)->Self:
75
+ """Return a copy with ``node`` recorded under ``name`` in bindings."""
63
76
  return replace(self, binding=self.binding.bind(name, node))
64
77
 
65
78
 
@@ -73,11 +86,19 @@ class ConstraintResult:
73
86
  unbound: frozenset[str] = frozenset()
74
87
  @dataclass(frozen=True)
75
88
  class Constraint:
89
+ """A composable boolean check over a set of bound values.
90
+
91
+ The check is a function from a mapping of names to tuples of values to a
92
+ ``ConstraintResult`` with a boolean outcome and any unbound requirements.
93
+ Constraints compose with logical operators (``&``, ``|``, ``^``, ``~``).
94
+ """
76
95
  run_f: Callable[[FrozenDict[str, Tuple[Any, ...]]], ConstraintResult]
77
96
  name: str = ""
78
97
  def __call__(self, bound: FrozenDict[str, Tuple[Any, ...]])->ConstraintResult:
98
+ """Evaluate this constraint against the provided bindings."""
79
99
  return self.run_f(bound)
80
100
  def __and__(self, other: Constraint) -> Constraint:
101
+ """Logical AND composition of two constraints."""
81
102
  def and_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
82
103
  res1 = self(bound)
83
104
  res2 = other(bound)
@@ -89,6 +110,7 @@ class Constraint:
89
110
  name=f"({self.name} && {other.name})"
90
111
  )
91
112
  def __or__(self, other: Constraint) -> Constraint:
113
+ """Logical OR composition of two constraints."""
92
114
  def or_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
93
115
  res1 = self(bound)
94
116
  res2 = other(bound)
@@ -100,6 +122,7 @@ class Constraint:
100
122
  name=f"({self.name} || {other.name})"
101
123
  )
102
124
  def __xor__(self, other: Constraint) -> Constraint:
125
+ """Logical XOR composition of two constraints."""
103
126
  def xor_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
104
127
  res1 = self(bound)
105
128
  res2 = other(bound)
@@ -111,6 +134,7 @@ class Constraint:
111
134
  name=f"({self.name} ^ {other.name})"
112
135
  )
113
136
  def __invert__(self) -> Constraint:
137
+ """Logical NOT of this constraint."""
114
138
  def invert_run(bound: FrozenDict[str, Tuple[Any, ...]]) -> ConstraintResult:
115
139
  res = self(bound)
116
140
  return ConstraintResult(result=not res.result, unbound=res.unbound)
@@ -169,6 +193,21 @@ def predicate(f: Callable[..., bool],
169
193
  name: Optional[str] = None,
170
194
  quant: Quantifier = Quantifier.FORALL,
171
195
  bimap: bool = True) -> Constraint:
196
+ """Create a constraint from a Python predicate function.
197
+
198
+ The predicate's parameters define the required bindings. When ``bimap`` is
199
+ true, arguments with a ``bimap()`` method are mapped to their forward value
200
+ before evaluation, making it convenient to write predicates over AST values.
201
+
202
+ Args:
203
+ f: The boolean function to wrap as a constraint.
204
+ name: Optional human-friendly name; defaults to ``f.__name__``.
205
+ quant: Quantification over bound values (forall or exists).
206
+ bimap: Whether to call ``bimap()`` on arguments before evaluation.
207
+
208
+ Returns:
209
+ Constraint: A composable constraint.
210
+ """
172
211
  name = name or f.__name__
173
212
  sig = inspect.signature(f)
174
213
  if bimap:
@@ -182,9 +221,11 @@ def predicate(f: Callable[..., bool],
182
221
  return Constraint.predicate(f, sig=sig, name=name, quant=quant)
183
222
 
184
223
  def forall(f: Callable[..., bool], name: Optional[str] = None, bimap: bool=True) -> Constraint:
224
+ """``forall`` wrapper around ``predicate`` (all combinations must satisfy)."""
185
225
  return predicate(f, name=name, quant=Quantifier.FORALL, bimap=bimap)
186
226
 
187
227
  def exists(f: Callable[..., bool], name: Optional[str] = None, bimap:bool = True) -> Constraint:
228
+ """``exists`` wrapper around ``predicate`` (at least one combination)."""
188
229
  return predicate(f, name=name, quant=Quantifier.EXISTS, bimap=bimap)
189
230
 
190
231
 
syncraft/finder.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import (
4
- Any, Tuple, Generator as YieldGen, TypeVar
4
+ Any, Tuple, Generator as YieldGen, TypeVar, Generic
5
5
  )
6
6
  from dataclasses import dataclass
7
7
  from syncraft.algebra import (
@@ -16,43 +16,100 @@ from syncraft.syntax import Syntax
16
16
 
17
17
  T=TypeVar('T', bound=TokenProtocol)
18
18
  @dataclass(frozen=True)
19
- class Finder(Generator[T]):
19
+ class Finder(Generator[T], Generic[T]):
20
+ """Generator backend used to search/inspect parse trees.
21
+
22
+ This class is passed to a ``Syntax`` to obtain an ``Algebra`` that can be
23
+ run against a ``GenState``. In this module it's used to implement tree-wide search utilities
24
+ such as ``matches`` and ``find``.
25
+ """
20
26
  @classmethod
21
27
  def anything(cls)->Algebra[Any, GenState[T]]:
28
+ """Match any node and return it unchanged.
29
+
30
+ Succeeds on any input ``GenState`` and returns the current AST node as
31
+ the value, leaving the state untouched. Useful as a catch‑all predicate
32
+ when searching a tree.
33
+
34
+ Returns:
35
+ Algebra[Any, GenState[T]]: An algebra that always succeeds with the
36
+ tuple ``(input.ast, input)``.
37
+ """
22
38
  def anything_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Any, GenState[T]]]:
23
39
  return Right((input.ast, input))
24
40
  return cls(anything_run, name=cls.__name__ + '.anything')
25
41
 
26
42
 
27
43
 
44
+ #: A ``Syntax`` that matches any node and returns it as the result without
45
+ #: consuming or modifying state.
28
46
  anything = Syntax(lambda cls: cls.factory('anything')).describe(name="Anything", fixity='infix')
29
47
 
30
- def matches(syntax: Syntax[Any, Any], data: ParseResult[Any])-> bool:
31
- gen = syntax(Finder)
48
+ def _matches(alg: Algebra[Any, GenState[Any]], data: ParseResult[Any])-> bool:
32
49
  state = GenState[Any].from_ast(ast = data, restore_pruned=True)
33
- result = gen.run(state, use_cache=True)
50
+ result = alg.run(state, use_cache=True)
34
51
  return isinstance(result, Right)
35
52
 
36
53
 
37
- def find(syntax: Syntax[Any, Any], data: ParseResult[Any]) -> YieldGen[ParseResult[Any], None, None]:
38
- if not isinstance(data, Marked):
39
- if matches(syntax, data):
54
+ def _find(alg: Algebra[Any, GenState[Any]], data: ParseResult[Any]) -> YieldGen[ParseResult[Any], None, None]:
55
+ if not isinstance(data, (Marked, Collect)):
56
+ if _matches(alg, data):
40
57
  yield data
41
58
  match data:
42
59
  case Then(left=left, right=right):
43
60
  if left is not None:
44
- yield from find(syntax, left)
61
+ yield from _find(alg, left)
45
62
  if right is not None:
46
- yield from find(syntax, right)
63
+ yield from _find(alg, right)
47
64
  case Many(value = value):
48
65
  for e in value:
49
- yield from find(syntax, e)
66
+ yield from _find(alg, e)
50
67
  case Marked(value=value):
51
- yield from find(syntax, value)
68
+ yield from _find(alg, value)
52
69
  case Choice(value=value):
53
70
  if value is not None:
54
- yield from find(syntax, value)
71
+ yield from _find(alg, value)
55
72
  case Collect(value=value):
56
- yield from find(syntax, value)
73
+ yield from _find(alg, value)
57
74
  case _:
58
75
  pass
76
+
77
+
78
+ def matches(syntax: Syntax[Any, Any], data: ParseResult[Any])-> bool:
79
+ """Check whether a syntax matches a specific node.
80
+
81
+ Runs the given ``syntax`` (compiled with ``Finder``) against ``data`` only;
82
+ it does not traverse the tree. ``Marked`` and ``Collect`` node are treated as transparent.
83
+
84
+ Args:
85
+ syntax: The ``Syntax`` to run.
86
+ data: The AST node (``ParseResult``) to test.
87
+
88
+ Returns:
89
+ bool: ``True`` if the syntax succeeds on ``data``, ``False`` otherwise.
90
+ """
91
+ gen = syntax(Finder)
92
+ if isinstance(data, (Marked, Collect)):
93
+ return _matches(gen, data.value)
94
+ else:
95
+ return _matches(gen, data)
96
+
97
+
98
+ def find(syntax: Syntax[Any, Any], data: ParseResult[Any]) -> YieldGen[ParseResult[Any], None, None]:
99
+ """Yield all subtrees that match a syntax.
100
+
101
+ Performs a depth‑first traversal of ``data`` and yields each node where the
102
+ provided ``syntax`` (compiled with ``Finder``) succeeds. Wrapper nodes like
103
+ ``Marked`` and ``Collect`` are treated as transparent for matching and are
104
+ not yielded themselves.
105
+
106
+ Args:
107
+ syntax: The ``Syntax`` predicate to apply at each node.
108
+ data: The root ``ParseResult`` to search.
109
+
110
+ Yields:
111
+ ParseResult[Any]: Each node that satisfies ``syntax`` (pre‑order: the
112
+ current node is tested before visiting its children).
113
+ """
114
+ gen = syntax(Finder)
115
+ yield from _find(gen, data)
syncraft/generator.py CHANGED
@@ -37,28 +37,90 @@ B = TypeVar('B')
37
37
 
38
38
  @dataclass(frozen=True)
39
39
  class GenState(Bindable, Generic[T]):
40
+ """Lightweight state passed between generator combinators.
41
+
42
+ Holds the current AST focus (or ``None`` when pruned), a flag controlling
43
+ whether traversals are allowed to access pruned branches, and a deterministic
44
+ seed for randomized generation paths.
45
+
46
+ Attributes:
47
+ ast: The current AST node or ``None`` if the branch is pruned.
48
+ restore_pruned: When true, allows navigation into branches that would
49
+ normally be considered pruned by the AST structure.
50
+ seed: Integer seed used to derive reproducible random choices.
51
+ """
40
52
  ast: Optional[ParseResult[T]] = None
41
53
  restore_pruned: bool = False
42
54
  seed: int = 0
43
55
  def map(self, f: Callable[[Any], Any]) -> GenState[T]:
56
+ """Return a copy with ``ast`` replaced by ``f(ast)``.
57
+
58
+ Args:
59
+ f: Mapping function applied to the current ``ast``.
60
+
61
+ Returns:
62
+ GenState[T]: A new state with the mapped ``ast``.
63
+ """
44
64
  return replace(self, ast=f(self.ast))
45
65
 
46
66
  def inject(self, a: Any) -> GenState[T]:
67
+ """Return a copy with ``ast`` set to ``a``.
68
+
69
+ Shorthand for ``map(lambda _: a)``.
70
+
71
+ Args:
72
+ a: The value to place into ``ast``.
73
+
74
+ Returns:
75
+ GenState[T]: A new state with ``ast`` equal to ``a``.
76
+ """
47
77
  return self.map(lambda _: a)
48
78
 
49
79
  def fork(self, tag: Any) -> GenState[T]:
80
+ """Create a deterministic fork of the state using ``tag``.
81
+
82
+ The new ``seed`` is derived from the current ``seed`` and ``tag`` so
83
+ that repeated forks with the same inputs are reproducible.
84
+
85
+ Args:
86
+ tag: Any value used to derive the child seed.
87
+
88
+ Returns:
89
+ GenState[T]: A new state with a forked ``seed``.
90
+ """
50
91
  return replace(self, seed=hash((self.seed, tag)))
51
92
 
52
93
  def rng(self, tag: Any = None) -> random.Random:
94
+ """Get a deterministic RNG for this state.
95
+
96
+ If ``tag`` is provided, the RNG seed is derived from ``(seed, tag)``;
97
+ otherwise the state's ``seed`` is used.
98
+
99
+ Args:
100
+ tag: Optional label to derive a sub-seed.
101
+
102
+ Returns:
103
+ random.Random: A RNG instance seeded deterministically.
104
+ """
53
105
  return random.Random(self.seed if tag is None else hash((self.seed, tag)))
54
106
 
55
107
 
56
108
 
57
109
  @cached_property
58
110
  def pruned(self)->bool:
111
+ """Whether the current branch is pruned (``ast`` is ``None``)."""
59
112
  return self.ast is None
60
113
 
61
114
  def left(self)-> GenState[T]:
115
+ """Focus on the left side of a ``Then`` node or prune.
116
+
117
+ When ``restore_pruned`` is true, traversal is allowed even if the
118
+ ``Then`` is marked as coming from the right branch.
119
+
120
+ Returns:
121
+ GenState[T]: State focused on the left child or pruned when not
122
+ applicable.
123
+ """
62
124
  if self.ast is None:
63
125
  return self
64
126
  if isinstance(self.ast, Then) and (self.ast.kind != ThenKind.RIGHT or self.restore_pruned):
@@ -66,6 +128,15 @@ class GenState(Bindable, Generic[T]):
66
128
  return replace(self, ast=None)
67
129
 
68
130
  def right(self) -> GenState[T]:
131
+ """Focus on the right side of a ``Then`` node or prune.
132
+
133
+ When ``restore_pruned`` is true, traversal is allowed even if the
134
+ ``Then`` is marked as coming from the left branch.
135
+
136
+ Returns:
137
+ GenState[T]: State focused on the right child or pruned when not
138
+ applicable.
139
+ """
69
140
  if self.ast is None:
70
141
  return self
71
142
  if isinstance(self.ast, Then) and (self.ast.kind != ThenKind.LEFT or self.restore_pruned):
@@ -73,6 +144,18 @@ class GenState(Bindable, Generic[T]):
73
144
  return replace(self, ast=None)
74
145
 
75
146
  def down(self, index: int) -> GenState[T]:
147
+ """Descend through wrapper nodes to reach the contained value.
148
+
149
+ Currently unwraps ``Marked`` nodes. Raises ``TypeError`` for other
150
+ node types.
151
+
152
+ Args:
153
+ index: Placeholder for a future multi-child descent API.
154
+
155
+ Returns:
156
+ GenState[T]: State focused on the unwrapped child or unchanged when
157
+ pruned.
158
+ """
76
159
  if self.ast is None:
77
160
  return self
78
161
  match self.ast:
@@ -121,13 +204,22 @@ class TokenGen(TokenSpec):
121
204
  return self.__str__()
122
205
 
123
206
  def gen(self) -> Token:
207
+ """Generate a token consistent with this specification.
208
+
209
+ Resolution order is: exact text, regex pattern, token type value, and
210
+ finally a generic placeholder literal.
211
+
212
+ Returns:
213
+ Token: A token whose ``token_type`` is derived from the generated
214
+ text when necessary.
215
+ """
124
216
  text: str
125
217
  if self.text is not None:
126
218
  text = self.text
127
219
  elif self.regex is not None:
128
220
  try:
129
221
  text = rstr.xeger(self.regex)
130
- except Exception as e:
222
+ except Exception:
131
223
  # If the regex is invalid or generation fails
132
224
  text = self.regex.pattern # fallback to pattern string
133
225
  elif self.token_type is not None:
@@ -146,9 +238,31 @@ class TokenGen(TokenSpec):
146
238
  class Generator(Algebra[ParseResult[T], GenState[T]]):
147
239
  @classmethod
148
240
  def state(cls, ast: Optional[ParseResult[T]] = None, seed: int = 0, restore_pruned: bool = False)->GenState[T]:
241
+ """Create an initial ``GenState`` for generation or checking.
242
+
243
+ Args:
244
+ ast: Optional root AST to validate/generate against.
245
+ seed: Seed for deterministic random generation.
246
+ restore_pruned: Allow traversing pruned branches.
247
+
248
+ Returns:
249
+ GenState[T]: The constructed initial state.
250
+ """
149
251
  return GenState.from_ast(ast=ast, seed=seed, restore_pruned=restore_pruned)
150
252
 
151
253
  def flat_map(self, f: Callable[[ParseResult[T]], Algebra[B, GenState[T]]]) -> Algebra[B, GenState[T]]:
254
+ """Sequence a dependent generator using the left child value.
255
+
256
+ Expects the input AST to be a ``Then`` node; applies ``self`` to the
257
+ left side, then passes the produced value to ``f`` and applies the
258
+ resulting algebra to the right side.
259
+
260
+ Args:
261
+ f: Function mapping the left value to the next algebra.
262
+
263
+ Returns:
264
+ Algebra[B, GenState[T]]: An algebra yielding the final result.
265
+ """
152
266
  def flat_map_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[B, GenState[T]]]:
153
267
  try:
154
268
  if not isinstance(input.ast, Then) or isinstance(input.ast, Nothing):
@@ -178,6 +292,24 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
178
292
 
179
293
 
180
294
  def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Many[ParseResult[T]], GenState[T]]:
295
+ """Apply ``self`` repeatedly with cardinality constraints.
296
+
297
+ In pruned mode, generates a random number of items in the inclusive
298
+ range ``[at_least, at_most or at_least+2]`` and attempts each
299
+ independently. Otherwise, validates an existing ``Many`` node and
300
+ applies ``self`` to each element.
301
+
302
+ Args:
303
+ at_least: Minimum number of successful applications required.
304
+ at_most: Optional maximum number allowed.
305
+
306
+ Returns:
307
+ Algebra[Many[ParseResult[T]], GenState[T]]: An algebra that yields a
308
+ ``Many`` of results.
309
+
310
+ Raises:
311
+ ValueError: If bounds are invalid.
312
+ """
181
313
  if at_least <=0 or (at_most is not None and at_most < at_least):
182
314
  raise ValueError(f"Invalid arguments for many: at_least={at_least}, at_most={at_most}")
183
315
  def many_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Many[ParseResult[T]], GenState[T]]]:
@@ -188,7 +320,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
188
320
  for i in range(count):
189
321
  forked_input = input.fork(tag=len(ret))
190
322
  match self.run(forked_input, use_cache):
191
- case Right((value, next_input)):
323
+ case Right((value, _)):
192
324
  ret.append(value)
193
325
  case Left(_):
194
326
  pass
@@ -201,7 +333,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
201
333
  ret = []
202
334
  for x in input.ast.value:
203
335
  match self.run(input.inject(x), use_cache):
204
- case Right((value, next_input)):
336
+ case Right((value, _)):
205
337
  ret.append(value)
206
338
  if at_most is not None and len(ret) > at_most:
207
339
  return Left(Error(
@@ -209,7 +341,7 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
209
341
  this=self,
210
342
  state=input.inject(x)
211
343
  ))
212
- case Left(e):
344
+ case Left(_):
213
345
  pass
214
346
  if len(ret) < at_least:
215
347
  return Left(Error(
@@ -224,6 +356,18 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
224
356
  def or_else(self, # type: ignore
225
357
  other: Algebra[ParseResult[T], GenState[T]]
226
358
  ) -> Algebra[Choice[ParseResult[T], ParseResult[T]], GenState[T]]:
359
+ """Try ``self``; if it fails without commitment, try ``other``.
360
+
361
+ In pruned mode, deterministically chooses a branch using a forked RNG.
362
+ With an existing ``Choice`` AST, it executes the indicated branch.
363
+
364
+ Args:
365
+ other: Fallback algebra to try when ``self`` is not committed.
366
+
367
+ Returns:
368
+ Algebra[Choice[ParseResult[T], ParseResult[T]], GenState[T]]: An
369
+ algebra yielding which branch succeeded and its value.
370
+ """
227
371
  def or_else_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[Choice[ParseResult[T], ParseResult[T]], GenState[T]]]:
228
372
  def exec(kind: ChoiceKind | None,
229
373
  left: GenState[T],
@@ -277,6 +421,22 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
277
421
  case_sensitive: bool = False,
278
422
  regex: Optional[re.Pattern[str]] = None
279
423
  )-> Algebra[ParseResult[T], GenState[T]]:
424
+ """Match or synthesize a single token.
425
+
426
+ When validating, succeeds if the current AST node is a ``Token`` that
427
+ satisfies this spec. When generating (pruned), produces a token based on
428
+ ``text``, ``regex``, or ``token_type``.
429
+
430
+ Args:
431
+ token_type: Expected token type.
432
+ text: Exact text to match or produce.
433
+ case_sensitive: Whether text matching respects case.
434
+ regex: Regular expression to synthesize text from when generating.
435
+
436
+ Returns:
437
+ Algebra[ParseResult[T], GenState[T]]: An algebra producing a Token
438
+ node or validating the current one.
439
+ """
280
440
  gen = TokenGen(token_type=token_type, text=text, case_sensitive=case_sensitive, regex=regex)
281
441
  lazy_self: Algebra[ParseResult[T], GenState[T]]
282
442
  def token_run(input: GenState[T], use_cache:bool) -> Either[Any, Tuple[ParseResult[Token], GenState[T]]]:
@@ -298,6 +458,23 @@ def generate(syntax: Syntax[Any, Any],
298
458
  data: Optional[ParseResult[Any]] = None,
299
459
  seed: int = 0,
300
460
  restore_pruned: bool = False) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
461
+ """Run a ``Syntax`` with the ``Generator`` backend.
462
+
463
+ In validation mode (``data`` provided), walks the structure and returns the
464
+ original AST and collected marks. In generation mode (``data`` is ``None``),
465
+ synthesizes an AST from the syntax using the given seed.
466
+
467
+ Args:
468
+ syntax: The syntax to execute.
469
+ data: Optional root AST to validate against.
470
+ seed: Seed for deterministic random generation.
471
+ restore_pruned: Allow traversing pruned branches when validating.
472
+
473
+ Returns:
474
+ Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]: The
475
+ resulting AST with marks when validating, or a synthesized AST when
476
+ generating.
477
+ """
301
478
  from syncraft.syntax import run
302
479
  return run(syntax, Generator, False, ast=data, seed=seed, restore_pruned=restore_pruned)
303
480