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,420 @@
1
+ """Closed proof pass for helper-backed integer floor arithmetic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from typing import Any, Mapping
7
+
8
+ from pycforge.converter.analysis.model import ValueCategory
9
+ from pycforge.converter.ir.python_ir import python_ir_reference_ids
10
+ from pycforge.converter.support_templates.factories import (
11
+ FLOOR_DIV_REFERENCE,
12
+ FLOOR_MOD_REFERENCE,
13
+ )
14
+
15
+ from .model import (
16
+ BoundedNumericAnalysis,
17
+ FloorArithmeticKind,
18
+ NumericAnalysisCanceled,
19
+ NumericAnalysisError,
20
+ NumericOperationFact,
21
+ )
22
+
23
+
24
+ _PROHIBITED_CONTEXTS = frozenset(
25
+ {
26
+ "AsyncFor",
27
+ "AsyncFunctionDef",
28
+ "AsyncWith",
29
+ "Await",
30
+ "DictComp",
31
+ "FormattedValue",
32
+ "GeneratorExp",
33
+ "JoinedStr",
34
+ "Lambda",
35
+ "ListComp",
36
+ "Match",
37
+ "NamedExpr",
38
+ "SetComp",
39
+ "Try",
40
+ "TryStar",
41
+ "With",
42
+ "Yield",
43
+ "YieldFrom",
44
+ "comprehension",
45
+ }
46
+ )
47
+ _MAX_DIRECT_MAGNITUDE = 2**63 - 1
48
+
49
+
50
+ def _stable_id(prefix: str, *parts: str) -> str:
51
+ digest = hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest()[:20]
52
+ return prefix + digest
53
+
54
+
55
+ class BoundedNumericAnalyzer:
56
+ """Prove every ``//`` and ``%`` occurrence in an active Phase 14 plan."""
57
+
58
+ def __init__(
59
+ self,
60
+ module: dict[str, Any],
61
+ *,
62
+ categories: Mapping[str, ValueCategory],
63
+ function_records: Mapping[str, Mapping[str, Any]],
64
+ supported_call_node_ids: frozenset[str],
65
+ supported_container_access_node_ids: frozenset[str],
66
+ supported_record_access_node_ids: frozenset[str],
67
+ rejected_expression_diagnostics: Mapping[str, tuple[str, str]],
68
+ cancellation: Any,
69
+ ) -> None:
70
+ self.module = module
71
+ self.nodes = {node["node_id"]: node for node in module["nodes"]}
72
+ self.ordinals = {
73
+ node["node_id"]: ordinal for ordinal, node in enumerate(module["nodes"])
74
+ }
75
+ self.categories = categories
76
+ self.function_records = function_records
77
+ self.supported_call_node_ids = supported_call_node_ids
78
+ self.supported_container_access_node_ids = supported_container_access_node_ids
79
+ self.supported_record_access_node_ids = supported_record_access_node_ids
80
+ self.rejected_expression_diagnostics = rejected_expression_diagnostics
81
+ self.cancellation = cancellation
82
+ self._approved_numeric_node_ids: set[str] = set()
83
+ self._integer_expression_cache: dict[str, bool] = {}
84
+ self.parents: dict[str, list[tuple[str, str]]] = {}
85
+ for parent in module["nodes"]:
86
+ self._check_cancellation()
87
+ for field_name, value in parent.get("fields", {}).items():
88
+ for child_id in python_ir_reference_ids(
89
+ parent["kind"], field_name, value, self.nodes
90
+ ):
91
+ self.parents.setdefault(child_id, []).append(
92
+ (parent["node_id"], field_name)
93
+ )
94
+
95
+ def analyze(self) -> BoundedNumericAnalysis:
96
+ operations: list[NumericOperationFact] = []
97
+ candidates: list[tuple[dict[str, Any], dict[str, Any]]] = []
98
+ for node in self.module["nodes"]:
99
+ self._check_cancellation()
100
+ if node.get("kind") != "BinOp":
101
+ continue
102
+ operator = self.nodes.get(node.get("fields", {}).get("op"), {})
103
+ if operator.get("kind") not in {"FloorDiv", "Mod"}:
104
+ continue
105
+ candidates.append((node, operator))
106
+ candidates.sort(
107
+ key=lambda item: (-self._depth(item[0]["node_id"]), self.ordinals[item[0]["node_id"]])
108
+ )
109
+ for node, operator in candidates:
110
+ operation = self._operation(node, operator)
111
+ operations.append(operation)
112
+ self._approved_numeric_node_ids.add(node["node_id"])
113
+ return BoundedNumericAnalysis(tuple(operations))
114
+
115
+ def _operation(
116
+ self,
117
+ node: dict[str, Any],
118
+ operator: dict[str, Any],
119
+ ) -> NumericOperationFact:
120
+ node_id = node["node_id"]
121
+ left_id = node.get("fields", {}).get("left")
122
+ right_id = node.get("fields", {}).get("right")
123
+ function_node_id = self._approved_context(node_id)
124
+ if (
125
+ not isinstance(left_id, str)
126
+ or left_id not in self.nodes
127
+ or not isinstance(right_id, str)
128
+ or right_id not in self.nodes
129
+ or function_node_id is None
130
+ ):
131
+ self._reject(
132
+ "PYC3701",
133
+ "Integer floor arithmetic is supported only in a direct expression context of an understood top-level function",
134
+ node,
135
+ )
136
+
137
+ rejection = self._first_rejected_expression(left_id)
138
+ if rejection is not None:
139
+ rejected_node_id, code, reason = rejection
140
+ self._reject(code, reason, self.nodes[rejected_node_id])
141
+
142
+ left_category = self.categories.get(left_id, ValueCategory.UNKNOWN)
143
+ right_category = self.categories.get(right_id, ValueCategory.UNKNOWN)
144
+ result_category = self.categories.get(node_id, ValueCategory.UNKNOWN)
145
+ if (
146
+ left_category is not ValueCategory.INTEGER
147
+ or right_category is not ValueCategory.INTEGER
148
+ or result_category is not ValueCategory.INTEGER
149
+ ):
150
+ self._reject(
151
+ "PYC3701",
152
+ "Integer // and % require exact integer-like operands and an integer-like result; bool and mixed categories are excluded",
153
+ node,
154
+ )
155
+ if not self._approved_integer_expression(left_id):
156
+ self._reject(
157
+ "PYC3701",
158
+ "The left operand of integer // or % must be an already supported scalar integer expression",
159
+ self.nodes[left_id],
160
+ )
161
+
162
+ divisor = self._signed_literal(self.nodes[right_id])
163
+ if divisor is None:
164
+ self._reject(
165
+ "PYC3702",
166
+ "The divisor of integer // or % must be a directly recognized, lowerable signed int64 literal",
167
+ self.nodes[right_id],
168
+ )
169
+ divisor_value, literal_ids, literal_shape = divisor
170
+ if divisor_value in {0, -1}:
171
+ reason = (
172
+ "A zero divisor is excluded because Python would raise ZeroDivisionError"
173
+ if divisor_value == 0
174
+ else "A divisor of -1 is excluded by the frozen int64 helper precondition"
175
+ )
176
+ self._reject("PYC3702", reason, self.nodes[right_id])
177
+
178
+ is_division = operator["kind"] == "FloorDiv"
179
+ kind = (
180
+ FloorArithmeticKind.FLOOR_DIVIDE
181
+ if is_division
182
+ else FloorArithmeticKind.FLOOR_MODULO
183
+ )
184
+ helper = FLOOR_DIV_REFERENCE if is_division else FLOOR_MOD_REFERENCE
185
+ owner = self.function_records[function_node_id]
186
+ module_id = owner.get("module_id")
187
+ document_id = owner.get("document_id")
188
+ logical_name = owner.get("logical_name")
189
+ if not all(
190
+ isinstance(item, str) and item
191
+ for item in (module_id, document_id, logical_name)
192
+ ):
193
+ self._reject(
194
+ "PYC3701",
195
+ "Integer floor arithmetic lacks exact module, document, or logical-source ownership",
196
+ node,
197
+ )
198
+ return NumericOperationFact(
199
+ operation_id=_stable_id("numeric-op-", node_id, kind.value),
200
+ binop_node_id=node_id,
201
+ function_node_id=function_node_id,
202
+ module_id=module_id,
203
+ document_id=document_id,
204
+ logical_name=logical_name,
205
+ operator_node_id=operator["node_id"],
206
+ operator_kind=kind,
207
+ left_node_id=left_id,
208
+ right_node_id=right_id,
209
+ left_category=left_category.value,
210
+ right_category=right_category.value,
211
+ result_category=result_category.value,
212
+ left_c_type="int64_t",
213
+ right_c_type="int64_t",
214
+ result_c_type="int64_t",
215
+ divisor_value=divisor_value,
216
+ divisor_literal_node_ids=literal_ids,
217
+ literal_shape=literal_shape,
218
+ divisor_in_admitted_domain=True,
219
+ divisor_nonzero_proved=True,
220
+ negative_one_divisor_excluded=True,
221
+ minimum_signed_divisor_excluded=True,
222
+ helper_requirement=helper.canonical,
223
+ evaluation_order=(left_id, right_id),
224
+ operands_evaluated_once=True,
225
+ c_type="int64_t",
226
+ failure_policy="caller-proved-no-runtime-failure-channel",
227
+ support_state="SupportedWithHelper",
228
+ parameter_ownership="scalar-values-by-value",
229
+ result_ownership="scalar-value-by-value",
230
+ allocation_model="none",
231
+ cleanup_model="none",
232
+ runtime_failure_channel="none",
233
+ target_contract="c11-portable-fixed-v1",
234
+ )
235
+
236
+ def _approved_integer_expression(
237
+ self,
238
+ node_id: str,
239
+ active: frozenset[str] = frozenset(),
240
+ ) -> bool:
241
+ self._check_cancellation()
242
+ if node_id in self._integer_expression_cache:
243
+ return self._integer_expression_cache[node_id]
244
+ if node_id in active or self.categories.get(node_id) is not ValueCategory.INTEGER:
245
+ return False
246
+ node = self.nodes[node_id]
247
+ kind = node["kind"]
248
+ fields = node.get("fields", {})
249
+ next_active = active | {node_id}
250
+ accepted = False
251
+ if kind == "Name":
252
+ accepted = True
253
+ elif kind == "Constant":
254
+ value = fields.get("value")
255
+ accepted = (
256
+ isinstance(value, int)
257
+ and not isinstance(value, bool)
258
+ and 0 <= value <= _MAX_DIRECT_MAGNITUDE
259
+ )
260
+ elif kind == "UnaryOp":
261
+ operator = self.nodes.get(fields.get("op"), {})
262
+ operand_id = fields.get("operand")
263
+ accepted = (
264
+ operator.get("kind") in {"UAdd", "USub"}
265
+ and isinstance(operand_id, str)
266
+ and operand_id in self.nodes
267
+ and self._approved_integer_expression(operand_id, next_active)
268
+ )
269
+ elif kind == "BinOp":
270
+ operator = self.nodes.get(fields.get("op"), {})
271
+ left_id = fields.get("left")
272
+ right_id = fields.get("right")
273
+ if operator.get("kind") in {"FloorDiv", "Mod"}:
274
+ accepted = node_id in self._approved_numeric_node_ids
275
+ else:
276
+ accepted = (
277
+ operator.get("kind") in {"Add", "Sub", "Mult"}
278
+ and isinstance(left_id, str)
279
+ and isinstance(right_id, str)
280
+ and left_id in self.nodes
281
+ and right_id in self.nodes
282
+ and self._approved_integer_expression(left_id, next_active)
283
+ and self._approved_integer_expression(right_id, next_active)
284
+ )
285
+ elif kind == "Call":
286
+ accepted = node_id in self.supported_call_node_ids
287
+ elif kind == "Subscript":
288
+ accepted = node_id in self.supported_container_access_node_ids
289
+ elif kind == "Attribute":
290
+ accepted = node_id in self.supported_record_access_node_ids
291
+ self._integer_expression_cache[node_id] = accepted
292
+ return accepted
293
+
294
+ def _first_rejected_expression(
295
+ self,
296
+ node_id: str,
297
+ active: frozenset[str] = frozenset(),
298
+ ) -> tuple[str, str, str] | None:
299
+ """Preserve an established nested call/container rejection diagnostic."""
300
+ self._check_cancellation()
301
+ if node_id in active:
302
+ return None
303
+ diagnostic = self.rejected_expression_diagnostics.get(node_id)
304
+ if diagnostic is not None:
305
+ return node_id, diagnostic[0], diagnostic[1]
306
+ node = self.nodes[node_id]
307
+ children: list[str] = []
308
+ for field_name, value in sorted(node.get("fields", {}).items()):
309
+ children.extend(
310
+ python_ir_reference_ids(node["kind"], field_name, value, self.nodes)
311
+ )
312
+ for child_id in sorted(
313
+ set(children),
314
+ key=lambda item: (self.ordinals.get(item, 2**63 - 1), item),
315
+ ):
316
+ rejection = self._first_rejected_expression(
317
+ child_id,
318
+ active | {node_id},
319
+ )
320
+ if rejection is not None:
321
+ return rejection
322
+ return None
323
+
324
+ def _depth(self, node_id: str) -> int:
325
+ depth = 0
326
+ current = node_id
327
+ seen: set[str] = set()
328
+ while current not in seen:
329
+ seen.add(current)
330
+ candidates = self.parents.get(current, ())
331
+ if not candidates:
332
+ break
333
+ current = min(
334
+ candidates,
335
+ key=lambda item: (self.ordinals.get(item[0], 2**63 - 1), item[1]),
336
+ )[0]
337
+ depth += 1
338
+ return depth
339
+
340
+ def _approved_context(self, node_id: str) -> str | None:
341
+ current = node_id
342
+ seen: set[str] = set()
343
+ while current not in seen:
344
+ self._check_cancellation()
345
+ seen.add(current)
346
+ candidates = self.parents.get(current, ())
347
+ if not candidates:
348
+ return None
349
+ parent_id, field_name = min(
350
+ candidates,
351
+ key=lambda item: (self.ordinals.get(item[0], 2**63 - 1), item[1]),
352
+ )
353
+ parent = self.nodes[parent_id]
354
+ if parent["kind"] == "FunctionDef":
355
+ return (
356
+ parent_id
357
+ if field_name == "body" and parent_id in self.function_records
358
+ else None
359
+ )
360
+ if parent["kind"] in _PROHIBITED_CONTEXTS or parent["kind"] == "ClassDef":
361
+ return None
362
+ current = parent_id
363
+ return None
364
+
365
+ def _signed_literal(
366
+ self,
367
+ node: dict[str, Any],
368
+ ) -> tuple[int, tuple[str, ...], str] | None:
369
+ """Recognize only source shapes the existing int64 literal lowerer closes.
370
+
371
+ ``-2**63`` has a positive Constant child outside that lowerer's signed
372
+ literal domain, so this mini-phase deliberately rejects it rather than
373
+ smuggling an unrepresentable child through a helper call.
374
+ """
375
+
376
+ if node.get("kind") == "Constant":
377
+ value = node.get("fields", {}).get("value")
378
+ if (
379
+ isinstance(value, int)
380
+ and not isinstance(value, bool)
381
+ and 0 <= value <= _MAX_DIRECT_MAGNITUDE
382
+ ):
383
+ return value, (node["node_id"],), "constant"
384
+ return None
385
+ if node.get("kind") != "UnaryOp":
386
+ return None
387
+ operator = self.nodes.get(node.get("fields", {}).get("op"), {})
388
+ operand = self.nodes.get(node.get("fields", {}).get("operand"), {})
389
+ value = operand.get("fields", {}).get("value")
390
+ if (
391
+ operator.get("kind") not in {"UAdd", "USub"}
392
+ or operand.get("kind") != "Constant"
393
+ or not isinstance(value, int)
394
+ or isinstance(value, bool)
395
+ or not 0 <= value <= _MAX_DIRECT_MAGNITUDE
396
+ ):
397
+ return None
398
+ signed = -value if operator["kind"] == "USub" else value
399
+ shape = "unary-minus" if operator["kind"] == "USub" else "unary-plus"
400
+ return signed, (node["node_id"], operator["node_id"], operand["node_id"]), shape
401
+
402
+ def _reject(
403
+ self,
404
+ code: str,
405
+ message: str,
406
+ node: dict[str, Any],
407
+ ) -> None:
408
+ raise NumericAnalysisError(
409
+ code,
410
+ message,
411
+ node["node_id"],
412
+ node.get("provenance", {}).get("source_span"),
413
+ )
414
+
415
+ def _check_cancellation(self) -> None:
416
+ if bool(getattr(self.cancellation, "is_canceled", False)):
417
+ raise NumericAnalysisCanceled
418
+
419
+
420
+ __all__ = ["BoundedNumericAnalyzer"]
@@ -0,0 +1,153 @@
1
+ """Helper-call lowering for independently validated numeric facts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable
8
+
9
+ from pycforge.converter.ir.c_ir import (
10
+ CCallExpr,
11
+ CIdentifierRef,
12
+ CIntegerLiteral,
13
+ CProvenance,
14
+ CType,
15
+ CUnaryExpr,
16
+ CUnaryOp,
17
+ )
18
+
19
+
20
+ def _sid(prefix: str, *parts: str) -> str:
21
+ digest = hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest()[:20]
22
+ return prefix + digest
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class NumericLoweringServices:
27
+ nodes: dict[str, dict[str, Any]]
28
+ operations: dict[str, dict[str, Any]]
29
+ expression: Callable[[dict[str, Any]], tuple[tuple[Any, ...], Any]]
30
+ temporary: Callable[[str, dict[str, Any], int, CType, Any, tuple[str, ...]], tuple[Any, Any]]
31
+ provenance: Callable[[dict[str, Any]], CProvenance]
32
+ synthetic_provenance: Callable[[tuple[str, ...], str | None], CProvenance]
33
+ reject: Callable[[str, str, dict[str, Any] | None], None]
34
+ check_cancellation: Callable[[], None]
35
+
36
+
37
+ class NumericCIRLowerer:
38
+ def __init__(self, services: NumericLoweringServices) -> None:
39
+ self.services = services
40
+
41
+ def operation(self, node: dict[str, Any]) -> tuple[tuple[Any, ...], Any]:
42
+ s = self.services
43
+ s.check_cancellation()
44
+ fact = s.operations.get(node["node_id"])
45
+ if not fact:
46
+ s.reject(
47
+ "PYC3701",
48
+ "Integer floor arithmetic lacks an independently validated numeric fact",
49
+ node,
50
+ )
51
+ left_node = s.nodes.get(fact["left_node_id"])
52
+ right_node = s.nodes.get(fact["right_node_id"])
53
+ if left_node is None or right_node is None:
54
+ s.reject("PYC3701", "Numeric fact references an absent operand", node)
55
+
56
+ prelude: list[Any] = []
57
+ left_prelude, left_expression = s.expression(left_node)
58
+ prelude.extend(left_prelude)
59
+ left_declaration, left_reference = s.temporary(
60
+ "numeric_left",
61
+ node,
62
+ 0,
63
+ CType("int64_t"),
64
+ left_expression,
65
+ (fact["left_node_id"], node["node_id"]),
66
+ )
67
+ prelude.append(left_declaration)
68
+
69
+ right_expression = self._literal_from_validated_fact(fact, right_node)
70
+ right_declaration, right_reference = s.temporary(
71
+ "numeric_right",
72
+ node,
73
+ 0,
74
+ CType("int64_t"),
75
+ right_expression,
76
+ (fact["right_node_id"], node["node_id"]),
77
+ )
78
+ prelude.append(right_declaration)
79
+
80
+ helper = fact["helper_requirement"]
81
+ callee = CIdentifierRef(
82
+ _sid("c-numeric-helper-ref-", node["node_id"], helper),
83
+ f"helper-binding:{helper}:function",
84
+ s.synthetic_provenance((node["node_id"],), node["node_id"]),
85
+ )
86
+ call = CCallExpr(
87
+ _sid("c-numeric-helper-call-", node["node_id"], helper),
88
+ callee,
89
+ (left_reference, right_reference),
90
+ s.provenance(node),
91
+ )
92
+ result_declaration, result_reference = s.temporary(
93
+ "numeric_result",
94
+ node,
95
+ 0,
96
+ CType("int64_t"),
97
+ call,
98
+ (node["node_id"],),
99
+ )
100
+ prelude.append(result_declaration)
101
+ s.check_cancellation()
102
+ return tuple(prelude), result_reference
103
+
104
+ def _literal_from_validated_fact(
105
+ self,
106
+ fact: dict[str, Any],
107
+ node: dict[str, Any],
108
+ ) -> Any:
109
+ value = fact.get("divisor_value")
110
+ if (
111
+ not isinstance(value, int)
112
+ or isinstance(value, bool)
113
+ or value in {0, -1}
114
+ or not -(2**63 - 1) <= value <= 2**63 - 1
115
+ ):
116
+ self.services.reject("PYC3702", "Numeric divisor proof is invalid", node)
117
+ magnitude = abs(value)
118
+ literal = CIntegerLiteral(
119
+ _sid("c-numeric-divisor-literal-", node["node_id"], str(value)),
120
+ magnitude,
121
+ "LL",
122
+ self.services.provenance(node),
123
+ )
124
+ if value < 0:
125
+ return CUnaryExpr(
126
+ _sid("c-numeric-divisor-negate-", node["node_id"], str(value)),
127
+ CUnaryOp.NEGATE,
128
+ literal,
129
+ self.services.provenance(node),
130
+ )
131
+ return literal
132
+
133
+
134
+ def bind_numeric_lowerer(owner: Any) -> NumericCIRLowerer:
135
+ """Bind the feature lowerer without expanding the cumulative hotspot."""
136
+
137
+ return NumericCIRLowerer(
138
+ NumericLoweringServices(
139
+ nodes=owner.nodes,
140
+ operations=owner._optional_values(
141
+ "numeric-operation-facts", "binop_node_id"
142
+ ),
143
+ expression=owner._expression,
144
+ temporary=owner._temporary,
145
+ provenance=owner._prov,
146
+ synthetic_provenance=owner._synthetic_prov,
147
+ reject=owner._reject,
148
+ check_cancellation=owner._check_cancel,
149
+ )
150
+ )
151
+
152
+
153
+ __all__ = ["NumericCIRLowerer", "NumericLoweringServices", "bind_numeric_lowerer"]
@@ -0,0 +1,123 @@
1
+ """Immutable evidence for the bounded Phase 14A numeric slice."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+
10
+ class FloorArithmeticKind(str, Enum):
11
+ FLOOR_DIVIDE = "floor-divide"
12
+ FLOOR_MODULO = "floor-modulo"
13
+
14
+
15
+ class NumericAnalysisCanceled(Exception):
16
+ """Raised internally when conversion cancellation retires numeric work."""
17
+
18
+
19
+ class NumericAnalysisError(Exception):
20
+ def __init__(
21
+ self,
22
+ code: str,
23
+ message: str,
24
+ node_id: str,
25
+ source_span: dict[str, object] | None,
26
+ ) -> None:
27
+ super().__init__(message)
28
+ self.code = code
29
+ self.message = message
30
+ self.node_id = node_id
31
+ self.source_span = source_span
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class NumericOperationFact:
36
+ operation_id: str
37
+ binop_node_id: str
38
+ function_node_id: str
39
+ module_id: str
40
+ document_id: str
41
+ logical_name: str
42
+ operator_node_id: str
43
+ operator_kind: FloorArithmeticKind
44
+ left_node_id: str
45
+ right_node_id: str
46
+ left_category: str
47
+ right_category: str
48
+ result_category: str
49
+ left_c_type: str
50
+ right_c_type: str
51
+ result_c_type: str
52
+ divisor_value: int
53
+ divisor_literal_node_ids: tuple[str, ...]
54
+ literal_shape: str
55
+ divisor_in_admitted_domain: bool
56
+ divisor_nonzero_proved: bool
57
+ negative_one_divisor_excluded: bool
58
+ minimum_signed_divisor_excluded: bool
59
+ helper_requirement: str
60
+ evaluation_order: tuple[str, str]
61
+ operands_evaluated_once: bool
62
+ c_type: str
63
+ failure_policy: str
64
+ support_state: str
65
+ parameter_ownership: str
66
+ result_ownership: str
67
+ allocation_model: str
68
+ cleanup_model: str
69
+ runtime_failure_channel: str
70
+ target_contract: str
71
+
72
+ def to_dict(self) -> dict[str, Any]:
73
+ return {
74
+ "operation_id": self.operation_id,
75
+ "binop_node_id": self.binop_node_id,
76
+ "function_node_id": self.function_node_id,
77
+ "module_id": self.module_id,
78
+ "document_id": self.document_id,
79
+ "logical_name": self.logical_name,
80
+ "operator_node_id": self.operator_node_id,
81
+ "operator_kind": self.operator_kind.value,
82
+ "left_node_id": self.left_node_id,
83
+ "right_node_id": self.right_node_id,
84
+ "left_category": self.left_category,
85
+ "right_category": self.right_category,
86
+ "result_category": self.result_category,
87
+ "left_c_type": self.left_c_type,
88
+ "right_c_type": self.right_c_type,
89
+ "result_c_type": self.result_c_type,
90
+ "divisor_value": self.divisor_value,
91
+ "divisor_literal_node_ids": list(self.divisor_literal_node_ids),
92
+ "literal_shape": self.literal_shape,
93
+ "divisor_in_admitted_domain": self.divisor_in_admitted_domain,
94
+ "divisor_nonzero_proved": self.divisor_nonzero_proved,
95
+ "negative_one_divisor_excluded": self.negative_one_divisor_excluded,
96
+ "minimum_signed_divisor_excluded": self.minimum_signed_divisor_excluded,
97
+ "helper_requirement": self.helper_requirement,
98
+ "evaluation_order": list(self.evaluation_order),
99
+ "operands_evaluated_once": self.operands_evaluated_once,
100
+ "c_type": self.c_type,
101
+ "failure_policy": self.failure_policy,
102
+ "support_state": self.support_state,
103
+ "parameter_ownership": self.parameter_ownership,
104
+ "result_ownership": self.result_ownership,
105
+ "allocation_model": self.allocation_model,
106
+ "cleanup_model": self.cleanup_model,
107
+ "runtime_failure_channel": self.runtime_failure_channel,
108
+ "target_contract": self.target_contract,
109
+ }
110
+
111
+
112
+ @dataclass(frozen=True, slots=True)
113
+ class BoundedNumericAnalysis:
114
+ operations: tuple[NumericOperationFact, ...]
115
+
116
+
117
+ __all__ = [
118
+ "BoundedNumericAnalysis",
119
+ "FloorArithmeticKind",
120
+ "NumericAnalysisCanceled",
121
+ "NumericAnalysisError",
122
+ "NumericOperationFact",
123
+ ]