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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Eager check speculation without weakening bindings, paths or memory order."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from annoscript import plan as p
|
|
8
|
+
from annoscript.compiler import compile_program
|
|
9
|
+
from annoscript.anno import GenerateResponse, constrain
|
|
10
|
+
from annoscript.plan_lowering import lower_to_plan
|
|
11
|
+
from annoscript._test_ir_runtime import evaluate as evaluate_ir
|
|
12
|
+
from annoscript._test_plan_runtime import _Executor, evaluate
|
|
13
|
+
from annoscript.test_plan import PureAnnoScript, checked_loop
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def checked_chain(anno):
|
|
17
|
+
text = anno.generate("check", max_length=512).text
|
|
18
|
+
constrain(text == "ok", message="check failed")
|
|
19
|
+
other = anno.generate("independent", max_length=512).text
|
|
20
|
+
return anno.generate(other, max_length=512).text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def checked_constant_return(anno):
|
|
24
|
+
text = anno.generate("check", max_length=512).text
|
|
25
|
+
constrain(text == "ok", message="check failed")
|
|
26
|
+
return "done"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def checked_nested_return(anno):
|
|
30
|
+
if anno.state:
|
|
31
|
+
if anno.state[0]:
|
|
32
|
+
text = anno.generate("check", max_length=512).text
|
|
33
|
+
constrain(text == "ok", message="check failed")
|
|
34
|
+
return "done"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def checked_unused_loop(anno):
|
|
38
|
+
for item in anno.state:
|
|
39
|
+
text = anno.generate(item, max_length=512).text
|
|
40
|
+
constrain(text == "ok", message="check failed")
|
|
41
|
+
return "done"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def checked_data(anno):
|
|
45
|
+
text = anno.generate("first", max_length=512).text
|
|
46
|
+
constrain(text != "")
|
|
47
|
+
return anno.generate(text + " second", max_length=512).text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def checked_control(anno):
|
|
51
|
+
if anno.generate("predicate", max_length=512).text == "selected":
|
|
52
|
+
text = anno.generate("selected", max_length=512).text
|
|
53
|
+
constrain(text == "ok", message="selected failed")
|
|
54
|
+
return anno.generate("independent", max_length=512).text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def checked_memory(anno):
|
|
58
|
+
data = {"value": "initial"}
|
|
59
|
+
alias = data
|
|
60
|
+
alias["value"] = anno.generate("updated", max_length=512).text
|
|
61
|
+
constrain(data["value"] == "updated", message=data["value"])
|
|
62
|
+
alias["value"] = "later"
|
|
63
|
+
return data["value"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def checked_container_truth(anno):
|
|
67
|
+
data = {}
|
|
68
|
+
constrain(data, message="empty container")
|
|
69
|
+
data["key"] = "later"
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def checked_message(anno):
|
|
74
|
+
data = {"message": "original diagnostic"}
|
|
75
|
+
constrain(anno.state, message=data["message"])
|
|
76
|
+
data["message"] = "changed diagnostic"
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def checked_rebinding(anno):
|
|
81
|
+
value = anno.generate("original", max_length=512).text
|
|
82
|
+
constrain(value == "original")
|
|
83
|
+
value = "rebound"
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def checked_shared(anno):
|
|
88
|
+
a = anno.generate("same", max_length=512).text
|
|
89
|
+
constrain(a == anno.state, message="shared check failed")
|
|
90
|
+
b = anno.generate("same", max_length=512).text
|
|
91
|
+
return [a, b]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def checked_memory_loop(anno):
|
|
95
|
+
data = {"value": "initial"}
|
|
96
|
+
for item in anno.state:
|
|
97
|
+
data["value"] = item
|
|
98
|
+
text = anno.generate(item, max_length=512).text
|
|
99
|
+
constrain(data["value"] == text, message="wrong iteration")
|
|
100
|
+
return data["value"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ConstraintPlanTests(unittest.TestCase):
|
|
104
|
+
def plan(self, function):
|
|
105
|
+
return lower_to_plan(compile_program(function))
|
|
106
|
+
|
|
107
|
+
def test_only_explicit_ordering_edges_are_relaxed(self):
|
|
108
|
+
plan = self.plan(checked_memory)
|
|
109
|
+
eager, strict = plan.dependencies_for(), plan.dependencies_for("strict")
|
|
110
|
+
self.assertEqual(strict, plan.dependencies)
|
|
111
|
+
self.assertTrue(any(d.kind == "constraint" and d.strict_only for d in strict))
|
|
112
|
+
for d in strict:
|
|
113
|
+
if d.kind in ("data", "control", "memory"):
|
|
114
|
+
self.assertIn(d, eager)
|
|
115
|
+
self.assertFalse(d.strict_only)
|
|
116
|
+
self.assertFalse(any(d.kind in ("constraint", "failure_order") for d in eager))
|
|
117
|
+
self.assertEqual(p.ExecutionPlan.from_json(plan.to_json()), plan)
|
|
118
|
+
# Old plans remain conservative. The constraint *kind* is not itself
|
|
119
|
+
# permission to remove an edge, and missing flags default to False.
|
|
120
|
+
legacy = plan.to_dict()
|
|
121
|
+
for d in legacy["dependencies"]:
|
|
122
|
+
d.pop("strict_only")
|
|
123
|
+
restored = p.ExecutionPlan.from_dict(legacy)
|
|
124
|
+
self.assertTrue(any(d.kind == "constraint" for d in restored.dependencies_for()))
|
|
125
|
+
for kind in ("data", "control", "memory"):
|
|
126
|
+
original = self.plan(checked_control if kind == "control" else checked_memory)
|
|
127
|
+
edge = next(d for d in original.dependencies if d.kind == kind)
|
|
128
|
+
broken = replace(original, dependencies=tuple(replace(d, strict_only=True) if d == edge else d for d in original.dependencies))
|
|
129
|
+
with self.assertRaisesRegex(p.PlanDecodeError, "cannot be strict-only"):
|
|
130
|
+
p.ExecutionPlan.from_json(broken.to_json())
|
|
131
|
+
|
|
132
|
+
def test_eager_constraint_failure_cancels_inflight_and_blocks_new_calls(self):
|
|
133
|
+
class Concurrent(PureAnnoScript):
|
|
134
|
+
async def generate(self, instructions, **kwargs):
|
|
135
|
+
self.calls.append((instructions, kwargs))
|
|
136
|
+
if instructions == "check":
|
|
137
|
+
await self.independent_started.wait()
|
|
138
|
+
return GenerateResponse("bad")
|
|
139
|
+
if instructions == "independent":
|
|
140
|
+
self.independent_started.set()
|
|
141
|
+
try:
|
|
142
|
+
await asyncio.Event().wait()
|
|
143
|
+
except asyncio.CancelledError:
|
|
144
|
+
self.cancelled = True
|
|
145
|
+
raise
|
|
146
|
+
raise AssertionError("dispatched after failure")
|
|
147
|
+
|
|
148
|
+
async def exercise():
|
|
149
|
+
anno = Concurrent()
|
|
150
|
+
anno.independent_started, anno.cancelled = asyncio.Event(), False
|
|
151
|
+
executor = _Executor(self.plan(checked_chain).to_dict(), anno, "eager", "reverse")
|
|
152
|
+
with self.assertRaisesRegex(ValueError, "check failed"):
|
|
153
|
+
await executor.run()
|
|
154
|
+
self.assertCountEqual([x[0] for x in anno.calls], ["check", "independent"])
|
|
155
|
+
self.assertTrue(anno.cancelled)
|
|
156
|
+
|
|
157
|
+
asyncio.run(asyncio.wait_for(exercise(), 2))
|
|
158
|
+
strict = PureAnnoScript()
|
|
159
|
+
with self.assertRaisesRegex(ValueError, "check failed"):
|
|
160
|
+
evaluate(self.plan(checked_chain).to_dict(), strict, mode="strict", order="reverse")
|
|
161
|
+
self.assertEqual([x[0] for x in strict.calls], ["check"])
|
|
162
|
+
|
|
163
|
+
def test_first_observed_infer_failure_can_precede_pending_check(self):
|
|
164
|
+
class Concurrent(PureAnnoScript):
|
|
165
|
+
async def generate(self, instructions, **kwargs):
|
|
166
|
+
self.calls.append((instructions, kwargs))
|
|
167
|
+
if instructions == "check":
|
|
168
|
+
self.check_started.set()
|
|
169
|
+
try:
|
|
170
|
+
await asyncio.Event().wait()
|
|
171
|
+
except asyncio.CancelledError:
|
|
172
|
+
self.cancelled = True
|
|
173
|
+
raise
|
|
174
|
+
await self.check_started.wait()
|
|
175
|
+
raise ValueError("independent failed first")
|
|
176
|
+
|
|
177
|
+
async def exercise():
|
|
178
|
+
anno = Concurrent()
|
|
179
|
+
anno.check_started, anno.cancelled = asyncio.Event(), False
|
|
180
|
+
executor = _Executor(self.plan(checked_chain).to_dict(), anno, "eager", "random")
|
|
181
|
+
with self.assertRaisesRegex(ValueError, "independent failed first"):
|
|
182
|
+
await executor.run()
|
|
183
|
+
self.assertTrue(anno.cancelled)
|
|
184
|
+
self.assertCountEqual([x[0] for x in anno.calls], ["check", "independent"])
|
|
185
|
+
|
|
186
|
+
asyncio.run(asyncio.wait_for(exercise(), 2))
|
|
187
|
+
|
|
188
|
+
def test_success_waits_for_root_nested_and_loop_checks(self):
|
|
189
|
+
class Delayed(PureAnnoScript):
|
|
190
|
+
async def generate(self, instructions, **kwargs):
|
|
191
|
+
self.started.set()
|
|
192
|
+
await self.release.wait()
|
|
193
|
+
return GenerateResponse(self.response)
|
|
194
|
+
|
|
195
|
+
async def exercise(function, state, valid):
|
|
196
|
+
anno = Delayed(state)
|
|
197
|
+
anno.started, anno.release = asyncio.Event(), asyncio.Event()
|
|
198
|
+
anno.response = "ok" if valid else "bad"
|
|
199
|
+
executor = _Executor(self.plan(function).to_dict(), anno, "eager", "reverse")
|
|
200
|
+
task = asyncio.create_task(executor.run())
|
|
201
|
+
try:
|
|
202
|
+
await anno.started.wait()
|
|
203
|
+
self.assertEqual(executor.returns, ["done"]) # candidate only
|
|
204
|
+
self.assertFalse(task.done())
|
|
205
|
+
anno.release.set()
|
|
206
|
+
if valid:
|
|
207
|
+
self.assertEqual(await task, "done")
|
|
208
|
+
else:
|
|
209
|
+
with self.assertRaisesRegex(ValueError, "check failed"):
|
|
210
|
+
await task
|
|
211
|
+
finally:
|
|
212
|
+
task.cancel()
|
|
213
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
214
|
+
|
|
215
|
+
for function, state in ((checked_constant_return, None), (checked_nested_return, [True]),
|
|
216
|
+
(checked_unused_loop, ["check", "check"])):
|
|
217
|
+
for valid in (False, True):
|
|
218
|
+
with self.subTest(function=function.__name__, valid=valid):
|
|
219
|
+
asyncio.run(asyncio.wait_for(exercise(function, state, valid), 2))
|
|
220
|
+
|
|
221
|
+
def test_data_and_branch_activation_still_gate_infer_and_check(self):
|
|
222
|
+
for mode in ("eager", "strict"):
|
|
223
|
+
for order in ("forward", "reverse", "random"):
|
|
224
|
+
anno = PureAnnoScript()
|
|
225
|
+
self.assertEqual(evaluate(self.plan(checked_data).to_dict(), anno, mode=mode, order=order), "first second")
|
|
226
|
+
self.assertEqual([x[0] for x in anno.calls], ["first", "first second"])
|
|
227
|
+
anno = PureAnnoScript()
|
|
228
|
+
self.assertEqual(evaluate(self.plan(checked_control).to_dict(), anno, mode=mode, order=order), "independent")
|
|
229
|
+
self.assertCountEqual([x[0] for x in anno.calls], ["predicate", "independent"])
|
|
230
|
+
self.assertEqual(evaluate(self.plan(checked_nested_return).to_dict(), PureAnnoScript([]), mode=mode, order=order), "done")
|
|
231
|
+
self.assertEqual(evaluate(self.plan(checked_unused_loop).to_dict(), PureAnnoScript([]), mode=mode, order=order), "done")
|
|
232
|
+
|
|
233
|
+
def test_memory_versions_diagnostics_and_rebinding_match_ir(self):
|
|
234
|
+
for function, state in ((checked_memory, None), (checked_container_truth, None), (checked_message, False),
|
|
235
|
+
(checked_rebinding, None), (checked_memory_loop, ["one", "two", "three"])):
|
|
236
|
+
program = compile_program(function)
|
|
237
|
+
failure, expected = None, None
|
|
238
|
+
try:
|
|
239
|
+
expected = evaluate_ir(program.to_dict(), PureAnnoScript(state))
|
|
240
|
+
except ValueError as exc:
|
|
241
|
+
failure = str(exc)
|
|
242
|
+
plan = lower_to_plan(program)
|
|
243
|
+
checks = {t.id for t in plan.tasks if isinstance(t, p.Check)}
|
|
244
|
+
if function is not checked_rebinding:
|
|
245
|
+
self.assertTrue(any(d.source in checks and d.kind == "memory" and d.hazard == "WAR"
|
|
246
|
+
for d in plan.dependencies_for()))
|
|
247
|
+
for mode in ("eager", "strict"):
|
|
248
|
+
for order in ("forward", "reverse", "random"):
|
|
249
|
+
with self.subTest(function=function.__name__, mode=mode, order=order):
|
|
250
|
+
if failure:
|
|
251
|
+
with self.assertRaises(ValueError) as caught:
|
|
252
|
+
evaluate(plan.to_dict(), PureAnnoScript(state), mode=mode, order=order)
|
|
253
|
+
self.assertEqual(str(caught.exception), failure)
|
|
254
|
+
else:
|
|
255
|
+
self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript(state), mode=mode, order=order), expected)
|
|
256
|
+
|
|
257
|
+
def test_unoptimized_ir_retains_check_bindings_and_memory_reads(self):
|
|
258
|
+
for function, state in ((checked_memory, None), (checked_rebinding, None),
|
|
259
|
+
(checked_memory_loop, ["one", "two"])):
|
|
260
|
+
program = compile_program(function, optimize=False)
|
|
261
|
+
expected = evaluate_ir(program.to_dict(), PureAnnoScript(state))
|
|
262
|
+
plan = lower_to_plan(program)
|
|
263
|
+
for mode in ("eager", "strict"):
|
|
264
|
+
self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript(state), mode=mode, order="reverse"), expected)
|
|
265
|
+
|
|
266
|
+
def test_shared_results_do_not_skip_checks(self):
|
|
267
|
+
plan = self.plan(checked_shared)
|
|
268
|
+
self.assertEqual(len(plan.requests), 1)
|
|
269
|
+
for mode in ("eager", "strict"):
|
|
270
|
+
for state in ("same", "wrong"):
|
|
271
|
+
anno = PureAnnoScript(state)
|
|
272
|
+
if state == "same":
|
|
273
|
+
self.assertEqual(evaluate(plan.to_dict(), anno, mode=mode, order="reverse"), ["same", "same"])
|
|
274
|
+
else:
|
|
275
|
+
with self.assertRaisesRegex(ValueError, "shared check failed"):
|
|
276
|
+
evaluate(plan.to_dict(), anno, mode=mode, order="reverse")
|
|
277
|
+
self.assertEqual(len(anno.calls), 1)
|
|
278
|
+
|
|
279
|
+
def test_checked_loop_allows_eager_infer_overlap_only(self):
|
|
280
|
+
class Concurrent(PureAnnoScript):
|
|
281
|
+
def __init__(self, state):
|
|
282
|
+
super().__init__(state)
|
|
283
|
+
self.active = self.peak = 0
|
|
284
|
+
|
|
285
|
+
async def generate(self, instructions, **kwargs):
|
|
286
|
+
self.calls.append((instructions, kwargs))
|
|
287
|
+
self.active += 1
|
|
288
|
+
self.peak = max(self.peak, self.active)
|
|
289
|
+
try:
|
|
290
|
+
await asyncio.sleep(0.01)
|
|
291
|
+
return GenerateResponse(instructions)
|
|
292
|
+
finally:
|
|
293
|
+
self.active -= 1
|
|
294
|
+
|
|
295
|
+
plan = self.plan(checked_loop)
|
|
296
|
+
for mode, peak in (("eager", 3), ("strict", 1)):
|
|
297
|
+
anno = Concurrent(["one", "two", "three"])
|
|
298
|
+
self.assertEqual(evaluate(plan.to_dict(), anno, mode=mode), anno.state)
|
|
299
|
+
self.assertEqual(anno.peak, peak)
|
|
300
|
+
self.assertEqual(anno.active, 0)
|
|
301
|
+
self.assertEqual(evaluate(plan.to_dict(), PureAnnoScript([])), [])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
unittest.main()
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Dependency projection, safe offline formats, CLI and saved-plan checks."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from annoscript import ir
|
|
12
|
+
from annoscript.compiler import compile_program
|
|
13
|
+
from annoscript.inspect import main
|
|
14
|
+
from annoscript.anno import constrain
|
|
15
|
+
from annoscript.plan import ExecutionPlan
|
|
16
|
+
from annoscript.plan_lowering import lower_to_plan
|
|
17
|
+
from annoscript.plan_formatter import analyze_plan, format_plan, format_plan_html, format_plan_mermaid
|
|
18
|
+
from annoscript import test_plan as examples
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def data_chain(anno):
|
|
25
|
+
a = anno.generate("first", max_length=512).text
|
|
26
|
+
b = anno.generate(a + " next", max_length=512).text
|
|
27
|
+
return b
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def checked_arm(anno):
|
|
31
|
+
if anno.state:
|
|
32
|
+
a = anno.generate("first", max_length=512).text
|
|
33
|
+
constrain(a != "")
|
|
34
|
+
return anno.generate("after", max_length=512).text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def exclusive(anno):
|
|
38
|
+
if anno.state:
|
|
39
|
+
return anno.generate("yes", max_length=512).text
|
|
40
|
+
else:
|
|
41
|
+
return anno.generate("no", max_length=512).text
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def true_recurrence(anno):
|
|
45
|
+
text = "seed"
|
|
46
|
+
for item in anno.state:
|
|
47
|
+
text = anno.generate(text, max_length=512).text
|
|
48
|
+
return text
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def internal_loop(anno):
|
|
52
|
+
text = anno.generate("first", max_length=512).text
|
|
53
|
+
result = []
|
|
54
|
+
for item in anno.state:
|
|
55
|
+
result = result + [text + item]
|
|
56
|
+
return anno.generate(json.dumps(result), max_length=512).text
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def loop_export(anno):
|
|
60
|
+
result = "initial"
|
|
61
|
+
for item in anno.state:
|
|
62
|
+
result = anno.generate(item, max_length=512).text
|
|
63
|
+
return anno.generate(result, max_length=512).text
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def dynamic_prompt(anno):
|
|
67
|
+
return anno.generate(f"判断品牌 {anno.state}", max_length=512).text
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def memory_path(anno):
|
|
71
|
+
data = {"text": "initial"}
|
|
72
|
+
alias = data
|
|
73
|
+
a = anno.generate(data["text"], max_length=512).text
|
|
74
|
+
alias["text"] = a
|
|
75
|
+
return anno.generate(data["text"], max_length=512).text
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def empty(anno):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PlanProjectionTests(unittest.TestCase):
|
|
83
|
+
def report(self, function, mode="eager", raw=False):
|
|
84
|
+
return analyze_plan(lower_to_plan(compile_program(function, optimize=not raw)), mode=mode)
|
|
85
|
+
|
|
86
|
+
def test_independent_calls_and_strict_failure_order(self):
|
|
87
|
+
eager = self.report(examples.independent)
|
|
88
|
+
self.assertEqual(eager["edges"], [])
|
|
89
|
+
self.assertEqual(eager["batches"], [{"layer": 0, "operation": "generate", "sites": ["I01", "I02"]}])
|
|
90
|
+
strict = self.report(examples.independent, "strict")
|
|
91
|
+
self.assertEqual(strict["batches"], [])
|
|
92
|
+
self.assertEqual([(e["source"], e["target"]) for e in strict["edges"]], [("I01", "I02")])
|
|
93
|
+
self.assertIn("failure_order", strict["edges"][0]["kinds"])
|
|
94
|
+
|
|
95
|
+
def test_data_path_projects_through_pure_tasks(self):
|
|
96
|
+
for raw in (False, True):
|
|
97
|
+
report = self.report(data_chain, raw=raw)
|
|
98
|
+
edge, = report["edges"]
|
|
99
|
+
self.assertEqual((edge["source"], edge["target"], edge["carried"]), ("I01", "I02", False))
|
|
100
|
+
self.assertIn("data", edge["kinds"])
|
|
101
|
+
self.assertGreater(len(edge["witness"]), 2)
|
|
102
|
+
for left, right in zip(edge["witness"], edge["witness"][1:]):
|
|
103
|
+
self.assertEqual(left["target"], right["source"])
|
|
104
|
+
self.assertEqual(report["parallel"], [])
|
|
105
|
+
|
|
106
|
+
def test_join_completion_and_constraint_barrier(self):
|
|
107
|
+
report = self.report(checked_arm)
|
|
108
|
+
self.assertEqual(report["edges"], [])
|
|
109
|
+
self.assertEqual(len(report["checks"]), 1)
|
|
110
|
+
self.assertEqual(report["parallel"][0]["sites"], ["I01", "I02"])
|
|
111
|
+
report = self.report(checked_arm, "strict")
|
|
112
|
+
edge, = report["edges"]
|
|
113
|
+
self.assertIn("completion", edge["kinds"])
|
|
114
|
+
self.assertIn("constraint", edge["kinds"])
|
|
115
|
+
self.assertTrue(edge["guards"])
|
|
116
|
+
self.assertEqual(len(report["checks"]), 1)
|
|
117
|
+
self.assertEqual(report["parallel"], [])
|
|
118
|
+
|
|
119
|
+
def test_memory_path_preserves_hazards_and_certainty(self):
|
|
120
|
+
report = self.report(memory_path)
|
|
121
|
+
edge, = report["edges"]
|
|
122
|
+
self.assertIn("memory", edge["kinds"])
|
|
123
|
+
self.assertIn("RAW", edge["hazards"])
|
|
124
|
+
self.assertTrue(edge["conservative"])
|
|
125
|
+
self.assertEqual(report["parallel"], [])
|
|
126
|
+
|
|
127
|
+
def test_mutually_exclusive_sites_are_not_a_batch(self):
|
|
128
|
+
report = self.report(exclusive)
|
|
129
|
+
self.assertEqual(report["edges"], [])
|
|
130
|
+
self.assertEqual(report["parallel"], [])
|
|
131
|
+
self.assertEqual(report["batches"], [])
|
|
132
|
+
self.assertTrue(all(n["gates"] for n in report["nodes"]))
|
|
133
|
+
|
|
134
|
+
def test_result_accumulation_is_not_an_infer_recurrence(self):
|
|
135
|
+
report = self.report(examples.independent_loop)
|
|
136
|
+
self.assertEqual(report["edges"], [])
|
|
137
|
+
loop, = report["loops"]
|
|
138
|
+
self.assertTrue(loop["parallel_iterations"])
|
|
139
|
+
self.assertTrue(loop["carried_values"])
|
|
140
|
+
self.assertEqual(loop["batch_sites"], ["I01"])
|
|
141
|
+
strict = self.report(examples.independent_loop, "strict")
|
|
142
|
+
self.assertFalse(strict["loops"][0]["parallel_iterations"])
|
|
143
|
+
self.assertTrue(any(e["carried"] and e["source"] == e["target"] for e in strict["edges"]))
|
|
144
|
+
|
|
145
|
+
def test_true_recurrence_blocks_cross_iteration_batching(self):
|
|
146
|
+
report = self.report(true_recurrence)
|
|
147
|
+
edge, = report["edges"]
|
|
148
|
+
self.assertEqual((edge["source"], edge["target"], edge["carried"]), ("I01", "I01", True))
|
|
149
|
+
self.assertIn({"loop": "L01", "distance": 1}, edge["recurrences"])
|
|
150
|
+
self.assertEqual(report["loops"][0]["batch_sites"], [])
|
|
151
|
+
self.assertFalse(report["loops"][0]["parallel_iterations"])
|
|
152
|
+
|
|
153
|
+
def test_internal_pure_loop_is_gathered_not_an_infer_recurrence(self):
|
|
154
|
+
report = self.report(internal_loop)
|
|
155
|
+
edge, = report["edges"]
|
|
156
|
+
self.assertFalse(edge["carried"])
|
|
157
|
+
self.assertEqual(edge["recurrences"], [])
|
|
158
|
+
self.assertEqual(edge["via_loops"], ["L01"])
|
|
159
|
+
self.assertEqual(report["unresolved_cycles"], [])
|
|
160
|
+
|
|
161
|
+
def test_loop_exit_gathers_and_nested_coordinates_are_retained(self):
|
|
162
|
+
report = self.report(loop_export)
|
|
163
|
+
edge, = report["edges"]
|
|
164
|
+
self.assertFalse(edge["carried"])
|
|
165
|
+
self.assertEqual(edge["gather"], ["L01"])
|
|
166
|
+
nested = self.report(examples.nested_loop, "strict")
|
|
167
|
+
self.assertEqual(nested["nodes"][0]["loops"], ["L01", "L02"])
|
|
168
|
+
loops = {r["loop"] for e in nested["edges"] for r in e["recurrences"]}
|
|
169
|
+
self.assertEqual(loops, {"L01", "L02"})
|
|
170
|
+
|
|
171
|
+
def test_shared_requests_are_not_counted_as_batch_work(self):
|
|
172
|
+
report = self.report(examples.duplicates)
|
|
173
|
+
self.assertEqual(report["nodes"][0]["request"], report["nodes"][1]["request"])
|
|
174
|
+
self.assertEqual(report["batches"], [])
|
|
175
|
+
invariant = self.report(examples.loop_invariant)
|
|
176
|
+
self.assertEqual(invariant["loops"][0]["shared_sites"], ["I01"])
|
|
177
|
+
self.assertEqual(invariant["loops"][0]["batch_sites"], [])
|
|
178
|
+
|
|
179
|
+
def test_loop_check_only_enforces_strict_iteration_order(self):
|
|
180
|
+
report = self.report(examples.checked_loop)
|
|
181
|
+
self.assertFalse(report["loops"][0]["serial"])
|
|
182
|
+
self.assertTrue(report["loops"][0]["parallel_iterations"])
|
|
183
|
+
self.assertEqual(report["edges"], [])
|
|
184
|
+
strict = self.report(examples.checked_loop, "strict")
|
|
185
|
+
self.assertTrue(strict["loops"][0]["serial"])
|
|
186
|
+
self.assertTrue(any("iteration_order" in e["kinds"] for e in strict["edges"]))
|
|
187
|
+
|
|
188
|
+
def test_serialization_determinism_and_source_independence(self):
|
|
189
|
+
plan = lower_to_plan(compile_program(dynamic_prompt))
|
|
190
|
+
before = plan.to_dict()
|
|
191
|
+
restored = ExecutionPlan.from_json(plan.to_json())
|
|
192
|
+
with patch("annoscript.compiler.compile_program", side_effect=AssertionError("no compilation")), patch(
|
|
193
|
+
"annoscript.plan_lowering.lower_to_plan", side_effect=AssertionError("no lowering")
|
|
194
|
+
):
|
|
195
|
+
report = analyze_plan(restored)
|
|
196
|
+
for formatter in (format_plan, format_plan_mermaid, format_plan_html):
|
|
197
|
+
self.assertEqual(formatter(plan), formatter(restored))
|
|
198
|
+
self.assertEqual(plan.to_dict(), before)
|
|
199
|
+
self.assertIn("判断品牌 {", report["nodes"][0]["label"])
|
|
200
|
+
self.assertIn("format_string", report["nodes"][0]["prompt"])
|
|
201
|
+
self.assertIn("annoscript.test_plan_formatter:", format_plan(plan, show_source=True))
|
|
202
|
+
report["nodes"][0]["label"] = "modified"
|
|
203
|
+
self.assertNotEqual(report, analyze_plan(plan))
|
|
204
|
+
|
|
205
|
+
def test_no_infer_and_invalid_mode(self):
|
|
206
|
+
plan = lower_to_plan(compile_program(empty))
|
|
207
|
+
self.assertEqual(analyze_plan(plan)["nodes"], [])
|
|
208
|
+
self.assertIn("0 Infer sites", format_plan(plan))
|
|
209
|
+
self.assertIn('"nodes": []', format_plan_html(plan))
|
|
210
|
+
with self.assertRaises(ValueError):
|
|
211
|
+
analyze_plan(plan, mode="invalid")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PlanFormatCommandTests(unittest.TestCase):
|
|
215
|
+
def setUp(self):
|
|
216
|
+
self.plan = lower_to_plan(compile_program(examples.independent))
|
|
217
|
+
|
|
218
|
+
def command(self, *args, text=None):
|
|
219
|
+
stdout, stderr = io.StringIO(), io.StringIO()
|
|
220
|
+
with patch("sys.stdin", io.StringIO(text or self.plan.to_json())), patch("sys.stdout", stdout), patch("sys.stderr", stderr):
|
|
221
|
+
status = main(list(args))
|
|
222
|
+
return status, stdout.getvalue(), stderr.getvalue()
|
|
223
|
+
|
|
224
|
+
def test_stdin_formats_and_mode(self):
|
|
225
|
+
for fmt, formatter in (("text", format_plan), ("mermaid", format_plan_mermaid), ("html", format_plan_html)):
|
|
226
|
+
status, out, err = self.command("-", "--format", fmt, "--mode", "strict", "--show-source")
|
|
227
|
+
self.assertEqual((status, err), (0, ""))
|
|
228
|
+
self.assertEqual(out, formatter(self.plan, mode="strict", show_source=True))
|
|
229
|
+
|
|
230
|
+
def test_file_output_and_no_overwriting_input(self):
|
|
231
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
232
|
+
source, dest = Path(directory) / "输入.json", Path(directory) / "图.html"
|
|
233
|
+
source.write_text(self.plan.to_json(), encoding="utf-8")
|
|
234
|
+
self.assertEqual(self.command(str(source), "--format", "html", "-o", str(dest)), (0, "", ""))
|
|
235
|
+
self.assertEqual(dest.read_text(encoding="utf-8"), format_plan_html(self.plan))
|
|
236
|
+
before = source.read_bytes()
|
|
237
|
+
status, out, err = self.command(str(source), "-o", str(source))
|
|
238
|
+
self.assertEqual((status, out), (2, ""))
|
|
239
|
+
self.assertIn("must differ", err)
|
|
240
|
+
self.assertEqual(source.read_bytes(), before)
|
|
241
|
+
|
|
242
|
+
def test_errors_use_strict_decoder_and_do_not_lower_ir(self):
|
|
243
|
+
invalid = self.plan.to_dict()
|
|
244
|
+
invalid["tasks"][0]["region"] = "missing"
|
|
245
|
+
for text in (json.dumps(invalid), self.plan.to_json().replace('"version": 1', '"version": 1, "version": 1'),
|
|
246
|
+
'{"format":"lev-execution-plan","version":NaN}'):
|
|
247
|
+
status, out, err = self.command("-", text=text)
|
|
248
|
+
self.assertEqual((status, out), (2, ""))
|
|
249
|
+
self.assertIn("annoscript.inspect:", err)
|
|
250
|
+
self.assertNotIn("Traceback", err)
|
|
251
|
+
status, out, err = self.command("-", "--format", "html", text=compile_program(empty).to_json())
|
|
252
|
+
self.assertEqual((status, out), (2, ""))
|
|
253
|
+
self.assertIn("lower the IR explicitly first", err)
|
|
254
|
+
|
|
255
|
+
def test_untrusted_strings_cannot_inject_html_or_mermaid(self):
|
|
256
|
+
program = compile_program(empty)
|
|
257
|
+
prompt = '中文</script><script>alert(1)</script>"\nclick I01 "https://example.com" __PLAN_DATA__'
|
|
258
|
+
statement = ir.Infer("reply", "generate", (ir.Literal(prompt),), (("max_length",ir.Literal(512)),), id="attack", source=ir.Source(program.source))
|
|
259
|
+
plan = lower_to_plan(replace(program, entry=prompt, body=(statement,) + program.body))
|
|
260
|
+
html = format_plan_html(plan)
|
|
261
|
+
self.assertEqual(html.count("<script"), 2)
|
|
262
|
+
self.assertEqual(html.count("</script>"), 2)
|
|
263
|
+
self.assertNotIn("<script>alert(1)", html)
|
|
264
|
+
payload = html.split('<script id="plan-data" type="application/json">', 1)[1].split("</script>", 1)[0]
|
|
265
|
+
self.assertEqual(json.loads(payload)["eager"]["nodes"][0]["prompt"], prompt)
|
|
266
|
+
mermaid = format_plan_mermaid(plan)
|
|
267
|
+
self.assertNotIn("<script", mermaid)
|
|
268
|
+
self.assertNotIn('\nclick ', mermaid)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
unittest.main()
|