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,444 @@
|
|
|
1
|
+
"""Compiler behavior and differential tests against ordinary Python execution."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from annoscript._test_artifacts import assert_saved_artifact
|
|
11
|
+
from annoscript.client import AnnoClient, precondition, system_prompt
|
|
12
|
+
from annoscript.compiler import CompileError, compile_program
|
|
13
|
+
from annoscript.anno import (
|
|
14
|
+
ChoiceResponse, ExtractResponse, GenerateListResponse, GenerateResponse,
|
|
15
|
+
NoulResponse, ScoreResponse, constrain,
|
|
16
|
+
)
|
|
17
|
+
from annoscript._test_ir_runtime import evaluate, walk_json
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _twice(value):
|
|
21
|
+
return value + value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _title(anno, suffix="!"):
|
|
25
|
+
text = anno.generate("title", max_length=512).text
|
|
26
|
+
return text + suffix
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def repeated_calls(anno):
|
|
30
|
+
text = "caller"
|
|
31
|
+
a = _twice(anno.generate("once", max_length=512).text)
|
|
32
|
+
b = _title(anno)
|
|
33
|
+
c = _title(anno, suffix="?")
|
|
34
|
+
return [a, b, c, text]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _optional(value):
|
|
38
|
+
if not value:
|
|
39
|
+
return None, []
|
|
40
|
+
if value == "special":
|
|
41
|
+
return value, [value, value]
|
|
42
|
+
result = [value]
|
|
43
|
+
return value, result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _nested(value):
|
|
47
|
+
main, related = _optional(value)
|
|
48
|
+
if main is None:
|
|
49
|
+
return related
|
|
50
|
+
return related + [main]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def loop_helpers(anno):
|
|
54
|
+
result = []
|
|
55
|
+
for value in anno.state:
|
|
56
|
+
result = result + [_nested(value)]
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _change(data):
|
|
61
|
+
data["value"] = "after"
|
|
62
|
+
return "changed"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def expression_order(anno):
|
|
66
|
+
data = {"value": "before"}
|
|
67
|
+
result = [data["value"], _change(data), data["value"]]
|
|
68
|
+
a = 1
|
|
69
|
+
b = a = a + 1
|
|
70
|
+
a, b = b + 1, a
|
|
71
|
+
return [result, a, b]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _pair(left, right="default"):
|
|
75
|
+
return [left, right]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def keyword_order(anno):
|
|
79
|
+
return _pair(right=anno.generate("right", max_length=512).text, left=anno.generate("left", max_length=512).text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def short_circuit(anno):
|
|
83
|
+
a = False and anno.generate("unreachable", max_length=512).text
|
|
84
|
+
b = "present" or anno.generate("unreachable", max_length=512).text
|
|
85
|
+
c = 3 < 2 < int(anno.generate("unreachable", max_length=512).text)
|
|
86
|
+
d = True and anno.generate("reachable", max_length=512).text
|
|
87
|
+
e = "" or anno.generate("fallback", max_length=512).text
|
|
88
|
+
return [a, b, c, d, e]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def context_alias(anno):
|
|
92
|
+
engine = anno
|
|
93
|
+
return _title(engine)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _falls_through(value):
|
|
97
|
+
if value:
|
|
98
|
+
return "yes"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def implicit_return(anno):
|
|
102
|
+
return _falls_through(anno.state)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def constrained(anno):
|
|
106
|
+
text = anno.generate("text", max_length=512).text
|
|
107
|
+
if anno.state:
|
|
108
|
+
constrain(len(text) > 0 and text.strip() != "", message="非空")
|
|
109
|
+
return text
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def all_operations(anno):
|
|
113
|
+
a = anno.noul("binary").noul
|
|
114
|
+
b = anno.choice("choose", {"a": "first"}).choice
|
|
115
|
+
c = anno.score("rate", ["bad", "good"]).score
|
|
116
|
+
d = anno.extract("extract", source_field="text", max_items=8, max_length_per_item=58).items
|
|
117
|
+
e = anno.generate("generate", max_length=512).text
|
|
118
|
+
f = anno.generate_list("list", max_items=8, max_length_per_item=58).items
|
|
119
|
+
return [a, b, c, d, e, f]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def formatting(anno):
|
|
123
|
+
result = []
|
|
124
|
+
for index, value in enumerate(anno.state, start=2):
|
|
125
|
+
result = result + [f"第{index:03d}项:{value!r}"]
|
|
126
|
+
result = result + [json.dumps({"条目": result}, ensure_ascii=False)]
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _never_check(data):
|
|
131
|
+
raise AssertionError("Preconditions must not execute while compiling")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@system_prompt("公共规则")
|
|
135
|
+
@precondition(_never_check)
|
|
136
|
+
def configured(anno):
|
|
137
|
+
return _title(anno)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_CAPTURE = {"names": ["初始"]}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def captured(anno):
|
|
144
|
+
return _CAPTURE
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def recursive(anno):
|
|
148
|
+
return recursive(anno)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _mutual_a(anno):
|
|
152
|
+
return _mutual_b(anno)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _mutual_b(anno):
|
|
156
|
+
return _mutual_a(anno)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def mutual_recursion(anno):
|
|
160
|
+
return _mutual_a(anno)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def unsupported_while(anno):
|
|
164
|
+
while anno.state:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def unsupported_comprehension(anno):
|
|
169
|
+
return [item for item in anno.state]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def unsupported_loop_return(anno):
|
|
173
|
+
for item in anno.state:
|
|
174
|
+
return item
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def unsupported_break(anno):
|
|
178
|
+
for item in anno.state:
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def unsupported_try(anno):
|
|
183
|
+
try:
|
|
184
|
+
return anno.state
|
|
185
|
+
except ValueError:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def dynamic_call(anno):
|
|
190
|
+
function = _title
|
|
191
|
+
return function(anno)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def missing_name(anno):
|
|
195
|
+
return nonexistent_symbol
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def unbound_name(anno):
|
|
199
|
+
if anno.state:
|
|
200
|
+
text = "set"
|
|
201
|
+
return text
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def write_input(anno):
|
|
205
|
+
data = anno.state
|
|
206
|
+
data["key"] = "bad"
|
|
207
|
+
return data
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def write_nested_input(anno):
|
|
211
|
+
data = {"input": anno.state}
|
|
212
|
+
alias = data["input"]
|
|
213
|
+
alias["key"] = "bad"
|
|
214
|
+
return data
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def write_capture(anno):
|
|
218
|
+
data = _CAPTURE
|
|
219
|
+
data["names"] = []
|
|
220
|
+
return data
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def write_loop_alias(anno):
|
|
224
|
+
data = {}
|
|
225
|
+
for index in range(2):
|
|
226
|
+
data["bad"] = index
|
|
227
|
+
data = anno.state
|
|
228
|
+
return data
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _bad_helper(data):
|
|
232
|
+
data["key"] = "bad"
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def write_helper_input(anno):
|
|
237
|
+
return _bad_helper(anno.state)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def invalid_model_arguments(anno):
|
|
241
|
+
return anno.generate("title", bogus=1, max_length=512).text
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def effectful_constraint(anno):
|
|
245
|
+
constrain(anno.generate("title", max_length=512).text != "")
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def forbidden_python_call(anno):
|
|
250
|
+
print("must not run")
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def invalid_unpack(anno):
|
|
255
|
+
a, *b = anno.state
|
|
256
|
+
return a
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def return_context(anno):
|
|
260
|
+
return anno
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def invalid_signature(a, b):
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Probe:
|
|
268
|
+
"""A deterministic model double with an observable call sequence."""
|
|
269
|
+
|
|
270
|
+
def __init__(self, state=None, text=None):
|
|
271
|
+
self.state = state
|
|
272
|
+
self.calls = []
|
|
273
|
+
self.text = text
|
|
274
|
+
|
|
275
|
+
def generate(self, instructions, **kwargs):
|
|
276
|
+
self.calls.append(("generate", instructions, deepcopy(kwargs)))
|
|
277
|
+
text = self.text if self.text is not None else f"{instructions}:{len(self.calls)}"
|
|
278
|
+
return GenerateResponse(text)
|
|
279
|
+
|
|
280
|
+
def choice(self, instructions, criteria):
|
|
281
|
+
self.calls.append(("choice", instructions, deepcopy(criteria)))
|
|
282
|
+
return ChoiceResponse(next(iter(criteria)), 0.9, {key: 1.0 for key in criteria})
|
|
283
|
+
|
|
284
|
+
def noul(self, instructions):
|
|
285
|
+
self.calls.append(("noul", instructions))
|
|
286
|
+
return NoulResponse(0.8)
|
|
287
|
+
|
|
288
|
+
def score(self, instructions, criteria):
|
|
289
|
+
self.calls.append(("score", instructions, deepcopy(criteria)))
|
|
290
|
+
return ScoreResponse({0: 0.2, 1: 0.8}, 0.8, {0: "bad", 1: "good"}, 0.9)
|
|
291
|
+
|
|
292
|
+
def extract(self, instructions, **kwargs):
|
|
293
|
+
self.calls.append(("extract", instructions, deepcopy(kwargs)))
|
|
294
|
+
return ExtractResponse([self.state[kwargs["source_field"]]])
|
|
295
|
+
|
|
296
|
+
def generate_list(self, instructions, **kwargs):
|
|
297
|
+
self.calls.append(("generate_list", instructions, deepcopy(kwargs)))
|
|
298
|
+
return GenerateListResponse(["entry"])
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class CompilerTests(unittest.TestCase):
|
|
302
|
+
def compare(self, function, state=None):
|
|
303
|
+
original = Probe(deepcopy(state))
|
|
304
|
+
expected = function(original)
|
|
305
|
+
for optimize in (False, True):
|
|
306
|
+
compiled = Probe(deepcopy(state))
|
|
307
|
+
program = compile_program(function, optimize=optimize)
|
|
308
|
+
actual = evaluate(json.loads(program.to_json()), compiled)
|
|
309
|
+
self.assertEqual(actual, expected)
|
|
310
|
+
self.assertEqual(compiled.calls, original.calls)
|
|
311
|
+
self.assertEqual(compiled.state, original.state)
|
|
312
|
+
return actual, compiled.calls, program.to_dict()
|
|
313
|
+
|
|
314
|
+
def test_repeated_calls_arguments_and_local_isolation(self):
|
|
315
|
+
actual, calls, data = self.compare(repeated_calls)
|
|
316
|
+
self.assertEqual(len(calls), 3)
|
|
317
|
+
self.assertEqual(actual, ["once:1once:1", "title:2!", "title:3?", "caller"])
|
|
318
|
+
self.assertFalse(any(n.get("kind") in ("call", "function") for n in walk_json(data)))
|
|
319
|
+
|
|
320
|
+
def test_nested_returns_and_finite_loops(self):
|
|
321
|
+
for state in ([], ["", "ordinary", "special", ""]):
|
|
322
|
+
with self.subTest(state=state):
|
|
323
|
+
self.compare(loop_helpers, state)
|
|
324
|
+
|
|
325
|
+
def test_expression_order_and_assignment(self):
|
|
326
|
+
self.assertEqual(self.compare(expression_order)[0], [["before", "changed", "after"], 3, 2])
|
|
327
|
+
self.compare(keyword_order)
|
|
328
|
+
|
|
329
|
+
def test_short_circuit_skips_inference(self):
|
|
330
|
+
_, calls, _ = self.compare(short_circuit)
|
|
331
|
+
self.assertEqual([c[1] for c in calls], ["reachable", "fallback"])
|
|
332
|
+
|
|
333
|
+
def test_injected_context_alias(self):
|
|
334
|
+
self.compare(context_alias)
|
|
335
|
+
|
|
336
|
+
def test_implicit_return(self):
|
|
337
|
+
self.assertIsNone(self.compare(implicit_return, False)[0])
|
|
338
|
+
self.assertEqual(self.compare(implicit_return, True)[0], "yes")
|
|
339
|
+
|
|
340
|
+
def test_all_six_operations_and_response_fields(self):
|
|
341
|
+
_, calls, data = self.compare(all_operations, {"text": "source"})
|
|
342
|
+
self.assertEqual(len(calls), 6)
|
|
343
|
+
self.assertEqual({n["operation"] for n in walk_json(data) if n.get("kind") == "infer"},
|
|
344
|
+
{"noul", "choice", "score", "extract", "generate", "generate_list"})
|
|
345
|
+
|
|
346
|
+
def test_fstrings_and_json_encoding(self):
|
|
347
|
+
self.compare(formatting, ["中文", 'a"b', "line\nbreak"])
|
|
348
|
+
|
|
349
|
+
def test_constraint_is_a_pure_expression_and_path_local(self):
|
|
350
|
+
data = compile_program(constrained).to_dict()
|
|
351
|
+
constraints = [n for n in walk_json(data) if n.get("kind") == "constrain"]
|
|
352
|
+
self.assertEqual(len(constraints), 1)
|
|
353
|
+
self.assertEqual(constraints[0]["condition"]["kind"], "bool_op")
|
|
354
|
+
self.assertIn("intrinsic", {n.get("kind") for n in walk_json(constraints[0])})
|
|
355
|
+
self.assertEqual(evaluate(data, Probe(False, text=" ")), " ")
|
|
356
|
+
with self.assertRaisesRegex(ValueError, "非空"):
|
|
357
|
+
evaluate(data, Probe(True, text=" "))
|
|
358
|
+
self.assertEqual(evaluate(data, Probe(True, text="ok")), "ok")
|
|
359
|
+
|
|
360
|
+
def test_config_and_preconditions_are_separate(self):
|
|
361
|
+
with patch.object(AnnoClient, "_validate_inputs", side_effect=AssertionError("must not run")):
|
|
362
|
+
data = compile_program(configured).to_dict()
|
|
363
|
+
self.assertEqual(data["config"], {"system_prompt": "公共规则"})
|
|
364
|
+
serialized = json.dumps(data)
|
|
365
|
+
self.assertNotIn("_never_check", serialized)
|
|
366
|
+
self.assertNotIn("precondition", serialized)
|
|
367
|
+
|
|
368
|
+
def test_captured_globals_are_detached_snapshots(self):
|
|
369
|
+
program = compile_program(captured)
|
|
370
|
+
_CAPTURE["names"].append("later")
|
|
371
|
+
try:
|
|
372
|
+
self.assertEqual(evaluate(program.to_dict(), Probe()), {"names": ["初始"]})
|
|
373
|
+
self.assertEqual(evaluate(compile_program(captured).to_dict(), Probe()), _CAPTURE)
|
|
374
|
+
finally:
|
|
375
|
+
_CAPTURE["names"].pop()
|
|
376
|
+
|
|
377
|
+
def test_compile_never_executes_the_function(self):
|
|
378
|
+
import sys
|
|
379
|
+
executed = []
|
|
380
|
+
watched = {repeated_calls.__code__, _title.__code__, _twice.__code__}
|
|
381
|
+
|
|
382
|
+
def trace(frame, event, arg):
|
|
383
|
+
if event == "call" and frame.f_code in watched:
|
|
384
|
+
executed.append(frame.f_code.co_name)
|
|
385
|
+
|
|
386
|
+
previous = sys.getprofile()
|
|
387
|
+
sys.setprofile(trace)
|
|
388
|
+
try:
|
|
389
|
+
compile_program(repeated_calls)
|
|
390
|
+
finally:
|
|
391
|
+
sys.setprofile(previous)
|
|
392
|
+
self.assertEqual(executed, [])
|
|
393
|
+
|
|
394
|
+
def test_determinism_and_inline_source_chain(self):
|
|
395
|
+
first = compile_program(loop_helpers)
|
|
396
|
+
second = compile_program(loop_helpers)
|
|
397
|
+
self.assertEqual(first.to_json(), second.to_json())
|
|
398
|
+
statements = [n for n in walk_json(first.to_dict()) if "source" in n and "id" in n]
|
|
399
|
+
self.assertEqual(len({n["id"] for n in statements}), len(statements))
|
|
400
|
+
self.assertTrue(any(len(n["source"]["calls"]) == 2 for n in statements))
|
|
401
|
+
self.assertNotIn(str(Path(__file__).resolve().parent), first.to_json())
|
|
402
|
+
|
|
403
|
+
def test_rejected_language_constructs_and_writes(self):
|
|
404
|
+
cases = {
|
|
405
|
+
recursive: "Recursive", mutual_recursion: "Recursive",
|
|
406
|
+
unsupported_while: "While", unsupported_comprehension: "ListComp",
|
|
407
|
+
unsupported_loop_return: "Returns inside loops", unsupported_break: "Break",
|
|
408
|
+
unsupported_try: "Try", dynamic_call: "Cannot capture function",
|
|
409
|
+
missing_name: "Unknown name", unbound_name: "may be unbound",
|
|
410
|
+
write_input: "read-only", write_nested_input: "read-only", write_capture: "read-only",
|
|
411
|
+
write_loop_alias: "read-only", write_helper_input: "read-only",
|
|
412
|
+
invalid_model_arguments: "Invalid call arguments", effectful_constraint: "must be pure",
|
|
413
|
+
forbidden_python_call: "Only known intrinsics", invalid_unpack: "only names",
|
|
414
|
+
return_context: "cannot be stored", invalid_signature: "exactly one",
|
|
415
|
+
}
|
|
416
|
+
for function, message in cases.items():
|
|
417
|
+
with self.subTest(function=function.__name__), self.assertRaisesRegex(CompileError, message):
|
|
418
|
+
compile_program(function)
|
|
419
|
+
|
|
420
|
+
def test_diagnostic_contains_source_and_inline_chain(self):
|
|
421
|
+
with self.assertRaises(CompileError) as caught:
|
|
422
|
+
compile_program(write_helper_input)
|
|
423
|
+
error = caught.exception
|
|
424
|
+
self.assertEqual(error.source.span.function, "_bad_helper")
|
|
425
|
+
self.assertEqual(error.source.calls[-1].function, "write_helper_input")
|
|
426
|
+
self.assertGreater(error.source.span.line, 0)
|
|
427
|
+
self.assertIn("inlined from", str(error))
|
|
428
|
+
|
|
429
|
+
def test_unavailable_source_and_closure(self):
|
|
430
|
+
with self.assertRaisesRegex(CompileError, "source is unavailable"):
|
|
431
|
+
compile_program(eval("lambda anno: None"))
|
|
432
|
+
data = "captured"
|
|
433
|
+
|
|
434
|
+
def closure(anno):
|
|
435
|
+
return data
|
|
436
|
+
|
|
437
|
+
with self.assertRaisesRegex(CompileError, "Closures"):
|
|
438
|
+
compile_program(closure)
|
|
439
|
+
with self.assertRaisesRegex(CompileError, "ordinary Python function"):
|
|
440
|
+
compile_program(len)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
unittest.main()
|