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/compiler.py
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
"""Compile a Python function object into AnnoScript IR without executing its body.
|
|
2
|
+
|
|
3
|
+
The public entry point is compile_program(function). Source must be available to
|
|
4
|
+
inspect. Only reachable same-module helpers are compiled; all calls are inlined.
|
|
5
|
+
Captured JSON constants are snapshots, while preconditions remain client-local.
|
|
6
|
+
Annotations and decorators are never evaluated by the compiler.
|
|
7
|
+
|
|
8
|
+
This is a deliberately restricted Python language. It supports structured finite
|
|
9
|
+
loops, conditional control flow, local containers and the documented intrinsics.
|
|
10
|
+
It rejects dynamic calls, recursion, closures, comprehensions, exceptions,
|
|
11
|
+
async, loop exits and writes to inputs/captured values. Definite assignment and
|
|
12
|
+
container ownership checks are conservative, particularly across loops. Runtime
|
|
13
|
+
type errors on input-dependent expressions remain runtime errors.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import builtins
|
|
20
|
+
from dataclasses import dataclass, field, replace
|
|
21
|
+
import inspect
|
|
22
|
+
import json
|
|
23
|
+
import math
|
|
24
|
+
import textwrap
|
|
25
|
+
from types import FunctionType
|
|
26
|
+
from typing import Callable
|
|
27
|
+
|
|
28
|
+
from annoscript import ir
|
|
29
|
+
from annoscript._client_common import JSONValue, get_program_config, precondition, system_prompt
|
|
30
|
+
from annoscript.anno import Example, Anno, constrain
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CompileError(ValueError):
|
|
34
|
+
"""A rejected program, with a source location and inline call chain."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str, source: ir.Source | None = None):
|
|
37
|
+
self.message = message
|
|
38
|
+
self.source = source
|
|
39
|
+
location = ""
|
|
40
|
+
if source is not None:
|
|
41
|
+
s = source.span
|
|
42
|
+
location = f"{s.module}:{s.line}:{s.column} ({s.function}): "
|
|
43
|
+
if source.calls:
|
|
44
|
+
message += "\n inlined from " + " -> ".join(
|
|
45
|
+
f"{s.module}:{s.line}:{s.column} ({s.function})" for s in source.calls
|
|
46
|
+
)
|
|
47
|
+
super().__init__(location + message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class _Info:
|
|
52
|
+
origin: str = "unknown"
|
|
53
|
+
container: str | None = None
|
|
54
|
+
items: tuple[_Info, ...] | None = None
|
|
55
|
+
element: _Info | None = None
|
|
56
|
+
response: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_UNKNOWN = _Info()
|
|
60
|
+
_SCALAR = _Info("scalar")
|
|
61
|
+
_STRING = _Info("scalar", "str")
|
|
62
|
+
_READONLY = _Info("readonly")
|
|
63
|
+
_CONTEXT = _Info("context")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _merge(a: _Info, b: _Info) -> _Info:
|
|
67
|
+
if a == b:
|
|
68
|
+
return a
|
|
69
|
+
origin = a.origin if a.origin == b.origin else "unknown"
|
|
70
|
+
container = a.container if a.container == b.container else None
|
|
71
|
+
items = None
|
|
72
|
+
if a.items is not None and b.items is not None and len(a.items) == len(b.items):
|
|
73
|
+
items = tuple(_merge(x, y) for x, y in zip(a.items, b.items))
|
|
74
|
+
element = _merge(a.element, b.element) if a.element and b.element else None
|
|
75
|
+
response = a.response if a.response == b.response else None
|
|
76
|
+
return _Info(origin, container, items, element, response)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _merge_env(a: dict[str, _Info], b: dict[str, _Info]) -> dict[str, _Info]:
|
|
80
|
+
return {name: _merge(info, b[name]) for name, info in a.items() if name in b}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class _Value:
|
|
85
|
+
expr: ir.Expr | None # context is a compile-time capability, never a value
|
|
86
|
+
info: _Info = _UNKNOWN
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class _Parsed:
|
|
91
|
+
function: FunctionType
|
|
92
|
+
node: ast.FunctionDef
|
|
93
|
+
signature: inspect.Signature
|
|
94
|
+
lines: list[str]
|
|
95
|
+
start: int
|
|
96
|
+
indentation: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class _Frame:
|
|
101
|
+
parsed: _Parsed
|
|
102
|
+
names: dict[str, str]
|
|
103
|
+
env: dict[str, _Info]
|
|
104
|
+
calls: tuple[ir.SourceSpan, ...] = ()
|
|
105
|
+
result: str | None = None
|
|
106
|
+
done: str | None = None
|
|
107
|
+
return_infos: list[_Info] = field(default_factory=list)
|
|
108
|
+
loop_depth: int = 0
|
|
109
|
+
|
|
110
|
+
def fork(self):
|
|
111
|
+
return replace(self, env=dict(self.env))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_OPERATIONS = ("noul", "choice", "score", "extract", "generate", "generate_list")
|
|
115
|
+
_RESPONSES = {
|
|
116
|
+
"noul": {"noul": _SCALAR},
|
|
117
|
+
"choice": {"choice": _STRING, "confidence": _SCALAR, "probabilities": _READONLY},
|
|
118
|
+
"score": {"score": _SCALAR, "confidence": _SCALAR, "probabilities": _READONLY, "legend": _READONLY},
|
|
119
|
+
"extract": {"items": _Info("readonly", "list", element=_STRING)},
|
|
120
|
+
"generate": {"text": _STRING},
|
|
121
|
+
"generate_list": {"items": _Info("readonly", "list", element=_STRING)},
|
|
122
|
+
}
|
|
123
|
+
_COMPARISONS = {
|
|
124
|
+
ast.Eq: "eq", ast.NotEq: "ne", ast.Lt: "lt", ast.LtE: "le",
|
|
125
|
+
ast.Gt: "gt", ast.GtE: "ge", ast.In: "in", ast.NotIn: "not_in",
|
|
126
|
+
ast.Is: "is", ast.IsNot: "is_not",
|
|
127
|
+
}
|
|
128
|
+
_INTRINSICS = ((builtins.len, "len"), (builtins.int, "int"),
|
|
129
|
+
(builtins.range, "range"), (builtins.enumerate, "enumerate"),
|
|
130
|
+
(json.dumps, "json.dumps"))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _Compiler:
|
|
134
|
+
def __init__(self, entry: FunctionType):
|
|
135
|
+
self.entry = entry
|
|
136
|
+
self.counter = 0
|
|
137
|
+
self.parsed: dict[FunctionType, _Parsed] = {}
|
|
138
|
+
self.active: list[FunctionType] = []
|
|
139
|
+
|
|
140
|
+
def fresh(self, hint: str) -> str:
|
|
141
|
+
self.counter += 1
|
|
142
|
+
if self.counter > 100_000:
|
|
143
|
+
raise CompileError("Expanded program exceeds the 100000-node/name compilation budget")
|
|
144
|
+
return f"v{self.counter}_{hint}"
|
|
145
|
+
|
|
146
|
+
def span(self, f: _Frame, node: ast.AST) -> ir.SourceSpan:
|
|
147
|
+
p = f.parsed
|
|
148
|
+
line = getattr(node, "lineno", p.node.lineno)
|
|
149
|
+
end = getattr(node, "end_lineno", line) or line
|
|
150
|
+
|
|
151
|
+
def column(lineno, offset):
|
|
152
|
+
# Python AST columns are UTF-8 byte offsets, not character offsets.
|
|
153
|
+
content = p.lines[lineno - 1] if lineno <= len(p.lines) else ""
|
|
154
|
+
return len(content.encode("utf-8")[:offset].decode("utf-8")) + p.indentation + 1
|
|
155
|
+
|
|
156
|
+
return ir.SourceSpan(
|
|
157
|
+
p.function.__module__, p.function.__qualname__, p.start + line - 1,
|
|
158
|
+
column(line, getattr(node, "col_offset", 0)), p.start + end - 1,
|
|
159
|
+
column(end, getattr(node, "end_col_offset", 0) or 0),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def source(self, f, node):
|
|
163
|
+
return ir.Source(self.span(f, node), f.calls)
|
|
164
|
+
|
|
165
|
+
def fail(self, f, node, message):
|
|
166
|
+
raise CompileError(message, self.source(f, node))
|
|
167
|
+
|
|
168
|
+
def stmt(self, cls, f, node, *args, **kwargs):
|
|
169
|
+
return cls(*args, **kwargs, id=self.fresh("stmt"), source=self.source(f, node))
|
|
170
|
+
|
|
171
|
+
def parse(self, function, caller=None, call=None):
|
|
172
|
+
def error(message):
|
|
173
|
+
if caller is not None:
|
|
174
|
+
self.fail(caller, call, message)
|
|
175
|
+
raise CompileError(message)
|
|
176
|
+
|
|
177
|
+
if not isinstance(function, FunctionType):
|
|
178
|
+
error("Expected an ordinary Python function")
|
|
179
|
+
if function.__closure__:
|
|
180
|
+
error("Closures are not supported")
|
|
181
|
+
if hasattr(function, "__wrapped__") or hasattr(function, "__signature__"):
|
|
182
|
+
error("Wrapped functions and overridden signatures are not supported")
|
|
183
|
+
if function in self.parsed:
|
|
184
|
+
return self.parsed[function]
|
|
185
|
+
try:
|
|
186
|
+
lines, start = inspect.getsourcelines(function)
|
|
187
|
+
raw = "".join(lines)
|
|
188
|
+
source = textwrap.dedent(raw)
|
|
189
|
+
tree = ast.parse(source)
|
|
190
|
+
signature = inspect.signature(function, follow_wrapped=False, eval_str=False)
|
|
191
|
+
except (OSError, IOError, TypeError, SyntaxError, ValueError) as exc:
|
|
192
|
+
error(f"Function source is unavailable or invalid: {exc}")
|
|
193
|
+
if len(tree.body) != 1 or not isinstance(tree.body[0], ast.FunctionDef):
|
|
194
|
+
error("Only synchronous function definitions are supported")
|
|
195
|
+
node = tree.body[0]
|
|
196
|
+
if node.name != function.__code__.co_name:
|
|
197
|
+
error("Source does not match the function's code name")
|
|
198
|
+
if any(p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD) for p in signature.parameters.values()):
|
|
199
|
+
error("Variadic function parameters are not supported")
|
|
200
|
+
indentation = len(lines[0]) - len(lines[0].lstrip())
|
|
201
|
+
parsed = _Parsed(function, node, signature, source.splitlines(), start, indentation)
|
|
202
|
+
self.parsed[function] = parsed
|
|
203
|
+
return parsed
|
|
204
|
+
|
|
205
|
+
def frame(self, parsed, calls=(), helper=False):
|
|
206
|
+
names = dict.fromkeys(parsed.signature.parameters)
|
|
207
|
+
for statement in parsed.node.body:
|
|
208
|
+
for node in ast.walk(statement):
|
|
209
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
210
|
+
names[node.id] = None
|
|
211
|
+
return _Frame(
|
|
212
|
+
parsed, {name: self.fresh(name) for name in names}, {}, calls,
|
|
213
|
+
self.fresh("result") if helper else None,
|
|
214
|
+
self.fresh("done") if helper else None,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def raw_name(self, f, node):
|
|
218
|
+
if node.id in f.names:
|
|
219
|
+
self.fail(f, node, "Dynamic calls and function-valued local variables are not supported")
|
|
220
|
+
namespace = f.parsed.function.__globals__
|
|
221
|
+
if node.id in namespace:
|
|
222
|
+
return namespace[node.id]
|
|
223
|
+
namespace = f.parsed.function.__builtins__
|
|
224
|
+
if node.id in namespace:
|
|
225
|
+
return namespace[node.id]
|
|
226
|
+
self.fail(f, node, f"Unknown name: {node.id}")
|
|
227
|
+
|
|
228
|
+
def decorators(self, f, entry=False):
|
|
229
|
+
for decorator in f.parsed.node.decorator_list:
|
|
230
|
+
if not entry:
|
|
231
|
+
self.fail(f, decorator, "Decorated helpers are not supported")
|
|
232
|
+
if not isinstance(decorator, ast.Call) or not isinstance(decorator.func, ast.Name):
|
|
233
|
+
self.fail(f, decorator, "Only system_prompt and precondition decorators are supported")
|
|
234
|
+
value = self.raw_name(f, decorator.func)
|
|
235
|
+
if value is not system_prompt and value is not precondition:
|
|
236
|
+
self.fail(f, decorator, "Only system_prompt and precondition decorators are supported")
|
|
237
|
+
|
|
238
|
+
def capture(self, value, f, node, seen=None):
|
|
239
|
+
seen = set() if seen is None else seen
|
|
240
|
+
if type(value) is Example:
|
|
241
|
+
return self.capture(value.to_dict(), f, node, seen)
|
|
242
|
+
if value is None or type(value) in (str, bool, int, float):
|
|
243
|
+
if type(value) is float and not math.isfinite(value):
|
|
244
|
+
self.fail(f, node, "Captured numbers must be finite")
|
|
245
|
+
return _Value(ir.Literal(value), _STRING if type(value) is str else _SCALAR)
|
|
246
|
+
if type(value) not in (list, dict):
|
|
247
|
+
self.fail(f, node, f"Cannot capture {type(value).__name__}; only JSON constants are supported")
|
|
248
|
+
if id(value) in seen:
|
|
249
|
+
self.fail(f, node, "Cyclic constants are not supported")
|
|
250
|
+
seen.add(id(value))
|
|
251
|
+
try:
|
|
252
|
+
if type(value) is list:
|
|
253
|
+
expr = ir.ListExpr(tuple(self.capture(item, f, node, seen).expr for item in value))
|
|
254
|
+
return _Value(expr, _Info("readonly", "list", element=_READONLY))
|
|
255
|
+
if any(type(key) is not str for key in value):
|
|
256
|
+
self.fail(f, node, "Captured dictionaries require string keys")
|
|
257
|
+
expr = ir.DictExpr(tuple(
|
|
258
|
+
(ir.Literal(key), self.capture(item, f, node, seen).expr) for key, item in value.items()
|
|
259
|
+
))
|
|
260
|
+
return _Value(expr, _Info("readonly", "dict", element=_STRING))
|
|
261
|
+
finally:
|
|
262
|
+
seen.remove(id(value))
|
|
263
|
+
|
|
264
|
+
def data(self, value, f, node):
|
|
265
|
+
if value.expr is None:
|
|
266
|
+
self.fail(f, node, "The injected AnnoScript context cannot be stored in data or returned")
|
|
267
|
+
return value.expr
|
|
268
|
+
|
|
269
|
+
def materialize(self, value, out, f, node, pure=False):
|
|
270
|
+
if pure or value.expr is None:
|
|
271
|
+
return value
|
|
272
|
+
name = self.fresh("tmp")
|
|
273
|
+
out.append(self.stmt(ir.Assign, f, node, name, value.expr))
|
|
274
|
+
return _Value(ir.Var(name), value.info)
|
|
275
|
+
|
|
276
|
+
def value(self, expr, info, out, f, node, pure):
|
|
277
|
+
return self.materialize(_Value(expr, info), out, f, node, pure)
|
|
278
|
+
|
|
279
|
+
def expr(self, node, out, f, pure=False):
|
|
280
|
+
if isinstance(node, ast.Constant):
|
|
281
|
+
return self.capture(node.value, f, node)
|
|
282
|
+
if isinstance(node, ast.Name):
|
|
283
|
+
if node.id in f.names:
|
|
284
|
+
if node.id not in f.env:
|
|
285
|
+
self.fail(f, node, f"Local variable may be unbound: {node.id}")
|
|
286
|
+
info = f.env[node.id]
|
|
287
|
+
return _Value(None if info == _CONTEXT else ir.Var(f.names[node.id]), info)
|
|
288
|
+
return self.materialize(self.capture(self.raw_name(f, node), f, node), out, f, node, pure)
|
|
289
|
+
if isinstance(node, (ast.List, ast.Tuple)):
|
|
290
|
+
values = [self.expr(item, out, f, pure) for item in node.elts]
|
|
291
|
+
expressions = tuple(self.data(v, f, node) for v in values)
|
|
292
|
+
if isinstance(node, ast.Tuple):
|
|
293
|
+
expression = ir.TupleExpr(expressions)
|
|
294
|
+
info = _Info("scalar", "tuple", tuple(v.info for v in values))
|
|
295
|
+
else:
|
|
296
|
+
expression = ir.ListExpr(expressions)
|
|
297
|
+
info = _Info("local", "list")
|
|
298
|
+
return self.value(expression, info, out, f, node, pure)
|
|
299
|
+
if isinstance(node, ast.Dict):
|
|
300
|
+
entries = []
|
|
301
|
+
for key, item in zip(node.keys, node.values):
|
|
302
|
+
if key is None:
|
|
303
|
+
self.fail(f, node, "Dictionary unpacking is not supported")
|
|
304
|
+
k = self.data(self.expr(key, out, f, pure), f, key)
|
|
305
|
+
v = self.data(self.expr(item, out, f, pure), f, item)
|
|
306
|
+
entries.append((k, v))
|
|
307
|
+
return self.value(ir.DictExpr(tuple(entries)), _Info("local", "dict"), out, f, node, pure)
|
|
308
|
+
if isinstance(node, ast.Attribute):
|
|
309
|
+
receiver = self.expr(node.value, out, f, pure)
|
|
310
|
+
if receiver.info == _CONTEXT and node.attr == "state":
|
|
311
|
+
return self.value(ir.State(), _READONLY, out, f, node, pure)
|
|
312
|
+
response = receiver.info.response
|
|
313
|
+
if response is None or node.attr not in _RESPONSES[response]:
|
|
314
|
+
self.fail(f, node, "Only AnnoScript.state and declared inference response fields can be read")
|
|
315
|
+
return self.value(ir.Field(receiver.expr, node.attr), _RESPONSES[response][node.attr], out, f, node, pure)
|
|
316
|
+
if isinstance(node, ast.Subscript):
|
|
317
|
+
receiver = self.expr(node.value, out, f, pure)
|
|
318
|
+
index = self.expr(node.slice, out, f, pure)
|
|
319
|
+
info = _READONLY if receiver.info.origin == "readonly" else _UNKNOWN
|
|
320
|
+
if receiver.info.container == "str":
|
|
321
|
+
info = _STRING
|
|
322
|
+
if receiver.info.items is not None and isinstance(node.slice, ast.Constant) and type(node.slice.value) is int:
|
|
323
|
+
try:
|
|
324
|
+
info = receiver.info.items[node.slice.value]
|
|
325
|
+
except IndexError:
|
|
326
|
+
pass # runtime IndexError, not a compiler failure
|
|
327
|
+
return self.value(ir.GetItem(self.data(receiver, f, node), self.data(index, f, node)), info, out, f, node, pure)
|
|
328
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in (ast.Not, ast.UAdd, ast.USub):
|
|
329
|
+
operand = self.data(self.expr(node.operand, out, f, pure), f, node)
|
|
330
|
+
op = {ast.Not: "not", ast.UAdd: "positive", ast.USub: "negative"}[type(node.op)]
|
|
331
|
+
return self.value(ir.Unary(op, operand), _SCALAR, out, f, node, pure)
|
|
332
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
333
|
+
a = self.expr(node.left, out, f, pure)
|
|
334
|
+
b = self.expr(node.right, out, f, pure)
|
|
335
|
+
info = _UNKNOWN
|
|
336
|
+
if a.info.container == b.info.container == "list":
|
|
337
|
+
info = _Info("local", "list")
|
|
338
|
+
elif a.info.container == "str" or b.info.container == "str":
|
|
339
|
+
info = _STRING
|
|
340
|
+
elif a.info.items is not None and b.info.items is not None:
|
|
341
|
+
info = _Info("scalar", "tuple", a.info.items + b.info.items)
|
|
342
|
+
return self.value(ir.Binary("add", self.data(a, f, node), self.data(b, f, node)), info, out, f, node, pure)
|
|
343
|
+
if isinstance(node, ast.BoolOp):
|
|
344
|
+
return self.bool_expr(node, out, f, pure)
|
|
345
|
+
if isinstance(node, ast.Compare):
|
|
346
|
+
return self.compare(node, out, f, pure)
|
|
347
|
+
if isinstance(node, ast.JoinedStr):
|
|
348
|
+
parts = tuple(self.data(self.expr(v, out, f, pure), f, v) for v in node.values)
|
|
349
|
+
return self.value(ir.FormatString(parts), _STRING, out, f, node, pure)
|
|
350
|
+
if isinstance(node, ast.FormattedValue):
|
|
351
|
+
value = self.data(self.expr(node.value, out, f, pure), f, node)
|
|
352
|
+
spec = self.data(self.expr(node.format_spec, out, f, pure), f, node) if node.format_spec else None
|
|
353
|
+
conversion = {-1: "none", 115: "s", 114: "r", 97: "a"}[node.conversion]
|
|
354
|
+
return self.value(ir.FormatValue(value, conversion, spec), _STRING, out, f, node, pure)
|
|
355
|
+
if isinstance(node, ast.Call):
|
|
356
|
+
return self.call(node, out, f, pure)
|
|
357
|
+
self.fail(f, node, f"Unsupported expression: {type(node).__name__}")
|
|
358
|
+
|
|
359
|
+
def bool_expr(self, node, out, f, pure):
|
|
360
|
+
op = "and" if isinstance(node.op, ast.And) else "or"
|
|
361
|
+
if pure:
|
|
362
|
+
values = [self.expr(v, out, f, True) for v in node.values]
|
|
363
|
+
info = values[0].info
|
|
364
|
+
for v in values[1:]:
|
|
365
|
+
info = _merge(info, v.info)
|
|
366
|
+
return _Value(ir.BoolOp(op, tuple(self.data(v, f, node) for v in values)), info)
|
|
367
|
+
first = self.expr(node.values[0], out, f)
|
|
368
|
+
result = self.fresh("bool")
|
|
369
|
+
out.append(self.stmt(ir.Assign, f, node, result, self.data(first, f, node)))
|
|
370
|
+
info = first.info
|
|
371
|
+
for child in node.values[1:]:
|
|
372
|
+
body = []
|
|
373
|
+
value = self.expr(child, body, f)
|
|
374
|
+
body.append(self.stmt(ir.Assign, f, child, result, self.data(value, f, child)))
|
|
375
|
+
condition = ir.Var(result) if op == "and" else ir.Unary("not", ir.Var(result))
|
|
376
|
+
out.append(self.stmt(ir.If, f, child, condition, tuple(body)))
|
|
377
|
+
info = _merge(info, value.info)
|
|
378
|
+
return _Value(ir.Var(result), info)
|
|
379
|
+
|
|
380
|
+
def compare(self, node, out, f, pure):
|
|
381
|
+
for op, left, right in zip(node.ops, [node.left, *node.comparators], node.comparators):
|
|
382
|
+
if isinstance(op, (ast.Is, ast.IsNot)) and not any(
|
|
383
|
+
isinstance(v, ast.Constant) and (v.value is None or type(v.value) is bool)
|
|
384
|
+
for v in (left, right)
|
|
385
|
+
):
|
|
386
|
+
self.fail(f, node, "Identity comparisons are supported only against None or booleans")
|
|
387
|
+
ops = tuple(_COMPARISONS[type(op)] for op in node.ops)
|
|
388
|
+
left = self.data(self.expr(node.left, out, f, pure), f, node)
|
|
389
|
+
if pure:
|
|
390
|
+
rights = tuple(self.data(self.expr(v, out, f, True), f, v) for v in node.comparators)
|
|
391
|
+
return _Value(ir.Compare(left, ops, rights), _SCALAR)
|
|
392
|
+
result = self.fresh("comparison")
|
|
393
|
+
for index, (op, child) in enumerate(zip(ops, node.comparators)):
|
|
394
|
+
body = out if index == 0 else []
|
|
395
|
+
right = self.data(self.expr(child, body, f), f, child)
|
|
396
|
+
body.append(self.stmt(ir.Assign, f, child, result, ir.Compare(left, (op,), (right,))))
|
|
397
|
+
if index:
|
|
398
|
+
out.append(self.stmt(ir.If, f, child, ir.Var(result), tuple(body)))
|
|
399
|
+
left = right
|
|
400
|
+
return _Value(ir.Var(result), _SCALAR)
|
|
401
|
+
|
|
402
|
+
def arguments(self, node, out, f, pure):
|
|
403
|
+
if any(isinstance(a, ast.Starred) for a in node.args) or any(k.arg is None for k in node.keywords):
|
|
404
|
+
self.fail(f, node, "Starred call arguments are not supported")
|
|
405
|
+
args = [self.expr(a, out, f, pure) for a in node.args]
|
|
406
|
+
kwargs = {}
|
|
407
|
+
for kw in node.keywords:
|
|
408
|
+
if kw.arg in kwargs:
|
|
409
|
+
self.fail(f, node, f"Duplicate keyword: {kw.arg}")
|
|
410
|
+
kwargs[kw.arg] = self.expr(kw.value, out, f, pure)
|
|
411
|
+
return args, kwargs
|
|
412
|
+
|
|
413
|
+
def bind(self, signature, args, kwargs, f, node):
|
|
414
|
+
try:
|
|
415
|
+
return signature.bind(*args, **kwargs)
|
|
416
|
+
except TypeError as exc:
|
|
417
|
+
self.fail(f, node, f"Invalid call arguments: {exc}")
|
|
418
|
+
|
|
419
|
+
def call(self, node, out, f, pure):
|
|
420
|
+
target = None
|
|
421
|
+
receiver = None
|
|
422
|
+
operation = None
|
|
423
|
+
intrinsic = None
|
|
424
|
+
if isinstance(node.func, ast.Name):
|
|
425
|
+
target = self.raw_name(f, node.func)
|
|
426
|
+
elif isinstance(node.func, ast.Attribute):
|
|
427
|
+
base = node.func.value
|
|
428
|
+
if isinstance(base, ast.Name) and base.id not in f.names and self.raw_name(f, base) is json:
|
|
429
|
+
if node.func.attr != "dumps":
|
|
430
|
+
self.fail(f, node, "Only json.dumps is supported")
|
|
431
|
+
target = json.dumps
|
|
432
|
+
else:
|
|
433
|
+
receiver = self.expr(base, out, f, pure)
|
|
434
|
+
if receiver.info == _CONTEXT and node.func.attr in _OPERATIONS:
|
|
435
|
+
operation = node.func.attr
|
|
436
|
+
elif node.func.attr == "strip" and receiver.expr is not None:
|
|
437
|
+
intrinsic = "str.strip"
|
|
438
|
+
else:
|
|
439
|
+
self.fail(f, node, "Unsupported method or dynamic call")
|
|
440
|
+
else:
|
|
441
|
+
self.fail(f, node, "Dynamic calls are not supported")
|
|
442
|
+
if target is constrain:
|
|
443
|
+
self.fail(f, node, "constrain must be a standalone statement")
|
|
444
|
+
if pure and (operation is not None or isinstance(target, FunctionType) and target is not json.dumps):
|
|
445
|
+
self.fail(f, node, "Constraint expressions must be pure; inference and helper calls are not allowed")
|
|
446
|
+
args, kwargs = self.arguments(node, out, f, pure)
|
|
447
|
+
if target is Example:
|
|
448
|
+
bound = self.bind(inspect.signature(Example), args, kwargs, f, node)
|
|
449
|
+
expression = ir.Intrinsic("example", tuple(
|
|
450
|
+
self.data(bound.arguments[name], f, node) for name in ("state", "answer")
|
|
451
|
+
))
|
|
452
|
+
return self.value(expression, _Info("readonly", "dict", element=_STRING),
|
|
453
|
+
out, f, node, pure)
|
|
454
|
+
if operation:
|
|
455
|
+
self.bind(inspect.signature(getattr(Anno, operation)), [None, *args], kwargs, f, node)
|
|
456
|
+
expr_args = tuple(self.data(v, f, node) for v in args)
|
|
457
|
+
expr_kwargs = tuple((k, self.data(v, f, node)) for k, v in kwargs.items())
|
|
458
|
+
name = self.fresh(operation)
|
|
459
|
+
out.append(self.stmt(ir.Infer, f, node, name, operation, expr_args, expr_kwargs))
|
|
460
|
+
return _Value(ir.Var(name), _Info("readonly", response=operation))
|
|
461
|
+
for function, name in _INTRINSICS:
|
|
462
|
+
if target is function:
|
|
463
|
+
intrinsic = name
|
|
464
|
+
break
|
|
465
|
+
if intrinsic:
|
|
466
|
+
if receiver is not None:
|
|
467
|
+
args = [receiver, *args]
|
|
468
|
+
info = self.check_intrinsic(intrinsic, args, kwargs, f, node)
|
|
469
|
+
expression = ir.Intrinsic(intrinsic, tuple(self.data(v, f, node) for v in args),
|
|
470
|
+
tuple((k, self.data(v, f, node)) for k, v in kwargs.items()))
|
|
471
|
+
return self.value(expression, info, out, f, node, pure)
|
|
472
|
+
if isinstance(target, FunctionType) and target.__module__ == self.entry.__module__:
|
|
473
|
+
return self.inline(target, args, kwargs, out, f, node)
|
|
474
|
+
self.fail(f, node, "Only known intrinsics and same-module helper functions can be called")
|
|
475
|
+
|
|
476
|
+
def check_intrinsic(self, name, args, kwargs, f, node):
|
|
477
|
+
valid = True
|
|
478
|
+
if name == "len":
|
|
479
|
+
valid = len(args) == 1 and not kwargs
|
|
480
|
+
info = _SCALAR
|
|
481
|
+
elif name == "int":
|
|
482
|
+
valid = len(args) <= 2 and set(kwargs) <= {"base"} and not (len(args) == 2 and kwargs)
|
|
483
|
+
info = _SCALAR
|
|
484
|
+
elif name == "range":
|
|
485
|
+
valid = 1 <= len(args) <= 3 and not kwargs
|
|
486
|
+
info = _Info("scalar", "range", element=_SCALAR)
|
|
487
|
+
elif name == "enumerate":
|
|
488
|
+
self.bind(inspect.signature(enumerate), args, kwargs, f, node)
|
|
489
|
+
iterable = args[0] if args else kwargs["iterable"]
|
|
490
|
+
element = _Info("scalar", "tuple", (_SCALAR, self.element(iterable.info)))
|
|
491
|
+
info = _Info("scalar", "enumerate", element=element)
|
|
492
|
+
elif name == "str.strip":
|
|
493
|
+
valid = 1 <= len(args) <= 2 and not kwargs
|
|
494
|
+
info = _STRING
|
|
495
|
+
else:
|
|
496
|
+
self.bind(inspect.signature(json.dumps), args, kwargs, f, node)
|
|
497
|
+
valid = set(kwargs) <= {"obj", "skipkeys", "ensure_ascii", "check_circular", "allow_nan", "indent", "separators", "sort_keys"}
|
|
498
|
+
info = _STRING
|
|
499
|
+
if not valid:
|
|
500
|
+
self.fail(f, node, f"Unsupported arguments for intrinsic {name}")
|
|
501
|
+
return info
|
|
502
|
+
|
|
503
|
+
def inline(self, function, args, kwargs, out, caller, call):
|
|
504
|
+
if function in self.active:
|
|
505
|
+
self.fail(caller, call, "Recursive helper calls are not supported")
|
|
506
|
+
parsed = self.parse(function, caller, call)
|
|
507
|
+
frame = self.frame(parsed, caller.calls + (self.span(caller, call),), helper=True)
|
|
508
|
+
self.decorators(frame)
|
|
509
|
+
bound = self.bind(parsed.signature, args, kwargs, caller, call)
|
|
510
|
+
bound.apply_defaults()
|
|
511
|
+
for name, value in bound.arguments.items():
|
|
512
|
+
if not isinstance(value, _Value):
|
|
513
|
+
value = self.capture(value, caller, call)
|
|
514
|
+
frame.env[name] = value.info
|
|
515
|
+
if value.expr is not None:
|
|
516
|
+
out.append(self.stmt(ir.Assign, caller, call, frame.names[name], value.expr))
|
|
517
|
+
out.append(self.stmt(ir.Assign, caller, call, frame.result, ir.Literal(None)))
|
|
518
|
+
out.append(self.stmt(ir.Assign, caller, call, frame.done, ir.Literal(False)))
|
|
519
|
+
self.active.append(function)
|
|
520
|
+
try:
|
|
521
|
+
body, falls, _ = self.block(parsed.node.body, frame)
|
|
522
|
+
finally:
|
|
523
|
+
self.active.pop()
|
|
524
|
+
out.extend(body)
|
|
525
|
+
infos = frame.return_infos + ([_SCALAR] if falls else [])
|
|
526
|
+
info = infos[0] if infos else _SCALAR
|
|
527
|
+
for other in infos[1:]:
|
|
528
|
+
info = _merge(info, other)
|
|
529
|
+
return _Value(ir.Var(frame.result), info)
|
|
530
|
+
|
|
531
|
+
def element(self, info):
|
|
532
|
+
if info.element is not None:
|
|
533
|
+
return info.element
|
|
534
|
+
if info.container == "str":
|
|
535
|
+
return _STRING
|
|
536
|
+
if info.container == "dict":
|
|
537
|
+
return _SCALAR
|
|
538
|
+
if info.items:
|
|
539
|
+
element = info.items[0]
|
|
540
|
+
for item in info.items[1:]:
|
|
541
|
+
element = _merge(element, item)
|
|
542
|
+
return element
|
|
543
|
+
return _READONLY if info.origin == "readonly" else _UNKNOWN
|
|
544
|
+
|
|
545
|
+
def target(self, node, info, f):
|
|
546
|
+
if isinstance(node, ast.Name):
|
|
547
|
+
# A join must never turn the AnnoScript capability into ordinary data.
|
|
548
|
+
if node.id in f.env and (f.env[node.id] == _CONTEXT) != (info == _CONTEXT):
|
|
549
|
+
self.fail(f, node, "Cannot rebind a AnnoScript context to data or vice versa")
|
|
550
|
+
f.env[node.id] = info
|
|
551
|
+
return ir.NameTarget(f.names[node.id])
|
|
552
|
+
if isinstance(node, (ast.Tuple, ast.List)):
|
|
553
|
+
if info.items is not None and len(info.items) == len(node.elts):
|
|
554
|
+
infos = info.items
|
|
555
|
+
else:
|
|
556
|
+
infos = (self.element(info),) * len(node.elts)
|
|
557
|
+
return ir.TupleTarget(tuple(self.target(n, i, f) for n, i in zip(node.elts, infos)))
|
|
558
|
+
self.fail(f, node, "Unpacking and loop targets must contain only names")
|
|
559
|
+
|
|
560
|
+
def assign(self, target, value, out, f):
|
|
561
|
+
if isinstance(target, ast.Subscript):
|
|
562
|
+
receiver = self.expr(target.value, out, f)
|
|
563
|
+
index = self.expr(target.slice, out, f)
|
|
564
|
+
if receiver.info.origin != "local" or receiver.info.container not in ("list", "dict"):
|
|
565
|
+
self.fail(f, target, "Only provably local list/dict containers may be modified; inputs and captures are read-only")
|
|
566
|
+
out.append(self.stmt(ir.SetItem, f, target, receiver.expr, self.data(index, f, target), self.data(value, f, target)))
|
|
567
|
+
return
|
|
568
|
+
compiled = self.target(target, value.info, f)
|
|
569
|
+
if value.info == _CONTEXT:
|
|
570
|
+
if not isinstance(compiled, ir.NameTarget):
|
|
571
|
+
self.fail(f, target, "Cannot unpack a AnnoScript context")
|
|
572
|
+
return
|
|
573
|
+
expression = self.data(value, f, target)
|
|
574
|
+
if isinstance(compiled, ir.NameTarget):
|
|
575
|
+
out.append(self.stmt(ir.Assign, f, target, compiled.name, expression))
|
|
576
|
+
else:
|
|
577
|
+
out.append(self.stmt(ir.Unpack, f, target, compiled, expression))
|
|
578
|
+
|
|
579
|
+
def block(self, nodes, f):
|
|
580
|
+
out = []
|
|
581
|
+
returned = False
|
|
582
|
+
for index, node in enumerate(nodes):
|
|
583
|
+
falls, may_return = self.statement(node, out, f)
|
|
584
|
+
returned |= may_return
|
|
585
|
+
if not falls:
|
|
586
|
+
if index + 1 < len(nodes):
|
|
587
|
+
self.fail(f, nodes[index + 1], "Unreachable statements after an unconditional return are not supported")
|
|
588
|
+
return tuple(out), False, returned
|
|
589
|
+
if may_return and f.done is not None and index + 1 < len(nodes):
|
|
590
|
+
suffix, falls, suffix_returns = self.block(nodes[index + 1:], f)
|
|
591
|
+
out.append(self.stmt(ir.If, f, node, ir.Unary("not", ir.Var(f.done)), suffix))
|
|
592
|
+
return tuple(out), falls, returned or suffix_returns
|
|
593
|
+
return tuple(out), True, returned
|
|
594
|
+
|
|
595
|
+
def statement(self, node, out, f):
|
|
596
|
+
if isinstance(node, ast.Expr):
|
|
597
|
+
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
598
|
+
return True, False # docstring
|
|
599
|
+
call = node.value
|
|
600
|
+
if isinstance(call, ast.Call) and isinstance(call.func, ast.Name) and call.func.id not in f.names:
|
|
601
|
+
if self.raw_name(f, call.func) is constrain:
|
|
602
|
+
args, kwargs = self.arguments(call, out, f, True)
|
|
603
|
+
bound = self.bind(inspect.signature(constrain), args, kwargs, f, call)
|
|
604
|
+
condition = self.data(bound.arguments["condition"], f, call)
|
|
605
|
+
message = bound.arguments.get("message")
|
|
606
|
+
out.append(self.stmt(ir.Constrain, f, node, condition, self.data(message, f, call) if message else None))
|
|
607
|
+
return True, False
|
|
608
|
+
value = self.expr(node.value, out, f)
|
|
609
|
+
self.data(value, f, node)
|
|
610
|
+
return True, False
|
|
611
|
+
if isinstance(node, ast.Assign):
|
|
612
|
+
value = self.expr(node.value, out, f)
|
|
613
|
+
# Evaluate the RHS once even when a target rebinds a referenced name.
|
|
614
|
+
value = self.materialize(value, out, f, node)
|
|
615
|
+
for target in node.targets:
|
|
616
|
+
self.assign(target, value, out, f)
|
|
617
|
+
return True, False
|
|
618
|
+
if isinstance(node, ast.If):
|
|
619
|
+
condition = self.data(self.expr(node.test, out, f), f, node.test)
|
|
620
|
+
a, b = f.fork(), f.fork()
|
|
621
|
+
yes, a_falls, a_returns = self.block(node.body, a)
|
|
622
|
+
no, b_falls, b_returns = self.block(node.orelse, b)
|
|
623
|
+
if a_falls and b_falls:
|
|
624
|
+
# Context/data mixtures cannot be represented as ordinary values.
|
|
625
|
+
for name in a.env.keys() & b.env.keys():
|
|
626
|
+
if (a.env[name] == _CONTEXT) != (b.env[name] == _CONTEXT):
|
|
627
|
+
self.fail(f, node, f"Inconsistent AnnoScript context binding across branches: {name}")
|
|
628
|
+
f.env = _merge_env(a.env, b.env)
|
|
629
|
+
elif a_falls:
|
|
630
|
+
f.env = a.env
|
|
631
|
+
elif b_falls:
|
|
632
|
+
f.env = b.env
|
|
633
|
+
out.append(self.stmt(ir.If, f, node, condition, yes, no))
|
|
634
|
+
return a_falls or b_falls, a_returns or b_returns
|
|
635
|
+
if isinstance(node, ast.For):
|
|
636
|
+
if node.orelse:
|
|
637
|
+
self.fail(f, node, "for/else is not supported")
|
|
638
|
+
iterable = self.expr(node.iter, out, f)
|
|
639
|
+
self.data(iterable, f, node)
|
|
640
|
+
# Compute a conservative loop invariant before accepting writes. This
|
|
641
|
+
# catches aliases that become read-only on a later iteration.
|
|
642
|
+
initial = dict(f.env)
|
|
643
|
+
invariant = initial
|
|
644
|
+
while True:
|
|
645
|
+
body_frame = f.fork()
|
|
646
|
+
body_frame.env = dict(invariant)
|
|
647
|
+
body_frame.loop_depth += 1
|
|
648
|
+
target = self.target(node.target, self.element(iterable.info), body_frame)
|
|
649
|
+
body, _, _ = self.block(node.body, body_frame)
|
|
650
|
+
merged = _merge_env(initial, body_frame.env)
|
|
651
|
+
merged = _merge_env(invariant, merged)
|
|
652
|
+
if merged == invariant:
|
|
653
|
+
break
|
|
654
|
+
invariant = merged
|
|
655
|
+
f.env = invariant
|
|
656
|
+
out.append(self.stmt(ir.For, f, node, target, iterable.expr, body))
|
|
657
|
+
return True, False
|
|
658
|
+
if isinstance(node, ast.Return):
|
|
659
|
+
if f.loop_depth:
|
|
660
|
+
self.fail(f, node, "Returns inside loops are not supported")
|
|
661
|
+
value = self.expr(node.value, out, f) if node.value else _Value(ir.Literal(None), _SCALAR)
|
|
662
|
+
expression = self.data(value, f, node)
|
|
663
|
+
if f.result is None:
|
|
664
|
+
out.append(self.stmt(ir.Return, f, node, expression))
|
|
665
|
+
else:
|
|
666
|
+
out.append(self.stmt(ir.Assign, f, node, f.result, expression))
|
|
667
|
+
out.append(self.stmt(ir.Assign, f, node, f.done, ir.Literal(True)))
|
|
668
|
+
f.return_infos.append(value.info)
|
|
669
|
+
return False, True
|
|
670
|
+
if isinstance(node, ast.Pass):
|
|
671
|
+
return True, False
|
|
672
|
+
self.fail(f, node, f"Unsupported statement: {type(node).__name__}")
|
|
673
|
+
|
|
674
|
+
def compile(self):
|
|
675
|
+
parsed = self.parse(self.entry)
|
|
676
|
+
parameters = list(parsed.signature.parameters.values())
|
|
677
|
+
if len(parameters) != 1 or parameters[0].kind not in (
|
|
678
|
+
inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD
|
|
679
|
+
) or parameters[0].default is not inspect.Parameter.empty:
|
|
680
|
+
raise CompileError("Entry must accept exactly one positional AnnoScript parameter without a default")
|
|
681
|
+
frame = self.frame(parsed)
|
|
682
|
+
frame.env[parameters[0].name] = _CONTEXT
|
|
683
|
+
self.decorators(frame, entry=True)
|
|
684
|
+
self.active.append(self.entry)
|
|
685
|
+
body, falls, _ = self.block(parsed.node.body, frame)
|
|
686
|
+
if falls:
|
|
687
|
+
body += (self.stmt(ir.Return, frame, parsed.node, ir.Literal(None)),)
|
|
688
|
+
config = get_program_config(self.entry)
|
|
689
|
+
program = ir.Program(
|
|
690
|
+
self.entry.__qualname__, ir.ProgramConfig(**config), body, self.span(frame, parsed.node)
|
|
691
|
+
)
|
|
692
|
+
program.to_dict() # ensure the emitted tree is serializable
|
|
693
|
+
return program
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def compile_program(program: Callable[[Anno], JSONValue], *, optimize: bool = True) -> ir.Program:
|
|
697
|
+
"""Compile one function and snapshot its reachable constants/configuration.
|
|
698
|
+
|
|
699
|
+
Importing the function is the caller's responsibility. Compilation performs
|
|
700
|
+
no input validation, model calls, user function execution or remote work.
|
|
701
|
+
Unsupported programs raise CompileError instead of falling back to Python.
|
|
702
|
+
Set optimize=False to inspect the original lowering before optimization.
|
|
703
|
+
"""
|
|
704
|
+
result = _Compiler(program).compile()
|
|
705
|
+
if optimize:
|
|
706
|
+
from annoscript.passes import optimize_program
|
|
707
|
+
result = optimize_program(result)
|
|
708
|
+
return result
|