annoscript 0.1.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.
Files changed (128) hide show
  1. annoscript/__init__.py +1 -0
  2. annoscript/_backend_profiles.py +17 -0
  3. annoscript/_client_common.py +287 -0
  4. annoscript/_data.py +37 -0
  5. annoscript/_dispatch_pool.py +47 -0
  6. annoscript/_env.py +19 -0
  7. annoscript/_examples.py +34 -0
  8. annoscript/_expressions.py +93 -0
  9. annoscript/_plan_viewer.html +953 -0
  10. annoscript/_remote_jobs.py +211 -0
  11. annoscript/_request_adapters.py +71 -0
  12. annoscript/_speculation.py +321 -0
  13. annoscript/_test_artifacts.py +33 -0
  14. annoscript/_test_evaluation_http.py +117 -0
  15. annoscript/_test_ir_runtime.py +138 -0
  16. annoscript/_test_job_process.py +166 -0
  17. annoscript/_test_plan_runtime.py +331 -0
  18. annoscript/_test_remote_client.py +46 -0
  19. annoscript/_test_remote_server.py +75 -0
  20. annoscript/_test_synthetic.py +67 -0
  21. annoscript/anno.py +270 -0
  22. annoscript/backends/__init__.py +13 -0
  23. annoscript/backends/_batch_wire.py +26 -0
  24. annoscript/backends/_bounded_grammar.py +97 -0
  25. annoscript/backends/_example_layout.py +23 -0
  26. annoscript/backends/_generation_policy.py +116 -0
  27. annoscript/backends/_grammar.py +83 -0
  28. annoscript/backends/_preview.py +13 -0
  29. annoscript/backends/_prompt.py +115 -0
  30. annoscript/backends/_shared_rules.py +38 -0
  31. annoscript/backends/adapters/__init__.py +17 -0
  32. annoscript/backends/adapters/chat.py +39 -0
  33. annoscript/backends/adapters/decision.py +119 -0
  34. annoscript/backends/base.py +427 -0
  35. annoscript/backends/config.py +67 -0
  36. annoscript/backends/decision.py +150 -0
  37. annoscript/backends/llm.py +338 -0
  38. annoscript/client.py +247 -0
  39. annoscript/client_tests.py +17 -0
  40. annoscript/compiler.py +708 -0
  41. annoscript/cross_version_tests.py +68 -0
  42. annoscript/fixtures/request-adapters-v1.json +4741 -0
  43. annoscript/formatter.py +171 -0
  44. annoscript/grammar_tests.py +176 -0
  45. annoscript/inspect.py +77 -0
  46. annoscript/ir.py +458 -0
  47. annoscript/job_cross_version_tests.py +118 -0
  48. annoscript/job_protocol.py +180 -0
  49. annoscript/jobs.py +255 -0
  50. annoscript/metrics.py +153 -0
  51. annoscript/p0_grammar_tests.py +147 -0
  52. annoscript/passes/__init__.py +40 -0
  53. annoscript/passes/_analysis.py +178 -0
  54. annoscript/passes/control_flow.py +68 -0
  55. annoscript/passes/dead_assignments.py +44 -0
  56. annoscript/passes/folding.py +104 -0
  57. annoscript/passes/propagation.py +53 -0
  58. annoscript/passes/temporaries.py +45 -0
  59. annoscript/plan.py +542 -0
  60. annoscript/plan_formatter.py +452 -0
  61. annoscript/plan_lowering.py +571 -0
  62. annoscript/preview.py +115 -0
  63. annoscript/remote.py +304 -0
  64. annoscript/runtime.py +861 -0
  65. annoscript/server/__init__.py +1 -0
  66. annoscript/server/__main__.py +49 -0
  67. annoscript/server/_plan_cache.py +79 -0
  68. annoscript/server/api.py +146 -0
  69. annoscript/server/metrics.py +140 -0
  70. annoscript/server/migrate.py +61 -0
  71. annoscript/server/releases.py +143 -0
  72. annoscript/server/store.py +429 -0
  73. annoscript/server/worker.py +290 -0
  74. annoscript/substring_tests.py +79 -0
  75. annoscript/test_advisory_lengths.py +115 -0
  76. annoscript/test_backends.py +181 -0
  77. annoscript/test_client.py +349 -0
  78. annoscript/test_compact_batch.py +95 -0
  79. annoscript/test_compiler.py +444 -0
  80. annoscript/test_decision_adapters.py +410 -0
  81. annoscript/test_decision_wire.py +190 -0
  82. annoscript/test_diagnostics.py +97 -0
  83. annoscript/test_engineering_p0.py +191 -0
  84. annoscript/test_environment.py +65 -0
  85. annoscript/test_example_backends.py +251 -0
  86. annoscript/test_examples.py +149 -0
  87. annoscript/test_field_names.py +221 -0
  88. annoscript/test_formatter.py +153 -0
  89. annoscript/test_fusion_scheduling.py +109 -0
  90. annoscript/test_generation_policy.py +180 -0
  91. annoscript/test_ir.py +148 -0
  92. annoscript/test_jobs.py +371 -0
  93. annoscript/test_jobs_server.py +511 -0
  94. annoscript/test_llm_batch.py +428 -0
  95. annoscript/test_metrics.py +59 -0
  96. annoscript/test_metrics_server.py +261 -0
  97. annoscript/test_minimal_fusion.py +59 -0
  98. annoscript/test_nullable_generate.py +169 -0
  99. annoscript/test_passes.py +408 -0
  100. annoscript/test_plan.py +522 -0
  101. annoscript/test_plan_constraints.py +305 -0
  102. annoscript/test_plan_formatter.py +272 -0
  103. annoscript/test_prompt_layout.py +140 -0
  104. annoscript/test_qwen_backend.py +181 -0
  105. annoscript/test_qwen_pilot_regression.py +180 -0
  106. annoscript/test_release_client.py +20 -0
  107. annoscript/test_remote.py +308 -0
  108. annoscript/test_request_adapters.py +295 -0
  109. annoscript/test_requirements.py +228 -0
  110. annoscript/test_rule_dedup.py +67 -0
  111. annoscript/test_runtime.py +314 -0
  112. annoscript/test_runtime_client.py +133 -0
  113. annoscript/test_source_fields.py +111 -0
  114. annoscript/test_speculation.py +458 -0
  115. annoscript/test_substring_backend.py +81 -0
  116. annoscript/test_synthetic_example.py +61 -0
  117. annoscript-0.1.0.dist-info/METADATA +145 -0
  118. annoscript-0.1.0.dist-info/RECORD +128 -0
  119. annoscript-0.1.0.dist-info/WHEEL +4 -0
  120. annoscript-0.1.0.dist-info/licenses/LICENSE +21 -0
  121. annoscript_examples/__init__.py +1 -0
  122. annoscript_examples/fixtures.py +3 -0
  123. annoscript_examples/request.execution_plan.json +3017 -0
  124. annoscript_examples/request.json +858 -0
  125. annoscript_examples/request.py +37 -0
  126. annoscript_examples/request_preview.decision.json +23 -0
  127. annoscript_examples/request_preview.generation.json +20 -0
  128. annoscript_examples/run.py +24 -0
@@ -0,0 +1,522 @@
1
+ """Plan structure, scheduling, sharing, failure and business differential tests."""
2
+
3
+ import asyncio
4
+ from collections import Counter
5
+ from copy import deepcopy
6
+ from dataclasses import replace
7
+ import inspect
8
+ import json
9
+ from pathlib import Path
10
+ import unittest
11
+ from unittest.mock import patch
12
+
13
+ from annoscript import ir, plan as p
14
+ from annoscript._test_artifacts import assert_saved_artifact
15
+ from annoscript.compiler import compile_program
16
+ from annoscript.plan_lowering import lower_to_plan, PlanLoweringError
17
+ from annoscript._test_plan_runtime import evaluate
18
+ from annoscript._test_ir_runtime import evaluate as evaluate_ir
19
+ from annoscript.anno import Anno, GenerateResponse, constrain, Example
20
+ from annoscript import test_compiler as compiler_tests
21
+
22
+
23
+ def _key(operation, args, kwargs):
24
+ def freeze(x):
25
+ if type(x) is Example:
26
+ return freeze(x.to_dict())
27
+ if isinstance(x, dict):
28
+ return ("dict", tuple((freeze(k), freeze(v)) for k, v in x.items()))
29
+ if isinstance(x, (tuple, list)):
30
+ return (type(x).__name__, tuple(freeze(v) for v in x))
31
+ return (type(x).__name__, x.hex() if type(x) is float else x)
32
+ defaults = {"generate":{"max_length":512},"generate_list":{"max_items":8,"max_length_per_item":58},"extract":{"max_items":8,"max_length_per_item":58}}.get(operation,{})
33
+ bound = inspect.signature(getattr(Anno, operation)).bind(None, *args, **{**defaults,**kwargs})
34
+ bound.apply_defaults()
35
+ return operation, freeze(tuple((k, v) for k, v in bound.arguments.items() if k != "self"))
36
+
37
+
38
+ class PureAnnoScript:
39
+ def __init__(self, state=None, fail=()):
40
+ self.state = state
41
+ self.calls = []
42
+ self.fail = fail
43
+
44
+ def generate(self, instructions, **kwargs):
45
+ self.calls.append((instructions, deepcopy(kwargs)))
46
+ if instructions in self.fail:
47
+ raise ValueError(instructions)
48
+ return GenerateResponse(instructions)
49
+
50
+
51
+ def independent(anno):
52
+ a = anno.generate("a", max_length=512).text
53
+ b = anno.generate("b", max_length=512).text
54
+ return [a, b]
55
+
56
+
57
+ def duplicates(anno):
58
+ a = anno.generate("中文", max_length=512).text
59
+ b = anno.generate(instructions="中文", min_length=0, max_length=512).text
60
+ return [a, b, None]
61
+
62
+
63
+ def branch_duplicates(anno):
64
+ if anno.state:
65
+ a = anno.generate("same", max_length=512).text
66
+ else:
67
+ a = anno.generate("same", max_length=512).text
68
+ b = anno.generate("same", max_length=512).text
69
+ return [a, b]
70
+
71
+
72
+ def unused_branch(anno):
73
+ if anno.state:
74
+ anno.generate("same", max_length=512)
75
+ return None
76
+
77
+
78
+ def loop_invariant(anno):
79
+ result = []
80
+ for item in anno.state:
81
+ if item:
82
+ text = anno.generate("same", max_length=512).text
83
+ result = result + [text]
84
+ return result
85
+
86
+
87
+ def independent_loop(anno):
88
+ result = []
89
+ for item in anno.state:
90
+ result = result + [anno.generate(item, max_length=512).text]
91
+ return result
92
+
93
+
94
+ def last_value(anno):
95
+ result = "initial"
96
+ for item in anno.state:
97
+ result = anno.generate(item, max_length=512).text
98
+ return result
99
+
100
+
101
+ def conditional_last(anno):
102
+ result = "initial"
103
+ for item in anno.state:
104
+ if item:
105
+ result = anno.generate(item, max_length=512).text
106
+ return result
107
+
108
+
109
+ def nested_loop(anno):
110
+ result = []
111
+ for row in anno.state:
112
+ for item in row:
113
+ result = result + [anno.generate(item, max_length=512).text]
114
+ return result
115
+
116
+
117
+ def local_container(anno):
118
+ result = []
119
+ for item in anno.state:
120
+ data = {"x": item}
121
+ data["x"] = item + "!"
122
+ result = result + [anno.generate(data["x"], max_length=512).text]
123
+ return result
124
+
125
+
126
+ def memory_snapshots(anno):
127
+ data = {"x": "before"}
128
+ alias = data
129
+ a = json.dumps(data)
130
+ first = anno.generate(a, max_length=512).text
131
+ alias["x"] = "after"
132
+ b = json.dumps(data)
133
+ second = anno.generate(b, max_length=512).text
134
+ return [first, second]
135
+
136
+
137
+ def iteration_alias(anno):
138
+ data = {"x": "before"}
139
+ result = []
140
+ for item in anno.state:
141
+ result = result + [anno.generate(data["x"], max_length=512).text]
142
+ data["x"] = item
143
+ return result
144
+
145
+
146
+ def nested_memory(anno):
147
+ data = {"x": "initial"}
148
+ result = []
149
+ for row in anno.state:
150
+ for item in row:
151
+ result = result + [anno.generate(data["x"], max_length=512).text]
152
+ data["x"] = item
153
+ return result
154
+
155
+
156
+ def iterator_twice(anno):
157
+ values = enumerate(anno.state)
158
+ first = []
159
+ for index, value in values:
160
+ first = first + [value]
161
+ second = []
162
+ for index, value in values:
163
+ second = second + [value]
164
+ return [first, second]
165
+
166
+
167
+ def early_return(anno):
168
+ if anno.state:
169
+ return anno.generate("first", max_length=512).text
170
+ return anno.generate("second", max_length=512).text
171
+
172
+
173
+ def nested_return(anno):
174
+ if anno.state:
175
+ if anno.state[0]:
176
+ return anno.generate("a", max_length=512).text
177
+ return anno.generate("b", max_length=512).text
178
+ return anno.generate("c", max_length=512).text
179
+
180
+
181
+ def partial_nested_return(anno):
182
+ if anno.state:
183
+ if anno.state[0]:
184
+ return anno.generate("early", max_length=512).text
185
+ anno.generate("inner", max_length=512)
186
+ return anno.generate("late", max_length=512).text
187
+
188
+
189
+ def shared_list(anno):
190
+ first = ["before"]
191
+ second = first
192
+ snapshot = first[0]
193
+ second[0] = "after"
194
+ return [snapshot, first, second]
195
+
196
+
197
+ def nested_store(anno):
198
+ data = {"inner": ["before"]}
199
+ alias = data["inner"]
200
+ first = alias[0]
201
+ alias[0] = "after"
202
+ return [first, data]
203
+
204
+
205
+ def iterator_unpack(anno):
206
+ iterator = enumerate(anno.state)
207
+ a, b = iterator
208
+ rest = []
209
+ for index, item in iterator:
210
+ rest = rest + [item]
211
+ return [a, b, rest]
212
+
213
+
214
+ def checked(anno):
215
+ a = anno.generate("before", max_length=512).text
216
+ constrain(a == anno.state, message="验证失败")
217
+ b = anno.generate("after", max_length=512).text
218
+ return [a, b]
219
+
220
+
221
+ def checked_branch(anno):
222
+ if anno.state:
223
+ constrain(False, message="branch failed")
224
+ return anno.generate("after", max_length=512).text
225
+
226
+
227
+ def checked_loop(anno):
228
+ result = []
229
+ for item in anno.state:
230
+ a = anno.generate(item, max_length=512).text
231
+ constrain(a != "bad")
232
+ result = result + [a]
233
+ return result
234
+
235
+
236
+ def unused_failure(anno):
237
+ anno.generate("bad", max_length=512)
238
+ return "done"
239
+
240
+
241
+ def expression_failure(anno):
242
+ a = anno.generate("first", max_length=512).text
243
+ bad = int("bad")
244
+ b = anno.generate("second", max_length=512).text
245
+ return [a, b]
246
+
247
+
248
+ def unpack_failure(anno):
249
+ first, second = anno.state
250
+ return anno.generate("after", max_length=512).text
251
+
252
+
253
+ def short_circuit(anno):
254
+ a = anno.state and anno.generate("yes", max_length=512).text
255
+ b = True or anno.generate("never", max_length=512).text
256
+ return [a, b]
257
+
258
+
259
+ class PlanTests(unittest.TestCase):
260
+ def compile(self, function, raw=False):
261
+ return lower_to_plan(compile_program(function, optimize=not raw))
262
+
263
+ def compare(self, function, states, *, expected_calls=None):
264
+ for raw in (True, False):
265
+ plan = self.compile(function, raw)
266
+ for state in states:
267
+ reference = PureAnnoScript(deepcopy(state))
268
+ expected = function(reference)
269
+ for mode in ("eager", "strict"):
270
+ for order in ("forward", "reverse", "random"):
271
+ with self.subTest(function=function.__name__, state=state, raw=raw, mode=mode, order=order):
272
+ backend = PureAnnoScript(deepcopy(state))
273
+ self.assertEqual(evaluate(plan.to_dict(), backend, mode=mode, order=order), expected)
274
+ if expected_calls is not None:
275
+ self.assertEqual(len(backend.calls), expected_calls(state))
276
+
277
+ def test_serialization_and_input_unchanged(self):
278
+ raw = compile_program(duplicates)
279
+ before = raw.to_json()
280
+ plan = lower_to_plan(raw)
281
+ self.assertEqual(raw.to_json(), before)
282
+ self.assertEqual(plan.to_json(), lower_to_plan(raw).to_json())
283
+ self.assertEqual(json.loads(plan.to_json()), plan.to_dict())
284
+ self.assertEqual(p.ExecutionPlan.from_json(plan.to_json()), plan)
285
+ self.assertIn("中文", plan.to_json())
286
+ detached = plan.to_dict()
287
+ detached["tasks"].clear()
288
+ self.assertTrue(plan.tasks)
289
+ self.assertGreater(len(plan.dependencies_for("strict")), len(plan.dependencies_for()))
290
+ with self.assertRaises(ValueError):
291
+ plan.dependencies_for("unknown")
292
+ from annoscript.passes._analysis import walk_statements
293
+ sources = {s.id: s.source for s in walk_statements(raw.body)}
294
+ self.assertTrue(all(t.source == sources[t.origin] for t in plan.tasks))
295
+
296
+ def test_duplicate_and_default_argument_sharing(self):
297
+ plan = self.compile(duplicates)
298
+ self.assertEqual(len(plan.requests), 1)
299
+ self.compare(duplicates, [None], expected_calls=lambda _: 1)
300
+
301
+ def test_branch_sharing_and_no_speculation(self):
302
+ self.assertEqual(len(self.compile(branch_duplicates).requests), 1)
303
+ self.compare(branch_duplicates, [True, False], expected_calls=lambda _: 1)
304
+ self.compare(unused_branch, [True, False], expected_calls=lambda s: int(s))
305
+ self.compare(short_circuit, [True, False], expected_calls=lambda s: int(s))
306
+
307
+ def test_loop_invariant_demand_sharing(self):
308
+ self.assertEqual(self.compile(loop_invariant).requests[0].scope, ())
309
+ self.compare(loop_invariant, [[], [False, False], [True, False, True]], expected_calls=lambda s: int(any(s)))
310
+
311
+ def test_loop_carried_data_and_false_dependencies(self):
312
+ plan = self.compile(independent_loop)
313
+ carries = [t for t in plan.tasks if isinstance(t, p.Carry)]
314
+ self.assertEqual(len(carries), 1)
315
+ self.assertTrue(any(d.mapping.distance == 1 and d.kind == "data" for d in plan.dependencies))
316
+ sites = {t.id for t in plan.tasks if isinstance(t, p.Infer)}
317
+ self.assertFalse(any(d.mapping.distance and d.target in sites for d in plan.dependencies))
318
+ self.compare(independent_loop, [[], ["a", "b", "a"]])
319
+ self.compare(last_value, [[], ["a", "b"]])
320
+ self.assertFalse(any(isinstance(t, p.Carry) for t in self.compile(last_value).tasks))
321
+ self.compare(conditional_last, [[], ["a", "", "b", ""]])
322
+
323
+ def test_nested_loops(self):
324
+ self.compare(nested_loop, [[], [[], []], [["a", "b"], [], ["c"]]])
325
+ self.compare(nested_memory, [[], [["a", "b"], [], ["c"], ["d", "e", "f"]]])
326
+
327
+ def test_mutation_and_iterators(self):
328
+ self.compare(memory_snapshots, [None])
329
+ self.compare(iteration_alias, [[], ["a", "b", "c"]])
330
+ self.compare(local_container, [[], ["a", "b"]])
331
+ self.compare(iterator_twice, [[], ["a", "b"]])
332
+ self.compare(iterator_unpack, [["a", "b"]])
333
+ self.compare(shared_list, [None])
334
+ plan = self.compile(local_container)
335
+ self.assertFalse(any(d.kind == "memory" and d.mapping.distance for d in plan.dependencies))
336
+ plan = self.compile(iteration_alias)
337
+ self.assertTrue(any(d.kind == "memory" and d.mapping.distance for d in plan.dependencies))
338
+
339
+ def test_nested_container_ir(self):
340
+ # Subscript-derived writable aliases are rejected by the current source
341
+ # compiler; Plan lowering also accepts independently constructed IR.
342
+ program = compile_program(shared_list)
343
+ source = program.body[0].source
344
+ L, V = ir.Literal, ir.Var
345
+ body = (
346
+ ir.Assign("data", ir.DictExpr(((L("inner"), ir.ListExpr((L("before"),))),)), id="a", source=source),
347
+ ir.Assign("alias", ir.GetItem(V("data"), L("inner")), id="b", source=source),
348
+ ir.Assign("first", ir.GetItem(V("alias"), L(0)), id="c", source=source),
349
+ ir.SetItem(V("alias"), L(0), L("after"), id="d", source=source),
350
+ ir.Return(ir.ListExpr((V("first"), V("data"))), id="e", source=source),
351
+ )
352
+ plan = lower_to_plan(replace(program, body=body))
353
+ for order in ("forward", "reverse", "random"):
354
+ self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript(), order=order), nested_store(PureAnnoScript()))
355
+
356
+ def test_early_returns_and_identity(self):
357
+ self.compare(early_return, [True, False], expected_calls=lambda _: 1)
358
+ self.compare(nested_return, [[], [False], [True]], expected_calls=lambda _: 1)
359
+ self.compare(partial_nested_return, [[], [False], [True]],
360
+ expected_calls=lambda s: 2 if s and not s[0] else 1)
361
+ # The source compiler restricts identity comparisons, but the serialized
362
+ # expression dialect can represent them and lowering must stay safe.
363
+ program = compile_program(duplicates, optimize=False)
364
+ sites = [s for s in program.body if isinstance(s, ir.Infer)]
365
+ ret = program.body[-1]
366
+ program = replace(program, body=program.body[:-1] +
367
+ (replace(ret, value=ir.Compare(ir.Var(sites[0].target), ("is",), (ir.Var(sites[1].target),))),))
368
+ plan = lower_to_plan(program)
369
+ self.assertTrue(all(not q.reusable for q in plan.requests))
370
+ anno = PureAnnoScript()
371
+ self.assertIs(evaluate(plan.to_dict(), anno), False)
372
+ self.assertEqual(len(anno.calls), 2)
373
+
374
+ def test_constraints_preserve_expression_and_bindings(self):
375
+ raw = compile_program(checked)
376
+ plan = lower_to_plan(raw)
377
+ check = next(t for t in plan.tasks if isinstance(t, p.Check))
378
+ original = next(s for s in raw.body if isinstance(s, ir.Constrain))
379
+ self.assertEqual(check.condition.expression, original.condition)
380
+ self.assertEqual(check.message.expression, original.message)
381
+ for mode in ("eager", "strict"):
382
+ for function, state, message in ((checked, "wrong", "验证失败"), (checked_branch, True, "branch failed"),
383
+ (checked_loop, ["bad", "later"], "Constraint failed")):
384
+ anno = PureAnnoScript(state)
385
+ with self.assertRaisesRegex(ValueError, message):
386
+ evaluate(self.compile(function).to_dict(), anno, mode=mode, order="reverse")
387
+ if mode == "strict":
388
+ self.assertFalse(any(call[0] in ("after", "later") for call in anno.calls))
389
+
390
+ def test_strict_failure_prefix_and_unused_calls(self):
391
+ for function, state, expected, failure in ((independent, None, ["a"], ("a",)),
392
+ (expression_failure, None, ["first"], ()),
393
+ (unpack_failure, [1], [], ()),
394
+ (unused_failure, None, ["bad"], ("bad",)),
395
+ (independent_loop, ["bad", "later"], ["bad"], ("bad",))):
396
+ anno = PureAnnoScript(state, fail=failure)
397
+ with self.assertRaises(ValueError):
398
+ evaluate(self.compile(function).to_dict(), anno, mode="strict", order="reverse")
399
+ self.assertEqual([x[0] for x in anno.calls], expected)
400
+
401
+ def test_eager_concurrency_and_first_observed_error(self):
402
+ class Concurrent(PureAnnoScript):
403
+ def __init__(self, state=None):
404
+ super().__init__(state)
405
+ self.active = 0
406
+ self.peak = 0
407
+
408
+ async def generate(self, instructions, **kwargs):
409
+ self.calls.append((instructions, kwargs))
410
+ self.active += 1
411
+ self.peak = max(self.active, self.peak)
412
+ try:
413
+ await asyncio.sleep(0.005 if instructions == "b" else 0.02)
414
+ if instructions in self.fail:
415
+ raise ValueError(instructions)
416
+ return GenerateResponse(instructions)
417
+ finally:
418
+ self.active -= 1
419
+ plan = self.compile(independent)
420
+ anno = Concurrent()
421
+ anno.fail = ("a", "b")
422
+ with self.assertRaisesRegex(ValueError, "b"):
423
+ evaluate(plan.to_dict(), anno)
424
+ self.assertEqual(anno.peak, 2)
425
+ self.assertEqual(anno.active, 0)
426
+ anno = Concurrent(["a", "b", "c"])
427
+ self.assertEqual(evaluate(self.compile(independent_loop).to_dict(), anno), ["a", "b", "c"])
428
+ self.assertEqual(anno.peak, 3)
429
+ anno = Concurrent(["a", "b"])
430
+ evaluate(self.compile(independent_loop).to_dict(), anno, mode="strict")
431
+ self.assertEqual(anno.peak, 1)
432
+ anno = Concurrent([True, True, True])
433
+ self.assertEqual(evaluate(self.compile(loop_invariant).to_dict(), anno), ["same"] * 3)
434
+ self.assertEqual(len(anno.calls), 1) # in-flight requests also coalesce
435
+
436
+ def test_reject_invalid_serialized_plans(self):
437
+ data = self.compile(independent_loop).to_dict()
438
+ for mutate in (
439
+ lambda d: d.update(version=2),
440
+ lambda d: d.update(extra=True),
441
+ lambda d: d["values"][0].update(producer="absent"),
442
+ lambda d: d["regions"][0].update(parent=d["regions"][0]["id"]),
443
+ lambda d: d["dependencies"][0].update(source="absent"),
444
+ lambda d: d["dependencies"][0]["mapping"].update(distance=-1),
445
+ lambda d: d["dependencies"].clear(),
446
+ lambda d: d["tasks"][0].update(kind=[]),
447
+ lambda d: d["tasks"][0].update(outputs=[]),
448
+ ):
449
+ broken = deepcopy(data)
450
+ mutate(broken)
451
+ with self.assertRaises(p.PlanDecodeError):
452
+ p.ExecutionPlan.from_dict(broken)
453
+ with self.assertRaises(p.PlanDecodeError):
454
+ p.ExecutionPlan.from_json('{"version":1,"version":1}')
455
+ with self.assertRaises(p.PlanDecodeError):
456
+ p.ExecutionPlan.from_json('{"value": NaN}')
457
+
458
+ def test_bad_merge_and_cycle(self):
459
+ data = self.compile(branch_duplicates).to_dict()
460
+ merge = next(t for t in data["tasks"] if t["kind"] == "merge")
461
+ merge["then_value"], merge["else_value"] = merge["else_value"], merge["then_value"]
462
+ with self.assertRaises(p.PlanDecodeError):
463
+ p.ExecutionPlan.from_dict(data)
464
+ data = self.compile(independent).to_dict()
465
+ edge = deepcopy(next(d for d in data["dependencies"] if d["kind"] == "data"))
466
+ edge.update(source=edge["target"], value=None, kind="control")
467
+ data["dependencies"].append(edge)
468
+ with self.assertRaisesRegex(p.PlanDecodeError, "cycle"):
469
+ p.ExecutionPlan.from_dict(data)
470
+
471
+ def test_type_preserving_request_keys(self):
472
+ original = compile_program(independent)
473
+ source = original.body[0].source
474
+ body = tuple(ir.Infer(f"v{i}", "generate", (ir.Literal(x),), (("max_length",ir.Literal(512)),), id=f"s{i}", source=source)
475
+ for i, x in enumerate((True, 1, 1.0, 0.0, -0.0)))
476
+ program = replace(original, body=body + (ir.Return(ir.Literal(None), id="ret", source=source),))
477
+ self.assertEqual(len(lower_to_plan(program).requests), 5)
478
+
479
+ def test_no_python_or_model_execution_during_lowering(self):
480
+ program = compile_program(compiler_tests.configured)
481
+ def forbidden(*args, **kwargs):
482
+ raise AssertionError("model executed")
483
+ forbidden.__signature__ = inspect.signature(Anno.generate)
484
+ with patch.object(compiler_tests, "configured", side_effect=AssertionError("program executed")),\
485
+ patch.object(compiler_tests, "_never_check", side_effect=AssertionError("precondition executed")),\
486
+ patch.object(Anno, "generate", forbidden):
487
+ lower_to_plan(program)
488
+ restored = ir.Program.from_json(program.to_json())
489
+ self.assertEqual(lower_to_plan(program).to_dict(), lower_to_plan(restored).to_dict())
490
+
491
+ def test_all_six_operations(self):
492
+ function = compiler_tests.all_operations
493
+ expected = function(compiler_tests.Probe({"text": "source"}, text="fixed"))
494
+ plan = self.compile(function)
495
+ self.assertEqual(len({q.operation for q in plan.requests}), 6)
496
+ for mode in ("eager", "strict"):
497
+ self.assertEqual(evaluate(plan.to_dict(), compiler_tests.Probe({"text": "source"}, text="fixed"), mode=mode, order="reverse"), expected)
498
+
499
+ def test_lowering_diagnostics(self):
500
+ program = compile_program(independent)
501
+ s = program.body[0]
502
+ broken = replace(program, body=(ir.Return(ir.Var("missing"), id=s.id, source=s.source),))
503
+ with self.assertRaisesRegex(PlanLoweringError, "not definitely assigned.*test_plan"):
504
+ lower_to_plan(broken)
505
+ broken = replace(program, body=(ir.SetItem(ir.State(), ir.Literal("x"), ir.Literal(1), id=s.id, source=s.source),
506
+ ir.Return(ir.Literal(None), id="ret", source=s.source)))
507
+ with self.assertRaisesRegex(PlanLoweringError, "local container"):
508
+ lower_to_plan(broken)
509
+ with self.assertRaises(TypeError):
510
+ lower_to_plan("not a Program")
511
+
512
+ def test_existing_compiler_semantics(self):
513
+ for function, state in ((compiler_tests.expression_order, None), (compiler_tests.loop_helpers, [[], ["special"]]),
514
+ (compiler_tests.formatting, ["中文", "x"]), (compiler_tests.implicit_return, False)):
515
+ program = compile_program(function)
516
+ expected = evaluate_ir(program.to_dict(), PureAnnoScript(state))
517
+ for order in ("reverse", "random"):
518
+ self.assertEqual(evaluate(lower_to_plan(program).to_dict(), PureAnnoScript(state), order=order), expected)
519
+
520
+
521
+ if __name__ == "__main__":
522
+ unittest.main()