syncraft 0.2.3__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.
- {syncraft-0.2.3/syncraft.egg-info → syncraft-0.2.5}/PKG-INFO +1 -1
- {syncraft-0.2.3 → syncraft-0.2.5}/pyproject.toml +1 -1
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/constraint.py +156 -1
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/generator.py +45 -17
- {syncraft-0.2.3 → syncraft-0.2.5/syncraft.egg-info}/PKG-INFO +1 -1
- {syncraft-0.2.3 → syncraft-0.2.5}/LICENSE +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/README.md +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/setup.cfg +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/__init__.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/algebra.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/ast.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/diagnostic.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/finder.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/parser.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/py.typed +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/sqlite3.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft/syntax.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft.egg-info/SOURCES.txt +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft.egg-info/dependency_links.txt +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft.egg-info/requires.txt +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/syncraft.egg-info/top_level.txt +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/tests/test_bimap.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/tests/test_constraint.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/tests/test_find.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/tests/test_parse.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/tests/test_to.py +0 -0
- {syncraft-0.2.3 → syncraft-0.2.5}/tests/test_until.py +0 -0
|
@@ -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
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
469
|
-
data:
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|