syncraft 0.2.4__tar.gz → 0.2.5__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 (27) hide show
  1. {syncraft-0.2.4/syncraft.egg-info → syncraft-0.2.5}/PKG-INFO +1 -1
  2. {syncraft-0.2.4 → syncraft-0.2.5}/pyproject.toml +1 -1
  3. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/constraint.py +156 -1
  4. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/generator.py +45 -17
  5. {syncraft-0.2.4 → syncraft-0.2.5/syncraft.egg-info}/PKG-INFO +1 -1
  6. {syncraft-0.2.4 → syncraft-0.2.5}/LICENSE +0 -0
  7. {syncraft-0.2.4 → syncraft-0.2.5}/README.md +0 -0
  8. {syncraft-0.2.4 → syncraft-0.2.5}/setup.cfg +0 -0
  9. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/__init__.py +0 -0
  10. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/algebra.py +0 -0
  11. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/ast.py +0 -0
  12. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/diagnostic.py +0 -0
  13. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/finder.py +0 -0
  14. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/parser.py +0 -0
  15. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/py.typed +0 -0
  16. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/sqlite3.py +0 -0
  17. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft/syntax.py +0 -0
  18. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft.egg-info/SOURCES.txt +0 -0
  19. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft.egg-info/dependency_links.txt +0 -0
  20. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft.egg-info/requires.txt +0 -0
  21. {syncraft-0.2.4 → syncraft-0.2.5}/syncraft.egg-info/top_level.txt +0 -0
  22. {syncraft-0.2.4 → syncraft-0.2.5}/tests/test_bimap.py +0 -0
  23. {syncraft-0.2.4 → syncraft-0.2.5}/tests/test_constraint.py +0 -0
  24. {syncraft-0.2.4 → syncraft-0.2.5}/tests/test_find.py +0 -0
  25. {syncraft-0.2.4 → syncraft-0.2.5}/tests/test_parse.py +0 -0
  26. {syncraft-0.2.4 → syncraft-0.2.5}/tests/test_to.py +0 -0
  27. {syncraft-0.2.4 → syncraft-0.2.5}/tests/test_until.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.2.4
3
+ Version: 0.2.5
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.4"
3
+ version = "0.2.5"
4
4
  description = "Parser combinator library"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import Callable, Generic, Tuple, TypeVar, Optional, Any, Self
2
+ from typing import Callable, Generic, Tuple, TypeVar, Optional, Any, Self, Generator
3
3
  from enum import Enum
4
4
  from dataclasses import dataclass, field, replace
5
5
  import collections.abc
@@ -20,6 +20,19 @@ class FrozenDict(collections.abc.Mapping, Generic[K, V]):
20
20
  def __init__(self, *args, **kwargs):
21
21
  self._data = dict(*args, **kwargs)
22
22
  self._hash = None
23
+
24
+ def __or__(self, other: collections.abc.Mapping) -> "FrozenDict[K, V]":
25
+ """Return a new FrozenDict with merged keys (other overrides self)."""
26
+ merged = dict(self._data)
27
+ merged.update(other)
28
+ return FrozenDict(merged)
29
+
30
+ def __ror__(self, other: collections.abc.Mapping) -> "FrozenDict[K, V]":
31
+ """Support other | self."""
32
+ merged = dict(other)
33
+ merged.update(self._data)
34
+ return FrozenDict(merged)
35
+
23
36
  def __getitem__(self, key):
24
37
  return self._data[key]
25
38
 
@@ -230,3 +243,145 @@ def exists(f: Callable[..., bool], name: Optional[str] = None, bimap:bool = True
230
243
 
231
244
 
232
245
 
246
+ def all_binding(a: FrozenDict[str, Tuple[Any, ...]], *names: str) -> Generator[FrozenDict[str, Any], None, None]:
247
+ """Yield all combinations of the provided bindings."""
248
+ if a:
249
+ names = tuple(a.keys()) if not names else names
250
+ values = [a[name] for name in names]
251
+ for combo in product(*values):
252
+ yield FrozenDict({name: value for name, value in zip(names, combo)})
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+ @dataclass(frozen=True)
262
+ class Relation:
263
+ run_f: Callable[[FrozenDict[str, Tuple[Any, ...]]], Generator[FrozenDict[str, Any], None, None]]
264
+ name: str = ""
265
+ args: frozenset[str]=field(default_factory=frozenset)
266
+
267
+ def __call__(self, bound: FrozenDict[str, Tuple[Any, ...]])->Generator[FrozenDict[str, Any], None, None]:
268
+ yield from self.run_f(bound)
269
+
270
+ def __and__(self, other: Relation) -> Relation:
271
+ def and_run(bound: FrozenDict[str, tuple]) -> Generator[FrozenDict[str, Any], None, None]:
272
+ for res1 in self(bound):
273
+ for res2 in other(bound):
274
+ merged = res1 | res2
275
+ if len(merged) == len(res1) + len(res2):
276
+ yield FrozenDict(merged)
277
+ return Relation(
278
+ run_f=and_run,
279
+ args = self.args.union(other.args),
280
+ name=f"({self.name} && {other.name})"
281
+ )
282
+
283
+ def __or__(self, other: Relation) -> Relation:
284
+ def or_run(bound: FrozenDict[str, tuple]) -> Generator[FrozenDict[str, Any], None, None]:
285
+ yield from self(bound)
286
+ yield from other(bound)
287
+ return Relation(
288
+ run_f=or_run,
289
+ name=f"({self.name} || {other.name})"
290
+ )
291
+
292
+ def __xor__(self, other: "Relation") -> "Relation":
293
+ def xor_run(bound: FrozenDict[str, tuple]) -> Generator[FrozenDict[str, Any], None, None]:
294
+ self_results = list(self(bound))
295
+ other_results = list(other(bound))
296
+ # yield from self only if not in other
297
+ for res1 in self_results:
298
+ if res1 not in other_results:
299
+ yield res1
300
+ # yield from other only if not in self
301
+ for res2 in other_results:
302
+ if res2 not in self_results:
303
+ yield res2
304
+
305
+ return Relation(
306
+ run_f=xor_run,
307
+ name=f"({self.name} ^ {other.name})"
308
+ )
309
+
310
+ @classmethod
311
+ def predicate(cls,
312
+ f: Callable[..., bool],
313
+ *,
314
+ sig: Signature,
315
+ name: str)->Relation:
316
+ pos_params = []
317
+ kw_params = []
318
+ for pname, param in sig.parameters.items():
319
+ if param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
320
+ pos_params.append(pname)
321
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
322
+ kw_params.append(pname)
323
+ else:
324
+ raise TypeError(f"Unsupported parameter kind: {param.kind}")
325
+ def run_f(bound: FrozenDict[str, Tuple[Any, ...]]) -> Generator[FrozenDict[str, Any], None, None]:
326
+ # positional argument values
327
+ pos_values = [bound.get(pname, ()) for pname in pos_params]
328
+ # keyword argument values
329
+ kw_values = [bound.get(pname, ()) for pname in kw_params]
330
+ # If any param is unbound, fail
331
+ all_params = pos_params + kw_params
332
+ all_values = pos_values + kw_values
333
+ unbound_args = [p for p, vs in zip(all_params, all_values) if not vs]
334
+ if not unbound_args:
335
+ # Cartesian product
336
+ for combo in product(*pos_values, *kw_values):
337
+ pos_args = combo[: len(pos_values)]
338
+ kw_args = dict(zip(kw_params, combo[len(pos_values) :]))
339
+ if f(*pos_args, **kw_args):
340
+ binding_dict = {name: arg for name, arg in zip(all_params, combo)}
341
+ yield FrozenDict(binding_dict)
342
+
343
+ return cls(run_f=run_f, name=name)
344
+
345
+
346
+ def relation(f: Callable[..., bool],
347
+ *,
348
+ name: Optional[str] = None,
349
+ bimap: bool = True) -> Relation:
350
+ name = name or f.__name__
351
+ sig = inspect.signature(f)
352
+ if bimap:
353
+ def wrapper(*args: Any, **kwargs:Any) -> bool:
354
+ mapped_args = [a.bimap()[0] if hasattr(a, "bimap") else a for a in args]
355
+ mapped_kwargs = {k: (v.bimap()[0] if hasattr(v, "bimap") else v) for k,v in kwargs.items()}
356
+ return f(*mapped_args, **mapped_kwargs)
357
+ return Relation.predicate(wrapper, sig=sig, name=name)
358
+ else:
359
+ return Relation.predicate(f, sig=sig, name=name)
360
+
361
+
362
+ def xforall(f: Callable[..., bool], *, name: str) -> Relation:
363
+ base_constraint = relation(f, name=name)
364
+
365
+ def run_f(bound: FrozenDict[str, Tuple[Any, ...]]) -> Generator[FrozenDict[str, Any], None, None]:
366
+ # Collect all satisfying bindings
367
+ all_bindings = list(base_constraint(bound))
368
+
369
+ # For FORALL, yield only if all combinations succeed
370
+ if all_bindings:
371
+ for b in all_bindings:
372
+ yield b
373
+ # If no bindings satisfy, nothing is yielded (FORALL fails)
374
+
375
+ return Relation(run_f=run_f, name=name)
376
+
377
+
378
+ def xexists(f: Callable[..., bool], *, name: str) -> Relation:
379
+ def run_f(bound: FrozenDict[str, Tuple[Any, ...]]) -> Generator[FrozenDict[str, Any], None, None]:
380
+ # For EXISTS, yield only the first satisfying binding
381
+ for b in relation(f, name=name)(bound):
382
+ yield b
383
+ return # stop at first satisfying tuple
384
+ return Relation(run_f=run_f, name=name)
385
+
386
+
387
+
@@ -454,29 +454,57 @@ class Generator(Algebra[ParseResult[T], GenState[T]]):
454
454
 
455
455
 
456
456
 
457
- def generate(syntax: Syntax[Any, Any],
458
- data: Optional[ParseResult[Any]] = None,
459
- seed: int = 0,
460
- restore_pruned: bool = False) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
461
- """Run a ``Syntax`` with the ``Generator`` backend.
457
+ def generate_with(
458
+ syntax: Syntax[Any, Any],
459
+ data: Optional[ParseResult[Any]] = None,
460
+ seed: int = 0,
461
+ restore_pruned: bool = False
462
+ ) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
463
+ """
464
+ Generate an AST from the given syntax, optionally constrained by a partial parse result.
462
465
 
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
+ Args:
467
+ syntax: The syntax specification to generate from.
468
+ data: An optional partial parse result (AST) to constrain generation.
469
+ seed: Random seed for reproducibility.
470
+ restore_pruned: Whether to restore pruned branches in the AST.
471
+
472
+ Returns:
473
+ A tuple of (AST, variable bindings) if successful, or (None, None) on failure.
474
+ """
475
+ from syncraft.syntax import run
476
+ return run(syntax, Generator, not restore_pruned, ast=data, seed=seed, restore_pruned=restore_pruned)
477
+
478
+
479
+ def validate(
480
+ syntax: Syntax[Any, Any],
481
+ data: ParseResult[Any]
482
+ ) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
483
+ """
484
+ Validate a parse result (AST) against the given syntax.
466
485
 
467
486
  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.
487
+ syntax: The syntax specification to validate against.
488
+ data: The parse result (AST) to validate.
472
489
 
473
490
  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.
491
+ A tuple of (AST, variable bindings) if valid, or (None, None) if invalid.
477
492
  """
478
493
  from syncraft.syntax import run
479
- return run(syntax, Generator, False, ast=data, seed=seed, restore_pruned=restore_pruned)
494
+ return run(syntax, Generator, True, ast=data, seed=0, restore_pruned=True)
495
+
496
+
497
+ def generate(
498
+ syntax
499
+ ) -> Tuple[AST, FrozenDict[str, Tuple[AST, ...]]] | Tuple[Any, None]:
500
+ """
501
+ Generate a random AST that conforms to the given syntax.
480
502
 
503
+ Args:
504
+ syntax: The syntax specification to generate from.
481
505
 
482
-
506
+ Returns:
507
+ A tuple of (AST, variable bindings) if successful, or (None, None) on failure.
508
+ """
509
+ from syncraft.syntax import run
510
+ return run(syntax, Generator, False, ast=None, seed=random.randint(0, 2**32 - 1), restore_pruned=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncraft
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Parser combinator library
5
5
  Author-email: Michael Afmokt <michael@esacca.com>
6
6
  License-Expression: MIT
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
File without changes
File without changes