pgc-runtime 2.0.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.
runtime/ct_execute.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ execute.py — CT-IR execution entry point.
3
+
4
+ Adapter between the dispatcher and CTExecutor.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from runtime.ct_errors import StructuredError
10
+ from runtime.ct_executor import CTExecutor
11
+
12
+ # Module-level executor singleton — avoids re-creating per call,
13
+ # preserves federated IR directory cache and atom loading.
14
+ # Lazy initialization to avoid import-time side effects (bootstrap requirement)
15
+ _executor: CTExecutor | None = None
16
+
17
+
18
+ def _get_executor() -> CTExecutor:
19
+ """Get or create the CT executor singleton (lazy initialization)."""
20
+ global _executor
21
+ if _executor is None:
22
+ _executor = CTExecutor()
23
+ return _executor
24
+
25
+
26
+ def execute_ct(ct_ir: dict[str, Any], inputs: dict[str, Any]) -> Any:
27
+ """
28
+ Execute CT-IR and adapt result to CC expectation.
29
+
30
+ CONTRACT (STRICT):
31
+ - CT-IR MUST be pre-validated by compiler before reaching execution
32
+ - CTExecutor blindly executes pre-validated CT-IR
33
+ - CT-IR declares exactly one output
34
+ - That output is returned directly (unwrapped)
35
+
36
+ SOVEREIGNTY: Execution layer is a blind executor.
37
+ Validation is compiler's responsibility.
38
+ """
39
+
40
+ # ---- Execution (CT-IR assumed pre-validated by compiler) ----
41
+ symbol_table = _get_executor().execute(
42
+ ct_ir=ct_ir,
43
+ inputs=inputs,
44
+ )
45
+
46
+ # ---- Output adaptation ----
47
+ outputs = ct_ir.get("outputs")
48
+ if not outputs:
49
+ raise StructuredError(
50
+ error_code="CT_EXECUTION_FAILED",
51
+ node_category="CT",
52
+ message="CT must declare at least one output",
53
+ )
54
+
55
+ # Build CT outputs by mapping from symbol table
56
+ ct_outputs = {}
57
+ for output_name, spec in outputs.items():
58
+ from_symbol = spec["from"]
59
+
60
+ if from_symbol not in symbol_table:
61
+ raise StructuredError(
62
+ error_code="CT_EXECUTION_FAILED",
63
+ node_category="CT",
64
+ message=f"CT output symbol '{from_symbol}' was not produced by CT execution",
65
+ )
66
+
67
+ symbol_value = symbol_table[from_symbol]
68
+
69
+ # Extract output field from atom result dict
70
+ # Atoms return a single dict containing all output fields
71
+ # e.g., {"valid": True, "failed_rule": None}
72
+ if isinstance(symbol_value, dict):
73
+ if output_name in symbol_value:
74
+ # Multi-output atom: extract this specific output field
75
+ symbol_value = symbol_value[output_name]
76
+ elif len(symbol_value) == 1:
77
+ # Single-output atom: unwrap the single field
78
+ symbol_value = next(iter(symbol_value.values()))
79
+
80
+ ct_outputs[output_name] = symbol_value
81
+
82
+ # Return contract-shaped outputs (always as dict, never unwrap)
83
+ # Contract declares output shape - return exactly that shape
84
+ return ct_outputs
runtime/ct_executor.py ADDED
@@ -0,0 +1,312 @@
1
+ import importlib
2
+ from typing import Any
3
+
4
+ from runtime.ct_errors import StructuredError
5
+
6
+ # importlib carve-out: permitted here for compile-time-sealed handler_ref execution.
7
+ # This is NOT discovery — the module path is embedded at compile time by materialize.py.
8
+ # Discovery via importlib is forbidden; execution of a sealed handler_ref is not.
9
+
10
+
11
+ class CTExecutionError(StructuredError):
12
+ def __init__(self, message: str):
13
+ super().__init__(
14
+ error_code="CT_EXECUTION_FAILED",
15
+ node_category="CT",
16
+ message=message,
17
+ )
18
+
19
+
20
+ class CTExecutor:
21
+ def __init__(self):
22
+ pass
23
+
24
+ # ---------------------------------------------------------
25
+ # Public entrypoint
26
+ # ---------------------------------------------------------
27
+
28
+ def execute(
29
+ self,
30
+ *,
31
+ ct_ir: dict[str, Any],
32
+ inputs: dict[str, Any],
33
+ ) -> dict[str, Any]:
34
+ """
35
+ Execute a CT-IR program.
36
+
37
+ Assumptions:
38
+ - ct_ir is already validated for host invariants
39
+ - atom_stream is structurally valid
40
+ """
41
+ ctx = _CTContext(inputs=inputs, input_types=ct_ir.get("input_types", {}))
42
+
43
+ steps: list[dict] = ct_ir.get("atom_stream")
44
+ if not steps:
45
+ raise CTExecutionError("CT-IR missing atom_stream")
46
+
47
+ for idx, step in enumerate(steps):
48
+ atom = step.get("atom")
49
+ if not atom:
50
+ raise CTExecutionError(
51
+ f"Missing atom at index {idx}"
52
+ )
53
+
54
+ # Check for loop construct
55
+ if "loop" in step:
56
+ self._execute_loop(ctx, step)
57
+ else:
58
+ # ---- IR format execution ----
59
+ # Pass complete step with all metadata + flattened args
60
+ # Step from IR has nested args, adapter expects flattened
61
+ invocation = {
62
+ **step, # Include all metadata (input_types, output_types, etc.)
63
+ **step.get("args", {}), # Flatten args to top level
64
+ "as": step.get("out") # Normalize output key
65
+ }
66
+
67
+ self._execute_handler_ref(ctx, invocation)
68
+
69
+ return ctx._vars
70
+
71
+ def _execute_handler_ref(self, ctx: "_CTContext | _LoopContext", step: dict[str, Any]) -> None:
72
+ """
73
+ Execute an atom step by dispatching to its compile-time-sealed handler_ref.
74
+
75
+ handler_ref is embedded in CT-IR at compile time by materialize.py.
76
+ No registry lookup. No discovery. Sealed module path only.
77
+ """
78
+ handler_ref = step.get("handler_ref")
79
+ if not handler_ref:
80
+ raise CTExecutionError(f"CT-IR step missing handler_ref: {step.get('atom')}")
81
+ module_path = handler_ref.get("module")
82
+ callable_name = handler_ref.get("callable")
83
+ if not module_path or not callable_name:
84
+ raise CTExecutionError(f"Incomplete handler_ref on step: {step.get('atom')}")
85
+
86
+ mod = importlib.import_module(module_path)
87
+ execute_fn = getattr(mod, callable_name)
88
+
89
+ # Adapter logic (migrated from atom_registry._register_execute_atom):
90
+ # Resolve $.path references; skip reserved and metadata keys.
91
+ RESERVED_KEYS = {"atom", "molecule", "kind", "as", "out", "loop", "args", "handler_ref", "input_types"}
92
+ resolved_inputs: dict[str, Any] = {}
93
+ for key, value in step.items():
94
+ if key in RESERVED_KEYS:
95
+ continue
96
+ if isinstance(value, str) and value.startswith("$."):
97
+ resolved_inputs[key] = ctx.resolve(value)
98
+ else:
99
+ resolved_inputs[key] = value
100
+
101
+ try:
102
+ result = execute_fn(inputs=resolved_inputs)
103
+ except CTExecutionError:
104
+ raise
105
+ except Exception as exc:
106
+ raise CTExecutionError(
107
+ f"Atom raised exception: {step.get('atom')}: {exc}"
108
+ ) from exc
109
+ if result is None:
110
+ raise CTExecutionError(f"Atom returned None: {step.get('atom')}")
111
+ out_key = step.get("as") or step.get("out")
112
+ if out_key:
113
+ ctx.set_value(out_key, result)
114
+
115
+ def _execute_loop(
116
+ self,
117
+ ctx: "_CTContext",
118
+ step: dict[str, Any],
119
+ ) -> None:
120
+ """Execute a loop construct."""
121
+ loop_spec = step["loop"]
122
+ out_key = step.get("out")
123
+
124
+ # Resolve the collection to iterate over
125
+ over_path = loop_spec.get("over")
126
+ collection = ctx.resolve(over_path) if over_path else []
127
+ if not isinstance(collection, (list, tuple)):
128
+ raise CTExecutionError(f"Loop 'over' must resolve to a list: {over_path}")
129
+
130
+ iterator_name = loop_spec.get("iterator", "item")
131
+
132
+ # Initialize accumulator
133
+ accumulator_spec = loop_spec.get("accumulator", {})
134
+ accumulator = {}
135
+ for key, value in accumulator_spec.items():
136
+ if isinstance(value, str) and value.startswith("$."):
137
+ # Resolve from results namespace
138
+ if value.startswith("$.results."):
139
+ var_path = value[10:] # Remove $.results.
140
+ parts = var_path.split(".", 1)
141
+ var_name = parts[0]
142
+ remaining = parts[1] if len(parts) > 1 else None
143
+ result = ctx.get_value(var_name)
144
+ if remaining and isinstance(result, dict):
145
+ for p in remaining.split("."):
146
+ result = result.get(p) if isinstance(result, dict) else None
147
+ accumulator[key] = result
148
+ else:
149
+ accumulator[key] = ctx.resolve(value)
150
+ else:
151
+ accumulator[key] = value
152
+
153
+ loop_inputs_spec = loop_spec.get("inputs", {})
154
+ update_spec = loop_spec.get("update_accumulator", {})
155
+
156
+ last_result = None
157
+
158
+ for item in collection:
159
+ # Build inputs for this iteration
160
+ loop_ctx = _LoopContext(ctx, accumulator, iterator_name, item)
161
+
162
+ # Resolve loop inputs
163
+ resolved_inputs = {}
164
+ for key, value in loop_inputs_spec.items():
165
+ if isinstance(value, str) and value.startswith("$."):
166
+ resolved_inputs[key] = loop_ctx.resolve(value)
167
+ else:
168
+ resolved_inputs[key] = value
169
+
170
+ # Build invocation (preserve ALL step metadata: input_types, output_types, etc.)
171
+ # Step from IR has nested args, adapter expects flattened
172
+ invocation = {
173
+ **step, # Include all metadata (input_types, output_types, loop, etc.)
174
+ **resolved_inputs, # Flatten resolved args to top level
175
+ "as": "__loop_result__" # Override output key for loop
176
+ }
177
+
178
+ self._execute_handler_ref(loop_ctx, invocation)
179
+ last_result = loop_ctx.get_value("__loop_result__")
180
+
181
+ # Update accumulator from results
182
+ for acc_key, result_path in update_spec.items():
183
+ if isinstance(result_path, str) and result_path.startswith("$.results."):
184
+ field = result_path[10:] # Remove $.results.
185
+ if isinstance(last_result, dict):
186
+ accumulator[acc_key] = last_result.get(field)
187
+
188
+ # Store final result
189
+ if out_key and last_result is not None:
190
+ ctx.set_value(out_key, last_result)
191
+
192
+
193
+ class _LoopContext:
194
+ """Context wrapper for loop iterations with accumulator and iterator."""
195
+
196
+ def __init__(self, parent_ctx: "_CTContext", accumulator: dict, iterator_name: str, iterator_value: Any):
197
+ self._parent = parent_ctx
198
+ self._accumulator = accumulator
199
+ self._iterator_name = iterator_name
200
+ self._iterator_value = iterator_value
201
+ self._vars: dict[str, Any] = {}
202
+
203
+ def get_input(self, name: str) -> Any:
204
+ return self._parent.get_input(name)
205
+
206
+ def set_value(self, name: str, value: Any) -> None:
207
+ self._vars[name] = value
208
+
209
+ def get_value(self, name: str) -> Any:
210
+ return self._vars.get(name)
211
+
212
+ def has_value(self, name: str) -> bool:
213
+ return name in self._vars
214
+
215
+ def resolve(self, path: str) -> Any:
216
+ if not path.startswith("$."):
217
+ return None
218
+
219
+ parts = path[2:].split(".")
220
+ if not parts:
221
+ return None
222
+
223
+ root = parts[0]
224
+
225
+ if root == "accumulator":
226
+ current = self._accumulator
227
+ elif root == "iterator":
228
+ return self._iterator_value
229
+ elif root == "inputs":
230
+ current = self._parent._inputs
231
+ elif root == "results":
232
+ if len(parts) > 1:
233
+ var_name = parts[1]
234
+ if var_name in self._vars:
235
+ current = self._vars[var_name]
236
+ parts = parts[1:]
237
+ elif self._parent.has_value(var_name):
238
+ current = self._parent.get_value(var_name)
239
+ parts = parts[1:]
240
+ else:
241
+ return None
242
+ else:
243
+ return None
244
+ else:
245
+ return None
246
+
247
+ for part in parts[1:]:
248
+ if isinstance(current, dict):
249
+ current = current.get(part)
250
+ else:
251
+ return None
252
+
253
+ return current
254
+
255
+
256
+ # ---------------------------------------------------------
257
+ # Internal execution context (CT-local)
258
+ # ---------------------------------------------------------
259
+
260
+ class _CTContext:
261
+ """_CTContext — isolated CT execution state."""
262
+
263
+ def __init__(self, *, inputs: dict[str, Any], input_types: dict[str, str] | None = None):
264
+ self._inputs = dict(inputs)
265
+ self._vars: dict[str, Any] = {}
266
+ self.input_types = input_types or {}
267
+
268
+ def get_input(self, name: str) -> Any:
269
+ return self._inputs.get(name)
270
+
271
+ def set_value(self, name: str, value: Any) -> None:
272
+ self._vars[name] = value
273
+
274
+ def get_value(self, name: str) -> Any:
275
+ return self._vars.get(name)
276
+
277
+ def has_value(self, name: str) -> bool:
278
+ return name in self._vars
279
+
280
+ def resolve(self, path: str) -> Any:
281
+ """Resolve a JSONPath-like string (e.g. "$.inputs.foo.bar" or "$.results.var.field")"""
282
+ if not path.startswith("$."):
283
+ return None
284
+
285
+ parts = path[2:].split(".")
286
+ if not parts:
287
+ return None
288
+
289
+ root = parts[0]
290
+
291
+ if root == "inputs":
292
+ current = self._inputs
293
+ remaining_parts = parts[1:]
294
+ elif root == "results":
295
+ if len(parts) < 2:
296
+ return None
297
+ var_name = parts[1]
298
+ current = self._vars.get(var_name)
299
+ remaining_parts = parts[2:]
300
+ elif root in self._vars:
301
+ current = self._vars[root]
302
+ remaining_parts = parts[1:]
303
+ else:
304
+ return None
305
+
306
+ for part in remaining_parts:
307
+ if isinstance(current, dict):
308
+ current = current.get(part)
309
+ else:
310
+ return None
311
+
312
+ return current