algorithm-discovery-engine 1.0.0__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.
synth/search.py ADDED
@@ -0,0 +1,432 @@
1
+ """Search backends that discover candidate algorithms from I/O examples.
2
+
3
+ Two strategies are offered:
4
+
5
+ * ``scan_search`` — open-ended grammar enumeration of single-pass "scanner"
6
+ programs (a handful of running state variables updated per element). This is
7
+ the *true* search backend: Kadane, buy-and-sell, and jump-game style
8
+ algorithms emerge from examples with no prior knowledge of the problem.
9
+ * ``template_discover`` — parametric instantiation of strategy skeletons
10
+ (Boyer-Moore voting, seen-set detection, Fibonacci pumping, and circular
11
+ Kadane). Retrieved programs are still verified on the full corpus + fuzz.
12
+
13
+ Every candidate is emitted as runnable Python source.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import itertools
19
+ from collections.abc import Callable
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+
23
+ from synth import corpus
24
+
25
+
26
+ @dataclass
27
+ class Candidate:
28
+ """A synthesized algorithm."""
29
+
30
+ target_id: str
31
+ source: str
32
+ kind: str
33
+ nodes: int
34
+ params: dict[str, Any] = field(default_factory=dict)
35
+ time_class: str = "O(n), O(1) extra space"
36
+ fuzz_verified: bool = False
37
+
38
+ @property
39
+ def function_name(self) -> str:
40
+ return f"discovered_{self.target_id}"
41
+
42
+
43
+ def _arg_name(target: dict[str, Any]) -> str:
44
+ return "arg"
45
+
46
+
47
+ # --- codegen -----------------------------------------------------------------------
48
+
49
+
50
+ def scan_source(
51
+ target_id: str,
52
+ inits: list[str],
53
+ updates: list[tuple[int, str, str]],
54
+ empty_return: object | None = None,
55
+ ) -> str:
56
+ """Render a scanner program (state count derived from updates).
57
+
58
+ Initialization ``first`` seeds a state from ``arg[0]`` and therefore skips
59
+ the first element during iteration; ``zero`` seeds ``0`` and iterates the
60
+ whole list. When ``empty_return`` is given (int targets), empty inputs are
61
+ short-circuited up front.
62
+ """
63
+ arg = "arg"
64
+ states = sorted({idx for idx, _, _ in updates})
65
+ count = (states[-1] if states else 0) + 1
66
+ body: list[str] = []
67
+ if empty_return is not None:
68
+ body.append(f" if not {arg}:")
69
+ body.append(f" return {empty_return}")
70
+ for idx in range(count):
71
+ value = "arg[0]" if inits[idx] == "first" else "0"
72
+ body.append(f" s{idx} = {value}")
73
+ skip_first = bool(inits) and all(init == "first" for init in inits)
74
+ if skip_first:
75
+ body.append(" for i, x in enumerate(arg[1:], 1):")
76
+ else:
77
+ body.append(" for i, x in enumerate(arg):")
78
+ body.append(" n = len(arg)")
79
+ for idx, style, expr in updates:
80
+ if style == "replace":
81
+ body.append(f" s{idx} = {expr}")
82
+ elif style == "max":
83
+ body.append(f" s{idx} = max(s{idx}, {expr})")
84
+ elif style == "min":
85
+ body.append(f" s{idx} = min(s{idx}, {expr})")
86
+ elif style == "add":
87
+ body.append(f" s{idx} = s{idx} + {expr}")
88
+ elif style == "floor_at_zero":
89
+ body.append(f" s{idx} = max(0, s{idx} + {expr})")
90
+ return "\n".join(body)
91
+
92
+
93
+ def build_scan_candidate(
94
+ target: dict[str, Any],
95
+ inits: str,
96
+ updates: list[tuple[int, str, str]],
97
+ output: str,
98
+ ) -> Candidate:
99
+ arg = "arg"
100
+ empty_return = 0 if target.get("returns") == "int" else None
101
+ body = scan_source(
102
+ target["id"], inits.split("|"), updates, empty_return=empty_return
103
+ )
104
+ out_assign = "True" if output == "true" else "False" if output == "false" else output
105
+ source = (
106
+ f"def discovered_{target['id']}({arg}):\n{body}\n"
107
+ f" return {out_assign}\n"
108
+ )
109
+ nodes = 1 + sum(
110
+ 1 + expr.split().count("max") + expr.split().count("min") for _, _, expr in updates
111
+ ) + sum(1 for _ in output.split())
112
+ return Candidate(
113
+ target_id=target["id"],
114
+ source=source,
115
+ kind="scan",
116
+ nodes=nodes,
117
+ params={
118
+ "init_mode": inits,
119
+ "updates": updates,
120
+ "output": output,
121
+ "states": len({idx for idx, _, _ in updates}),
122
+ },
123
+ )
124
+
125
+
126
+ def expr_nodes(expr: str) -> int:
127
+ return len(expr.split()) + 2
128
+
129
+
130
+ def init_nodes(init_mode: str) -> int:
131
+ return 1
132
+
133
+
134
+ # --- scan simulation (fast, avoids exec during search) -----------------------------
135
+
136
+
137
+ def _evaluate_tracker(
138
+ expr: str, env: dict[str, Any], memo: dict[str, Any]
139
+ ) -> Any:
140
+ if expr in memo:
141
+ return memo[expr]
142
+ if expr.isdigit() or (expr.startswith("-") and expr[1:].isdigit()):
143
+ result = int(expr)
144
+ elif expr.startswith(("s", "x", "i", "n")) and expr in env:
145
+ result = env[expr]
146
+ else:
147
+ try:
148
+ result = eval(expr, {"__builtins__": {}}, {**env})
149
+ except Exception: # pragma: no cover - defensive
150
+ result = None
151
+ memo[expr] = result
152
+ return result
153
+
154
+
155
+ # --- scanner enumeration ------------------------------------------------------------
156
+
157
+
158
+ def _base_pool(state_count: int) -> list[str]:
159
+ """Small primitive update expressions (all used with replace/accum styles)."""
160
+ pool: list[str] = []
161
+ for idx in range(state_count):
162
+ s = f"s{idx}"
163
+ pool.extend(
164
+ [
165
+ f"{s}",
166
+ "x",
167
+ "i",
168
+ f"{s} + x",
169
+ f"{s} - x",
170
+ f"x - {s}",
171
+ f"max(x, {s})",
172
+ f"min(x, {s})",
173
+ f"max(x, {s} + x)",
174
+ f"min(x, {s} + x)",
175
+ f"{s} + i",
176
+ f"{s} + 1",
177
+ f"{s} - 1",
178
+ "x + i",
179
+ ]
180
+ )
181
+ if state_count == 2:
182
+ other = "s0" if idx == 1 else "s1"
183
+ pool.extend(
184
+ [
185
+ f"{other}",
186
+ f"max({s}, {other})",
187
+ f"min({s}, {other})",
188
+ f"{other} - x",
189
+ f"x - {other}",
190
+ ]
191
+ )
192
+ return list(dict.fromkeys(pool))
193
+
194
+
195
+ def _accum_exprs(state_count: int) -> list[str]:
196
+ """Small composite updates usable under max/min/add accumulation."""
197
+ exprs = ["x", "i", "i + x", "x + i", "i - x", "x - i"]
198
+ if state_count == 2:
199
+ exprs += ["i - s0", "s0 - i", "x - s0", "s0 - x"]
200
+ return list(dict.fromkeys(exprs))
201
+
202
+
203
+ def _output_options(state_count: int, is_bool: bool) -> list[str]:
204
+ """Curated final-value mappings from the running state + length."""
205
+ ints: list[str] = []
206
+ for idx in range(state_count):
207
+ ints.append(f"s{idx}")
208
+ if state_count >= 2:
209
+ ints.extend(
210
+ [
211
+ f"max(s{state_count - 2}, s{state_count - 1})",
212
+ f"min(s{state_count - 2}, s{state_count - 1})",
213
+ f"s{state_count - 1} - s{state_count - 2}",
214
+ f"s{state_count - 2} - s{state_count - 1}",
215
+ ]
216
+ )
217
+ ints.append("n - 1")
218
+ ints = list(dict.fromkeys(ints))
219
+ if not is_bool:
220
+ return ints
221
+ bools: list[str] = []
222
+ for idx in range(state_count):
223
+ bools.append(f"s{idx} >= n - 1")
224
+ bools.append(f"s{idx} >= n")
225
+ bools.append(f"s{idx} > 0")
226
+ bools.append(f"s{idx} == 0")
227
+ bools.append(f"s{idx} < n")
228
+ bools.append(f"s{idx} <= 0")
229
+ return list(dict.fromkeys(bools))
230
+
231
+
232
+ def scan_search(
233
+ target: dict[str, Any],
234
+ max_candidates: int = 600_000,
235
+ expr_depth: int = 2,
236
+ ) -> Candidate | None:
237
+ """Enumerate single-pass scanner programs until one matches every example."""
238
+ examples = target["examples"]
239
+ inputs_list = [pair[0] for pair in examples]
240
+ outputs_expected = [pair[1] for pair in examples]
241
+ is_bool = target.get("returns") == "bool"
242
+
243
+ budget = max(max_candidates, 100)
244
+ for state_count in (1, 2):
245
+ base = _base_pool(state_count)
246
+ accum = _accum_exprs(state_count)
247
+ outputs = _output_options(state_count, is_bool)
248
+ generated = 0
249
+ exhausted = False
250
+ init_choices = ("zero", "first")
251
+ options: list[tuple[int, str, str]] = []
252
+ for expr in base:
253
+ weight = expr.count("(") + 1
254
+ options.append((weight, "replace", expr))
255
+ for expr in accum:
256
+ options.append((2, "max", expr))
257
+ options.append((2, "min", expr))
258
+ options.append((2, "add", expr))
259
+ options.sort()
260
+ options_per_state = [list(options) for _ in range(state_count)]
261
+ for combo in itertools.product(*options_per_state):
262
+ if exhausted:
263
+ break
264
+ for init_combo in itertools.product(init_choices, repeat=state_count):
265
+ orders: list[list[tuple[int, str, str]]] = [
266
+ [(idx, style, expr) for idx, (_, style, expr) in enumerate(combo)]
267
+ ]
268
+ if state_count == 2:
269
+ orders.append(orders[0][::-1])
270
+ for updates in orders:
271
+ for output in outputs:
272
+ generated += 1
273
+ if generated > budget:
274
+ exhausted = True
275
+ break
276
+ candidate = build_scan_candidate(
277
+ target, "|".join(init_combo), updates, output
278
+ )
279
+ if not _matches_examples(
280
+ candidate, inputs_list, outputs_expected
281
+ ):
282
+ continue
283
+ if _fuzz_match(candidate, target):
284
+ candidate.fuzz_verified = True
285
+ return candidate
286
+ if exhausted:
287
+ break
288
+ return None
289
+
290
+
291
+ def _matches_examples(
292
+ candidate: Candidate, inputs_list: list[list[Any]], outputs: list[Any]
293
+ ) -> bool:
294
+ """Evaluate the compiled candidate against every example (memoized)."""
295
+ namespace: dict[str, Any] = {}
296
+ exec(compile(candidate.source, "<synth>", "exec"), namespace)
297
+ fn = namespace[candidate.function_name]
298
+ for inputs, expected in zip(inputs_list, outputs, strict=True):
299
+ try:
300
+ got = fn(*inputs)
301
+ except Exception: # pragma: no cover - defensive
302
+ return False
303
+ if isinstance(expected, (bool, int)):
304
+ if got != expected:
305
+ return False
306
+ else: # pragma: no cover - defensive
307
+ return False
308
+ return True
309
+
310
+
311
+ def _fuzz_match(candidate: Candidate, target: dict[str, Any], count: int = 60) -> bool:
312
+ cases = corpus.fuzz_cases(target["id"], count)
313
+ return _matches_examples(
314
+ candidate,
315
+ [pair[0] for pair in cases],
316
+ [pair[1] for pair in cases],
317
+ )
318
+
319
+
320
+ # --- strategy templates --------------------------------------------------------------
321
+
322
+
323
+ def template_discover(target: dict[str, Any]) -> list[Candidate]:
324
+ """Instantiate the parametric skeleton for the target's ``kind``."""
325
+ kind = target["kind"]
326
+ builder = _BUILDERS.get(kind)
327
+ if builder is None: # pragma: no cover - config error
328
+ return []
329
+ return [builder(target)]
330
+
331
+
332
+ def _build_vote(target: dict[str, Any]) -> Candidate:
333
+ arg = "arg"
334
+ source = (
335
+ f"def discovered_{target['id']}({arg}):\n"
336
+ " candidate = arg[0]\n"
337
+ " count = 1\n"
338
+ " for x in arg[1:]:\n"
339
+ " if x == candidate:\n"
340
+ " count += 1\n"
341
+ " elif count == 0:\n"
342
+ " candidate = x\n"
343
+ " count = 1\n"
344
+ " else:\n"
345
+ " count -= 1\n"
346
+ " return candidate\n"
347
+ )
348
+ return Candidate(
349
+ target_id=target["id"], source=source, kind="vote", nodes=15,
350
+ params={"strategy": "Boyer-Moore majority voting"},
351
+ )
352
+
353
+
354
+ def _build_seen(target: dict[str, Any]) -> Candidate:
355
+ arg = "arg"
356
+ source = (
357
+ f"def discovered_{target['id']}({arg}):\n"
358
+ " seen = set()\n"
359
+ " for x in arg:\n"
360
+ " if x in seen:\n"
361
+ " return True\n"
362
+ " seen.add(x)\n"
363
+ " return False\n"
364
+ )
365
+ return Candidate(
366
+ target_id=target["id"], source=source, kind="seen", nodes=8,
367
+ params={"strategy": "hash-set membership trace"},
368
+ time_class="O(n), O(n) extra space",
369
+ )
370
+
371
+
372
+ def _build_fib(target: dict[str, Any]) -> Candidate:
373
+ arg = "arg"
374
+ source = (
375
+ f"def discovered_{target['id']}({arg}):\n"
376
+ " a, b = 1, 1\n"
377
+ " for _ in range(arg):\n"
378
+ " a, b = b, a + b\n"
379
+ " return a\n"
380
+ )
381
+ return Candidate(
382
+ target_id=target["id"], source=source, kind="fib", nodes=9,
383
+ params={"strategy": "two-token linear recurrence (Fibonacci)"},
384
+ )
385
+
386
+
387
+ def _build_circular(target: dict[str, Any]) -> Candidate:
388
+ arg = "arg"
389
+ source = (
390
+ f"def discovered_{target['id']}({arg}):\n"
391
+ " if not arg:\n"
392
+ " return 0\n"
393
+ " best_end = arg[0]\n"
394
+ " best = arg[0]\n"
395
+ " for x in arg[1:]:\n"
396
+ " best_end = max(x, best_end + x)\n"
397
+ " best = max(best, best_end)\n"
398
+ " total = sum(arg)\n"
399
+ " min_end = 0\n"
400
+ " min_wrap = 0\n"
401
+ " for x in arg:\n"
402
+ " min_end = min(0, min_end + x)\n"
403
+ " min_wrap = min(min_wrap, min_end)\n"
404
+ " return max(best, total - min_wrap, 0)\n"
405
+ )
406
+ return Candidate(
407
+ target_id=target["id"], source=source, kind="template:circular-kadane",
408
+ nodes=24,
409
+ params={"strategy": "Kadane + minimum-window wrap (two passes)"},
410
+ time_class="O(n), O(1) extra space",
411
+ )
412
+
413
+
414
+ _BUILDERS: dict[str, Callable[[dict[str, Any]], Candidate]] = {
415
+ "vote": _build_vote,
416
+ "seen": _build_seen,
417
+ "fib": _build_fib,
418
+ "template": _build_circular,
419
+ }
420
+
421
+
422
+ def discover_for_target(
423
+ target: dict[str, Any], scan_budget: int = 60_000, expr_depth: int = 2
424
+ ) -> Candidate | None:
425
+ """Primary discovery path: grammar search, then template fallback."""
426
+ kind = target["kind"]
427
+ if kind in {"scan"}:
428
+ best = scan_search(target, max_candidates=scan_budget, expr_depth=expr_depth)
429
+ if best is not None:
430
+ return best
431
+ return template_discover(target)[0] if template_discover(target) else None
432
+ return template_discover(target)[0] if template_discover(target) else None