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,1976 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import replace
5
+ from types import MappingProxyType
6
+ from typing import Any
7
+
8
+ from pycforge.converter.contracts.configuration import (
9
+ PHASE9_RULE_SET,
10
+ PHASE11_RULE_SET,
11
+ supports_containers,
12
+ supports_conditional_regions,
13
+ supports_functions,
14
+ supports_keyword_calls,
15
+ supports_keyword_only_calls,
16
+ supports_modules,
17
+ supports_numeric,
18
+ supports_records,
19
+ )
20
+ from pycforge.converter.contracts.versions import (
21
+ CONDITIONAL_FACT_SCHEMA,
22
+ CONTAINER_FACT_SCHEMA,
23
+ CONVERSION_PLAN_SCHEMA,
24
+ NUMERIC_FACT_SCHEMA,
25
+ PHASE14B_CONVERSION_PLAN_SCHEMA,
26
+ PHASE14C_CONVERSION_PLAN_SCHEMA,
27
+ PHASE14A_CONVERSION_PLAN_SCHEMA,
28
+ PHASE13_CONVERSION_PLAN_SCHEMA,
29
+ PHASE12_CONVERSION_PLAN_SCHEMA,
30
+ RECORD_FACT_SCHEMA,
31
+ )
32
+ from pycforge.converter.containers.analysis import BoundedContainerAnalyzer
33
+ from pycforge.converter.core.diagnostics import Diagnostic
34
+ from pycforge.converter.core.enums import Severity, StageTerminal
35
+ from pycforge.converter.core.fingerprint import fingerprint
36
+ from pycforge.converter.core.stage_artifact import StageArtifact
37
+ from pycforge.converter.core.stage_outcome import StageOutcome
38
+ from pycforge.converter.records import (
39
+ RecordAnalysisCanceled,
40
+ RecordAnalysisError,
41
+ StaticRecordAnalyzer,
42
+ )
43
+ from pycforge.converter.numeric_semantics import (
44
+ BoundedNumericAnalyzer,
45
+ NumericAnalysisCanceled,
46
+ NumericAnalysisError,
47
+ )
48
+ from pycforge.converter.conditional_regions.analysis import ConditionalRegionAnalyzer
49
+ from pycforge.converter.conditional_regions.model import (
50
+ CONDITIONAL_REGION_KEY_DOMAIN,
51
+ CONDITIONAL_REGION_PROVENANCE_EVIDENCE,
52
+ CONDITIONAL_REGION_TABLE_DEPENDENCIES,
53
+ CONDITIONAL_REGION_TABLE_ID,
54
+ ConditionalRegionAnalysisCanceled,
55
+ ConditionalRegionAnalysisError,
56
+ ConditionalRegionValidationCanceled,
57
+ )
58
+ from pycforge.converter.keyword_calls.analysis import KeywordCallAnalyzer
59
+ from pycforge.converter.keyword_calls.model import (
60
+ CUMULATIVE_KEYWORD_TARGET_DIAGNOSTIC_CODE,
61
+ CUMULATIVE_KEYWORD_TARGET_REASON,
62
+ KEYWORD_CALL_KEY_DOMAIN,
63
+ KEYWORD_CALL_PROVENANCE_EVIDENCE,
64
+ KEYWORD_CALL_TABLE_DEPENDENCIES,
65
+ KEYWORD_CALL_TABLE_ID,
66
+ KeywordCallAnalysis,
67
+ KeywordCallAnalysisCanceled,
68
+ KeywordCallAnalysisError,
69
+ KeywordCallRejection,
70
+ KeywordCallValidationCanceled,
71
+ )
72
+ from pycforge.converter.contracts.versions import KEYWORD_CALL_FACT_SCHEMA
73
+ from pycforge.converter.keyword_only_calls.analysis import KeywordOnlyCallAnalyzer
74
+ from pycforge.converter.keyword_only_calls.model import (
75
+ CUMULATIVE_KEYWORD_ONLY_TARGET_DIAGNOSTIC_CODE,
76
+ CUMULATIVE_KEYWORD_ONLY_TARGET_REASON,
77
+ KEYWORD_ONLY_CALL_KEY_DOMAIN,
78
+ KEYWORD_ONLY_CALL_PROVENANCE_EVIDENCE,
79
+ KEYWORD_ONLY_CALL_TABLE_DEPENDENCIES,
80
+ KEYWORD_ONLY_CALL_TABLE_ID,
81
+ KeywordOnlyCallAnalysis,
82
+ KeywordOnlyCallAnalysisCanceled,
83
+ KeywordOnlyCallAnalysisError,
84
+ KeywordOnlyCallRejection,
85
+ KeywordOnlyCallValidationCanceled,
86
+ )
87
+ from pycforge.converter.contracts.versions import KEYWORD_ONLY_CALL_FACT_SCHEMA
88
+
89
+ from .functions import (
90
+ CallTargetFact,
91
+ FunctionAnalysisCanceled,
92
+ FunctionFactsAnalyzer,
93
+ )
94
+ from .model import Completeness, FactProvenance, FactRecord, FactTable, SupportState, ValueCategory
95
+ from .planning import AnalysisCanceled, AnalysisPlanner, RulePlan, default_registry, stable_id
96
+ from .symbols import SymbolScopeAnalyzer
97
+ from .validation import validate_analysis_payload
98
+
99
+
100
+ def _artifact(prior: StageArtifact, payload: dict[str, object], version: str) -> StageArtifact:
101
+ fp = fingerprint("stage-artifact", {"kind": "conversion_plan", "conversion_id": prior.conversion_id, "parent": prior.artifact_fingerprint.value, "payload": payload})
102
+ return StageArtifact("conversion_plan", version, prior.conversion_id, prior.artifact_fingerprint, MappingProxyType(payload), fp)
103
+
104
+
105
+ def _extend_plan(plan: RulePlan, *, facts: tuple[str, ...] = (), obligations: tuple[str, ...] = (), explanation: tuple[str, ...] = ()) -> RulePlan:
106
+ merged_facts = tuple(sorted(set(plan.facts_used) | set(facts)))
107
+ merged_obligations = tuple(dict.fromkeys(plan.semantic_obligations + obligations))
108
+ return replace(
109
+ plan,
110
+ facts_used=merged_facts,
111
+ semantic_obligations=merged_obligations,
112
+ resolved_obligations=merged_obligations,
113
+ explanation_tokens=plan.explanation_tokens + explanation,
114
+ )
115
+
116
+
117
+ class AnalysisPlanningStage:
118
+ stage_id = "analysis.plan"
119
+ input_schema = "python-ir/0.4"
120
+ input_schemas = ("python-ir/0.3", "python-ir/0.4")
121
+ output_schema = CONVERSION_PLAN_SCHEMA
122
+
123
+ def run(self, artifact: StageArtifact, services: Any) -> StageOutcome:
124
+ if services.context.cancellation.is_canceled:
125
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled"),))
126
+ module = artifact.payload["python_ir"]
127
+ module_fact_tables = tuple(artifact.payload.get("module_fact_tables", ()))
128
+ module_table_values = {
129
+ table["table_id"]: tuple(record["value"] for record in table.get("records", ()))
130
+ for table in module_fact_tables
131
+ }
132
+ module_imports = module_table_values.get("module-import-facts", ())
133
+ module_functions = module_table_values.get("module-function-facts", ())
134
+ module_identities = module_table_values.get("module-identity-facts", ())
135
+ module_initialization = module_table_values.get("module-initialization-facts", ())
136
+ module_sources = module_table_values.get("module-source-facts", ())
137
+ imports_by_node: dict[str, list[dict[str, Any]]] = {}
138
+ for item in module_imports:
139
+ imports_by_node.setdefault(item.get("import_node_id", ""), []).append(item)
140
+ imports_by_alias = {item.get("alias_node_id"): item for item in module_imports}
141
+ functions_by_node = {item.get("function_node_id"): item for item in module_functions}
142
+ document_plan_nodes = {item.get("document_plan_node_id"): item for item in module_identities}
143
+ initialization_by_node = {item.get("initialization_node_id"): item for item in module_initialization}
144
+ assembly_node_id = artifact.payload.get("module_bundle_assembly_node_id")
145
+ request = services.context.canonical.request
146
+ records_enabled = supports_records(request.rule_set_version)
147
+ numeric_enabled = supports_numeric(request.rule_set_version)
148
+ conditional_enabled = supports_conditional_regions(request.rule_set_version)
149
+ keyword_calls_enabled = supports_keyword_calls(request.rule_set_version)
150
+ keyword_only_calls_enabled = supports_keyword_only_calls(
151
+ request.rule_set_version
152
+ )
153
+ containers_enabled = supports_containers(request.rule_set_version)
154
+ modules_enabled = supports_modules(request.rule_set_version)
155
+ scopes, bindings, scope_for_node = SymbolScopeAnalyzer().analyze(
156
+ module,
157
+ allow_records=records_enabled,
158
+ )
159
+ if services.context.cancellation.is_canceled:
160
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during symbol analysis"),))
161
+ binding_dicts = tuple(binding.to_dict() for binding in bindings)
162
+ occurrence_bindings = {occurrence: binding.binding_id for binding in bindings for occurrence in binding.occurrence_node_ids}
163
+ binding_by_declaration = {binding.declaration_node_id: binding.binding_id for binding in bindings}
164
+ nodes_by_id = {node["node_id"]: node for node in module["nodes"]}
165
+ binding_categories = self._annotation_categories(module, bindings)
166
+ module_source_by_id = {item.get("module_id"): item for item in module_sources}
167
+ function_records = {
168
+ node_id: {
169
+ **item,
170
+ "logical_name": module_source_by_id.get(item.get("module_id"), {}).get("logical_name", "<memory>"),
171
+ }
172
+ for node_id, item in functions_by_node.items()
173
+ }
174
+ module_records = {
175
+ node_id: {
176
+ **dict(item),
177
+ "binding_id": binding_by_declaration.get(node_id),
178
+ "logical_name": module_source_by_id.get(item.get("module_id"), {}).get("logical_name", "<memory>"),
179
+ }
180
+ for node_id, item in dict(artifact.payload.get("module_record_by_node", {})).items()
181
+ } if records_enabled else {}
182
+ record_class_binding_ids = {
183
+ item["binding_id"] for item in module_records.values() if item.get("binding_id")
184
+ }
185
+ for binding_id in record_class_binding_ids:
186
+ binding_categories[binding_id] = ValueCategory.CALLABLE
187
+ annotation_binding_categories = dict(binding_categories)
188
+ constructor_call_ids = frozenset(
189
+ node["node_id"]
190
+ for node in module["nodes"]
191
+ if node["kind"] == "Call"
192
+ and occurrence_bindings.get(node["fields"].get("func")) in record_class_binding_ids
193
+ )
194
+ try:
195
+ function_analyzer = FunctionFactsAnalyzer(
196
+ module,
197
+ binding_dicts,
198
+ ignored_call_node_ids=constructor_call_ids,
199
+ allow_required_keyword_only=keyword_only_calls_enabled,
200
+ cancellation=services.context.cancellation,
201
+ )
202
+ signatures = function_analyzer.signatures()
203
+ except FunctionAnalysisCanceled:
204
+ return StageOutcome(
205
+ StageTerminal.CANCELED,
206
+ diagnostics=(Diagnostic(
207
+ "PYC1901",
208
+ Severity.ERROR,
209
+ self.stage_id,
210
+ "Conversion canceled during signature analysis",
211
+ ),),
212
+ )
213
+ if services.context.cancellation.is_canceled:
214
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during signature analysis"),))
215
+ planner = AnalysisPlanner()
216
+ preliminary_returns = function_analyzer.preliminary_call_returns(signatures)
217
+ preliminary_returns.update({node_id: ValueCategory.RECORD for node_id in constructor_call_ids})
218
+ try:
219
+ categories = planner.analyze_categories(
220
+ module,
221
+ occurrence_bindings,
222
+ binding_categories,
223
+ preliminary_returns,
224
+ services.context.cancellation,
225
+ )
226
+ except AnalysisCanceled:
227
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled"),))
228
+ container_shapes = ()
229
+ container_bindings = ()
230
+ container_accesses = ()
231
+ container_iterations = ()
232
+ access_categories: dict[str, ValueCategory] = {}
233
+ iteration_categories: dict[str, ValueCategory] = {}
234
+ if containers_enabled:
235
+ container_shapes, container_bindings, container_accesses, container_iterations = BoundedContainerAnalyzer(
236
+ module,
237
+ binding_dicts,
238
+ categories,
239
+ ).analyze()
240
+ access_categories = {
241
+ item.subscript_node_id: item.result_category
242
+ for item in container_accesses
243
+ if item.supported
244
+ }
245
+ iteration_categories = {
246
+ item.for_node_id: item.target_category
247
+ for item in container_iterations
248
+ if item.supported
249
+ }
250
+ binding_categories.clear()
251
+ binding_categories.update(annotation_binding_categories)
252
+ try:
253
+ categories = planner.analyze_categories(
254
+ module,
255
+ occurrence_bindings,
256
+ binding_categories,
257
+ preliminary_returns,
258
+ services.context.cancellation,
259
+ access_categories,
260
+ iteration_categories,
261
+ )
262
+ except AnalysisCanceled:
263
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled"),))
264
+ container_shapes, container_bindings, container_accesses, container_iterations = BoundedContainerAnalyzer(
265
+ module,
266
+ binding_dicts,
267
+ categories,
268
+ ).analyze()
269
+ record_analysis = None
270
+ record_access_categories: dict[str, ValueCategory] = {}
271
+ if records_enabled:
272
+ primary_identity = next(
273
+ (item for item in module_identities if item.get("is_primary")),
274
+ module_identities[0] if module_identities else {},
275
+ )
276
+ try:
277
+ record_analysis = StaticRecordAnalyzer(
278
+ module,
279
+ module_records=module_records,
280
+ function_records=function_records,
281
+ bindings=binding_dicts,
282
+ categories=categories,
283
+ cancellation=services.context.cancellation,
284
+ default_module_id=str(primary_identity.get("module_id", "__main__")),
285
+ default_logical_name=str(primary_identity.get("logical_name", "<memory>")),
286
+ ).analyze()
287
+ except RecordAnalysisCanceled:
288
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during static-record analysis"),))
289
+ except RecordAnalysisError as exc:
290
+ return self._record_rejection(exc)
291
+ record_access_categories = {
292
+ item.access_node_id: item.field_category for item in record_analysis.accesses
293
+ }
294
+ binding_categories.clear()
295
+ binding_categories.update(annotation_binding_categories)
296
+ try:
297
+ categories = planner.analyze_categories(
298
+ module,
299
+ occurrence_bindings,
300
+ binding_categories,
301
+ preliminary_returns,
302
+ services.context.cancellation,
303
+ access_categories,
304
+ iteration_categories,
305
+ record_access_categories,
306
+ )
307
+ except AnalysisCanceled:
308
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled"),))
309
+ if containers_enabled:
310
+ container_shapes, container_bindings, container_accesses, container_iterations = BoundedContainerAnalyzer(
311
+ module,
312
+ binding_dicts,
313
+ categories,
314
+ ).analyze()
315
+ access_categories = {
316
+ item.subscript_node_id: item.result_category
317
+ for item in container_accesses
318
+ if item.supported
319
+ }
320
+ iteration_categories = {
321
+ item.for_node_id: item.target_category
322
+ for item in container_iterations
323
+ if item.supported
324
+ }
325
+ binding_categories.clear()
326
+ binding_categories.update(annotation_binding_categories)
327
+ try:
328
+ categories = planner.analyze_categories(
329
+ module,
330
+ occurrence_bindings,
331
+ binding_categories,
332
+ preliminary_returns,
333
+ services.context.cancellation,
334
+ access_categories,
335
+ iteration_categories,
336
+ record_access_categories,
337
+ )
338
+ except AnalysisCanceled:
339
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled"),))
340
+ try:
341
+ record_analysis = StaticRecordAnalyzer(
342
+ module,
343
+ module_records=module_records,
344
+ function_records=function_records,
345
+ bindings=binding_dicts,
346
+ categories=categories,
347
+ cancellation=services.context.cancellation,
348
+ default_module_id=str(primary_identity.get("module_id", "__main__")),
349
+ default_logical_name=str(primary_identity.get("logical_name", "<memory>")),
350
+ ).analyze()
351
+ except RecordAnalysisCanceled:
352
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during static-record analysis"),))
353
+ except RecordAnalysisError as exc:
354
+ return self._record_rejection(exc)
355
+ keyword_call_analysis = None
356
+ if keyword_calls_enabled:
357
+ try:
358
+ keyword_call_analysis = KeywordCallAnalyzer(
359
+ module,
360
+ bindings=binding_dicts,
361
+ signatures=signatures,
362
+ categories=categories,
363
+ cancellation=services.context.cancellation,
364
+ ignored_call_node_ids=constructor_call_ids,
365
+ ).analyze()
366
+ except KeywordCallAnalysisCanceled:
367
+ return StageOutcome(
368
+ StageTerminal.CANCELED,
369
+ diagnostics=(Diagnostic(
370
+ "PYC1901",
371
+ Severity.ERROR,
372
+ self.stage_id,
373
+ "Conversion canceled during keyword-call analysis",
374
+ ),),
375
+ )
376
+ except KeywordCallAnalysisError as exc:
377
+ return self._keyword_call_rejection(exc)
378
+ keyword_only_call_analysis = None
379
+ if keyword_only_calls_enabled:
380
+ try:
381
+ keyword_only_call_analysis = KeywordOnlyCallAnalyzer(
382
+ module,
383
+ bindings=binding_dicts,
384
+ signatures=signatures,
385
+ categories=categories,
386
+ cancellation=services.context.cancellation,
387
+ ignored_call_node_ids=constructor_call_ids,
388
+ ).analyze()
389
+ except KeywordOnlyCallAnalysisCanceled:
390
+ return StageOutcome(
391
+ StageTerminal.CANCELED,
392
+ diagnostics=(Diagnostic(
393
+ "PYC1901",
394
+ Severity.ERROR,
395
+ self.stage_id,
396
+ "Conversion canceled during keyword-only call analysis",
397
+ ),),
398
+ )
399
+ except KeywordOnlyCallAnalysisError as exc:
400
+ return self._keyword_only_call_rejection(exc)
401
+ calls = function_analyzer.calls(signatures, categories)
402
+ if keyword_call_analysis is not None:
403
+ keyword_facts_by_call = keyword_call_analysis.fact_by_call_node_id
404
+ calls = tuple(
405
+ replace(
406
+ call,
407
+ argument_node_ids=feature.source_argument_node_ids,
408
+ argument_categories=tuple(
409
+ ValueCategory(item)
410
+ for item in feature.source_argument_categories
411
+ ),
412
+ evaluation_order=feature.evaluation_order,
413
+ arguments_evaluated_once=feature.arguments_evaluated_once,
414
+ resolution=(
415
+ "understood-source-function"
416
+ if feature.supported
417
+ else "ineligible-source-function"
418
+ ),
419
+ supported=feature.supported,
420
+ diagnostic_code=feature.diagnostic_code,
421
+ reason=feature.reason,
422
+ )
423
+ if (feature := keyword_facts_by_call.get(call.call_node_id)) is not None
424
+ else call
425
+ for call in calls
426
+ )
427
+ if keyword_only_call_analysis is not None:
428
+ keyword_only_facts_by_call = (
429
+ keyword_only_call_analysis.fact_by_call_node_id
430
+ )
431
+ calls = tuple(
432
+ replace(
433
+ call,
434
+ argument_node_ids=feature.source_argument_node_ids,
435
+ argument_categories=tuple(
436
+ ValueCategory(item)
437
+ for item in feature.source_argument_categories
438
+ ),
439
+ evaluation_order=feature.evaluation_order,
440
+ arguments_evaluated_once=feature.arguments_evaluated_once,
441
+ resolution=(
442
+ "understood-source-function"
443
+ if feature.supported
444
+ else "ineligible-source-function"
445
+ ),
446
+ supported=feature.supported,
447
+ diagnostic_code=feature.diagnostic_code,
448
+ reason=feature.reason,
449
+ )
450
+ if (
451
+ feature := keyword_only_facts_by_call.get(call.call_node_id)
452
+ ) is not None
453
+ else call
454
+ for call in calls
455
+ )
456
+ if services.context.cancellation.is_canceled:
457
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during call analysis"),))
458
+ graph = function_analyzer.call_graph(calls)
459
+ if services.context.cancellation.is_canceled:
460
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during call-graph analysis"),))
461
+ recursive_calls = set(graph.recursive_call_node_ids)
462
+ if recursive_calls:
463
+ calls = tuple(
464
+ replace(
465
+ call,
466
+ resolution="recursive-target",
467
+ supported=False,
468
+ diagnostic_code="PYC2920",
469
+ reason="Direct and mutual recursion are unsupported in Phase 9",
470
+ ) if call.call_node_id in recursive_calls else call
471
+ for call in calls
472
+ )
473
+ numeric_analysis = None
474
+ if numeric_enabled:
475
+ try:
476
+ numeric_analysis = BoundedNumericAnalyzer(
477
+ module,
478
+ categories=categories,
479
+ function_records=function_records,
480
+ supported_call_node_ids=frozenset(
481
+ item.call_node_id for item in calls if item.supported
482
+ ),
483
+ supported_container_access_node_ids=frozenset(
484
+ item.subscript_node_id
485
+ for item in container_accesses
486
+ if item.supported
487
+ ),
488
+ supported_record_access_node_ids=frozenset(
489
+ item.access_node_id
490
+ for item in (record_analysis.accesses if record_analysis else ())
491
+ ),
492
+ rejected_expression_diagnostics={
493
+ **{
494
+ item.call_node_id: (
495
+ item.diagnostic_code or "PYC2901",
496
+ item.reason or "Call expression is unsupported",
497
+ )
498
+ for item in calls
499
+ if not item.supported
500
+ },
501
+ **{
502
+ item.subscript_node_id: (
503
+ item.diagnostic_code or "PYC3404",
504
+ item.reason or "Container access is unsupported",
505
+ )
506
+ for item in container_accesses
507
+ if not item.supported
508
+ },
509
+ },
510
+ cancellation=services.context.cancellation,
511
+ ).analyze()
512
+ except NumericAnalysisCanceled:
513
+ return StageOutcome(
514
+ StageTerminal.CANCELED,
515
+ diagnostics=(Diagnostic(
516
+ "PYC1901",
517
+ Severity.ERROR,
518
+ self.stage_id,
519
+ "Conversion canceled during bounded numeric analysis",
520
+ ),),
521
+ )
522
+ except NumericAnalysisError as exc:
523
+ return self._numeric_rejection(exc)
524
+ return_paths = function_analyzer.return_paths(signatures, categories)
525
+ if services.context.cancellation.is_canceled:
526
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during return-path analysis"),))
527
+ local_facts = function_analyzer.local_declarations(categories, signatures)
528
+ if services.context.cancellation.is_canceled:
529
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled during local-declaration analysis"),))
530
+ owners = function_analyzer.owner_by_node()
531
+
532
+ conditional_analysis = None
533
+ if conditional_enabled:
534
+ try:
535
+ conditional_analysis = ConditionalRegionAnalyzer(
536
+ module,
537
+ categories=categories,
538
+ function_records=function_records,
539
+ owner_by_node=owners,
540
+ supported_call_node_ids=frozenset(
541
+ item.call_node_id for item in calls if item.supported
542
+ ),
543
+ numeric_operation_node_ids=frozenset(
544
+ item.binop_node_id
545
+ for item in (numeric_analysis.operations if numeric_analysis else ())
546
+ ),
547
+ supported_container_access_node_ids=frozenset(
548
+ item.subscript_node_id
549
+ for item in container_accesses
550
+ if item.supported
551
+ ),
552
+ supported_record_access_node_ids=frozenset(
553
+ item.access_node_id
554
+ for item in (record_analysis.accesses if record_analysis else ())
555
+ ),
556
+ cancellation=services.context.cancellation,
557
+ target_contract=request.target_contract,
558
+ ).analyze()
559
+ except ConditionalRegionAnalysisCanceled:
560
+ return StageOutcome(
561
+ StageTerminal.CANCELED,
562
+ diagnostics=(Diagnostic(
563
+ "PYC1901",
564
+ Severity.ERROR,
565
+ self.stage_id,
566
+ "Conversion canceled during conditional-region analysis",
567
+ ),),
568
+ )
569
+ except ConditionalRegionAnalysisError as exc:
570
+ return self._conditional_rejection(exc)
571
+
572
+ signature_by_node = {item.function_node_id: item for item in signatures}
573
+ return_by_function = {item.function_node_id: item for item in return_paths}
574
+ local_by_function = {item.function_node_id: item for item in local_facts}
575
+ try:
576
+ function_eligible = self._cumulative_function_eligibility(
577
+ signature_by_node,
578
+ return_by_function,
579
+ local_by_function,
580
+ calls,
581
+ owners,
582
+ graph,
583
+ services.context.cancellation,
584
+ )
585
+ except AnalysisCanceled:
586
+ return StageOutcome(
587
+ StageTerminal.CANCELED,
588
+ diagnostics=(Diagnostic(
589
+ "PYC1901",
590
+ Severity.ERROR,
591
+ self.stage_id,
592
+ "Conversion canceled during cumulative function eligibility analysis",
593
+ ),),
594
+ )
595
+ if keyword_call_analysis is not None:
596
+ keyword_call_analysis, calls = self._gate_keyword_calls_by_target(
597
+ keyword_call_analysis,
598
+ calls,
599
+ function_eligible,
600
+ nodes_by_id,
601
+ )
602
+ if keyword_only_call_analysis is not None:
603
+ keyword_only_call_analysis, calls = (
604
+ self._gate_keyword_only_calls_by_target(
605
+ keyword_only_call_analysis,
606
+ calls,
607
+ function_eligible,
608
+ nodes_by_id,
609
+ )
610
+ )
611
+ call_by_node = {item.call_node_id: item for item in calls}
612
+
613
+ if modules_enabled:
614
+ for imported in sorted(module_imports, key=lambda item: item.get("import_item_id", "")):
615
+ target_function_id = imported.get("target_function_node_id")
616
+ if function_eligible.get(target_function_id, False):
617
+ continue
618
+ alias_node = nodes_by_id.get(imported.get("alias_node_id"), {})
619
+ target_node = nodes_by_id.get(target_function_id, {})
620
+ source_span = alias_node.get("provenance", {}).get("source_span")
621
+ target_span = target_node.get("provenance", {}).get("source_span")
622
+ return StageOutcome(
623
+ StageTerminal.REJECTED,
624
+ diagnostics=(Diagnostic(
625
+ "PYC3505",
626
+ Severity.ERROR,
627
+ self.stage_id,
628
+ f"Imported member {imported.get('target_module_id')}.{imported.get('imported_name')} is not an eligible direct function",
629
+ source_span=source_span,
630
+ related_spans=(target_span,) if isinstance(target_span, dict) else (),
631
+ fact_references=(f"module-import:{imported.get('import_item_id')}", f"module-function:{target_function_id}"),
632
+ obligation_references=("imported-target-eligible",),
633
+ explanation="Imported functions must satisfy the cumulative function, call, return, local, and recursion boundaries.",
634
+ ),),
635
+ )
636
+
637
+ registry = default_registry(
638
+ include_records=records_enabled,
639
+ include_numeric=numeric_enabled,
640
+ include_conditional_regions=conditional_enabled,
641
+ include_keyword_calls=keyword_calls_enabled,
642
+ include_keyword_only_calls=keyword_only_calls_enabled,
643
+ )
644
+ target = request.target_contract
645
+ semantics = request.semantic_policy
646
+ ruleset = request.rule_set_version
647
+ plans: list[dict[str, Any]] = []
648
+ support: list[dict[str, Any]] = []
649
+ representations: list[dict[str, Any]] = []
650
+ effects: list[dict[str, Any]] = []
651
+ truthiness: list[dict[str, Any]] = []
652
+ order: list[dict[str, Any]] = []
653
+ shape_by_node = {item.literal_node_id: item for item in container_shapes}
654
+ container_binding_by_assignment = {item.assignment_node_id: item for item in container_bindings}
655
+ container_binding_by_id = {item.binding_id: item for item in container_bindings}
656
+ access_by_node = {item.subscript_node_id: item for item in container_accesses}
657
+ iteration_by_node = {item.for_node_id: item for item in container_iterations}
658
+ record_definition_by_class = {
659
+ item.class_node_id: item for item in (record_analysis.definitions if record_analysis else ())
660
+ }
661
+ record_field_by_declaration = {
662
+ item.declaration_node_id: item for item in (record_analysis.fields if record_analysis else ())
663
+ }
664
+ record_initializer_by_function = {
665
+ item.function_node_id: item for item in (record_analysis.initializers if record_analysis else ())
666
+ }
667
+ record_instance_by_construction = {
668
+ item.construction_node_id: item for item in (record_analysis.instances if record_analysis else ())
669
+ }
670
+ record_instance_by_assignment = {
671
+ item.assignment_node_id: item for item in (record_analysis.instances if record_analysis else ())
672
+ }
673
+ record_binding_by_id = {
674
+ item.binding_id: item for item in (record_analysis.bindings if record_analysis else ())
675
+ }
676
+ record_access_by_node = {
677
+ item.access_node_id: item for item in (record_analysis.accesses if record_analysis else ())
678
+ }
679
+ record_name_node_ids = {
680
+ occurrence
681
+ for binding in binding_dicts
682
+ if binding.get("binding_id") in record_class_binding_ids
683
+ for occurrence in binding.get("occurrence_node_ids", ())
684
+ }
685
+ record_name_node_ids.update(
686
+ occurrence
687
+ for binding in record_binding_by_id.values()
688
+ for occurrence in binding.occurrence_node_ids
689
+ )
690
+ numeric_operation_by_node = {
691
+ item.binop_node_id: item
692
+ for item in (numeric_analysis.operations if numeric_analysis else ())
693
+ }
694
+ conditional_region_by_node = {
695
+ item.region_node_id: item
696
+ for item in (conditional_analysis.regions if conditional_analysis else ())
697
+ }
698
+ keyword_call_by_node = (
699
+ keyword_call_analysis.fact_by_call_node_id
700
+ if keyword_call_analysis is not None
701
+ else {}
702
+ )
703
+ keyword_only_call_by_node = (
704
+ keyword_only_call_analysis.fact_by_call_node_id
705
+ if keyword_only_call_analysis is not None
706
+ else {}
707
+ )
708
+
709
+ for ordinal, node in enumerate(sorted(module["nodes"], key=lambda item: item["node_id"])):
710
+ if services.context.cancellation.is_canceled:
711
+ return StageOutcome(StageTerminal.CANCELED, diagnostics=(Diagnostic("PYC1901", Severity.ERROR, self.stage_id, "Conversion canceled"),))
712
+ node_id = node["node_id"]
713
+ decision_key = stable_id("decision-", node_id, scope_for_node.get(node_id, ""), target, semantics, ruleset)
714
+ category = categories[node_id]
715
+ selectable_node = dict(node)
716
+ if node["kind"] == "BinOp":
717
+ op_node = nodes_by_id.get(node["fields"].get("op"))
718
+ selectable_node["op_kind"] = op_node["kind"] if op_node else None
719
+ if node["kind"] == "Call":
720
+ selectable_node["call_target_kind"] = call_by_node.get(node_id).resolution if node_id in call_by_node and call_by_node[node_id].supported else None
721
+ if node["kind"] == "FunctionDef":
722
+ selectable_node["function_eligible"] = function_eligible.get(node_id, False)
723
+ if node["kind"] == "Return":
724
+ owner = owners.get(node_id)
725
+ expected = signature_by_node.get(owner).return_category if owner in signature_by_node else ValueCategory.UNKNOWN
726
+ selectable_node["return_eligible"] = category is expected and expected not in {ValueCategory.UNKNOWN, ValueCategory.CONTRADICTORY}
727
+ if modules_enabled:
728
+ if node_id in document_plan_nodes:
729
+ selectable_node["module_document_supported"] = True
730
+ if node_id in imports_by_node:
731
+ selectable_node["module_import_supported"] = all(item.get("supported") for item in imports_by_node[node_id])
732
+ if node_id in imports_by_alias:
733
+ selectable_node["module_alias_supported"] = bool(imports_by_alias[node_id].get("supported"))
734
+ if node_id in functions_by_node:
735
+ selectable_node["module_function_supported"] = True
736
+ if node_id in initialization_by_node:
737
+ selectable_node["module_initialization_supported"] = True
738
+ if node_id == assembly_node_id:
739
+ selectable_node["module_bundle_assembly_supported"] = True
740
+ if node["kind"] == "Call" and node_id in call_by_node:
741
+ call = call_by_node[node_id]
742
+ caller = functions_by_node.get(owners.get(node_id))
743
+ callee = functions_by_node.get(call.target_function_node_id)
744
+ selectable_node["cross_module_call_supported"] = bool(
745
+ call.supported and caller and callee and caller.get("module_id") != callee.get("module_id")
746
+ )
747
+ if containers_enabled:
748
+ if node_id in shape_by_node:
749
+ selectable_node["container_shape_supported"] = shape_by_node[node_id].valid
750
+ if node_id in access_by_node:
751
+ selectable_node["container_access_supported"] = access_by_node[node_id].supported
752
+ if node_id in iteration_by_node:
753
+ selectable_node["container_iteration_supported"] = iteration_by_node[node_id].supported
754
+ if node_id in container_binding_by_assignment:
755
+ selectable_node["container_binding_supported"] = container_binding_by_assignment[node_id].valid
756
+ if node["kind"] == "Name":
757
+ binding_id = occurrence_bindings.get(node_id)
758
+ container_binding = container_binding_by_id.get(binding_id)
759
+ selectable_node["container_name_supported"] = bool(
760
+ container_binding
761
+ and container_binding.valid
762
+ and node_id in container_binding.allowed_use_node_ids
763
+ )
764
+ if records_enabled:
765
+ if node_id in record_definition_by_class:
766
+ selectable_node["record_class_supported"] = True
767
+ if node_id in record_field_by_declaration:
768
+ selectable_node["record_field_supported"] = True
769
+ if node_id in record_initializer_by_function:
770
+ selectable_node["record_initializer_supported"] = True
771
+ if node_id in record_instance_by_construction:
772
+ selectable_node["record_construction_supported"] = True
773
+ if node_id in record_instance_by_assignment:
774
+ selectable_node["record_binding_supported"] = True
775
+ if node_id in record_name_node_ids:
776
+ selectable_node["record_name_supported"] = True
777
+ if node_id in record_access_by_node:
778
+ selectable_node["record_access_supported"] = True
779
+ if numeric_enabled and node_id in numeric_operation_by_node:
780
+ selectable_node["numeric_operation_supported"] = True
781
+ if conditional_enabled and node_id in conditional_region_by_node:
782
+ region = conditional_region_by_node[node_id]
783
+ selectable_node[
784
+ "conditional_boolean_region_supported"
785
+ if region.region_kind.value == "boolean-short-circuit"
786
+ else "conditional_comparison_region_supported"
787
+ ] = True
788
+ if keyword_calls_enabled and node_id in keyword_call_by_node:
789
+ selectable_node["keyword_call_binding_supported"] = bool(
790
+ keyword_call_by_node[node_id].supported
791
+ and call_by_node.get(node_id)
792
+ and call_by_node[node_id].supported
793
+ )
794
+ if keyword_only_calls_enabled and node_id in keyword_only_call_by_node:
795
+ selectable_node["keyword_only_call_binding_supported"] = bool(
796
+ keyword_only_call_by_node[node_id].supported
797
+ and call_by_node.get(node_id)
798
+ and call_by_node[node_id].supported
799
+ )
800
+
801
+ plan = registry.select(selectable_node, categories, decision_key)
802
+ if plan:
803
+ plan = self._phase9_plan(plan, node, signature_by_node, call_by_node, owners, return_by_function, local_by_function)
804
+ if containers_enabled:
805
+ plan = self._phase11_plan(
806
+ plan,
807
+ node,
808
+ shape_by_node,
809
+ container_binding_by_assignment,
810
+ access_by_node,
811
+ iteration_by_node,
812
+ )
813
+ if modules_enabled:
814
+ plan = self._phase12_plan(
815
+ plan,
816
+ node,
817
+ imports_by_node,
818
+ imports_by_alias,
819
+ functions_by_node,
820
+ document_plan_nodes,
821
+ initialization_by_node,
822
+ call_by_node,
823
+ owners,
824
+ assembly_node_id,
825
+ )
826
+ if records_enabled:
827
+ plan = self._phase13_plan(
828
+ plan,
829
+ node,
830
+ record_definition_by_class,
831
+ record_field_by_declaration,
832
+ record_initializer_by_function,
833
+ record_instance_by_construction,
834
+ record_instance_by_assignment,
835
+ record_binding_by_id,
836
+ record_access_by_node,
837
+ occurrence_bindings,
838
+ )
839
+ if numeric_enabled:
840
+ plan = self._phase14_plan(
841
+ plan,
842
+ node,
843
+ numeric_operation_by_node,
844
+ )
845
+ if conditional_enabled:
846
+ plan = self._phase14b_plan(
847
+ plan,
848
+ node,
849
+ conditional_region_by_node,
850
+ )
851
+ if keyword_calls_enabled:
852
+ plan = self._phase14c_plan(
853
+ plan,
854
+ node,
855
+ keyword_call_by_node,
856
+ )
857
+ if keyword_only_calls_enabled:
858
+ plan = self._phase14d_plan(
859
+ plan,
860
+ node,
861
+ keyword_only_call_by_node,
862
+ signature_by_node,
863
+ nodes_by_id,
864
+ )
865
+ state = plan.support_state if plan else SupportState.UNSUPPORTED
866
+ cause = self._unsupported_cause(node, signature_by_node, call_by_node, owners, return_by_function, local_by_function)
867
+ if not plan and containers_enabled:
868
+ if node_id in shape_by_node:
869
+ cause = shape_by_node[node_id].reason or "Container literal lacks an approved fixed representation"
870
+ elif node_id in access_by_node:
871
+ cause = access_by_node[node_id].reason or "Container access is not statically proved"
872
+ elif node_id in iteration_by_node:
873
+ cause = iteration_by_node[node_id].reason or "Container iteration is not bounded"
874
+ elif node_id in container_binding_by_assignment:
875
+ cause = container_binding_by_assignment[node_id].reason or "Container binding is invalid"
876
+ elif node["kind"] == "Name":
877
+ binding_id = occurrence_bindings.get(node_id)
878
+ container_binding = container_binding_by_id.get(binding_id)
879
+ if container_binding and not container_binding.valid:
880
+ cause = container_binding.reason
881
+ support.append({"decision_key": decision_key, "node_id": node_id, "state": state.value, "rule_plan_id": plan.plan_id if plan else None, "cause": None if plan else cause})
882
+ if plan:
883
+ plan_dict = plan.to_dict()
884
+ plans.append(plan_dict)
885
+ services.trace.record({"kind": "rule_plan", "stage": self.stage_id, "plan": plan_dict})
886
+ representations.append(planner.representation(decision_key, category).to_dict())
887
+ effects.append({"node_id": node_id, "effects": [effect.value for effect in planner.effects(node)]})
888
+ truthiness.append({"node_id": node_id, "category": category.value, "strategy": self._truthiness_strategy(category)})
889
+ order.append({"node_id": node_id, "ordinal": ordinal, "child_order": list(function_analyzer.index.child_ids(node))})
890
+
891
+ fact_tables: list[FactTable] = [
892
+ FactTable("fact-table/0.5", "scope-facts", self.stage_id, "scope-id", Completeness.COMPLETE, (artifact.artifact_fingerprint.value,), tuple(FactRecord(item.scope_id, item, FactProvenance((item.owner_node_id,), ("lexical-structure",))) for item in scopes)),
893
+ FactTable("fact-table/0.5", "binding-facts", self.stage_id, "binding-id", Completeness.COMPLETE, (artifact.artifact_fingerprint.value,), tuple(FactRecord(item.binding_id, item, FactProvenance((item.declaration_node_id,) + item.occurrence_node_ids, ("lexical-binding",))) for item in bindings)),
894
+ FactTable("fact-table/0.5", "value-category-facts", self.stage_id, "python-node-id", Completeness.COMPLETE, ("binding-facts",), tuple(FactRecord(key, value, FactProvenance((key,), ("literal-annotation-call-or-constraint",))) for key, value in sorted(categories.items()))),
895
+ ]
896
+ functions_enabled = supports_functions(ruleset)
897
+ if functions_enabled:
898
+ fact_tables.extend(
899
+ (
900
+ FactTable("fact-table/0.9", "function-signature-facts", self.stage_id, "function-node-id", Completeness.COMPLETE, ("binding-facts", "value-category-facts"), tuple(FactRecord(item.function_node_id, item, FactProvenance(tuple(filter(None, (item.function_node_id, item.return_annotation_node_id))) + tuple(parameter.parameter_node_id for parameter in item.parameters), ("exact-built-in-annotations", "phase9-signature-policy"))) for item in signatures)),
901
+ FactTable("fact-table/0.9", "call-target-facts", self.stage_id, "call-node-id", Completeness.COMPLETE, ("function-signature-facts", "binding-facts", "value-category-facts"), tuple(FactRecord(item.call_node_id, item, FactProvenance((item.call_node_id,) + item.argument_node_ids, ("deterministic-target-resolution", "positional-source-order"))) for item in calls)),
902
+ FactTable("fact-table/0.9", "return-path-facts", self.stage_id, "function-node-id", Completeness.COMPLETE, ("function-signature-facts", "value-category-facts"), tuple(FactRecord(item.function_node_id, item, FactProvenance((item.function_node_id,) + item.return_node_ids, ("reachable-return-analysis", "implicit-none-policy"))) for item in return_paths)),
903
+ FactTable("fact-table/0.9", "local-declaration-facts", self.stage_id, "function-node-id", Completeness.COMPLETE, ("binding-facts", "evaluation-order-facts"), tuple(FactRecord(item.function_node_id, item, FactProvenance((item.function_node_id,), ("definite-binding-analysis", "loop-target-lifetime"))) for item in local_facts)),
904
+ FactTable("fact-table/0.9", "call-graph-facts", self.stage_id, "module", Completeness.COMPLETE, ("call-target-facts",), (FactRecord("module-call-graph", graph, FactProvenance((module["root_node_id"],), ("direct-and-mutual-recursion-policy",))),)),
905
+ )
906
+ )
907
+ if containers_enabled:
908
+ fact_tables.extend(
909
+ (
910
+ FactTable(CONTAINER_FACT_SCHEMA, "container-shape-facts", self.stage_id, "python-container-literal-node-id", Completeness.COMPLETE, ("value-category-facts",), tuple(FactRecord(item.literal_node_id, item, FactProvenance((item.literal_node_id,) + item.element_node_ids + item.key_node_ids + item.value_node_ids, ("fixed-capacity", "homogeneous-representation", "approved-container-profile"))) for item in container_shapes)),
911
+ FactTable(CONTAINER_FACT_SCHEMA, "container-binding-facts", self.stage_id, "binding-id", Completeness.COMPLETE, ("binding-facts", "container-shape-facts"), tuple(FactRecord(item.binding_id, item, FactProvenance((item.assignment_node_id, item.target_node_id, item.literal_node_id) + item.allowed_use_node_ids + item.invalid_use_node_ids, ("single-local-binding", "no-alias-or-escape"))) for item in container_bindings)),
912
+ FactTable(CONTAINER_FACT_SCHEMA, "container-access-facts", self.stage_id, "subscript-node-id", Completeness.COMPLETE, ("container-binding-facts",), tuple(FactRecord(item.subscript_node_id, item, FactProvenance(tuple(filter(None, (item.subscript_node_id, item.slice_node_id))), ("static-index-or-key-proof", "fixed-capacity-or-presence-proof"))) for item in container_accesses)),
913
+ FactTable(CONTAINER_FACT_SCHEMA, "container-iteration-facts", self.stage_id, "for-node-id", Completeness.COMPLETE, ("container-binding-facts",), tuple(FactRecord(item.for_node_id, item, FactProvenance(tuple(filter(None, (item.for_node_id, item.target_node_id))), ("fixed-bound", "source-insertion-order"))) for item in container_iterations)),
914
+ )
915
+ )
916
+ if records_enabled:
917
+ fact_tables.extend(self._record_fact_tables(record_analysis))
918
+ if numeric_enabled:
919
+ fact_tables.append(self._numeric_fact_table(numeric_analysis))
920
+ if conditional_enabled:
921
+ fact_tables.append(self._conditional_fact_table(conditional_analysis))
922
+ if keyword_calls_enabled:
923
+ fact_tables.append(self._keyword_call_fact_table(keyword_call_analysis))
924
+ if keyword_only_calls_enabled:
925
+ fact_tables.append(
926
+ self._keyword_only_call_fact_table(keyword_only_call_analysis)
927
+ )
928
+ if modules_enabled:
929
+ fact_tables.extend(module_fact_tables)
930
+
931
+ plan_schema = (
932
+ CONVERSION_PLAN_SCHEMA
933
+ if keyword_only_calls_enabled
934
+ else PHASE14C_CONVERSION_PLAN_SCHEMA
935
+ if keyword_calls_enabled
936
+ else PHASE14B_CONVERSION_PLAN_SCHEMA
937
+ if conditional_enabled
938
+ else PHASE14A_CONVERSION_PLAN_SCHEMA
939
+ if numeric_enabled
940
+ else PHASE13_CONVERSION_PLAN_SCHEMA
941
+ if records_enabled
942
+ else PHASE12_CONVERSION_PLAN_SCHEMA
943
+ if modules_enabled
944
+ else "conversion-plan/0.11"
945
+ if containers_enabled
946
+ else "conversion-plan/0.9"
947
+ if functions_enabled
948
+ else "conversion-plan/0.5"
949
+ )
950
+ artifact_version = "0.14.3" if keyword_only_calls_enabled else "0.14.2" if keyword_calls_enabled else "0.14.1" if conditional_enabled else "0.14" if numeric_enabled else "0.13" if records_enabled else "0.12" if modules_enabled else "0.11" if containers_enabled else "0.9" if functions_enabled else "0.5"
951
+ helper_requirements = sorted(
952
+ {
953
+ requirement
954
+ for plan in plans
955
+ for requirement in plan.get("helper_requirements", ())
956
+ }
957
+ )
958
+ generated_name_inputs = tuple(
959
+ {
960
+ **binding,
961
+ "module_generated_name": bool(
962
+ functions_by_node.get(binding.get("declaration_node_id"), {}).get("module_generated_name")
963
+ or module_records.get(binding.get("declaration_node_id"), {}).get("module_generated_name")
964
+ ),
965
+ }
966
+ for binding in binding_dicts
967
+ ) + tuple(
968
+ {
969
+ "binding_id": field.field_id,
970
+ "source_name": field.source_name,
971
+ "binding_kind": "record-layout-field",
972
+ "declaration_node_id": field.declaration_node_id,
973
+ "occurrence_node_ids": (),
974
+ }
975
+ for field in (record_analysis.fields if record_analysis else ())
976
+ )
977
+ payload = {
978
+ "stage_order": tuple(artifact.payload["stage_order"]) + (self.stage_id,),
979
+ "python_ir": module,
980
+ "python_ir_fingerprint": artifact.artifact_fingerprint.value,
981
+ "target_contract": target,
982
+ "semantic_policy": semantics,
983
+ "rule_set_version": ruleset,
984
+ "renderer_version": request.renderer_version,
985
+ "helper_policy_version": request.helper_policy_version,
986
+ "container_policy_version": request.container_policy_version,
987
+ "fact_tables": [table.to_dict() if hasattr(table, "to_dict") else dict(table) for table in fact_tables],
988
+ "evaluation_order_facts": order,
989
+ "effect_facts": effects,
990
+ "truthiness_facts": truthiness,
991
+ "representation_plans": representations,
992
+ "generated_name_plans": [
993
+ item.to_dict()
994
+ for item in planner.generated_names(generated_name_inputs)
995
+ ],
996
+ "support_decisions": support,
997
+ "rule_registry_manifest": list(registry.manifest),
998
+ "rule_plans": plans,
999
+ "helper_requirements": helper_requirements,
1000
+ "module_rejection": function_analyzer.module_rejection(
1001
+ allow_imports=modules_enabled,
1002
+ allow_records=records_enabled,
1003
+ ),
1004
+ "module_policy_version": request.module_policy_version,
1005
+ "schema_version": plan_schema,
1006
+ }
1007
+ if records_enabled:
1008
+ payload["record_policy_version"] = request.record_policy_version
1009
+ if numeric_enabled:
1010
+ payload["numeric_policy_version"] = request.numeric_policy_version
1011
+ if modules_enabled:
1012
+ for key in ("module_bundle", "module_resolution", "module_bundle_assembly_node_id"):
1013
+ if key in artifact.payload:
1014
+ payload[key] = artifact.payload[key]
1015
+ if records_enabled and "module_record_by_node" in artifact.payload:
1016
+ payload["module_record_by_node"] = artifact.payload["module_record_by_node"]
1017
+ try:
1018
+ valid, message = validate_analysis_payload(
1019
+ payload,
1020
+ cancellation=services.context.cancellation,
1021
+ )
1022
+ except ConditionalRegionValidationCanceled:
1023
+ return StageOutcome(
1024
+ StageTerminal.CANCELED,
1025
+ diagnostics=(Diagnostic(
1026
+ "PYC1901",
1027
+ Severity.ERROR,
1028
+ self.stage_id,
1029
+ "Conversion canceled during conditional-region validation",
1030
+ ),),
1031
+ )
1032
+ except KeywordCallValidationCanceled:
1033
+ return StageOutcome(
1034
+ StageTerminal.CANCELED,
1035
+ diagnostics=(Diagnostic(
1036
+ "PYC1901",
1037
+ Severity.ERROR,
1038
+ self.stage_id,
1039
+ "Conversion canceled during keyword-call validation",
1040
+ ),),
1041
+ )
1042
+ except KeywordOnlyCallValidationCanceled:
1043
+ return StageOutcome(
1044
+ StageTerminal.CANCELED,
1045
+ diagnostics=(Diagnostic(
1046
+ "PYC1901",
1047
+ Severity.ERROR,
1048
+ self.stage_id,
1049
+ "Conversion canceled during keyword-only call validation",
1050
+ ),),
1051
+ )
1052
+ if not valid:
1053
+ return StageOutcome(StageTerminal.INTERNAL_FAILURE, diagnostics=(Diagnostic("PYC9501", Severity.INTERNAL_ERROR, self.stage_id, message),))
1054
+ return StageOutcome(StageTerminal.COMPLETED, _artifact(artifact, payload, artifact_version))
1055
+
1056
+ def validate(self, artifact: StageArtifact, services: Any) -> tuple[bool, str]:
1057
+ if artifact.kind != "conversion_plan":
1058
+ return False, "invalid analysis artifact kind"
1059
+ try:
1060
+ return validate_analysis_payload(
1061
+ dict(artifact.payload),
1062
+ cancellation=services.context.cancellation,
1063
+ )
1064
+ except (
1065
+ ConditionalRegionValidationCanceled,
1066
+ KeywordCallValidationCanceled,
1067
+ KeywordOnlyCallValidationCanceled,
1068
+ ):
1069
+ # The facade checks the token immediately after validation and
1070
+ # retires the unpublished successor artifact as Canceled.
1071
+ return True, ""
1072
+
1073
+ @staticmethod
1074
+ def _cumulative_function_eligibility(
1075
+ signatures: dict[str, Any],
1076
+ returns: dict[str, Any],
1077
+ locals_by_function: dict[str, Any],
1078
+ calls: tuple[CallTargetFact, ...],
1079
+ owners: dict[str, str],
1080
+ graph: Any,
1081
+ cancellation: Any,
1082
+ ) -> dict[str, bool]:
1083
+ calls_by_owner: dict[str, list[CallTargetFact]] = {}
1084
+ callers_by_target: dict[str, list[str]] = {}
1085
+ for call in calls:
1086
+ if bool(getattr(cancellation, "is_canceled", False)):
1087
+ raise AnalysisCanceled
1088
+ owner = owners.get(call.call_node_id)
1089
+ if owner is not None:
1090
+ calls_by_owner.setdefault(owner, []).append(call)
1091
+ if call.target_function_node_id in signatures:
1092
+ callers_by_target.setdefault(
1093
+ call.target_function_node_id,
1094
+ [],
1095
+ ).append(owner)
1096
+ recursive_functions = set(graph.recursive_function_node_ids)
1097
+ eligible: dict[str, bool] = {}
1098
+ for function_id, signature in signatures.items():
1099
+ if bool(getattr(cancellation, "is_canceled", False)):
1100
+ raise AnalysisCanceled
1101
+ eligible[function_id] = bool(
1102
+ signature.eligible
1103
+ and returns[function_id].compatible
1104
+ and not returns[function_id].fallthrough_possible
1105
+ and locals_by_function[function_id].valid
1106
+ and function_id not in recursive_functions
1107
+ and all(
1108
+ call.supported
1109
+ for call in calls_by_owner.get(function_id, ())
1110
+ )
1111
+ )
1112
+ pending = deque(
1113
+ function_id
1114
+ for function_id, supported in eligible.items()
1115
+ if not supported
1116
+ )
1117
+ while pending:
1118
+ if bool(getattr(cancellation, "is_canceled", False)):
1119
+ raise AnalysisCanceled
1120
+ target = pending.popleft()
1121
+ for caller in callers_by_target.get(target, ()):
1122
+ if eligible.get(caller, False):
1123
+ eligible[caller] = False
1124
+ pending.append(caller)
1125
+ return eligible
1126
+
1127
+ @staticmethod
1128
+ def _gate_keyword_calls_by_target(
1129
+ analysis: KeywordCallAnalysis,
1130
+ calls: tuple[CallTargetFact, ...],
1131
+ function_eligible: dict[str, bool],
1132
+ nodes_by_id: dict[str, dict[str, Any]],
1133
+ ) -> tuple[KeywordCallAnalysis, tuple[CallTargetFact, ...]]:
1134
+ downgraded_call_ids = {
1135
+ fact.call_node_id
1136
+ for fact in analysis.facts
1137
+ if fact.supported
1138
+ and not function_eligible.get(fact.target_function_node_id, False)
1139
+ }
1140
+ if not downgraded_call_ids:
1141
+ return analysis, calls
1142
+ facts = tuple(
1143
+ replace(
1144
+ fact,
1145
+ runtime_binding_failure="compile-time-rejected",
1146
+ supported=False,
1147
+ diagnostic_code=CUMULATIVE_KEYWORD_TARGET_DIAGNOSTIC_CODE,
1148
+ reason=CUMULATIVE_KEYWORD_TARGET_REASON,
1149
+ rejection_node_id=fact.target_function_node_id,
1150
+ )
1151
+ if fact.call_node_id in downgraded_call_ids
1152
+ else fact
1153
+ for fact in analysis.facts
1154
+ )
1155
+ rejections = tuple(
1156
+ KeywordCallRejection(
1157
+ fact.call_node_id,
1158
+ fact.diagnostic_code or "PYC2910",
1159
+ fact.reason or "Unsupported keyword-call binding",
1160
+ fact.rejection_node_id or fact.call_node_id,
1161
+ nodes_by_id.get(
1162
+ fact.rejection_node_id or fact.call_node_id,
1163
+ {},
1164
+ ).get("provenance", {}).get("source_span"),
1165
+ )
1166
+ for fact in facts
1167
+ if not fact.supported
1168
+ )
1169
+ gated_calls = tuple(
1170
+ replace(
1171
+ call,
1172
+ resolution="ineligible-source-function",
1173
+ supported=False,
1174
+ diagnostic_code=CUMULATIVE_KEYWORD_TARGET_DIAGNOSTIC_CODE,
1175
+ reason=CUMULATIVE_KEYWORD_TARGET_REASON,
1176
+ )
1177
+ if call.call_node_id in downgraded_call_ids
1178
+ else call
1179
+ for call in calls
1180
+ )
1181
+ return KeywordCallAnalysis(facts, rejections), gated_calls
1182
+
1183
+ @staticmethod
1184
+ def _gate_keyword_only_calls_by_target(
1185
+ analysis: KeywordOnlyCallAnalysis,
1186
+ calls: tuple[CallTargetFact, ...],
1187
+ function_eligible: dict[str, bool],
1188
+ nodes_by_id: dict[str, dict[str, Any]],
1189
+ ) -> tuple[KeywordOnlyCallAnalysis, tuple[CallTargetFact, ...]]:
1190
+ downgraded_call_ids = {
1191
+ fact.call_node_id
1192
+ for fact in analysis.facts
1193
+ if fact.supported
1194
+ and not function_eligible.get(fact.target_function_node_id, False)
1195
+ }
1196
+ if not downgraded_call_ids:
1197
+ return analysis, calls
1198
+ facts = tuple(
1199
+ replace(
1200
+ fact,
1201
+ runtime_binding_failure="compile-time-rejected",
1202
+ supported=False,
1203
+ diagnostic_code=CUMULATIVE_KEYWORD_ONLY_TARGET_DIAGNOSTIC_CODE,
1204
+ reason=CUMULATIVE_KEYWORD_ONLY_TARGET_REASON,
1205
+ rejection_node_id=fact.target_function_node_id,
1206
+ )
1207
+ if fact.call_node_id in downgraded_call_ids
1208
+ else fact
1209
+ for fact in analysis.facts
1210
+ )
1211
+ rejections = tuple(
1212
+ KeywordOnlyCallRejection(
1213
+ fact.call_node_id,
1214
+ fact.diagnostic_code or "PYC2910",
1215
+ fact.reason or "Unsupported required keyword-only call",
1216
+ fact.rejection_node_id or fact.call_node_id,
1217
+ nodes_by_id.get(
1218
+ fact.rejection_node_id or fact.call_node_id,
1219
+ {},
1220
+ ).get("provenance", {}).get("source_span"),
1221
+ )
1222
+ for fact in facts
1223
+ if not fact.supported
1224
+ )
1225
+ gated_calls = tuple(
1226
+ replace(
1227
+ call,
1228
+ resolution="ineligible-source-function",
1229
+ supported=False,
1230
+ diagnostic_code=CUMULATIVE_KEYWORD_ONLY_TARGET_DIAGNOSTIC_CODE,
1231
+ reason=CUMULATIVE_KEYWORD_ONLY_TARGET_REASON,
1232
+ )
1233
+ if call.call_node_id in downgraded_call_ids
1234
+ else call
1235
+ for call in calls
1236
+ )
1237
+ return KeywordOnlyCallAnalysis(facts, rejections), gated_calls
1238
+
1239
+ def _record_rejection(self, exc: RecordAnalysisError) -> StageOutcome:
1240
+ return StageOutcome(
1241
+ StageTerminal.REJECTED,
1242
+ diagnostics=(Diagnostic(
1243
+ exc.code,
1244
+ Severity.ERROR,
1245
+ self.stage_id,
1246
+ exc.message,
1247
+ source_span=exc.source_span,
1248
+ fact_references=(f"python-node:{exc.node_id}",),
1249
+ obligation_references=("phase13-static-record-profile",),
1250
+ explanation=exc.message,
1251
+ remediation="Use the documented immutable, module-local, automatic-record subset.",
1252
+ source_module_id=exc.module_id,
1253
+ source_logical_name=exc.logical_name,
1254
+ ),),
1255
+ )
1256
+
1257
+ def _numeric_rejection(self, exc: NumericAnalysisError) -> StageOutcome:
1258
+ return StageOutcome(
1259
+ StageTerminal.REJECTED,
1260
+ diagnostics=(Diagnostic(
1261
+ exc.code,
1262
+ Severity.ERROR,
1263
+ self.stage_id,
1264
+ exc.message,
1265
+ source_span=exc.source_span,
1266
+ fact_references=(f"python-node:{exc.node_id}",),
1267
+ obligation_references=("phase14-proved-floor-arithmetic",),
1268
+ explanation=exc.message,
1269
+ remediation=(
1270
+ "Use exact int operands and a direct signed int64 divisor literal "
1271
+ "other than 0 or -1 inside an understood top-level function."
1272
+ ),
1273
+ ),),
1274
+ )
1275
+
1276
+ def _keyword_call_rejection(self, exc: KeywordCallAnalysisError) -> StageOutcome:
1277
+ return StageOutcome(
1278
+ StageTerminal.REJECTED,
1279
+ diagnostics=(Diagnostic(
1280
+ exc.code,
1281
+ Severity.ERROR,
1282
+ self.stage_id,
1283
+ exc.message,
1284
+ source_span=exc.source_span,
1285
+ fact_references=(f"python-node:{exc.node_id}",),
1286
+ obligation_references=("phase14c-exact-keyword-call-profile",),
1287
+ explanation=exc.message,
1288
+ remediation=(
1289
+ "Use only explicit positional and named arguments that bind "
1290
+ "every required source-function parameter exactly once."
1291
+ ),
1292
+ ),),
1293
+ )
1294
+
1295
+ def _keyword_only_call_rejection(
1296
+ self,
1297
+ exc: KeywordOnlyCallAnalysisError,
1298
+ ) -> StageOutcome:
1299
+ return StageOutcome(
1300
+ StageTerminal.REJECTED,
1301
+ diagnostics=(Diagnostic(
1302
+ exc.code,
1303
+ Severity.ERROR,
1304
+ self.stage_id,
1305
+ exc.message,
1306
+ source_span=exc.source_span,
1307
+ fact_references=(f"python-node:{exc.node_id}",),
1308
+ obligation_references=(
1309
+ "phase14d-required-keyword-only-call-profile",
1310
+ ),
1311
+ explanation=exc.message,
1312
+ remediation=(
1313
+ "Use only required, exactly annotated keyword-only "
1314
+ "parameters supplied once by explicit keyword."
1315
+ ),
1316
+ ),),
1317
+ )
1318
+
1319
+ def _conditional_rejection(
1320
+ self,
1321
+ exc: ConditionalRegionAnalysisError,
1322
+ ) -> StageOutcome:
1323
+ return StageOutcome(
1324
+ StageTerminal.REJECTED,
1325
+ diagnostics=(Diagnostic(
1326
+ exc.code,
1327
+ Severity.ERROR,
1328
+ self.stage_id,
1329
+ exc.message,
1330
+ source_span=exc.source_span,
1331
+ fact_references=(f"python-node:{exc.node_id}",),
1332
+ obligation_references=("phase14b-conditional-region-profile",),
1333
+ explanation=exc.message,
1334
+ remediation=(
1335
+ "Keep the conditional expression inside the documented "
1336
+ "already-supported scalar subset, or use an explicit statement."
1337
+ ),
1338
+ ),),
1339
+ )
1340
+
1341
+ def _numeric_fact_table(self, analysis: Any) -> FactTable:
1342
+ operations = tuple(
1343
+ sorted(analysis.operations, key=lambda item: item.binop_node_id)
1344
+ )
1345
+ return FactTable(
1346
+ NUMERIC_FACT_SCHEMA,
1347
+ "numeric-operation-facts",
1348
+ self.stage_id,
1349
+ "binop-node-id",
1350
+ Completeness.COMPLETE,
1351
+ ("value-category-facts", "evaluation-order-facts"),
1352
+ tuple(
1353
+ FactRecord(
1354
+ item.binop_node_id,
1355
+ item,
1356
+ FactProvenance(
1357
+ (
1358
+ item.binop_node_id,
1359
+ item.function_node_id,
1360
+ item.operator_node_id,
1361
+ item.left_node_id,
1362
+ *item.divisor_literal_node_ids,
1363
+ ),
1364
+ (
1365
+ "exact-int64-categories",
1366
+ "direct-safe-divisor-literal",
1367
+ "left-to-right-once-staging",
1368
+ "frozen-floor-helper-contract",
1369
+ ),
1370
+ ),
1371
+ )
1372
+ for item in operations
1373
+ ),
1374
+ )
1375
+
1376
+ def _conditional_fact_table(self, analysis: Any) -> FactTable:
1377
+ regions = tuple(
1378
+ sorted(analysis.regions, key=lambda item: item.region_node_id)
1379
+ )
1380
+ return FactTable(
1381
+ CONDITIONAL_FACT_SCHEMA,
1382
+ CONDITIONAL_REGION_TABLE_ID,
1383
+ self.stage_id,
1384
+ CONDITIONAL_REGION_KEY_DOMAIN,
1385
+ Completeness.COMPLETE,
1386
+ CONDITIONAL_REGION_TABLE_DEPENDENCIES,
1387
+ tuple(
1388
+ FactRecord(
1389
+ item.region_node_id,
1390
+ item,
1391
+ FactProvenance(
1392
+ item.provenance_node_ids,
1393
+ CONDITIONAL_REGION_PROVENANCE_EVIDENCE,
1394
+ ),
1395
+ )
1396
+ for item in regions
1397
+ ),
1398
+ )
1399
+
1400
+ def _keyword_call_fact_table(self, analysis: Any) -> FactTable:
1401
+ facts = tuple(sorted(analysis.facts, key=lambda item: item.call_node_id))
1402
+ return FactTable(
1403
+ KEYWORD_CALL_FACT_SCHEMA,
1404
+ KEYWORD_CALL_TABLE_ID,
1405
+ self.stage_id,
1406
+ KEYWORD_CALL_KEY_DOMAIN,
1407
+ Completeness.COMPLETE,
1408
+ KEYWORD_CALL_TABLE_DEPENDENCIES,
1409
+ tuple(
1410
+ FactRecord(
1411
+ item.call_node_id,
1412
+ item,
1413
+ FactProvenance(
1414
+ item.provenance_node_ids,
1415
+ KEYWORD_CALL_PROVENANCE_EVIDENCE,
1416
+ ),
1417
+ )
1418
+ for item in facts
1419
+ ),
1420
+ )
1421
+
1422
+ def _keyword_only_call_fact_table(self, analysis: Any) -> FactTable:
1423
+ facts = tuple(sorted(analysis.facts, key=lambda item: item.call_node_id))
1424
+ return FactTable(
1425
+ KEYWORD_ONLY_CALL_FACT_SCHEMA,
1426
+ KEYWORD_ONLY_CALL_TABLE_ID,
1427
+ self.stage_id,
1428
+ KEYWORD_ONLY_CALL_KEY_DOMAIN,
1429
+ Completeness.COMPLETE,
1430
+ KEYWORD_ONLY_CALL_TABLE_DEPENDENCIES,
1431
+ tuple(
1432
+ FactRecord(
1433
+ item.call_node_id,
1434
+ item,
1435
+ FactProvenance(
1436
+ item.provenance_node_ids,
1437
+ KEYWORD_ONLY_CALL_PROVENANCE_EVIDENCE,
1438
+ ),
1439
+ )
1440
+ for item in facts
1441
+ ),
1442
+ )
1443
+
1444
+ def _record_fact_tables(self, analysis: Any) -> tuple[FactTable, ...]:
1445
+ definitions = tuple(sorted(analysis.definitions, key=lambda item: item.record_id))
1446
+ fields = tuple(sorted(analysis.fields, key=lambda item: item.field_id))
1447
+ initializers = tuple(sorted(analysis.initializers, key=lambda item: item.initializer_id))
1448
+ instances = tuple(sorted(analysis.instances, key=lambda item: item.instance_id))
1449
+ bindings = tuple(sorted(analysis.bindings, key=lambda item: item.binding_id))
1450
+ accesses = tuple(sorted(analysis.accesses, key=lambda item: item.access_node_id))
1451
+ fields_by_record: dict[str, tuple[Any, ...]] = {}
1452
+ for definition in definitions:
1453
+ fields_by_record[definition.record_id] = tuple(
1454
+ item for item in fields if item.record_id == definition.record_id
1455
+ )
1456
+ initializer_by_id = {item.initializer_id: item for item in initializers}
1457
+ return (
1458
+ FactTable(
1459
+ RECORD_FACT_SCHEMA,
1460
+ "record-definition-facts",
1461
+ self.stage_id,
1462
+ "record-id",
1463
+ Completeness.COMPLETE,
1464
+ ("binding-facts", "module-identity-facts"),
1465
+ tuple(
1466
+ FactRecord(
1467
+ item.record_id,
1468
+ item,
1469
+ FactProvenance(
1470
+ (item.class_node_id,)
1471
+ + tuple(field.declaration_node_id for field in fields_by_record[item.record_id])
1472
+ + (initializer_by_id[item.initializer_id].function_node_id,),
1473
+ ("closed-class-shape", "automatic-inline-record", "immutable-noalias-noescape"),
1474
+ ),
1475
+ )
1476
+ for item in definitions
1477
+ ),
1478
+ ),
1479
+ FactTable(
1480
+ RECORD_FACT_SCHEMA,
1481
+ "record-field-facts",
1482
+ self.stage_id,
1483
+ "record-field-id",
1484
+ Completeness.COMPLETE,
1485
+ ("record-definition-facts", "binding-facts"),
1486
+ tuple(
1487
+ FactRecord(
1488
+ item.field_id,
1489
+ item,
1490
+ FactProvenance(
1491
+ (item.declaration_node_id, item.target_node_id, item.annotation_node_id),
1492
+ ("exact-scalar-annotation", "source-field-order", "immutable-field"),
1493
+ ),
1494
+ )
1495
+ for item in fields
1496
+ ),
1497
+ ),
1498
+ FactTable(
1499
+ RECORD_FACT_SCHEMA,
1500
+ "record-initializer-facts",
1501
+ self.stage_id,
1502
+ "record-initializer-id",
1503
+ Completeness.COMPLETE,
1504
+ ("record-definition-facts", "record-field-facts"),
1505
+ tuple(
1506
+ FactRecord(
1507
+ item.initializer_id,
1508
+ item,
1509
+ FactProvenance(
1510
+ (item.function_node_id, item.arguments_node_id, item.self_parameter_node_id)
1511
+ + item.parameter_node_ids
1512
+ + item.assignment_node_ids,
1513
+ ("exact-positional-initializer", "all-fields-once", "left-to-right-field-order"),
1514
+ ),
1515
+ )
1516
+ for item in initializers
1517
+ ),
1518
+ ),
1519
+ FactTable(
1520
+ RECORD_FACT_SCHEMA,
1521
+ "record-instance-facts",
1522
+ self.stage_id,
1523
+ "record-instance-id",
1524
+ Completeness.COMPLETE,
1525
+ ("record-definition-facts", "record-initializer-facts", "value-category-facts"),
1526
+ tuple(
1527
+ FactRecord(
1528
+ item.instance_id,
1529
+ item,
1530
+ FactProvenance(
1531
+ (item.assignment_node_id, item.target_node_id, item.construction_node_id)
1532
+ + item.argument_node_ids,
1533
+ ("fresh-function-local", "automatic-storage", "no-allocation-or-cleanup"),
1534
+ ),
1535
+ )
1536
+ for item in instances
1537
+ ),
1538
+ ),
1539
+ FactTable(
1540
+ RECORD_FACT_SCHEMA,
1541
+ "record-binding-facts",
1542
+ self.stage_id,
1543
+ "binding-id",
1544
+ Completeness.COMPLETE,
1545
+ ("record-instance-facts", "binding-facts"),
1546
+ tuple(
1547
+ FactRecord(
1548
+ item.binding_id,
1549
+ item,
1550
+ FactProvenance(
1551
+ (item.declaration_node_id,)
1552
+ + item.occurrence_node_ids
1553
+ + item.allowed_field_access_node_ids,
1554
+ ("single-assignment", "no-alias", "no-escape"),
1555
+ ),
1556
+ )
1557
+ for item in bindings
1558
+ ),
1559
+ ),
1560
+ FactTable(
1561
+ RECORD_FACT_SCHEMA,
1562
+ "record-access-facts",
1563
+ self.stage_id,
1564
+ "attribute-node-id",
1565
+ Completeness.COMPLETE,
1566
+ ("record-binding-facts", "record-field-facts"),
1567
+ tuple(
1568
+ FactRecord(
1569
+ item.access_node_id,
1570
+ item,
1571
+ FactProvenance(
1572
+ (item.access_node_id,),
1573
+ ("direct-owner-read", "statically-bound-field", "immutable-after-construction"),
1574
+ ),
1575
+ )
1576
+ for item in accesses
1577
+ ),
1578
+ ),
1579
+ )
1580
+
1581
+ @staticmethod
1582
+ def _phase13_plan(
1583
+ plan: RulePlan,
1584
+ node: dict[str, Any],
1585
+ definitions: dict[str, Any],
1586
+ fields: dict[str, Any],
1587
+ initializers: dict[str, Any],
1588
+ constructions: dict[str, Any],
1589
+ assignments: dict[str, Any],
1590
+ bindings: dict[str, Any],
1591
+ accesses: dict[str, Any],
1592
+ occurrence_bindings: dict[str, str],
1593
+ ) -> RulePlan:
1594
+ node_id = node["node_id"]
1595
+ if node_id in definitions:
1596
+ item = definitions[node_id]
1597
+ return _extend_plan(
1598
+ plan,
1599
+ facts=(f"record-definition:{item.record_id}", f"record-layout-fields:{len(item.field_ids)}"),
1600
+ obligations=("record-automatic-storage", "record-no-allocation-cleanup-or-null", "record-immutable-noalias-noescape"),
1601
+ explanation=("materialized-c-record", item.source_name),
1602
+ )
1603
+ if node_id in fields:
1604
+ item = fields[node_id]
1605
+ return _extend_plan(
1606
+ plan,
1607
+ facts=(f"record-field:{item.field_id}", f"record-field-ordinal:{item.ordinal}", f"record-field-category:{item.category.value}"),
1608
+ )
1609
+ if node_id in initializers:
1610
+ item = initializers[node_id]
1611
+ return _extend_plan(
1612
+ plan,
1613
+ facts=(f"record-initializer:{item.initializer_id}", f"record-definition:{item.record_id}"),
1614
+ obligations=("initializer-erased-after-exact-field-copy-proof",),
1615
+ )
1616
+ if node_id in constructions:
1617
+ item = constructions[node_id]
1618
+ return _extend_plan(
1619
+ plan,
1620
+ facts=(f"record-instance:{item.instance_id}", f"record-definition:{item.record_id}"),
1621
+ obligations=("constructor-arguments-staged-left-to-right-once", "const-aggregate-initialization"),
1622
+ )
1623
+ if node_id in assignments:
1624
+ item = assignments[node_id]
1625
+ return _extend_plan(
1626
+ plan,
1627
+ facts=(f"record-instance:{item.instance_id}", f"record-binding:{item.binding_id}"),
1628
+ obligations=("unique-lexical-owner", "record-binding-single-assignment"),
1629
+ )
1630
+ if node_id in accesses:
1631
+ item = accesses[node_id]
1632
+ return _extend_plan(
1633
+ plan,
1634
+ facts=(f"record-access:{item.access_node_id}", f"record-field:{item.field_id}", f"record-binding:{item.binding_id}"),
1635
+ obligations=("direct-member-read", "field-mutation-absent"),
1636
+ )
1637
+ binding = bindings.get(occurrence_bindings.get(node_id, ""))
1638
+ if binding is not None:
1639
+ return _extend_plan(
1640
+ plan,
1641
+ facts=(f"record-binding:{binding.binding_id}", f"record-instance:{binding.instance_id}"),
1642
+ obligations=("record-name-use-context-approved",),
1643
+ )
1644
+ if node["kind"] == "Name" and occurrence_bindings.get(node_id):
1645
+ return _extend_plan(
1646
+ plan,
1647
+ facts=(f"record-class-binding:{occurrence_bindings[node_id]}",),
1648
+ obligations=("direct-constructor-name-only",),
1649
+ )
1650
+ return plan
1651
+
1652
+ @staticmethod
1653
+ def _phase14_plan(
1654
+ plan: RulePlan,
1655
+ node: dict[str, Any],
1656
+ operations: dict[str, Any],
1657
+ ) -> RulePlan:
1658
+ operation = operations.get(node["node_id"])
1659
+ if operation is None:
1660
+ return plan
1661
+ extended = _extend_plan(
1662
+ plan,
1663
+ facts=(
1664
+ f"numeric-operation:{operation.binop_node_id}",
1665
+ f"numeric-divisor:{operation.divisor_value}",
1666
+ f"numeric-helper:{operation.helper_requirement}",
1667
+ f"numeric-target:{operation.target_contract}",
1668
+ ),
1669
+ obligations=(
1670
+ "operands-materialized-left-to-right-once",
1671
+ "helper-result-materialized-once",
1672
+ "zero-and-negative-one-divisors-excluded",
1673
+ ),
1674
+ explanation=(
1675
+ operation.operator_kind.value,
1676
+ "via",
1677
+ operation.helper_requirement,
1678
+ ),
1679
+ )
1680
+ return replace(
1681
+ extended,
1682
+ support_state=SupportState.SUPPORTED_WITH_HELPER,
1683
+ helper_requirements=(operation.helper_requirement,),
1684
+ )
1685
+
1686
+ @staticmethod
1687
+ def _phase14b_plan(
1688
+ plan: RulePlan,
1689
+ node: dict[str, Any],
1690
+ regions: dict[str, Any],
1691
+ ) -> RulePlan:
1692
+ region = regions.get(node["node_id"])
1693
+ if region is None:
1694
+ return plan
1695
+ return _extend_plan(
1696
+ plan,
1697
+ facts=region.rule_facts,
1698
+ explanation=region.explanation_tokens,
1699
+ )
1700
+
1701
+ @staticmethod
1702
+ def _phase14c_plan(
1703
+ plan: RulePlan,
1704
+ node: dict[str, Any],
1705
+ bindings: dict[str, Any],
1706
+ ) -> RulePlan:
1707
+ binding = bindings.get(node["node_id"])
1708
+ if binding is None or not binding.supported:
1709
+ return plan
1710
+ return _extend_plan(
1711
+ plan,
1712
+ facts=binding.rule_facts,
1713
+ explanation=binding.explanation_tokens,
1714
+ )
1715
+
1716
+ @staticmethod
1717
+ def _phase14d_plan(
1718
+ plan: RulePlan,
1719
+ node: dict[str, Any],
1720
+ bindings: dict[str, Any],
1721
+ signatures: dict[str, Any],
1722
+ nodes: dict[str, dict[str, Any]],
1723
+ ) -> RulePlan:
1724
+ binding = bindings.get(node["node_id"])
1725
+ if binding is not None and binding.supported:
1726
+ return _extend_plan(
1727
+ plan,
1728
+ facts=binding.rule_facts,
1729
+ explanation=binding.explanation_tokens,
1730
+ )
1731
+ signature = signatures.get(node["node_id"])
1732
+ if signature is None or not signature.eligible:
1733
+ return plan
1734
+ arguments = nodes.get(node.get("fields", {}).get("args"), {})
1735
+ fields = arguments.get("fields", {})
1736
+ keyword_only_ids = tuple(fields.get("kwonlyargs", ()))
1737
+ if (
1738
+ not keyword_only_ids
1739
+ or tuple(fields.get("kw_defaults", ()))
1740
+ != (None,) * len(keyword_only_ids)
1741
+ or fields.get("defaults")
1742
+ or fields.get("vararg")
1743
+ or fields.get("kwarg")
1744
+ ):
1745
+ return plan
1746
+ keyword_only_names = tuple(
1747
+ str(nodes.get(parameter_id, {}).get("fields", {}).get("arg", ""))
1748
+ for parameter_id in keyword_only_ids
1749
+ )
1750
+ return _extend_plan(
1751
+ plan,
1752
+ facts=(
1753
+ f"keyword-only-signature:{node['node_id']}",
1754
+ f"keyword-only-parameter-count:{len(keyword_only_ids)}",
1755
+ "keyword-only-c-interface:mode-erased-after-static-binding",
1756
+ *(
1757
+ f"keyword-only-parameter:{ordinal}:{parameter_id}:{name}"
1758
+ for ordinal, (parameter_id, name) in enumerate(
1759
+ zip(keyword_only_ids, keyword_only_names)
1760
+ )
1761
+ ),
1762
+ ),
1763
+ obligations=(
1764
+ "required-keyword-only-parameters-exact",
1765
+ "keyword-only-parameter-kinds-preserved",
1766
+ "c-interface-mode-erasure-after-static-binding",
1767
+ "defaults-and-variadics-absent",
1768
+ ),
1769
+ explanation=(
1770
+ "required-keyword-only-signature",
1771
+ str(len(keyword_only_ids)),
1772
+ "c-interface-mode-erasure",
1773
+ "after-static-binding",
1774
+ ),
1775
+ )
1776
+
1777
+ @staticmethod
1778
+ def _phase9_plan(plan: RulePlan, node: dict[str, Any], signatures: dict[str, Any], calls: dict[str, CallTargetFact], owners: dict[str, str], returns: dict[str, Any], locals_by_function: dict[str, Any]) -> RulePlan:
1779
+ if node["kind"] == "FunctionDef" and node["node_id"] in signatures:
1780
+ signature = signatures[node["node_id"]]
1781
+ facts = tuple(filter(None, (f"signature:{signature.function_node_id}", f"return-annotation:{signature.return_annotation_node_id}"))) + tuple(f"parameter-annotation:{item.annotation_node_id}" for item in signature.parameters)
1782
+ return _extend_plan(plan, facts=facts, obligations=("prototype-signature-consistent", "all-reachable-returns-compatible", "implicit-none-fallthrough-rejected", "parameter-return-ownership-explicit", "local-declarations-definite", "recursion-policy-explicit"), explanation=("signature", signature.source_name, "uses", "exact-built-in-annotation-evidence"))
1783
+ if node["kind"] == "Call" and node["node_id"] in calls:
1784
+ call = calls[node["node_id"]]
1785
+ facts = (f"call-target:{call.target_binding_id}",) + tuple(f"annotation-evidence:{item}" for item in call.annotation_evidence) + tuple(f"argument-category:{item.value}" for item in call.argument_categories)
1786
+ return _extend_plan(plan, facts=facts, obligations=("call-result-representation-known", "source-output-call-mapping-required"), explanation=("resolved-target", call.target_name or "range", "argument-order", *call.evaluation_order))
1787
+ if node["kind"] == "Return":
1788
+ owner = owners.get(node["node_id"])
1789
+ if owner in returns:
1790
+ return _extend_plan(plan, facts=(f"return-path:{owner}", f"expected-category:{returns[owner].expected_category.value}"), obligations=("fallthrough-policy-checked", "return-ownership-boundary-explicit"))
1791
+ if node["kind"] == "Assign":
1792
+ owner = owners.get(node["node_id"])
1793
+ if owner in locals_by_function:
1794
+ return _extend_plan(plan, facts=(f"local-declarations:{owner}",), obligations=("use-before-binding-absent", "generated-name-collision-absent"))
1795
+ return plan
1796
+
1797
+ @staticmethod
1798
+ def _phase11_plan(
1799
+ plan: RulePlan,
1800
+ node: dict[str, Any],
1801
+ shapes: dict[str, Any],
1802
+ bindings: dict[str, Any],
1803
+ accesses: dict[str, Any],
1804
+ iterations: dict[str, Any],
1805
+ ) -> RulePlan:
1806
+ node_id = node["node_id"]
1807
+ if node_id in shapes:
1808
+ shape = shapes[node_id]
1809
+ return _extend_plan(
1810
+ plan,
1811
+ facts=(
1812
+ f"container-shape:{node_id}",
1813
+ f"container-kind:{shape.container_kind}",
1814
+ f"container-capacity:{shape.capacity}",
1815
+ ),
1816
+ obligations=(
1817
+ "container-storage-automatic",
1818
+ "container-allocation-failure-absent",
1819
+ "container-cleanup-not-required",
1820
+ ),
1821
+ explanation=("fixed-container", shape.container_kind, "capacity", str(shape.capacity)),
1822
+ )
1823
+ if node_id in bindings:
1824
+ binding = bindings[node_id]
1825
+ return _extend_plan(
1826
+ plan,
1827
+ facts=(
1828
+ f"container-binding:{binding.binding_id}",
1829
+ f"container-literal:{binding.literal_node_id}",
1830
+ ),
1831
+ obligations=("container-single-assignment-proved", "container-alias-and-escape-absent"),
1832
+ )
1833
+ if node_id in accesses:
1834
+ access = accesses[node_id]
1835
+ return _extend_plan(
1836
+ plan,
1837
+ facts=(
1838
+ f"container-access:{node_id}",
1839
+ f"container-binding:{access.binding_id}",
1840
+ f"container-offset:{access.resolved_offset}",
1841
+ ),
1842
+ obligations=("container-access-failure-proved-absent", "container-access-mapping-required"),
1843
+ )
1844
+ if node_id in iterations:
1845
+ iteration = iterations[node_id]
1846
+ return _extend_plan(
1847
+ plan,
1848
+ facts=(
1849
+ f"container-iteration:{node_id}",
1850
+ f"container-binding:{iteration.binding_id}",
1851
+ f"container-capacity:{iteration.capacity}",
1852
+ ),
1853
+ obligations=("container-iteration-bound-fixed", "container-iteration-order-explicit"),
1854
+ )
1855
+ return plan
1856
+
1857
+ @staticmethod
1858
+ def _phase12_plan(
1859
+ plan: RulePlan,
1860
+ node: dict[str, Any],
1861
+ imports_by_node: dict[str, list[dict[str, Any]]],
1862
+ imports_by_alias: dict[str, dict[str, Any]],
1863
+ functions_by_node: dict[str, dict[str, Any]],
1864
+ document_plan_nodes: dict[str, dict[str, Any]],
1865
+ initialization_by_node: dict[str, dict[str, Any]],
1866
+ calls: dict[str, CallTargetFact],
1867
+ owners: dict[str, str],
1868
+ assembly_node_id: str | None,
1869
+ ) -> RulePlan:
1870
+ node_id = node["node_id"]
1871
+ if node_id in document_plan_nodes:
1872
+ item = document_plan_nodes[node_id]
1873
+ return _extend_plan(
1874
+ plan,
1875
+ facts=(f"module:{item.get('module_id')}", f"document:{item.get('document_id')}"),
1876
+ explanation=("explicit-module-document", str(item.get("module_id"))),
1877
+ )
1878
+ if node_id in imports_by_node:
1879
+ items = imports_by_node[node_id]
1880
+ return _extend_plan(
1881
+ plan,
1882
+ facts=tuple(
1883
+ value
1884
+ for item in items
1885
+ for value in (
1886
+ f"module-import:{item.get('import_item_id')}",
1887
+ f"target-module:{item.get('target_module_id')}",
1888
+ f"target-function:{item.get('target_function_node_id')}",
1889
+ )
1890
+ ),
1891
+ explanation=("sourcebundle-only-import", *(str(item.get("target_module_id")) for item in items)),
1892
+ )
1893
+ if node_id in imports_by_alias:
1894
+ item = imports_by_alias[node_id]
1895
+ return _extend_plan(
1896
+ plan,
1897
+ facts=(f"module-import:{item.get('import_item_id')}", f"local-import-binding:{item.get('local_name')}"),
1898
+ )
1899
+ if node_id in functions_by_node:
1900
+ item = functions_by_node[node_id]
1901
+ return _extend_plan(
1902
+ plan,
1903
+ facts=(f"module:{item.get('module_id')}", f"module-function:{node_id}", f"linkage:{item.get('linkage')}"),
1904
+ explanation=("module-function", str(item.get("source_name")), "generated-as", str(item.get("flattened_name"))),
1905
+ )
1906
+ if node_id in calls:
1907
+ call = calls[node_id]
1908
+ caller = functions_by_node.get(owners.get(node_id))
1909
+ callee = functions_by_node.get(call.target_function_node_id)
1910
+ if caller and callee and caller.get("module_id") != callee.get("module_id"):
1911
+ return _extend_plan(
1912
+ plan,
1913
+ facts=(f"caller-module:{caller.get('module_id')}", f"callee-module:{callee.get('module_id')}", f"target-function:{call.target_function_node_id}"),
1914
+ obligations=("cross-module-call-uses-target-binding", "bundle-wide-recursion-policy-explicit"),
1915
+ explanation=("cross-module-direct-call", str(caller.get("module_id")), "to", str(callee.get("module_id"))),
1916
+ )
1917
+ if node_id in initialization_by_node:
1918
+ item = initialization_by_node[node_id]
1919
+ return _extend_plan(
1920
+ plan,
1921
+ facts=tuple(f"module-order:{ordinal}:{module_id}" for ordinal, module_id in enumerate(item.get("module_order", ()))),
1922
+ explanation=("compile-time-module-order", *(str(value) for value in item.get("module_order", ()))),
1923
+ )
1924
+ if node_id == assembly_node_id:
1925
+ return _extend_plan(
1926
+ plan,
1927
+ facts=("translation-unit-count:1", "module-runtime-initializers:0"),
1928
+ explanation=("single-c-translation-unit",),
1929
+ )
1930
+ return plan
1931
+
1932
+ @staticmethod
1933
+ def _unsupported_cause(node: dict[str, Any], signatures: dict[str, Any], calls: dict[str, CallTargetFact], owners: dict[str, str], returns: dict[str, Any], locals_by_function: dict[str, Any]) -> str | None:
1934
+ if node["kind"] == "Call" and node["node_id"] in calls:
1935
+ return calls[node["node_id"]].reason
1936
+ if node["kind"] == "FunctionDef" and node["node_id"] in signatures:
1937
+ signature = signatures[node["node_id"]]
1938
+ if not signature.eligible:
1939
+ return signature.rejection_reason
1940
+ path = returns.get(node["node_id"])
1941
+ if path and (not path.compatible or path.fallthrough_possible):
1942
+ return "Return representations are incompatible or a reachable path falls through"
1943
+ local = locals_by_function.get(node["node_id"])
1944
+ if local and not local.valid:
1945
+ return "Local declaration or lifetime policy is not satisfied"
1946
+ if node["kind"] == "Return":
1947
+ owner = owners.get(node["node_id"])
1948
+ path = returns.get(owner)
1949
+ if path and not path.compatible:
1950
+ return "Return representation is incompatible with the declared signature"
1951
+ return None
1952
+
1953
+ @staticmethod
1954
+ def _annotation_categories(module: dict[str, Any], bindings: tuple[Any, ...]) -> dict[str, ValueCategory]:
1955
+ nodes = {node["node_id"]: node for node in module["nodes"]}
1956
+ by_decl = {binding.declaration_node_id: binding.binding_id for binding in bindings}
1957
+ result: dict[str, ValueCategory] = {}
1958
+ mapping = {"int": ValueCategory.INTEGER, "float": ValueCategory.FLOAT, "bool": ValueCategory.BOOLEAN, "str": ValueCategory.STRING}
1959
+ for node_id, binding_id in by_decl.items():
1960
+ node = nodes.get(node_id)
1961
+ if not node or node["kind"] != "arg":
1962
+ continue
1963
+ annotation = nodes.get(node["fields"].get("annotation"))
1964
+ if annotation and annotation["kind"] == "Name":
1965
+ result[binding_id] = mapping.get(annotation["fields"].get("id"), ValueCategory.UNKNOWN)
1966
+ return result
1967
+
1968
+ @staticmethod
1969
+ def _truthiness_strategy(category: ValueCategory) -> str:
1970
+ return {
1971
+ ValueCategory.INTEGER: "nonzero",
1972
+ ValueCategory.FLOAT: "nonzero",
1973
+ ValueCategory.BOOLEAN: "identity",
1974
+ ValueCategory.STRING: "nonempty",
1975
+ ValueCategory.NONE: "always-false",
1976
+ }.get(category, "unresolved")