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,723 @@
1
+ """Phase 9 function, call, return-path, and local-declaration facts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from .model import ValueCategory
10
+ from .symbols import PythonIRIndex
11
+
12
+
13
+ ANNOTATION_CATEGORIES = {
14
+ "int": ValueCategory.INTEGER,
15
+ "float": ValueCategory.FLOAT,
16
+ "bool": ValueCategory.BOOLEAN,
17
+ "str": ValueCategory.STRING,
18
+ }
19
+
20
+ CATEGORY_C_TYPES = {
21
+ ValueCategory.INTEGER: "int64_t",
22
+ ValueCategory.FLOAT: "double",
23
+ ValueCategory.BOOLEAN: "bool",
24
+ ValueCategory.STRING: "const char *",
25
+ }
26
+
27
+
28
+ class FunctionAnalysisCanceled(Exception):
29
+ """Raised at bounded safe points before function facts are published."""
30
+
31
+
32
+ def _boundary(category: ValueCategory) -> tuple[str, str, str]:
33
+ if category is ValueCategory.STRING:
34
+ return "borrowed-pointer", "borrowed", "caller-managed-valid-for-call"
35
+ return "by-value", "not-applicable", "callee-activation"
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class ParameterFact:
40
+ parameter_node_id: str
41
+ binding_id: str
42
+ source_name: str
43
+ ordinal: int
44
+ category: ValueCategory
45
+ c_type: str | None
46
+ annotation_node_id: str | None
47
+ annotation_spelling: str | None
48
+ passing: str
49
+ ownership: str
50
+ lifetime: str
51
+
52
+ def to_dict(self) -> dict[str, Any]:
53
+ return {
54
+ "parameter_node_id": self.parameter_node_id,
55
+ "binding_id": self.binding_id,
56
+ "source_name": self.source_name,
57
+ "ordinal": self.ordinal,
58
+ "category": self.category.value,
59
+ "c_type": self.c_type,
60
+ "annotation_node_id": self.annotation_node_id,
61
+ "annotation_spelling": self.annotation_spelling,
62
+ "passing": self.passing,
63
+ "ownership": self.ownership,
64
+ "lifetime": self.lifetime,
65
+ }
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class FunctionSignatureFact:
70
+ function_node_id: str
71
+ binding_id: str | None
72
+ source_name: str
73
+ parameters: tuple[ParameterFact, ...]
74
+ return_category: ValueCategory
75
+ return_c_type: str | None
76
+ return_annotation_node_id: str | None
77
+ return_annotation_spelling: str | None
78
+ return_passing: str
79
+ return_ownership: str
80
+ return_lifetime: str
81
+ prototype_required: bool
82
+ eligible: bool
83
+ rejection_reason: str | None
84
+
85
+ def to_dict(self) -> dict[str, Any]:
86
+ return {
87
+ "function_node_id": self.function_node_id,
88
+ "binding_id": self.binding_id,
89
+ "source_name": self.source_name,
90
+ "parameters": [item.to_dict() for item in self.parameters],
91
+ "return_category": self.return_category.value,
92
+ "return_c_type": self.return_c_type,
93
+ "return_annotation_node_id": self.return_annotation_node_id,
94
+ "return_annotation_spelling": self.return_annotation_spelling,
95
+ "return_passing": self.return_passing,
96
+ "return_ownership": self.return_ownership,
97
+ "return_lifetime": self.return_lifetime,
98
+ "prototype_required": self.prototype_required,
99
+ "eligible": self.eligible,
100
+ "rejection_reason": self.rejection_reason,
101
+ }
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class CallTargetFact:
106
+ call_node_id: str
107
+ callee_node_id: str | None
108
+ target_function_node_id: str | None
109
+ target_binding_id: str | None
110
+ target_name: str | None
111
+ resolution: str
112
+ argument_node_ids: tuple[str, ...]
113
+ argument_categories: tuple[ValueCategory, ...]
114
+ parameter_categories: tuple[ValueCategory, ...]
115
+ return_category: ValueCategory
116
+ evaluation_order: tuple[str, ...]
117
+ arguments_evaluated_once: bool
118
+ ownership_boundary: tuple[str, ...]
119
+ annotation_evidence: tuple[str, ...]
120
+ supported: bool
121
+ diagnostic_code: str | None
122
+ reason: str | None
123
+
124
+ def to_dict(self) -> dict[str, Any]:
125
+ return {
126
+ "call_node_id": self.call_node_id,
127
+ "callee_node_id": self.callee_node_id,
128
+ "target_function_node_id": self.target_function_node_id,
129
+ "target_binding_id": self.target_binding_id,
130
+ "target_name": self.target_name,
131
+ "resolution": self.resolution,
132
+ "argument_node_ids": list(self.argument_node_ids),
133
+ "argument_categories": [item.value for item in self.argument_categories],
134
+ "parameter_categories": [item.value for item in self.parameter_categories],
135
+ "return_category": self.return_category.value,
136
+ "evaluation_order": list(self.evaluation_order),
137
+ "arguments_evaluated_once": self.arguments_evaluated_once,
138
+ "ownership_boundary": list(self.ownership_boundary),
139
+ "annotation_evidence": list(self.annotation_evidence),
140
+ "supported": self.supported,
141
+ "diagnostic_code": self.diagnostic_code,
142
+ "reason": self.reason,
143
+ }
144
+
145
+
146
+ @dataclass(frozen=True, slots=True)
147
+ class ReturnPathFact:
148
+ function_node_id: str
149
+ return_node_ids: tuple[str, ...]
150
+ return_categories: tuple[ValueCategory, ...]
151
+ expected_category: ValueCategory
152
+ compatible: bool
153
+ fallthrough_possible: bool
154
+ cleanup: str
155
+
156
+ def to_dict(self) -> dict[str, Any]:
157
+ return {
158
+ "function_node_id": self.function_node_id,
159
+ "return_node_ids": list(self.return_node_ids),
160
+ "return_categories": [item.value for item in self.return_categories],
161
+ "expected_category": self.expected_category.value,
162
+ "compatible": self.compatible,
163
+ "fallthrough_possible": self.fallthrough_possible,
164
+ "cleanup": self.cleanup,
165
+ }
166
+
167
+
168
+ @dataclass(frozen=True, slots=True)
169
+ class LocalDeclarationFact:
170
+ function_node_id: str
171
+ use_before_binding_node_ids: tuple[str, ...]
172
+ loop_target_escape_node_ids: tuple[str, ...]
173
+ first_definitions_in_control_node_ids: tuple[str, ...]
174
+ representation_conflict_node_ids: tuple[str, ...]
175
+ loop_target_rebind_node_ids: tuple[str, ...]
176
+ loop_target_mutation_node_ids: tuple[str, ...]
177
+
178
+ @property
179
+ def valid(self) -> bool:
180
+ return not (
181
+ self.use_before_binding_node_ids
182
+ or self.loop_target_escape_node_ids
183
+ or self.first_definitions_in_control_node_ids
184
+ or self.representation_conflict_node_ids
185
+ or self.loop_target_rebind_node_ids
186
+ or self.loop_target_mutation_node_ids
187
+ )
188
+
189
+ def to_dict(self) -> dict[str, Any]:
190
+ return {
191
+ "function_node_id": self.function_node_id,
192
+ "use_before_binding_node_ids": list(self.use_before_binding_node_ids),
193
+ "loop_target_escape_node_ids": list(self.loop_target_escape_node_ids),
194
+ "first_definitions_in_control_node_ids": list(self.first_definitions_in_control_node_ids),
195
+ "representation_conflict_node_ids": list(self.representation_conflict_node_ids),
196
+ "loop_target_rebind_node_ids": list(self.loop_target_rebind_node_ids),
197
+ "loop_target_mutation_node_ids": list(self.loop_target_mutation_node_ids),
198
+ "valid": self.valid,
199
+ }
200
+
201
+
202
+ @dataclass(frozen=True, slots=True)
203
+ class CallGraphFact:
204
+ edges: tuple[tuple[str, str], ...]
205
+ recursive_function_node_ids: tuple[str, ...]
206
+ recursive_call_node_ids: tuple[str, ...]
207
+ policy: str = "direct-and-mutual-recursion-unsupported-in-phase9"
208
+
209
+ def to_dict(self) -> dict[str, Any]:
210
+ return {
211
+ "edges": [{"caller": left, "callee": right} for left, right in self.edges],
212
+ "recursive_function_node_ids": list(self.recursive_function_node_ids),
213
+ "recursive_call_node_ids": list(self.recursive_call_node_ids),
214
+ "policy": self.policy,
215
+ }
216
+
217
+
218
+ class FunctionFactsAnalyzer:
219
+ def __init__(
220
+ self,
221
+ module: dict[str, Any],
222
+ bindings: tuple[dict[str, Any], ...],
223
+ *,
224
+ ignored_call_node_ids: frozenset[str] = frozenset(),
225
+ allow_required_keyword_only: bool = False,
226
+ cancellation: Any = None,
227
+ ) -> None:
228
+ self.module = module
229
+ self.index = PythonIRIndex(module)
230
+ self.bindings = bindings
231
+ self.ignored_call_node_ids = ignored_call_node_ids
232
+ self.allow_required_keyword_only = allow_required_keyword_only
233
+ self.cancellation = cancellation
234
+ self.binding_by_decl = {item["declaration_node_id"]: item for item in bindings}
235
+ self.binding_by_occurrence = {node_id: item for item in bindings for node_id in item["occurrence_node_ids"]}
236
+ self.parent_by_node: dict[str, str] = {}
237
+ for parent in self.index.nodes.values():
238
+ for child in self.index.child_ids(parent):
239
+ self.parent_by_node.setdefault(child, parent["node_id"])
240
+ root = self.index.node(module["root_node_id"])
241
+ self.module_body = tuple(root["fields"].get("body", []))
242
+ self.function_ids = tuple(node_id for node_id in self.module_body if self.index.node(node_id)["kind"] == "FunctionDef")
243
+
244
+ def signatures(self) -> tuple[FunctionSignatureFact, ...]:
245
+ self._check_cancellation()
246
+ names: list[str] = []
247
+ for node_id in self.function_ids:
248
+ self._check_cancellation()
249
+ names.append(self.index.node(node_id)["fields"].get("name", ""))
250
+ duplicates = {name for name, count in Counter(names).items() if count > 1}
251
+ facts: list[FunctionSignatureFact] = []
252
+ for node_id in self.function_ids:
253
+ self._check_cancellation()
254
+ facts.append(self._signature(self.index.node(node_id), duplicates))
255
+ return tuple(sorted(facts, key=lambda item: item.function_node_id))
256
+
257
+ def _check_cancellation(self) -> None:
258
+ if self.cancellation is not None and bool(
259
+ getattr(self.cancellation, "is_canceled", False)
260
+ ):
261
+ raise FunctionAnalysisCanceled
262
+
263
+ def preliminary_call_returns(self, signatures: tuple[FunctionSignatureFact, ...]) -> dict[str, ValueCategory]:
264
+ by_binding = {item.binding_id: item for item in signatures if item.binding_id and item.eligible}
265
+ result: dict[str, ValueCategory] = {}
266
+ for node in self.index.nodes.values():
267
+ if (
268
+ node["kind"] != "Call"
269
+ or node["node_id"] in self.ignored_call_node_ids
270
+ or self._is_range_iterator(node)
271
+ ):
272
+ continue
273
+ callee_id = node["fields"].get("func")
274
+ callee_binding = self.binding_by_occurrence.get(callee_id)
275
+ signature = by_binding.get(callee_binding["binding_id"] if callee_binding else None)
276
+ if signature:
277
+ result[node["node_id"]] = signature.return_category
278
+ return result
279
+
280
+ def calls(self, signatures: tuple[FunctionSignatureFact, ...], categories: dict[str, ValueCategory]) -> tuple[CallTargetFact, ...]:
281
+ by_binding = {item.binding_id: item for item in signatures if item.binding_id}
282
+ facts: list[CallTargetFact] = []
283
+ for node in self.index.nodes.values():
284
+ if node["kind"] != "Call":
285
+ continue
286
+ if node["node_id"] in self.ignored_call_node_ids:
287
+ continue
288
+ if self._is_range_iterator(node):
289
+ facts.append(self._range_call_fact(node, categories))
290
+ continue
291
+ fields = node["fields"]
292
+ callee_id = fields.get("func")
293
+ callee = self.index.nodes.get(callee_id)
294
+ args = tuple(fields.get("args", []))
295
+ arg_categories = tuple(categories.get(item, ValueCategory.UNKNOWN) for item in args)
296
+ if not callee or callee["kind"] != "Name":
297
+ facts.append(self._unsupported_call(node, callee_id, args, arg_categories, "dynamic-target", "PYC2901", "Call target is not a directly resolved function name"))
298
+ continue
299
+ binding = self.binding_by_occurrence.get(callee_id)
300
+ signature = by_binding.get(binding["binding_id"] if binding else None)
301
+ if not signature:
302
+ facts.append(self._unsupported_call(node, callee_id, args, arg_categories, "unresolved-target", "PYC2901", f"Unresolved call target: {callee['fields'].get('id')}"))
303
+ continue
304
+ if fields.get("keywords"):
305
+ facts.append(self._call_from_signature(node, signature, args, arg_categories, False, "PYC2910", "Keyword and unpacked keyword arguments are unsupported"))
306
+ continue
307
+ if any(self.index.node(item)["kind"] == "Starred" for item in args):
308
+ facts.append(self._call_from_signature(node, signature, args, arg_categories, False, "PYC2910", "Unpacked positional arguments are unsupported"))
309
+ continue
310
+ if not signature.eligible:
311
+ facts.append(self._call_from_signature(node, signature, args, arg_categories, False, "PYC2902", signature.rejection_reason or "Target signature is ineligible"))
312
+ continue
313
+ params = tuple(item.category for item in signature.parameters)
314
+ if len(args) != len(params):
315
+ facts.append(self._call_from_signature(node, signature, args, arg_categories, False, "PYC2904", "Positional argument count does not match the target signature"))
316
+ continue
317
+ if arg_categories != params:
318
+ facts.append(self._call_from_signature(node, signature, args, arg_categories, False, "PYC2905", "Argument representations do not match the target signature"))
319
+ continue
320
+ facts.append(self._call_from_signature(node, signature, args, arg_categories, True, None, None))
321
+ return tuple(sorted(facts, key=lambda item: item.call_node_id))
322
+
323
+ def return_paths(self, signatures: tuple[FunctionSignatureFact, ...], categories: dict[str, ValueCategory]) -> tuple[ReturnPathFact, ...]:
324
+ by_node = {item.function_node_id: item for item in signatures}
325
+ result: list[ReturnPathFact] = []
326
+ for function_id in self.function_ids:
327
+ function = self.index.node(function_id)
328
+ signature = by_node[function_id]
329
+ returns: list[str] = []
330
+ self._collect_returns(function["fields"].get("body", []), returns)
331
+ return_categories = tuple(categories.get(self.index.node(item)["fields"].get("value"), ValueCategory.NONE) for item in returns)
332
+ compatible = bool(returns) and all(item is signature.return_category for item in return_categories)
333
+ result.append(ReturnPathFact(function_id, tuple(returns), return_categories, signature.return_category, compatible, not self._block_guarantees_return(function["fields"].get("body", [])), "no-cleanup-required"))
334
+ return tuple(sorted(result, key=lambda item: item.function_node_id))
335
+
336
+ def local_declarations(self, categories: dict[str, ValueCategory], signatures: tuple[FunctionSignatureFact, ...]) -> tuple[LocalDeclarationFact, ...]:
337
+ signature_by_function = {item.function_node_id: item for item in signatures}
338
+ results: list[LocalDeclarationFact] = []
339
+ for function_id in self.function_ids:
340
+ function = self.index.node(function_id)
341
+ args_node = self.index.node(function["fields"]["args"])
342
+ parameter_ids = list(args_node["fields"].get("posonlyargs", [])) + list(args_node["fields"].get("args", []))
343
+ if self.allow_required_keyword_only:
344
+ parameter_ids += list(args_node["fields"].get("kwonlyargs", []))
345
+ initialized = {self.binding_by_decl[item]["binding_id"] for item in parameter_ids if item in self.binding_by_decl}
346
+ use_before: list[str] = []
347
+ loop_escape: list[str] = []
348
+ control_defs: list[str] = []
349
+ self._analyze_statements(function["fields"].get("body", []), initialized, set(), 0, use_before, loop_escape, control_defs)
350
+ signature = signature_by_function[function_id]
351
+ binding_categories = {item.binding_id: item.category for item in signature.parameters}
352
+ representation_conflicts: list[str] = []
353
+ target_rebinds: list[str] = []
354
+ target_mutations: list[str] = []
355
+ target_store_escapes: list[str] = []
356
+ self._binding_hazards(
357
+ function["fields"].get("body", []),
358
+ categories,
359
+ binding_categories,
360
+ set(),
361
+ representation_conflicts,
362
+ target_rebinds,
363
+ target_mutations,
364
+ target_store_escapes,
365
+ )
366
+ loop_escape.extend(target_store_escapes)
367
+ results.append(LocalDeclarationFact(
368
+ function_id,
369
+ tuple(dict.fromkeys(use_before)),
370
+ tuple(dict.fromkeys(loop_escape)),
371
+ tuple(dict.fromkeys(control_defs)),
372
+ tuple(dict.fromkeys(representation_conflicts)),
373
+ tuple(dict.fromkeys(target_rebinds)),
374
+ tuple(dict.fromkeys(target_mutations)),
375
+ ))
376
+ return tuple(sorted(results, key=lambda item: item.function_node_id))
377
+
378
+ def call_graph(self, calls: tuple[CallTargetFact, ...]) -> CallGraphFact:
379
+ owner_by_node = self.owner_by_node()
380
+ edges: list[tuple[str, str]] = []
381
+ call_for_edge: dict[tuple[str, str], list[str]] = {}
382
+ for call in calls:
383
+ caller = owner_by_node.get(call.call_node_id)
384
+ callee = call.target_function_node_id
385
+ if caller and callee:
386
+ edge = (caller, callee)
387
+ edges.append(edge)
388
+ call_for_edge.setdefault(edge, []).append(call.call_node_id)
389
+ graph = {item: set() for item in self.function_ids}
390
+ for left, right in edges:
391
+ graph.setdefault(left, set()).add(right)
392
+ # Iterative Kosaraju SCC analysis is linear in the call graph and does
393
+ # not consume Python recursion depth on large but bounded modules.
394
+ visited: set[str] = set()
395
+ finish: list[str] = []
396
+ for start in sorted(graph):
397
+ if start in visited:
398
+ continue
399
+ stack: list[tuple[str, bool]] = [(start, False)]
400
+ while stack:
401
+ current, expanded = stack.pop()
402
+ if expanded:
403
+ finish.append(current)
404
+ continue
405
+ if current in visited:
406
+ continue
407
+ visited.add(current)
408
+ stack.append((current, True))
409
+ for child in reversed(sorted(graph.get(current, ()))):
410
+ if child not in visited:
411
+ stack.append((child, False))
412
+ reverse = {item: set() for item in graph}
413
+ for left, children in graph.items():
414
+ for right in children:
415
+ reverse.setdefault(right, set()).add(left)
416
+ component_for: dict[str, int] = {}
417
+ recursive: set[str] = set()
418
+ recursive_components: set[int] = set()
419
+ component_number = 0
420
+ for start in reversed(finish):
421
+ if start in component_for:
422
+ continue
423
+ component_number += 1
424
+ component: list[str] = []
425
+ stack = [start]
426
+ component_for[start] = component_number
427
+ while stack:
428
+ current = stack.pop()
429
+ component.append(current)
430
+ for parent in reversed(sorted(reverse.get(current, ()))):
431
+ if parent not in component_for:
432
+ component_for[parent] = component_number
433
+ stack.append(parent)
434
+ if len(component) > 1 or any(item in graph.get(item, ()) for item in component):
435
+ recursive.update(component)
436
+ recursive_components.add(component_number)
437
+ recursive_calls = sorted(
438
+ call_id
439
+ for edge, call_ids in call_for_edge.items()
440
+ if component_for.get(edge[0]) == component_for.get(edge[1])
441
+ and component_for.get(edge[0]) in recursive_components
442
+ for call_id in call_ids
443
+ )
444
+ return CallGraphFact(tuple(sorted(set(edges))), tuple(sorted(recursive)), tuple(recursive_calls))
445
+
446
+ def owner_by_node(self) -> dict[str, str]:
447
+ result: dict[str, str] = {}
448
+ for function_id in self.function_ids:
449
+ self._mark_owned(function_id, function_id, result)
450
+ return result
451
+
452
+ def module_rejection(
453
+ self,
454
+ *,
455
+ allow_imports: bool = False,
456
+ allow_records: bool = False,
457
+ ) -> tuple[str, str, str] | None:
458
+ for node_id in self.module_body:
459
+ node = self.index.node(node_id)
460
+ if (
461
+ node["kind"] != "FunctionDef"
462
+ and not (allow_imports and node["kind"] == "ImportFrom")
463
+ and not (allow_records and node["kind"] == "ClassDef")
464
+ ):
465
+ return "PYC2902", "Phase 9 accepts only top-level function definitions in the selected module subset", node_id
466
+ for function_id in self.function_ids:
467
+ stack = list(reversed(self.index.node(function_id)["fields"].get("body", [])))
468
+ while stack:
469
+ node = self.index.node(stack.pop())
470
+ if node["kind"] in {"FunctionDef", "AsyncFunctionDef"}:
471
+ return "PYC2915", "Nested functions and closures are unsupported in Phase 9", node["node_id"]
472
+ for field_name in ("body", "orelse", "finalbody"):
473
+ children = node["fields"].get(field_name, [])
474
+ if isinstance(children, list):
475
+ stack.extend(reversed(children))
476
+ return None
477
+
478
+ def _signature(self, node: dict[str, Any], duplicates: set[str]) -> FunctionSignatureFact:
479
+ self._check_cancellation()
480
+ fields = node["fields"]
481
+ name = fields.get("name", "")
482
+ binding = self.binding_by_decl.get(node["node_id"])
483
+ args_node = self.index.node(fields["args"])
484
+ pos_ids = list(args_node["fields"].get("posonlyargs", [])) + list(args_node["fields"].get("args", []))
485
+ kwonly_ids = list(args_node["fields"].get("kwonlyargs", []))
486
+ parameter_ids = pos_ids + (kwonly_ids if self.allow_required_keyword_only else [])
487
+ parameters: list[ParameterFact] = []
488
+ reasons: list[str] = []
489
+ parameter_names: list[str] = []
490
+ for parameter_id in parameter_ids:
491
+ self._check_cancellation()
492
+ parameter_names.append(
493
+ self.index.node(parameter_id)["fields"].get("arg", "")
494
+ )
495
+ if len(parameter_names) != len(set(parameter_names)):
496
+ reasons.append("duplicate positional parameter names are unsupported")
497
+ if name in duplicates:
498
+ reasons.append("top-level function name is rebound")
499
+ if fields.get("decorator_list") or fields.get("type_params"):
500
+ reasons.append("decorated or generic functions are unsupported")
501
+ keyword_defaults = args_node["fields"].get("kw_defaults", [])
502
+ keyword_only_valid = bool(
503
+ self.allow_required_keyword_only
504
+ and kwonly_ids
505
+ and isinstance(keyword_defaults, list)
506
+ and len(keyword_defaults) == len(kwonly_ids)
507
+ and all(item is None for item in keyword_defaults)
508
+ )
509
+ if (
510
+ args_node["fields"].get("vararg")
511
+ or args_node["fields"].get("kwarg")
512
+ or args_node["fields"].get("defaults")
513
+ or (kwonly_ids and not keyword_only_valid)
514
+ ):
515
+ reasons.append("default, keyword-only, or variadic parameters are unsupported")
516
+ for ordinal, parameter_id in enumerate(parameter_ids):
517
+ self._check_cancellation()
518
+ parameter = self.index.node(parameter_id)
519
+ category, annotation_id, spelling = self._annotation(parameter["fields"].get("annotation"))
520
+ parameter_binding = self.binding_by_decl.get(parameter_id)
521
+ passing, ownership, lifetime = _boundary(category)
522
+ parameters.append(ParameterFact(parameter_id, parameter_binding["binding_id"] if parameter_binding else "", parameter["fields"].get("arg", ""), ordinal, category, CATEGORY_C_TYPES.get(category), annotation_id, spelling, passing, ownership, lifetime))
523
+ if category not in CATEGORY_C_TYPES or not parameter_binding:
524
+ reasons.append(f"parameter {parameter['fields'].get('arg', '')} lacks an exact supported annotation")
525
+ return_category, return_annotation_id, return_spelling = self._annotation(fields.get("returns"))
526
+ if return_category not in CATEGORY_C_TYPES:
527
+ reasons.append("function return lacks an exact supported annotation")
528
+ return_passing, return_ownership, return_lifetime = _boundary(return_category)
529
+ return FunctionSignatureFact(node["node_id"], binding["binding_id"] if binding else None, name, tuple(parameters), return_category, CATEGORY_C_TYPES.get(return_category), return_annotation_id, return_spelling, return_passing, return_ownership, return_lifetime, True, not reasons, "; ".join(dict.fromkeys(reasons)) or None)
530
+
531
+ def _annotation(self, node_id: str | None) -> tuple[ValueCategory, str | None, str | None]:
532
+ if not node_id or node_id not in self.index.nodes:
533
+ return ValueCategory.UNKNOWN, None, None
534
+ node = self.index.node(node_id)
535
+ if node["kind"] != "Name":
536
+ return ValueCategory.UNKNOWN, node_id, None
537
+ spelling = node["fields"].get("id")
538
+ return ANNOTATION_CATEGORIES.get(spelling, ValueCategory.UNKNOWN), node_id, spelling
539
+
540
+ def _is_range_iterator(self, call: dict[str, Any]) -> bool:
541
+ parent_id = self.parent_by_node.get(call["node_id"])
542
+ if not parent_id:
543
+ return False
544
+ parent = self.index.node(parent_id)
545
+ if parent["kind"] != "For" or parent["fields"].get("iter") != call["node_id"]:
546
+ return False
547
+ callee_id = call["fields"].get("func")
548
+ callee = self.index.nodes.get(callee_id)
549
+ binding = self.binding_by_occurrence.get(callee_id)
550
+ return bool(
551
+ callee
552
+ and callee["kind"] == "Name"
553
+ and callee["fields"].get("id") == "range"
554
+ and binding
555
+ and binding["binding_kind"] == "implicit-global"
556
+ )
557
+
558
+ def _range_call_fact(self, node: dict[str, Any], categories: dict[str, ValueCategory]) -> CallTargetFact:
559
+ args = tuple(node["fields"].get("args", []))
560
+ actual = tuple(categories.get(item, ValueCategory.UNKNOWN) for item in args)
561
+ supported = not node["fields"].get("keywords") and 1 <= len(args) <= 3 and all(item is ValueCategory.INTEGER for item in actual)
562
+ return CallTargetFact(node["node_id"], node["fields"].get("func"), None, None, "range", "recognized-range" if supported else "invalid-range", args, actual, tuple(ValueCategory.INTEGER for _ in args), ValueCategory.CALLABLE, args, True, tuple("by-value" for _ in args), (), supported, None if supported else "PYC2842", None if supported else "range requires one to three positional integer arguments")
563
+
564
+ def _unsupported_call(self, node: dict[str, Any], callee_id: str | None, args: tuple[str, ...], categories: tuple[ValueCategory, ...], resolution: str, code: str, reason: str) -> CallTargetFact:
565
+ return CallTargetFact(node["node_id"], callee_id, None, None, None, resolution, args, categories, (), ValueCategory.UNKNOWN, args, True, (), (), False, code, reason)
566
+
567
+ def _call_from_signature(self, node: dict[str, Any], signature: FunctionSignatureFact, args: tuple[str, ...], categories: tuple[ValueCategory, ...], supported: bool, code: str | None, reason: str | None) -> CallTargetFact:
568
+ params = tuple(item.category for item in signature.parameters)
569
+ ownership = tuple(f"{item.passing}:{item.ownership}:{item.lifetime}" for item in signature.parameters)
570
+ annotations = tuple(item.annotation_node_id for item in signature.parameters if item.annotation_node_id) + ((signature.return_annotation_node_id,) if signature.return_annotation_node_id else ())
571
+ return CallTargetFact(node["node_id"], node["fields"].get("func"), signature.function_node_id, signature.binding_id, signature.source_name, "understood-source-function" if supported else "ineligible-source-function", args, categories, params, signature.return_category, args, True, ownership, annotations, supported, code, reason)
572
+
573
+ def _collect_returns(self, node_ids: list[str], result: list[str]) -> None:
574
+ for node_id in node_ids:
575
+ node = self.index.node(node_id)
576
+ if node["kind"] in {"FunctionDef", "AsyncFunctionDef"}:
577
+ continue
578
+ if node["kind"] == "Return":
579
+ result.append(node_id)
580
+ for name in ("body", "orelse", "finalbody"):
581
+ children = node["fields"].get(name, [])
582
+ if isinstance(children, list):
583
+ self._collect_returns(children, result)
584
+
585
+ def _block_guarantees_return(self, node_ids: list[str]) -> bool:
586
+ for node_id in node_ids:
587
+ node = self.index.node(node_id)
588
+ if node["kind"] == "Return":
589
+ return True
590
+ if node["kind"] == "If":
591
+ body = node["fields"].get("body", [])
592
+ other = node["fields"].get("orelse", [])
593
+ if other and self._block_guarantees_return(body) and self._block_guarantees_return(other):
594
+ return True
595
+ return False
596
+
597
+ def _expression_loads(self, node_id: str) -> tuple[str, ...]:
598
+ result: list[str] = []
599
+ stack = [node_id]
600
+ while stack:
601
+ current = stack.pop()
602
+ node = self.index.node(current)
603
+ if node["kind"] == "Name" and current in self.binding_by_occurrence:
604
+ result.append(current)
605
+ stack.extend(reversed(self.index.child_ids(node)))
606
+ return tuple(result)
607
+
608
+ def _check_expression(self, node_id: str | None, initialized: set[str], loop_only: set[str], use_before: list[str], loop_escape: list[str]) -> None:
609
+ if not node_id:
610
+ return
611
+ for occurrence in self._expression_loads(node_id):
612
+ binding = self.binding_by_occurrence[occurrence]
613
+ binding_id = binding["binding_id"]
614
+ if binding["binding_kind"] in {"local", "loop-target"} and binding_id not in initialized:
615
+ (loop_escape if binding_id in loop_only or binding["binding_kind"] == "loop-target" else use_before).append(occurrence)
616
+
617
+ def _analyze_statements(self, node_ids: list[str], initialized: set[str], loop_only: set[str], control_depth: int, use_before: list[str], loop_escape: list[str], control_defs: list[str]) -> set[str]:
618
+ state = set(initialized)
619
+ for node_id in node_ids:
620
+ node = self.index.node(node_id)
621
+ fields = node["fields"]
622
+ kind = node["kind"]
623
+ if kind == "Assign":
624
+ self._check_expression(fields.get("value"), state, loop_only, use_before, loop_escape)
625
+ for target_id in fields.get("targets", []):
626
+ binding = self.binding_by_occurrence.get(target_id)
627
+ if binding:
628
+ if control_depth and binding["binding_id"] not in state:
629
+ control_defs.append(target_id)
630
+ state.add(binding["binding_id"])
631
+ elif kind in {"Return", "Expr"}:
632
+ self._check_expression(fields.get("value"), state, loop_only, use_before, loop_escape)
633
+ elif kind == "If":
634
+ self._check_expression(fields.get("test"), state, loop_only, use_before, loop_escape)
635
+ left = self._analyze_statements(fields.get("body", []), set(state), set(loop_only), control_depth + 1, use_before, loop_escape, control_defs)
636
+ right = self._analyze_statements(fields.get("orelse", []), set(state), set(loop_only), control_depth + 1, use_before, loop_escape, control_defs) if fields.get("orelse") else set(state)
637
+ state |= left & right
638
+ elif kind == "While":
639
+ self._check_expression(fields.get("test"), state, loop_only, use_before, loop_escape)
640
+ self._analyze_statements(fields.get("body", []), set(state), set(loop_only), control_depth + 1, use_before, loop_escape, control_defs)
641
+ elif kind == "For":
642
+ self._check_expression(fields.get("iter"), state, loop_only, use_before, loop_escape)
643
+ target = self.binding_by_occurrence.get(fields.get("target"))
644
+ body_state = set(state)
645
+ body_loop_only = set(loop_only)
646
+ if target:
647
+ body_state.add(target["binding_id"])
648
+ body_loop_only.add(target["binding_id"])
649
+ self._analyze_statements(fields.get("body", []), body_state, body_loop_only, control_depth + 1, use_before, loop_escape, control_defs)
650
+ if target:
651
+ loop_only.add(target["binding_id"])
652
+ elif kind in {"FunctionDef", "AsyncFunctionDef"}:
653
+ continue
654
+ initialized.clear()
655
+ initialized.update(state)
656
+ return state
657
+
658
+ def _binding_hazards(
659
+ self,
660
+ node_ids: list[str],
661
+ categories: dict[str, ValueCategory],
662
+ binding_categories: dict[str, ValueCategory],
663
+ active_loop_targets: set[str],
664
+ representation_conflicts: list[str],
665
+ target_rebinds: list[str],
666
+ target_mutations: list[str],
667
+ target_store_escapes: list[str],
668
+ ) -> None:
669
+ for node_id in node_ids:
670
+ node = self.index.node(node_id)
671
+ fields = node["fields"]
672
+ kind = node["kind"]
673
+ if kind == "Assign":
674
+ value_category = categories.get(fields.get("value"), ValueCategory.UNKNOWN)
675
+ for target_id in fields.get("targets", []):
676
+ binding = self.binding_by_occurrence.get(target_id)
677
+ if not binding:
678
+ continue
679
+ binding_id = binding["binding_id"]
680
+ prior = binding_categories.get(binding_id, ValueCategory.UNKNOWN)
681
+ if value_category is ValueCategory.CONTRADICTORY or (
682
+ prior not in {ValueCategory.UNKNOWN, value_category}
683
+ and value_category is not ValueCategory.UNKNOWN
684
+ ):
685
+ representation_conflicts.append(target_id)
686
+ elif prior is ValueCategory.UNKNOWN and value_category is not ValueCategory.UNKNOWN:
687
+ binding_categories[binding_id] = value_category
688
+ if binding["binding_kind"] == "loop-target":
689
+ if binding_id in active_loop_targets:
690
+ target_mutations.append(target_id)
691
+ else:
692
+ target_store_escapes.append(target_id)
693
+ elif kind == "For":
694
+ target_id = fields.get("target")
695
+ binding = self.binding_by_occurrence.get(target_id)
696
+ nested_targets = set(active_loop_targets)
697
+ if binding:
698
+ binding_id = binding["binding_id"]
699
+ if binding["declaration_node_id"] != target_id:
700
+ target_rebinds.append(target_id)
701
+ prior = binding_categories.get(binding_id, ValueCategory.UNKNOWN)
702
+ target_category = categories.get(target_id, ValueCategory.INTEGER)
703
+ if target_category is ValueCategory.UNKNOWN:
704
+ target_category = ValueCategory.INTEGER
705
+ if prior not in {ValueCategory.UNKNOWN, target_category}:
706
+ representation_conflicts.append(target_id)
707
+ binding_categories[binding_id] = target_category
708
+ nested_targets.add(binding_id)
709
+ self._binding_hazards(fields.get("body", []), categories, binding_categories, nested_targets, representation_conflicts, target_rebinds, target_mutations, target_store_escapes)
710
+ elif kind in {"If", "While"}:
711
+ self._binding_hazards(fields.get("body", []), categories, binding_categories, set(active_loop_targets), representation_conflicts, target_rebinds, target_mutations, target_store_escapes)
712
+ self._binding_hazards(fields.get("orelse", []), categories, binding_categories, set(active_loop_targets), representation_conflicts, target_rebinds, target_mutations, target_store_escapes)
713
+ elif kind in {"FunctionDef", "AsyncFunctionDef"}:
714
+ continue
715
+
716
+ def _mark_owned(self, node_id: str, owner: str, result: dict[str, str]) -> None:
717
+ if node_id in result:
718
+ return
719
+ result[node_id] = owner
720
+ node = self.index.node(node_id)
721
+ for child in self.index.child_ids(node):
722
+ if child != node_id and self.index.node(child)["kind"] not in {"FunctionDef", "AsyncFunctionDef"}:
723
+ self._mark_owned(child, owner, result)