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.
- annoscript/__init__.py +1 -0
- annoscript/_backend_profiles.py +17 -0
- annoscript/_client_common.py +287 -0
- annoscript/_data.py +37 -0
- annoscript/_dispatch_pool.py +47 -0
- annoscript/_env.py +19 -0
- annoscript/_examples.py +34 -0
- annoscript/_expressions.py +93 -0
- annoscript/_plan_viewer.html +953 -0
- annoscript/_remote_jobs.py +211 -0
- annoscript/_request_adapters.py +71 -0
- annoscript/_speculation.py +321 -0
- annoscript/_test_artifacts.py +33 -0
- annoscript/_test_evaluation_http.py +117 -0
- annoscript/_test_ir_runtime.py +138 -0
- annoscript/_test_job_process.py +166 -0
- annoscript/_test_plan_runtime.py +331 -0
- annoscript/_test_remote_client.py +46 -0
- annoscript/_test_remote_server.py +75 -0
- annoscript/_test_synthetic.py +67 -0
- annoscript/anno.py +270 -0
- annoscript/backends/__init__.py +13 -0
- annoscript/backends/_batch_wire.py +26 -0
- annoscript/backends/_bounded_grammar.py +97 -0
- annoscript/backends/_example_layout.py +23 -0
- annoscript/backends/_generation_policy.py +116 -0
- annoscript/backends/_grammar.py +83 -0
- annoscript/backends/_preview.py +13 -0
- annoscript/backends/_prompt.py +115 -0
- annoscript/backends/_shared_rules.py +38 -0
- annoscript/backends/adapters/__init__.py +17 -0
- annoscript/backends/adapters/chat.py +39 -0
- annoscript/backends/adapters/decision.py +119 -0
- annoscript/backends/base.py +427 -0
- annoscript/backends/config.py +67 -0
- annoscript/backends/decision.py +150 -0
- annoscript/backends/llm.py +338 -0
- annoscript/client.py +247 -0
- annoscript/client_tests.py +17 -0
- annoscript/compiler.py +708 -0
- annoscript/cross_version_tests.py +68 -0
- annoscript/fixtures/request-adapters-v1.json +4741 -0
- annoscript/formatter.py +171 -0
- annoscript/grammar_tests.py +176 -0
- annoscript/inspect.py +77 -0
- annoscript/ir.py +458 -0
- annoscript/job_cross_version_tests.py +118 -0
- annoscript/job_protocol.py +180 -0
- annoscript/jobs.py +255 -0
- annoscript/metrics.py +153 -0
- annoscript/p0_grammar_tests.py +147 -0
- annoscript/passes/__init__.py +40 -0
- annoscript/passes/_analysis.py +178 -0
- annoscript/passes/control_flow.py +68 -0
- annoscript/passes/dead_assignments.py +44 -0
- annoscript/passes/folding.py +104 -0
- annoscript/passes/propagation.py +53 -0
- annoscript/passes/temporaries.py +45 -0
- annoscript/plan.py +542 -0
- annoscript/plan_formatter.py +452 -0
- annoscript/plan_lowering.py +571 -0
- annoscript/preview.py +115 -0
- annoscript/remote.py +304 -0
- annoscript/runtime.py +861 -0
- annoscript/server/__init__.py +1 -0
- annoscript/server/__main__.py +49 -0
- annoscript/server/_plan_cache.py +79 -0
- annoscript/server/api.py +146 -0
- annoscript/server/metrics.py +140 -0
- annoscript/server/migrate.py +61 -0
- annoscript/server/releases.py +143 -0
- annoscript/server/store.py +429 -0
- annoscript/server/worker.py +290 -0
- annoscript/substring_tests.py +79 -0
- annoscript/test_advisory_lengths.py +115 -0
- annoscript/test_backends.py +181 -0
- annoscript/test_client.py +349 -0
- annoscript/test_compact_batch.py +95 -0
- annoscript/test_compiler.py +444 -0
- annoscript/test_decision_adapters.py +410 -0
- annoscript/test_decision_wire.py +190 -0
- annoscript/test_diagnostics.py +97 -0
- annoscript/test_engineering_p0.py +191 -0
- annoscript/test_environment.py +65 -0
- annoscript/test_example_backends.py +251 -0
- annoscript/test_examples.py +149 -0
- annoscript/test_field_names.py +221 -0
- annoscript/test_formatter.py +153 -0
- annoscript/test_fusion_scheduling.py +109 -0
- annoscript/test_generation_policy.py +180 -0
- annoscript/test_ir.py +148 -0
- annoscript/test_jobs.py +371 -0
- annoscript/test_jobs_server.py +511 -0
- annoscript/test_llm_batch.py +428 -0
- annoscript/test_metrics.py +59 -0
- annoscript/test_metrics_server.py +261 -0
- annoscript/test_minimal_fusion.py +59 -0
- annoscript/test_nullable_generate.py +169 -0
- annoscript/test_passes.py +408 -0
- annoscript/test_plan.py +522 -0
- annoscript/test_plan_constraints.py +305 -0
- annoscript/test_plan_formatter.py +272 -0
- annoscript/test_prompt_layout.py +140 -0
- annoscript/test_qwen_backend.py +181 -0
- annoscript/test_qwen_pilot_regression.py +180 -0
- annoscript/test_release_client.py +20 -0
- annoscript/test_remote.py +308 -0
- annoscript/test_request_adapters.py +295 -0
- annoscript/test_requirements.py +228 -0
- annoscript/test_rule_dedup.py +67 -0
- annoscript/test_runtime.py +314 -0
- annoscript/test_runtime_client.py +133 -0
- annoscript/test_source_fields.py +111 -0
- annoscript/test_speculation.py +458 -0
- annoscript/test_substring_backend.py +81 -0
- annoscript/test_synthetic_example.py +61 -0
- annoscript-0.1.0.dist-info/METADATA +145 -0
- annoscript-0.1.0.dist-info/RECORD +128 -0
- annoscript-0.1.0.dist-info/WHEEL +4 -0
- annoscript-0.1.0.dist-info/licenses/LICENSE +21 -0
- annoscript_examples/__init__.py +1 -0
- annoscript_examples/fixtures.py +3 -0
- annoscript_examples/request.execution_plan.json +3017 -0
- annoscript_examples/request.json +858 -0
- annoscript_examples/request.py +37 -0
- annoscript_examples/request_preview.decision.json +23 -0
- annoscript_examples/request_preview.generation.json +20 -0
- annoscript_examples/run.py +24 -0
annoscript/test_plan.py
ADDED
|
@@ -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()
|