annoscript 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. annoscript/__init__.py +1 -0
  2. annoscript/_backend_profiles.py +17 -0
  3. annoscript/_client_common.py +287 -0
  4. annoscript/_data.py +37 -0
  5. annoscript/_dispatch_pool.py +47 -0
  6. annoscript/_env.py +19 -0
  7. annoscript/_examples.py +34 -0
  8. annoscript/_expressions.py +93 -0
  9. annoscript/_plan_viewer.html +953 -0
  10. annoscript/_remote_jobs.py +211 -0
  11. annoscript/_request_adapters.py +71 -0
  12. annoscript/_speculation.py +321 -0
  13. annoscript/_test_artifacts.py +33 -0
  14. annoscript/_test_evaluation_http.py +117 -0
  15. annoscript/_test_ir_runtime.py +138 -0
  16. annoscript/_test_job_process.py +166 -0
  17. annoscript/_test_plan_runtime.py +331 -0
  18. annoscript/_test_remote_client.py +46 -0
  19. annoscript/_test_remote_server.py +75 -0
  20. annoscript/_test_synthetic.py +67 -0
  21. annoscript/anno.py +270 -0
  22. annoscript/backends/__init__.py +13 -0
  23. annoscript/backends/_batch_wire.py +26 -0
  24. annoscript/backends/_bounded_grammar.py +97 -0
  25. annoscript/backends/_example_layout.py +23 -0
  26. annoscript/backends/_generation_policy.py +116 -0
  27. annoscript/backends/_grammar.py +83 -0
  28. annoscript/backends/_preview.py +13 -0
  29. annoscript/backends/_prompt.py +115 -0
  30. annoscript/backends/_shared_rules.py +38 -0
  31. annoscript/backends/adapters/__init__.py +17 -0
  32. annoscript/backends/adapters/chat.py +39 -0
  33. annoscript/backends/adapters/decision.py +119 -0
  34. annoscript/backends/base.py +427 -0
  35. annoscript/backends/config.py +67 -0
  36. annoscript/backends/decision.py +150 -0
  37. annoscript/backends/llm.py +338 -0
  38. annoscript/client.py +247 -0
  39. annoscript/client_tests.py +17 -0
  40. annoscript/compiler.py +708 -0
  41. annoscript/cross_version_tests.py +68 -0
  42. annoscript/fixtures/request-adapters-v1.json +4741 -0
  43. annoscript/formatter.py +171 -0
  44. annoscript/grammar_tests.py +176 -0
  45. annoscript/inspect.py +77 -0
  46. annoscript/ir.py +458 -0
  47. annoscript/job_cross_version_tests.py +118 -0
  48. annoscript/job_protocol.py +180 -0
  49. annoscript/jobs.py +255 -0
  50. annoscript/metrics.py +153 -0
  51. annoscript/p0_grammar_tests.py +147 -0
  52. annoscript/passes/__init__.py +40 -0
  53. annoscript/passes/_analysis.py +178 -0
  54. annoscript/passes/control_flow.py +68 -0
  55. annoscript/passes/dead_assignments.py +44 -0
  56. annoscript/passes/folding.py +104 -0
  57. annoscript/passes/propagation.py +53 -0
  58. annoscript/passes/temporaries.py +45 -0
  59. annoscript/plan.py +542 -0
  60. annoscript/plan_formatter.py +452 -0
  61. annoscript/plan_lowering.py +571 -0
  62. annoscript/preview.py +115 -0
  63. annoscript/remote.py +304 -0
  64. annoscript/runtime.py +861 -0
  65. annoscript/server/__init__.py +1 -0
  66. annoscript/server/__main__.py +49 -0
  67. annoscript/server/_plan_cache.py +79 -0
  68. annoscript/server/api.py +146 -0
  69. annoscript/server/metrics.py +140 -0
  70. annoscript/server/migrate.py +61 -0
  71. annoscript/server/releases.py +143 -0
  72. annoscript/server/store.py +429 -0
  73. annoscript/server/worker.py +290 -0
  74. annoscript/substring_tests.py +79 -0
  75. annoscript/test_advisory_lengths.py +115 -0
  76. annoscript/test_backends.py +181 -0
  77. annoscript/test_client.py +349 -0
  78. annoscript/test_compact_batch.py +95 -0
  79. annoscript/test_compiler.py +444 -0
  80. annoscript/test_decision_adapters.py +410 -0
  81. annoscript/test_decision_wire.py +190 -0
  82. annoscript/test_diagnostics.py +97 -0
  83. annoscript/test_engineering_p0.py +191 -0
  84. annoscript/test_environment.py +65 -0
  85. annoscript/test_example_backends.py +251 -0
  86. annoscript/test_examples.py +149 -0
  87. annoscript/test_field_names.py +221 -0
  88. annoscript/test_formatter.py +153 -0
  89. annoscript/test_fusion_scheduling.py +109 -0
  90. annoscript/test_generation_policy.py +180 -0
  91. annoscript/test_ir.py +148 -0
  92. annoscript/test_jobs.py +371 -0
  93. annoscript/test_jobs_server.py +511 -0
  94. annoscript/test_llm_batch.py +428 -0
  95. annoscript/test_metrics.py +59 -0
  96. annoscript/test_metrics_server.py +261 -0
  97. annoscript/test_minimal_fusion.py +59 -0
  98. annoscript/test_nullable_generate.py +169 -0
  99. annoscript/test_passes.py +408 -0
  100. annoscript/test_plan.py +522 -0
  101. annoscript/test_plan_constraints.py +305 -0
  102. annoscript/test_plan_formatter.py +272 -0
  103. annoscript/test_prompt_layout.py +140 -0
  104. annoscript/test_qwen_backend.py +181 -0
  105. annoscript/test_qwen_pilot_regression.py +180 -0
  106. annoscript/test_release_client.py +20 -0
  107. annoscript/test_remote.py +308 -0
  108. annoscript/test_request_adapters.py +295 -0
  109. annoscript/test_requirements.py +228 -0
  110. annoscript/test_rule_dedup.py +67 -0
  111. annoscript/test_runtime.py +314 -0
  112. annoscript/test_runtime_client.py +133 -0
  113. annoscript/test_source_fields.py +111 -0
  114. annoscript/test_speculation.py +458 -0
  115. annoscript/test_substring_backend.py +81 -0
  116. annoscript/test_synthetic_example.py +61 -0
  117. annoscript-0.1.0.dist-info/METADATA +145 -0
  118. annoscript-0.1.0.dist-info/RECORD +128 -0
  119. annoscript-0.1.0.dist-info/WHEEL +4 -0
  120. annoscript-0.1.0.dist-info/licenses/LICENSE +21 -0
  121. annoscript_examples/__init__.py +1 -0
  122. annoscript_examples/fixtures.py +3 -0
  123. annoscript_examples/request.execution_plan.json +3017 -0
  124. annoscript_examples/request.json +858 -0
  125. annoscript_examples/request.py +37 -0
  126. annoscript_examples/request_preview.decision.json +23 -0
  127. annoscript_examples/request_preview.generation.json +20 -0
  128. annoscript_examples/run.py +24 -0
@@ -0,0 +1,45 @@
1
+ """Fuse a single-use assignment into the next whole-expression consumer.
2
+
3
+ No computation crosses another statement or enters a conditional subexpression.
4
+ Global static use/definition counts deliberately reject ambiguous bindings.
5
+ """
6
+
7
+ from collections import Counter
8
+ from dataclasses import replace
9
+
10
+ from annoscript import ir
11
+ from ._analysis import definitions, direct_reads, walk_statements
12
+
13
+
14
+ def merge_temporaries(program: ir.Program) -> ir.Program:
15
+ uses, writes = Counter(), Counter()
16
+ for statement in walk_statements(program.body):
17
+ uses.update(direct_reads(statement))
18
+ writes.update(definitions(statement))
19
+
20
+ def block(body):
21
+ output = []
22
+ for statement in body:
23
+ if isinstance(statement, ir.If):
24
+ statement = replace(statement, then_body=block(statement.then_body), else_body=block(statement.else_body))
25
+ elif isinstance(statement, ir.For):
26
+ statement = replace(statement, body=block(statement.body))
27
+ field = None
28
+ if isinstance(statement, (ir.Assign, ir.Return)):
29
+ field = "value"
30
+ elif isinstance(statement, ir.If):
31
+ field = "condition"
32
+ elif isinstance(statement, ir.For):
33
+ field = "iterable"
34
+ while field and output and isinstance(output[-1], ir.Assign):
35
+ previous = output[-1]
36
+ value = getattr(statement, field)
37
+ if not (isinstance(value, ir.Var) and value.name == previous.target
38
+ and uses[value.name] == writes[value.name] == 1):
39
+ break
40
+ statement = replace(statement, **{field: previous.value})
41
+ output.pop()
42
+ output.append(statement)
43
+ return tuple(output)
44
+
45
+ return replace(program, body=block(program.body))
annoscript/plan.py ADDED
@@ -0,0 +1,542 @@
1
+ """Serializable dependency templates for a future dynamic AnnoScript scheduler.
2
+
3
+ Tasks are instantiated per request and loop coordinate. An edge's common loop
4
+ coordinates match through its carried loop (or the entire common prefix when
5
+ there is no carried loop), with the specified positive distance subtracted.
6
+ Deeper source coordinates are gathered and deeper destination coordinates are
7
+ broadcast, including nested loops whose trip counts vary between outer rounds.
8
+ Inactive control paths satisfy completion edges, but never produce values.
9
+
10
+ Infer sites demand a shared request, *after* their own dependencies are ready.
11
+ Sharing is local to a request, reuse scope, and dependency/repair generation.
12
+ It is not eager hoisting, global memoization, retrying, or a batch assignment.
13
+ Response values are read-only. Identity-observing programs disable sharing.
14
+
15
+ Eager execution omits failure_order and explicitly strict_only ordering edges,
16
+ allowing independent work to pass pending checks. Strict execution includes them,
17
+ preserving the sequential failure prefix (with equivalent requests shared).
18
+ Both report the first observed error, stop dispatch on failure, best-effort cancel
19
+ in-flight work, and await all active work, including checks, before success.
20
+ Resource exhaustion is outside the static expression safety model.
21
+
22
+ Speculative execution uses the same formal dependencies and serial loop rules
23
+ as eager. A separate bounded evaluator first speculates Decision requests across
24
+ unknown control gates, then batches generation requests on confirmed paths.
25
+ It never commits local effects or checks. Results and request-local errors are
26
+ adopted only on demand with matching arguments.
27
+
28
+ BoundExpr captures value bindings, not copies of their containers. State and
29
+ inference responses are read-only; Store addresses local storage. Branch.done
30
+ means its predicate is known; Join.done means its selected region has completed,
31
+ and Join.continue is false if that path returned (including nested returns).
32
+ Loop.enter activates iteration regions; Loop.done gathers all active iterations
33
+ and publishes exports. serial_modes additionally orders iterator advancement
34
+ after the preceding region finishes, preserving iterator faults in strict mode
35
+ and necessary mutable-iterator ordering in both modes.
36
+ Carry uses its initial value in iteration zero and update from the prior round.
37
+ Merge reads only the selected arm. Return supplies a candidate result: success
38
+ also requires completion of all activated required work in the root region.
39
+
40
+ Checks validate captured expressions on their original paths. Data, control and
41
+ memory dependencies apply in both modes; source-order check barriers apply only
42
+ in strict mode. A check alone does not serialize eager loop iterations. Checks
43
+ do not introduce solving scopes or authorize changing branch choices.
44
+ Any future repair must invalidate downstream values and shared request caches
45
+ for the affected dependency generation; this module implements no repair.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from dataclasses import MISSING, dataclass, fields, is_dataclass
51
+ import json
52
+ from types import UnionType
53
+ from typing import ClassVar, Literal, Union, get_args, get_origin, get_type_hints
54
+
55
+ from annoscript import ir
56
+
57
+ FailureMode = Literal["eager", "strict", "speculative"]
58
+
59
+
60
+ class PlanDecodeError(ValueError):
61
+ def __init__(self, message, path="$"):
62
+ self.path = path
63
+ super().__init__(f"{path}: {message}")
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class Binding:
68
+ name: str
69
+ value: str
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class BoundExpr:
74
+ expression: ir.Expr
75
+ bindings: tuple[Binding, ...] = ()
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Gate:
80
+ task: str
81
+ port: Literal["true", "false", "continue", "enter"]
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class Region:
86
+ id: str
87
+ parent: str | None
88
+ kind: Literal["root", "then", "else", "loop"]
89
+ owner: str | None
90
+ tasks: tuple[str, ...]
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class Value:
95
+ id: str
96
+ producer: str
97
+ name: str | None = None
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class InstanceMap:
102
+ source_loops: tuple[str, ...] = ()
103
+ target_loops: tuple[str, ...] = ()
104
+ carried_loop: str | None = None
105
+ distance: int = 0
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class Dependency:
110
+ source: str
111
+ target: str
112
+ kind: Literal["data", "control", "memory", "constraint", "failure_order"]
113
+ mapping: InstanceMap
114
+ port: Literal["done", "enter", "true", "false", "continue"] = "done"
115
+ value: str | None = None
116
+ hazard: Literal["RAW", "WAR", "WAW"] | None = None
117
+ certainty: Literal["proven", "conservative"] = "proven"
118
+ condition: Gate | None = None
119
+ seed: str | None = None
120
+ # Constraint ordering can be relaxed without dropping check input/memory
121
+ # dependencies. Absent flags retain the stronger semantics of older plans.
122
+ # failure_order edges are always strict-only, including legacy plans.
123
+ strict_only: bool = False
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class InferRequest:
128
+ id: str
129
+ operation: Literal["noul", "choice", "score", "extract", "generate", "generate_list"]
130
+ scope: tuple[str, ...]
131
+ sites: tuple[str, ...]
132
+ reusable: bool
133
+
134
+
135
+ @dataclass(frozen=True, kw_only=True)
136
+ class Task(ir.Node):
137
+ id: str
138
+ region: str
139
+ origin: str
140
+ source: ir.Source
141
+ outputs: tuple[str, ...] = ()
142
+ gates: tuple[Gate, ...] = ()
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class Eval(Task):
147
+ kind: ClassVar[str] = "eval"
148
+ expression: BoundExpr
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class Unpack(Task):
153
+ kind: ClassVar[str] = "unpack"
154
+ expression: BoundExpr
155
+ target: ir.TupleTarget
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class Store(Task):
160
+ kind: ClassVar[str] = "store"
161
+ container: BoundExpr
162
+ index: BoundExpr
163
+ item: BoundExpr
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class Infer(Task):
168
+ kind: ClassVar[str] = "infer_site"
169
+ request: str
170
+ arguments: str # Eval output: (positional tuple, keyword-value tuple)
171
+ keywords: tuple[str, ...]
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class Branch(Task):
176
+ kind: ClassVar[str] = "branch"
177
+ condition: BoundExpr
178
+ then_region: str
179
+ else_region: str
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class Join(Task):
184
+ kind: ClassVar[str] = "join"
185
+ branch: str
186
+
187
+
188
+ @dataclass(frozen=True)
189
+ class Merge(Task):
190
+ kind: ClassVar[str] = "merge"
191
+ branch: str
192
+ then_value: str | None
193
+ else_value: str | None
194
+
195
+
196
+ @dataclass(frozen=True)
197
+ class Carry(Task):
198
+ kind: ClassVar[str] = "carry"
199
+ loop: str
200
+ initial: str
201
+ update: str
202
+
203
+
204
+ @dataclass(frozen=True)
205
+ class Iteration(Task):
206
+ kind: ClassVar[str] = "iteration"
207
+ loop: str
208
+ target: ir.Target
209
+
210
+
211
+ @dataclass(frozen=True)
212
+ class LoopExport:
213
+ output: str
214
+ initial: str
215
+ update: str
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class Loop(Task):
220
+ kind: ClassVar[str] = "loop"
221
+ iterable: BoundExpr
222
+ body: str
223
+ exports: tuple[LoopExport, ...]
224
+ # Iterator advancement waits for the preceding body in these modes.
225
+ # This is independent of the data-carry edges between individual tasks.
226
+ serial_modes: tuple[FailureMode, ...] = ("strict",)
227
+
228
+
229
+ @dataclass(frozen=True)
230
+ class Check(Task):
231
+ kind: ClassVar[str] = "check"
232
+ condition: BoundExpr
233
+ message: BoundExpr | None
234
+
235
+
236
+ @dataclass(frozen=True)
237
+ class Return(Task):
238
+ kind: ClassVar[str] = "return"
239
+ expression: BoundExpr
240
+
241
+
242
+ _TASKS = {t.kind: t for t in (Eval, Unpack, Store, Infer, Branch, Join, Merge,
243
+ Carry, Iteration, Loop, Check, Return)}
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class ExecutionPlan:
248
+ entry: str
249
+ config: ir.ProgramConfig
250
+ source: ir.SourceSpan
251
+ regions: tuple[Region, ...]
252
+ tasks: tuple[Task, ...]
253
+ values: tuple[Value, ...]
254
+ dependencies: tuple[Dependency, ...]
255
+ requests: tuple[InferRequest, ...]
256
+ format: str = "lev-execution-plan"
257
+ version: int = 1
258
+
259
+ def dependencies_for(self, mode: FailureMode = "eager") -> tuple[Dependency, ...]:
260
+ """Select edges; region, loop, demand and root-completion rules still apply."""
261
+ if mode not in ("eager", "strict", "speculative"):
262
+ raise ValueError(f"unknown failure mode {mode!r}")
263
+ return tuple(d for d in self.dependencies
264
+ if mode == "strict" or (d.kind != "failure_order" and not d.strict_only))
265
+
266
+ def to_dict(self) -> dict:
267
+ return ir._encode(self)
268
+
269
+ def to_json(self, *, indent: int | None = 2) -> str:
270
+ return json.dumps(self.to_dict(), ensure_ascii=False, allow_nan=False, indent=indent)
271
+
272
+ @classmethod
273
+ def from_dict(cls, data: dict) -> ExecutionPlan:
274
+ try:
275
+ plan = _decode(data, cls, "$")
276
+ plan.validate()
277
+ return plan
278
+ except (ir.IRDecodeError, RecursionError) as exc:
279
+ raise PlanDecodeError(str(exc)) from exc
280
+
281
+ @classmethod
282
+ def from_json(cls, text: str) -> ExecutionPlan:
283
+ def pairs(items):
284
+ result = {}
285
+ for key, value in items:
286
+ if key in result:
287
+ raise PlanDecodeError(f"duplicate JSON key {key!r}")
288
+ result[key] = value
289
+ return result
290
+
291
+ def constant(value):
292
+ raise PlanDecodeError(f"non-finite number {value}")
293
+
294
+ try:
295
+ return cls.from_dict(json.loads(text, object_pairs_hook=pairs, parse_constant=constant))
296
+ except (json.JSONDecodeError, RecursionError, UnicodeError) as exc:
297
+ raise PlanDecodeError(f"invalid JSON: {exc}") from exc
298
+
299
+ def validate(self) -> None:
300
+ """Check references, ownership, availability, seeded carries and DAGs."""
301
+ _validate(self)
302
+
303
+
304
+ def _decode(value, expected, path):
305
+ origin, args = get_origin(expected), get_args(expected)
306
+ if origin in (Union, UnionType):
307
+ if value is None and type(None) in args:
308
+ return None
309
+ return _decode(value, next(t for t in args if t is not type(None)), path)
310
+ if origin is Literal:
311
+ if not any(type(value) is type(a) and value == a for a in args):
312
+ raise PlanDecodeError(f"expected one of {args}", path)
313
+ return value
314
+ if origin is tuple:
315
+ if type(value) is not list:
316
+ raise PlanDecodeError("expected array", path)
317
+ return tuple(_decode(v, args[0], f"{path}[{i}]") for i, v in enumerate(value))
318
+ if expected in (str, int, bool, type(None)):
319
+ if type(value) is not expected:
320
+ raise PlanDecodeError(f"expected {expected.__name__}", path)
321
+ return value
322
+ if isinstance(expected, type) and expected.__module__ == ir.__name__:
323
+ return ir._Decoder().decode(value, expected, path)
324
+ if type(value) is not dict:
325
+ raise PlanDecodeError("expected object", path)
326
+ cls = expected
327
+ tagged = isinstance(expected, type) and issubclass(expected, Task)
328
+ if tagged:
329
+ kind = value.get("kind")
330
+ cls = _TASKS.get(kind) if type(kind) is str else None
331
+ if cls is None or not issubclass(cls, expected):
332
+ raise PlanDecodeError("unknown or incorrect task kind", path)
333
+ if not is_dataclass(cls):
334
+ raise TypeError(f"unknown plan schema type {cls}")
335
+ definitions = fields(cls)
336
+ unknown = value.keys() - ({f.name for f in definitions} | ({"kind"} if tagged else set()))
337
+ if unknown:
338
+ raise PlanDecodeError(f"unknown fields {sorted(unknown)}", path)
339
+ types, kwargs = get_type_hints(cls), {}
340
+ for f in definitions:
341
+ if f.name in value:
342
+ kwargs[f.name] = _decode(value[f.name], types[f.name], f"{path}.{f.name}")
343
+ elif f.default is MISSING and f.default_factory is MISSING:
344
+ raise PlanDecodeError("missing required field", f"{path}.{f.name}")
345
+ return cls(**kwargs)
346
+
347
+
348
+ def expressions(task):
349
+ return tuple(getattr(task, f.name) for f in fields(task)
350
+ if isinstance(getattr(task, f.name), BoundExpr))
351
+
352
+
353
+ def loop_path(region, regions):
354
+ result = []
355
+ while region is not None:
356
+ r = regions[region]
357
+ if r.kind == "loop":
358
+ result.append(r.owner)
359
+ region = r.parent
360
+ return tuple(reversed(result))
361
+
362
+
363
+ def _validate(plan):
364
+ from annoscript.passes._analysis import expression_reads
365
+
366
+ def require(condition, message, path="$"):
367
+ if not condition:
368
+ raise PlanDecodeError(message, path)
369
+
370
+ def index(items, name):
371
+ result = {x.id: x for x in items}
372
+ require(len(result) == len(items) and all(result), "IDs must be unique and nonempty", name)
373
+ return result
374
+
375
+ require(plan.format == "lev-execution-plan" and type(plan.version) is int and plan.version == 1,
376
+ "unsupported execution plan format/version")
377
+ ts, rs, vs, qs = (index(getattr(plan, name), f"$.{name}")
378
+ for name in ("tasks", "regions", "values", "requests"))
379
+ require(len(set(ts) | set(rs) | set(vs) | set(qs)) == len(ts) + len(rs) + len(vs) + len(qs),
380
+ "identifiers must be unique across the plan")
381
+ require(sum(r.kind == "root" for r in rs.values()) == 1, "expected one root region")
382
+ for r in rs.values():
383
+ require((r.parent is None) == (r.kind == "root"), "incorrect region parent", r.id)
384
+ require(r.kind != "root" or r.owner is None, "root region cannot have an owner", r.id)
385
+ require(r.parent is None or r.parent in rs, "unknown parent region", r.id)
386
+ seen, cur = set(), r
387
+ while cur.parent is not None:
388
+ require(cur.id not in seen, "cyclic region hierarchy", r.id)
389
+ seen.add(cur.id)
390
+ require(cur.parent in rs, "unknown ancestor region", r.id)
391
+ cur = rs[cur.parent]
392
+ require(len(set(r.tasks)) == len(r.tasks), "duplicate region task", r.id)
393
+ require(all(t in ts and ts[t].region == r.id for t in r.tasks), "incorrect task ownership", r.id)
394
+ if r.parent is not None:
395
+ require(r.owner in ts and ts[r.owner].region == r.parent, "incorrect region owner", r.id)
396
+ owner = ts[r.owner]
397
+ require((isinstance(owner, Loop) and r.kind == "loop" and owner.body == r.id) or
398
+ (isinstance(owner, Branch) and ((r.kind == "then" and owner.then_region == r.id) or
399
+ (r.kind == "else" and owner.else_region == r.id))),
400
+ "region is not selected by its owner", r.id)
401
+ for v in vs.values():
402
+ require(v.producer in ts and v.id in ts[v.producer].outputs, "invalid value producer", v.id)
403
+
404
+ def ref(v, where):
405
+ require(v in vs, f"unknown value {v!r}", where)
406
+
407
+ def ancestors(region):
408
+ result = set()
409
+ while region is not None:
410
+ result.add(region)
411
+ region = rs[region].parent
412
+ return result
413
+
414
+ def available(v, region, where):
415
+ ref(v, where)
416
+ require(ts[vs[v].producer].region in ancestors(region), "value is unavailable in this region", where)
417
+
418
+ def data_edge(v, target, condition=None):
419
+ require(any(d.target == target and d.value == v and d.kind == "data" and d.condition == condition
420
+ for d in plan.dependencies), "missing value dependency", target)
421
+
422
+ for t in ts.values():
423
+ require(t.region in rs and t.id in rs[t.region].tasks, "unlisted task", t.id)
424
+ require(bool(t.origin), "missing source statement identity", t.id)
425
+ require(len(set(t.outputs)) == len(t.outputs), "duplicate output", t.id)
426
+ if isinstance(t, (Eval, Infer, Merge, Carry)):
427
+ require(len(t.outputs) == 1, "task requires one output", t.id)
428
+ elif isinstance(t, (Store, Branch, Join, Check, Return)):
429
+ require(not t.outputs, "task cannot define outputs", t.id)
430
+ elif isinstance(t, (Unpack, Iteration)):
431
+ def count(target):
432
+ return 1 if isinstance(target, ir.NameTarget) else sum(count(x) for x in target.items)
433
+ require(count(t.target) == len(t.outputs), "unpack target/output mismatch", t.id)
434
+ for v in t.outputs:
435
+ ref(v, t.id)
436
+ require(vs[v].producer == t.id, "output owned by another task", t.id)
437
+ for b in expressions(t):
438
+ names = [v.name for v in b.bindings]
439
+ require(len(names) == len(set(names)) and set(names) == set(expression_reads(b.expression)),
440
+ "expression bindings do not match its variables", t.id)
441
+ for binding in b.bindings:
442
+ available(binding.value, t.region, t.id)
443
+ data_edge(binding.value, t.id)
444
+ for g in t.gates:
445
+ require(g.task in ts, "unknown gate task", t.id)
446
+ owner = ts[g.task]
447
+ require((g.port in ("true", "false") and isinstance(owner, Branch)) or
448
+ (g.port == "continue" and isinstance(owner, Join)) or
449
+ (g.port == "enter" and isinstance(owner, Loop)), "incorrect gate port", t.id)
450
+ require(any(d.source == g.task and d.target == t.id and d.port == g.port and d.kind == "control"
451
+ for d in plan.dependencies), "missing control dependency", t.id)
452
+ if isinstance(t, Branch):
453
+ for region, kind in ((t.then_region, "then"), (t.else_region, "else")):
454
+ require(region in rs and rs[region].kind == kind and rs[region].owner == t.id,
455
+ "invalid branch region", t.id)
456
+ if isinstance(t, (Join, Merge)):
457
+ require(isinstance(ts.get(t.branch), Branch), "unknown branch", t.id)
458
+ require(ts[t.branch].region == t.region, "branch and merge/join must share a region", t.id)
459
+ require(any(d.source == t.branch and d.target == t.id and d.kind == "control" and d.port == "done"
460
+ for d in plan.dependencies), "missing branch dependency", t.id)
461
+ if isinstance(t, Merge):
462
+ branch = ts[t.branch]
463
+ for v, arm, port in ((t.then_value, branch.then_region, "true"), (t.else_value, branch.else_region, "false")):
464
+ if v is not None:
465
+ ref(v, t.id)
466
+ producer_region = ts[vs[v].producer].region
467
+ eligible, r = {arm}, rs[t.region]
468
+ while True:
469
+ eligible.add(r.id)
470
+ if r.parent is None:
471
+ break
472
+ r = rs[r.parent]
473
+ require(producer_region in eligible, "merge value belongs to the wrong arm", t.id)
474
+ data_edge(v, t.id, Gate(t.branch, port))
475
+ require(t.then_value is not None or t.else_value is not None, "merge has no incoming values", t.id)
476
+ if isinstance(t, Loop):
477
+ require(t.body in rs and rs[t.body].owner == t.id and rs[t.body].kind == "loop", "invalid loop body", t.id)
478
+ require(set(t.serial_modes) <= {"strict", "eager"} and "strict" in t.serial_modes,
479
+ "strict iteration sequencing is required", t.id)
480
+ require(tuple(e.output for e in t.exports) == t.outputs, "loop exports do not match outputs", t.id)
481
+ for e in t.exports:
482
+ available(e.initial, t.region, t.id)
483
+ data_edge(e.initial, t.id)
484
+ ref(e.update, t.id)
485
+ require(ts[vs[e.update].producer].region == t.body, "loop update must be in its body", t.id)
486
+ if isinstance(t, (Carry, Iteration)):
487
+ require(isinstance(ts.get(t.loop), Loop) and ts[t.loop].body == t.region, "incorrect iteration owner", t.id)
488
+ if isinstance(t, Carry):
489
+ available(t.initial, rs[t.region].parent, t.id)
490
+ data_edge(t.initial, t.loop)
491
+ ref(t.update, t.id)
492
+ require(ts[vs[t.update].producer].region == t.region, "invalid carry update", t.id)
493
+ require(t.loop not in loop_path(ts[vs[t.initial].producer].region, rs), "carry seed must precede loop", t.id)
494
+ if isinstance(t, Infer):
495
+ available(t.arguments, t.region, t.id)
496
+ data_edge(t.arguments, t.id)
497
+ require(len(t.keywords) == len(set(t.keywords)) and all(t.keywords), "invalid keyword names", t.id)
498
+ require(t.request in qs and t.id in qs[t.request].sites, "invalid shared request", t.id)
499
+ for q in qs.values():
500
+ require(bool(q.sites) and len(q.sites) == len(set(q.sites)), "invalid request sites", q.id)
501
+ for site in q.sites:
502
+ require(isinstance(ts.get(site), Infer) and ts[site].request == q.id, "request/site mismatch", q.id)
503
+ path = loop_path(ts[site].region, rs)
504
+ require(path[:len(q.scope)] == q.scope, "request scope is not an enclosing scope", q.id)
505
+
506
+ graph = {t: set() for t in ts}
507
+ for d in plan.dependencies:
508
+ require(d.source in ts and d.target in ts, "unknown dependency endpoint")
509
+ require(type(d.strict_only) is bool, "strict_only must be a boolean", d.target)
510
+ require(not d.strict_only or d.kind in ("constraint", "failure_order"),
511
+ "data, control and memory dependencies cannot be strict-only", d.target)
512
+ m = d.mapping
513
+ require(m.source_loops == loop_path(ts[d.source].region, rs)
514
+ and m.target_loops == loop_path(ts[d.target].region, rs), "incorrect instance mapping", d.target)
515
+ require((m.carried_loop is None and m.distance == 0) or
516
+ (m.carried_loop in m.source_loops and m.carried_loop in m.target_loops and m.distance > 0),
517
+ "invalid carried distance", d.target)
518
+ if d.value is not None:
519
+ ref(d.value, d.target)
520
+ require(vs[d.value].producer == d.source, "dependency has wrong value producer", d.target)
521
+ if d.seed is not None:
522
+ ref(d.seed, d.target)
523
+ if d.condition:
524
+ require(isinstance(ts.get(d.condition.task), Branch), "invalid dependency condition", d.target)
525
+ require(d.condition.port in ("true", "false"), "invalid branch condition port", d.target)
526
+ if d.kind == "memory":
527
+ require(d.hazard is not None, "memory dependency requires a hazard", d.target)
528
+ else:
529
+ require(d.hazard is None, "non-memory dependency cannot carry a memory hazard", d.target)
530
+ if not m.distance and d.port == "done":
531
+ graph[d.target].add(d.source)
532
+ # Structural loop/join completion is not a normal same-instance graph edge.
533
+ remaining = {k: set(v) for k, v in graph.items()}
534
+ while remaining:
535
+ ready = {k for k, v in remaining.items() if not v}
536
+ require(bool(ready), "unseeded zero-distance dependency cycle")
537
+ remaining = {k: v - ready for k, v in remaining.items() if k not in ready}
538
+ for t in ts.values():
539
+ if isinstance(t, Carry):
540
+ require(any(d.target == t.id and d.value == t.update and d.seed == t.initial
541
+ and d.mapping.carried_loop == t.loop and d.mapping.distance == 1
542
+ for d in plan.dependencies), "carry requires a seeded previous-iteration edge", t.id)