pycforge 0.15.2__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 (221) hide show
  1. pycforge/__init__.py +8 -0
  2. pycforge/__main__.py +2 -0
  3. pycforge/_version.py +1 -0
  4. pycforge/converter/__init__.py +0 -0
  5. pycforge/converter/analysis/__init__.py +5 -0
  6. pycforge/converter/analysis/functions.py +723 -0
  7. pycforge/converter/analysis/model.py +158 -0
  8. pycforge/converter/analysis/planning.py +464 -0
  9. pycforge/converter/analysis/stage.py +1976 -0
  10. pycforge/converter/analysis/symbols.py +237 -0
  11. pycforge/converter/analysis/validation.py +2166 -0
  12. pycforge/converter/c_output/__init__.py +2 -0
  13. pycforge/converter/c_output/conformance.py +297 -0
  14. pycforge/converter/c_output/renderer.py +220 -0
  15. pycforge/converter/conditional_regions/__init__.py +71 -0
  16. pycforge/converter/conditional_regions/analysis.py +481 -0
  17. pycforge/converter/conditional_regions/lowering.py +579 -0
  18. pycforge/converter/conditional_regions/model.py +243 -0
  19. pycforge/converter/conditional_regions/validation.py +690 -0
  20. pycforge/converter/containers/__init__.py +19 -0
  21. pycforge/converter/containers/analysis.py +542 -0
  22. pycforge/converter/containers/lowering.py +290 -0
  23. pycforge/converter/containers/model.py +146 -0
  24. pycforge/converter/contracts/__init__.py +0 -0
  25. pycforge/converter/contracts/configuration.py +178 -0
  26. pycforge/converter/contracts/identifiers.py +68 -0
  27. pycforge/converter/contracts/versions.py +71 -0
  28. pycforge/converter/core/__init__.py +0 -0
  29. pycforge/converter/core/artifact_io.py +87 -0
  30. pycforge/converter/core/cancellation.py +7 -0
  31. pycforge/converter/core/canonicalization.py +254 -0
  32. pycforge/converter/core/context.py +13 -0
  33. pycforge/converter/core/diagnostics.py +95 -0
  34. pycforge/converter/core/enums.py +23 -0
  35. pycforge/converter/core/fingerprint.py +22 -0
  36. pycforge/converter/core/pipeline.py +8 -0
  37. pycforge/converter/core/progress.py +25 -0
  38. pycforge/converter/core/request.py +50 -0
  39. pycforge/converter/core/resource_policy.py +31 -0
  40. pycforge/converter/core/result.py +23 -0
  41. pycforge/converter/core/serialization.py +36 -0
  42. pycforge/converter/core/stage.py +10 -0
  43. pycforge/converter/core/stage_artifact.py +80 -0
  44. pycforge/converter/core/stage_outcome.py +15 -0
  45. pycforge/converter/decision_trace/__init__.py +0 -0
  46. pycforge/converter/decision_trace/recorder.py +52 -0
  47. pycforge/converter/facade.py +459 -0
  48. pycforge/converter/frontend/__init__.py +4 -0
  49. pycforge/converter/frontend/normalizer.py +59 -0
  50. pycforge/converter/frontend/parser.py +20 -0
  51. pycforge/converter/frontend/source_document.py +93 -0
  52. pycforge/converter/frontend/stages.py +518 -0
  53. pycforge/converter/frontend/validation.py +76 -0
  54. pycforge/converter/io/__init__.py +0 -0
  55. pycforge/converter/io/atomic_writer.py +33 -0
  56. pycforge/converter/ir/__init__.py +1 -0
  57. pycforge/converter/ir/c_ir/__init__.py +4 -0
  58. pycforge/converter/ir/c_ir/builder.py +26 -0
  59. pycforge/converter/ir/c_ir/model.py +318 -0
  60. pycforge/converter/ir/c_ir/serialization.py +88 -0
  61. pycforge/converter/ir/c_ir/validation.py +834 -0
  62. pycforge/converter/ir/python_ir/__init__.py +17 -0
  63. pycforge/converter/ir/python_ir/nodes.py +86 -0
  64. pycforge/converter/keyword_calls/__init__.py +68 -0
  65. pycforge/converter/keyword_calls/analysis.py +458 -0
  66. pycforge/converter/keyword_calls/lowering.py +271 -0
  67. pycforge/converter/keyword_calls/model.py +304 -0
  68. pycforge/converter/keyword_calls/validation.py +802 -0
  69. pycforge/converter/keyword_only_calls/__init__.py +68 -0
  70. pycforge/converter/keyword_only_calls/analysis.py +640 -0
  71. pycforge/converter/keyword_only_calls/lowering.py +376 -0
  72. pycforge/converter/keyword_only_calls/model.py +328 -0
  73. pycforge/converter/keyword_only_calls/validation.py +1357 -0
  74. pycforge/converter/lowering.py +1000 -0
  75. pycforge/converter/modules/__init__.py +13 -0
  76. pycforge/converter/modules/analysis.py +1250 -0
  77. pycforge/converter/modules/lowering.py +200 -0
  78. pycforge/converter/modules/model.py +73 -0
  79. pycforge/converter/modules/stage.py +192 -0
  80. pycforge/converter/numeric_semantics/__init__.py +23 -0
  81. pycforge/converter/numeric_semantics/analysis.py +420 -0
  82. pycforge/converter/numeric_semantics/lowering.py +153 -0
  83. pycforge/converter/numeric_semantics/model.py +123 -0
  84. pycforge/converter/records/__init__.py +34 -0
  85. pycforge/converter/records/analysis.py +1394 -0
  86. pycforge/converter/records/lowering.py +393 -0
  87. pycforge/converter/records/model.py +319 -0
  88. pycforge/converter/rules/__init__.py +3 -0
  89. pycforge/converter/support_templates/__init__.py +49 -0
  90. pycforge/converter/support_templates/assembly.py +106 -0
  91. pycforge/converter/support_templates/factories.py +348 -0
  92. pycforge/converter/support_templates/model.py +159 -0
  93. pycforge/converter/support_templates/registry.py +388 -0
  94. pycforge/converter/telemetry/__init__.py +0 -0
  95. pycforge/converter/telemetry/sink.py +39 -0
  96. pycforge/ide/__init__.py +19 -0
  97. pycforge/ide/__main__.py +5 -0
  98. pycforge/ide/_worker_event_codec.py +276 -0
  99. pycforge/ide/_worker_protocol_json.py +197 -0
  100. pycforge/ide/_worker_protocol_types.py +90 -0
  101. pycforge/ide/_worker_request_codec.py +200 -0
  102. pycforge/ide/_worker_request_data.py +260 -0
  103. pycforge/ide/_worker_result_data.py +343 -0
  104. pycforge/ide/action_contract.py +469 -0
  105. pycforge/ide/action_validation.py +229 -0
  106. pycforge/ide/command_palette.py +223 -0
  107. pycforge/ide/controller.py +904 -0
  108. pycforge/ide/controller_conversion.py +546 -0
  109. pycforge/ide/controller_io.py +205 -0
  110. pycforge/ide/editor.py +951 -0
  111. pycforge/ide/editor_commands_qt.py +274 -0
  112. pycforge/ide/editor_lexical.py +43 -0
  113. pycforge/ide/editor_sidebars.py +236 -0
  114. pycforge/ide/editor_syntax.py +243 -0
  115. pycforge/ide/find_replace.py +535 -0
  116. pycforge/ide/icons.py +91 -0
  117. pycforge/ide/io_service.py +196 -0
  118. pycforge/ide/model.py +164 -0
  119. pycforge/ide/panels.py +903 -0
  120. pycforge/ide/positions.py +220 -0
  121. pycforge/ide/process_worker.py +130 -0
  122. pycforge/ide/qt.py +230 -0
  123. pycforge/ide/qt_actions.py +418 -0
  124. pycforge/ide/qt_close.py +169 -0
  125. pycforge/ide/qt_command_palette.py +297 -0
  126. pycforge/ide/qt_contract.py +220 -0
  127. pycforge/ide/qt_documents.py +590 -0
  128. pycforge/ide/qt_editor_buffers.py +113 -0
  129. pycforge/ide/qt_editor_surfaces.py +112 -0
  130. pycforge/ide/qt_menus.py +324 -0
  131. pycforge/ide/qt_projection.py +500 -0
  132. pycforge/ide/qt_shell.py +348 -0
  133. pycforge/ide/qt_shell_interactions.py +320 -0
  134. pycforge/ide/qt_state.py +393 -0
  135. pycforge/ide/qt_workspace_features.py +398 -0
  136. pycforge/ide/qt_workspace_navigation.py +208 -0
  137. pycforge/ide/qt_workspace_observers.py +173 -0
  138. pycforge/ide/qt_workspace_panels.py +545 -0
  139. pycforge/ide/qt_workspace_widgets.py +228 -0
  140. pycforge/ide/resources/icons/about.svg +1 -0
  141. pycforge/ide/resources/icons/add-document.svg +1 -0
  142. pycforge/ide/resources/icons/brand-mark.svg +1 -0
  143. pycforge/ide/resources/icons/bundle-search.svg +1 -0
  144. pycforge/ide/resources/icons/cancel.svg +1 -0
  145. pycforge/ide/resources/icons/check.svg +1 -0
  146. pycforge/ide/resources/icons/chevron-right.svg +1 -0
  147. pycforge/ide/resources/icons/close.svg +1 -0
  148. pycforge/ide/resources/icons/collapse-all.svg +1 -0
  149. pycforge/ide/resources/icons/command-palette.svg +1 -0
  150. pycforge/ide/resources/icons/convert.svg +1 -0
  151. pycforge/ide/resources/icons/copy.svg +1 -0
  152. pycforge/ide/resources/icons/cut.svg +1 -0
  153. pycforge/ide/resources/icons/decision-trace.svg +1 -0
  154. pycforge/ide/resources/icons/details.svg +1 -0
  155. pycforge/ide/resources/icons/diagnostics.svg +1 -0
  156. pycforge/ide/resources/icons/duplicate-line.svg +1 -0
  157. pycforge/ide/resources/icons/exit.svg +1 -0
  158. pycforge/ide/resources/icons/expand-all.svg +1 -0
  159. pycforge/ide/resources/icons/export.svg +1 -0
  160. pycforge/ide/resources/icons/find.svg +1 -0
  161. pycforge/ide/resources/icons/go-to-line.svg +1 -0
  162. pycforge/ide/resources/icons/go-to-output.svg +1 -0
  163. pycforge/ide/resources/icons/go-to-source.svg +1 -0
  164. pycforge/ide/resources/icons/history.svg +1 -0
  165. pycforge/ide/resources/icons/indent.svg +1 -0
  166. pycforge/ide/resources/icons/link-c.svg +1 -0
  167. pycforge/ide/resources/icons/mappings.svg +1 -0
  168. pycforge/ide/resources/icons/module.svg +1 -0
  169. pycforge/ide/resources/icons/move-down.svg +1 -0
  170. pycforge/ide/resources/icons/move-line-down.svg +1 -0
  171. pycforge/ide/resources/icons/move-line-up.svg +1 -0
  172. pycforge/ide/resources/icons/move-up.svg +1 -0
  173. pycforge/ide/resources/icons/next.svg +1 -0
  174. pycforge/ide/resources/icons/open.svg +1 -0
  175. pycforge/ide/resources/icons/outdent.svg +1 -0
  176. pycforge/ide/resources/icons/outline.svg +1 -0
  177. pycforge/ide/resources/icons/paste.svg +1 -0
  178. pycforge/ide/resources/icons/previous.svg +1 -0
  179. pycforge/ide/resources/icons/primary-module.svg +1 -0
  180. pycforge/ide/resources/icons/redo.svg +1 -0
  181. pycforge/ide/resources/icons/remove-document.svg +1 -0
  182. pycforge/ide/resources/icons/replace.svg +1 -0
  183. pycforge/ide/resources/icons/save-as.svg +1 -0
  184. pycforge/ide/resources/icons/save.svg +1 -0
  185. pycforge/ide/resources/icons/select-all.svg +1 -0
  186. pycforge/ide/resources/icons/settings.svg +1 -0
  187. pycforge/ide/resources/icons/show-c.svg +1 -0
  188. pycforge/ide/resources/icons/split-view.svg +1 -0
  189. pycforge/ide/resources/icons/summary.svg +1 -0
  190. pycforge/ide/resources/icons/telemetry.svg +1 -0
  191. pycforge/ide/resources/icons/toggle-comment.svg +1 -0
  192. pycforge/ide/resources/icons/toggle-fold.svg +1 -0
  193. pycforge/ide/resources/icons/undo.svg +1 -0
  194. pycforge/ide/resources/icons/whitespace.svg +1 -0
  195. pycforge/ide/revisions.py +400 -0
  196. pycforge/ide/search_service.py +280 -0
  197. pycforge/ide/session_history.py +150 -0
  198. pycforge/ide/source_editing.py +417 -0
  199. pycforge/ide/source_structure.py +558 -0
  200. pycforge/ide/source_structure_async.py +191 -0
  201. pycforge/ide/supervisor.py +503 -0
  202. pycforge/ide/theme.py +119 -0
  203. pycforge/ide/theme_stylesheet.py +598 -0
  204. pycforge/ide/theme_workspace_stylesheet.py +155 -0
  205. pycforge/ide/visual_tokens.py +142 -0
  206. pycforge/ide/worker_protocol.py +80 -0
  207. pycforge/ide/workspace_search.py +462 -0
  208. pycforge/ide/workspace_session.py +256 -0
  209. pycforge/laboratory/__init__.py +6 -0
  210. pycforge/laboratory/audits.py +2244 -0
  211. pycforge/laboratory/checkpoint_e.py +2685 -0
  212. pycforge/laboratory/cli.py +119 -0
  213. pycforge/laboratory/exit_codes.py +11 -0
  214. pycforge/laboratory/keyword_audit.py +661 -0
  215. pycforge/laboratory/keyword_only_audit.py +481 -0
  216. pycforge-0.15.2.dist-info/METADATA +209 -0
  217. pycforge-0.15.2.dist-info/RECORD +221 -0
  218. pycforge-0.15.2.dist-info/WHEEL +5 -0
  219. pycforge-0.15.2.dist-info/entry_points.txt +5 -0
  220. pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
  221. pycforge-0.15.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,481 @@
1
+ """Closed, linear proof pass for conditional temporary placement."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+ from pycforge.converter.analysis.model import ValueCategory
8
+ from pycforge.converter.ir.python_ir import python_ir_reference_ids
9
+
10
+ from .model import (
11
+ CONDITIONAL_REGION_LOWERING_SHAPE,
12
+ ConditionalEvaluationMode,
13
+ ConditionalGuardPolarity,
14
+ ConditionalOperandPlacementFact,
15
+ ConditionalRegionAnalysis,
16
+ ConditionalRegionAnalysisCanceled,
17
+ ConditionalRegionAnalysisError,
18
+ ConditionalRegionFact,
19
+ ConditionalRegionKind,
20
+ conditional_region_id,
21
+ )
22
+
23
+
24
+ _SCALAR_CATEGORIES = frozenset(
25
+ {
26
+ ValueCategory.INTEGER.value,
27
+ ValueCategory.FLOAT.value,
28
+ ValueCategory.BOOLEAN.value,
29
+ ValueCategory.STRING.value,
30
+ }
31
+ )
32
+ _COMPARABLE_CATEGORIES = frozenset(
33
+ {
34
+ ValueCategory.INTEGER.value,
35
+ ValueCategory.FLOAT.value,
36
+ ValueCategory.BOOLEAN.value,
37
+ }
38
+ )
39
+ _COMPARISON_OPERATORS = frozenset({"Eq", "NotEq", "Lt", "LtE", "Gt", "GtE"})
40
+
41
+
42
+ def _category_value(value: ValueCategory | str | None) -> str:
43
+ return value.value if isinstance(value, ValueCategory) else str(value or "unknown")
44
+
45
+
46
+ class _PrerequisiteClosure:
47
+ """Persistent, ordered prerequisite rope.
48
+
49
+ Building one closure costs only the number of direct child expressions.
50
+ Flattening is delayed until a published fact needs the sequence, making
51
+ nested wrapper analysis linear plus the size of serialized fact output.
52
+ """
53
+
54
+ __slots__ = ("parts", "node_id", "nonempty")
55
+
56
+ def __init__(
57
+ self,
58
+ parts: tuple["_PrerequisiteClosure", ...] = (),
59
+ node_id: str | None = None,
60
+ ) -> None:
61
+ self.parts = tuple(part for part in parts if part.nonempty)
62
+ self.node_id = node_id
63
+ self.nonempty = bool(self.parts) or node_id is not None
64
+
65
+ def materialize(self, check_cancellation: Any) -> tuple[str, ...]:
66
+ if not self.nonempty:
67
+ return ()
68
+ result: list[str] = []
69
+ seen_node_ids: set[str] = set()
70
+ seen_closures: set[int] = set()
71
+ stack: list[_PrerequisiteClosure | str] = [self]
72
+ while stack:
73
+ check_cancellation()
74
+ item = stack.pop()
75
+ if isinstance(item, str):
76
+ if item not in seen_node_ids:
77
+ seen_node_ids.add(item)
78
+ result.append(item)
79
+ continue
80
+ identity = id(item)
81
+ if identity in seen_closures:
82
+ continue
83
+ seen_closures.add(identity)
84
+ if item.node_id is not None:
85
+ stack.append(item.node_id)
86
+ stack.extend(reversed(item.parts))
87
+ return tuple(result)
88
+
89
+
90
+ _EMPTY_PREREQUISITES = _PrerequisiteClosure()
91
+
92
+
93
+ def _closure(
94
+ parts: tuple[_PrerequisiteClosure, ...] = (),
95
+ node_id: str | None = None,
96
+ ) -> _PrerequisiteClosure:
97
+ if node_id is None and not any(part.nonempty for part in parts):
98
+ return _EMPTY_PREREQUISITES
99
+ return _PrerequisiteClosure(parts, node_id)
100
+
101
+
102
+ class ConditionalRegionAnalyzer:
103
+ """Prove statement-prerequisite placement for existing scalar expressions.
104
+
105
+ The pass is deliberately independent of C IR. It computes one postorder
106
+ staging-demand map and then selects only source forms rejected by the
107
+ predecessor's eager-prelude boundary.
108
+ """
109
+
110
+ def __init__(
111
+ self,
112
+ module: dict[str, Any],
113
+ *,
114
+ categories: Mapping[str, ValueCategory | str],
115
+ function_records: Mapping[str, Mapping[str, Any]],
116
+ owner_by_node: Mapping[str, str],
117
+ supported_call_node_ids: frozenset[str],
118
+ numeric_operation_node_ids: frozenset[str],
119
+ supported_container_access_node_ids: frozenset[str] = frozenset(),
120
+ supported_record_access_node_ids: frozenset[str] = frozenset(),
121
+ cancellation: Any,
122
+ target_contract: str = "c11-portable-fixed-v1",
123
+ ) -> None:
124
+ self.module = module
125
+ self.cancellation = cancellation
126
+ self.nodes: dict[str, dict[str, Any]] = {}
127
+ self.ordinals: dict[str, int] = {}
128
+ for ordinal, node in enumerate(module["nodes"]):
129
+ self._check_cancellation()
130
+ self.nodes[node["node_id"]] = node
131
+ self.ordinals[node["node_id"]] = ordinal
132
+ self.categories = {
133
+ node_id: _category_value(category) for node_id, category in categories.items()
134
+ }
135
+ self.function_records = function_records
136
+ self.owner_by_node = owner_by_node
137
+ self.supported_calls = supported_call_node_ids
138
+ self.numeric_operations = numeric_operation_node_ids
139
+ self.supported_container_accesses = supported_container_access_node_ids
140
+ self.supported_record_accesses = supported_record_access_node_ids
141
+ self.target_contract = target_contract
142
+ self.children: dict[str, tuple[str, ...]] = {}
143
+ for node_id, node in self.nodes.items():
144
+ self._check_cancellation()
145
+ self.children[node_id] = self._children(node)
146
+ self.approved: dict[str, bool] = {}
147
+ self.prerequisites: dict[str, _PrerequisiteClosure] = {}
148
+
149
+ def analyze(self) -> ConditionalRegionAnalysis:
150
+ for node_id in self._postorder():
151
+ self._check_cancellation()
152
+ self.approved[node_id] = self._approved_expression(self.nodes[node_id])
153
+ self.prerequisites[node_id] = self._statement_prerequisites(
154
+ self.nodes[node_id]
155
+ )
156
+
157
+ regions: list[ConditionalRegionFact] = []
158
+ for node in self.module["nodes"]:
159
+ self._check_cancellation()
160
+ if not self.approved.get(node["node_id"], False):
161
+ continue
162
+ if node["kind"] == "BoolOp" and self._boolean_requires_region(node):
163
+ regions.append(self._region(node, ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT))
164
+ elif node["kind"] == "Compare" and self._comparison_requires_region(node):
165
+ regions.append(self._region(node, ConditionalRegionKind.CHAINED_COMPARISON))
166
+ return ConditionalRegionAnalysis(
167
+ tuple(sorted(regions, key=lambda item: item.region_node_id))
168
+ )
169
+
170
+ def _children(self, node: dict[str, Any]) -> tuple[str, ...]:
171
+ found: list[str] = []
172
+ for field_name, value in node.get("fields", {}).items():
173
+ found.extend(
174
+ python_ir_reference_ids(node["kind"], field_name, value, self.nodes)
175
+ )
176
+ return tuple(found)
177
+
178
+ def _postorder(self) -> tuple[str, ...]:
179
+ """Return a deterministic iterative postorder over the bounded IR graph."""
180
+
181
+ state: dict[str, int] = {}
182
+ result: list[str] = []
183
+ for start in sorted(self.nodes, key=self.ordinals.__getitem__):
184
+ if state.get(start) == 2:
185
+ continue
186
+ stack: list[tuple[str, bool]] = [(start, False)]
187
+ while stack:
188
+ self._check_cancellation()
189
+ node_id, expanded = stack.pop()
190
+ if expanded:
191
+ if state.get(node_id) != 2:
192
+ state[node_id] = 2
193
+ result.append(node_id)
194
+ continue
195
+ current = state.get(node_id, 0)
196
+ if current == 2:
197
+ continue
198
+ if current == 1:
199
+ self._error(
200
+ "PYC2950",
201
+ "Conditional-region analysis encountered a cyclic Python IR reference",
202
+ self.nodes[node_id],
203
+ )
204
+ state[node_id] = 1
205
+ stack.append((node_id, True))
206
+ for child_id in reversed(self.children[node_id]):
207
+ if state.get(child_id, 0) == 1:
208
+ self._error(
209
+ "PYC2950",
210
+ "Conditional-region analysis encountered a cyclic Python IR reference",
211
+ self.nodes[child_id],
212
+ )
213
+ if state.get(child_id, 0) != 2:
214
+ stack.append((child_id, False))
215
+ return tuple(result)
216
+
217
+ def _approved_expression(self, node: dict[str, Any]) -> bool:
218
+ kind = node["kind"]
219
+ fields = node.get("fields", {})
220
+ node_id = node["node_id"]
221
+ category = self.categories.get(node_id, "unknown")
222
+ if kind in {"Name", "Constant"}:
223
+ return category in _SCALAR_CATEGORIES
224
+ if kind == "Attribute":
225
+ return node_id in self.supported_record_accesses and category in _SCALAR_CATEGORIES
226
+ if kind == "Subscript":
227
+ return node_id in self.supported_container_accesses and category in _SCALAR_CATEGORIES
228
+ if kind == "Call":
229
+ arguments = tuple(fields.get("args", ()))
230
+ return (
231
+ node_id in self.supported_calls
232
+ and category in _SCALAR_CATEGORIES
233
+ and all(self.approved.get(item, False) for item in arguments)
234
+ )
235
+ if kind == "UnaryOp":
236
+ operator = self.nodes.get(fields.get("op"), {}).get("kind")
237
+ operand_id = fields.get("operand")
238
+ operand_category = self.categories.get(operand_id, "unknown")
239
+ if not isinstance(operand_id, str) or not self.approved.get(operand_id, False):
240
+ return False
241
+ if operator == "Not":
242
+ return category == ValueCategory.BOOLEAN.value and operand_category in {
243
+ ValueCategory.INTEGER.value,
244
+ ValueCategory.FLOAT.value,
245
+ ValueCategory.BOOLEAN.value,
246
+ }
247
+ return operator in {"UAdd", "USub"} and category in {
248
+ ValueCategory.INTEGER.value,
249
+ ValueCategory.FLOAT.value,
250
+ }
251
+ if kind == "BinOp":
252
+ left_id, right_id = fields.get("left"), fields.get("right")
253
+ operator = self.nodes.get(fields.get("op"), {}).get("kind")
254
+ if not (
255
+ isinstance(left_id, str)
256
+ and isinstance(right_id, str)
257
+ and self.approved.get(left_id, False)
258
+ and self.approved.get(right_id, False)
259
+ ):
260
+ return False
261
+ if operator in {"FloorDiv", "Mod"}:
262
+ return node_id in self.numeric_operations
263
+ if operator == "Div":
264
+ return category == ValueCategory.FLOAT.value
265
+ return operator in {"Add", "Sub", "Mult"} and category in {
266
+ ValueCategory.INTEGER.value,
267
+ ValueCategory.FLOAT.value,
268
+ }
269
+ if kind == "BoolOp":
270
+ values = tuple(fields.get("values", ()))
271
+ operator = self.nodes.get(fields.get("op"), {}).get("kind")
272
+ return (
273
+ operator in {"And", "Or"}
274
+ and len(values) >= 2
275
+ and category == ValueCategory.BOOLEAN.value
276
+ and all(
277
+ self.categories.get(item) == ValueCategory.BOOLEAN.value
278
+ and self.approved.get(item, False)
279
+ for item in values
280
+ )
281
+ )
282
+ if kind == "Compare":
283
+ operands = (fields.get("left"), *tuple(fields.get("comparators", ())))
284
+ operators = tuple(fields.get("ops", ()))
285
+ operand_categories = tuple(self.categories.get(item, "unknown") for item in operands)
286
+ operator_kinds = tuple(
287
+ self.nodes.get(item, {}).get("kind") for item in operators
288
+ )
289
+ return (
290
+ bool(operators)
291
+ and len(operands) == len(operators) + 1
292
+ and all(isinstance(item, str) and self.approved.get(item, False) for item in operands)
293
+ and all(item in _COMPARISON_OPERATORS for item in operator_kinds)
294
+ and operand_categories
295
+ and operand_categories[0] in _COMPARABLE_CATEGORIES
296
+ and all(item == operand_categories[0] for item in operand_categories)
297
+ and category == ValueCategory.BOOLEAN.value
298
+ )
299
+ return False
300
+
301
+ def _statement_prerequisites(
302
+ self,
303
+ node: dict[str, Any],
304
+ ) -> _PrerequisiteClosure:
305
+ if not self.approved.get(node["node_id"], False):
306
+ return _EMPTY_PREREQUISITES
307
+ kind = node["kind"]
308
+ fields = node.get("fields", {})
309
+ node_id = node["node_id"]
310
+
311
+ def merge(node_ids: tuple[str, ...] | list[str]) -> _PrerequisiteClosure:
312
+ return _closure(
313
+ tuple(
314
+ self.prerequisites.get(child_id, _EMPTY_PREREQUISITES)
315
+ for child_id in node_ids
316
+ )
317
+ )
318
+
319
+ if kind == "Call":
320
+ return _closure((merge(tuple(fields.get("args", ()))),), node_id)
321
+ if kind == "UnaryOp":
322
+ operand_id = fields.get("operand")
323
+ return self.prerequisites.get(operand_id, _EMPTY_PREREQUISITES)
324
+ if kind == "BinOp":
325
+ children = (fields.get("left"), fields.get("right"))
326
+ inherited = merge(tuple(item for item in children if isinstance(item, str)))
327
+ if node_id in self.numeric_operations:
328
+ return _closure((inherited,), node_id)
329
+ return inherited
330
+ if kind == "BoolOp":
331
+ inherited = merge(tuple(fields.get("values", ())))
332
+ return (
333
+ _closure((inherited,), node_id)
334
+ if inherited.nonempty
335
+ else _EMPTY_PREREQUISITES
336
+ )
337
+ if kind == "Compare":
338
+ operands = (fields.get("left"), *tuple(fields.get("comparators", ())))
339
+ inherited = merge(tuple(item for item in operands if isinstance(item, str)))
340
+ if len(tuple(fields.get("ops", ()))) > 1:
341
+ return _closure((inherited,), node_id)
342
+ return inherited
343
+ return _EMPTY_PREREQUISITES
344
+
345
+ def _boolean_requires_region(self, node: dict[str, Any]) -> bool:
346
+ return any(
347
+ self.prerequisites.get(item, _EMPTY_PREREQUISITES).nonempty
348
+ for item in node.get("fields", {}).get("values", ())
349
+ )
350
+
351
+ def _comparison_requires_region(self, node: dict[str, Any]) -> bool:
352
+ fields = node.get("fields", {})
353
+ operators = tuple(fields.get("ops", ()))
354
+ operands = (fields.get("left"), *tuple(fields.get("comparators", ())))
355
+ if len(operators) <= 1:
356
+ return False
357
+ return any(
358
+ self.nodes[item]["kind"] not in {"Name", "Constant"}
359
+ or self.prerequisites.get(item, _EMPTY_PREREQUISITES).nonempty
360
+ for item in operands[2:]
361
+ )
362
+
363
+ def _region(
364
+ self,
365
+ node: dict[str, Any],
366
+ kind: ConditionalRegionKind,
367
+ ) -> ConditionalRegionFact:
368
+ fields = node["fields"]
369
+ if kind is ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT:
370
+ operand_ids = tuple(fields.get("values", ()))
371
+ operator_ids = (fields["op"],)
372
+ prefix_count = 1
373
+ boolean_operator = self.nodes[operator_ids[0]]["kind"]
374
+ guarded_polarity = (
375
+ ConditionalGuardPolarity.WHEN_RESULT_TRUE
376
+ if boolean_operator == "And"
377
+ else ConditionalGuardPolarity.WHEN_RESULT_FALSE
378
+ )
379
+ else:
380
+ operand_ids = (fields["left"], *tuple(fields.get("comparators", ())))
381
+ operator_ids = tuple(fields.get("ops", ()))
382
+ prefix_count = 2
383
+ guarded_polarity = ConditionalGuardPolarity.WHEN_RESULT_TRUE
384
+
385
+ owner_id = self.owner_by_node.get(node["node_id"])
386
+ owner = self.function_records.get(owner_id or "")
387
+ if not isinstance(owner_id, str) or not isinstance(owner, Mapping):
388
+ self._error(
389
+ "PYC2950" if kind is ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT else "PYC2951",
390
+ "Conditional temporary placement requires an understood top-level function owner",
391
+ node,
392
+ )
393
+ module_id = owner.get("module_id")
394
+ document_id = owner.get("document_id")
395
+ logical_name = owner.get("logical_name")
396
+ if not all(isinstance(item, str) and item for item in (module_id, document_id, logical_name)):
397
+ self._error(
398
+ "PYC2950" if kind is ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT else "PYC2951",
399
+ "Conditional temporary placement lacks exact module or source ownership",
400
+ node,
401
+ )
402
+
403
+ placements: list[ConditionalOperandPlacementFact] = []
404
+ for ordinal, operand_id in enumerate(operand_ids):
405
+ self._check_cancellation()
406
+ unconditional = ordinal < prefix_count
407
+ prerequisite_node_ids = self.prerequisites.get(
408
+ operand_id,
409
+ _EMPTY_PREREQUISITES,
410
+ ).materialize(self._check_cancellation)
411
+ placements.append(
412
+ ConditionalOperandPlacementFact(
413
+ operand_node_id=operand_id,
414
+ ordinal=ordinal,
415
+ category=self.categories[operand_id],
416
+ evaluation_mode=(
417
+ ConditionalEvaluationMode.UNCONDITIONAL
418
+ if unconditional
419
+ else ConditionalEvaluationMode.GUARDED
420
+ ),
421
+ guard_polarity=(
422
+ ConditionalGuardPolarity.NONE
423
+ if unconditional
424
+ else guarded_polarity
425
+ ),
426
+ guard_after_operand_ordinal=None if unconditional else ordinal - 1,
427
+ requires_statement_prelude=bool(prerequisite_node_ids),
428
+ prerequisite_node_ids=prerequisite_node_ids,
429
+ legacy_direct_safe=self.nodes[operand_id]["kind"] in {"Name", "Constant"},
430
+ )
431
+ )
432
+ prerequisite_values: list[str] = []
433
+ prerequisite_seen: set[str] = set()
434
+ for placement in placements:
435
+ for prerequisite in placement.prerequisite_node_ids:
436
+ self._check_cancellation()
437
+ if prerequisite not in prerequisite_seen:
438
+ prerequisite_seen.add(prerequisite)
439
+ prerequisite_values.append(prerequisite)
440
+ prerequisites = tuple(prerequisite_values)
441
+ return ConditionalRegionFact(
442
+ region_id=conditional_region_id(node["node_id"], kind),
443
+ region_node_id=node["node_id"],
444
+ region_kind=kind,
445
+ function_node_id=owner_id,
446
+ module_id=module_id,
447
+ document_id=document_id,
448
+ logical_name=logical_name,
449
+ operator_node_ids=operator_ids,
450
+ operator_kinds=tuple(self.nodes[item]["kind"] for item in operator_ids),
451
+ operand_node_ids=operand_ids,
452
+ operand_categories=tuple(self.categories[item] for item in operand_ids),
453
+ unconditional_prefix_count=prefix_count,
454
+ placements=tuple(placements),
455
+ guarded_operand_node_ids=operand_ids[prefix_count:],
456
+ prerequisite_node_ids=prerequisites,
457
+ result_category=ValueCategory.BOOLEAN.value,
458
+ result_c_type="bool",
459
+ evaluation_order=operand_ids,
460
+ operands_evaluated_once=True,
461
+ lowering_shape=CONDITIONAL_REGION_LOWERING_SHAPE,
462
+ allocation_model="none",
463
+ cleanup_model="none",
464
+ runtime_failure_channel="unchanged",
465
+ target_contract=self.target_contract,
466
+ )
467
+
468
+ def _error(self, code: str, message: str, node: dict[str, Any]) -> None:
469
+ raise ConditionalRegionAnalysisError(
470
+ code,
471
+ message,
472
+ node["node_id"],
473
+ node.get("provenance", {}).get("source_span"),
474
+ )
475
+
476
+ def _check_cancellation(self) -> None:
477
+ if bool(getattr(self.cancellation, "is_canceled", False)):
478
+ raise ConditionalRegionAnalysisCanceled
479
+
480
+
481
+ __all__ = ["ConditionalRegionAnalyzer"]