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,1394 @@
1
+ """Deterministic analyzer for the first Phase 13 static-record slice.
2
+
3
+ The accepted profile is intentionally smaller than Python's object model:
4
+
5
+ * top-level, base-less, undecorated classes;
6
+ * one to sixty-four value-less ``int``/``float``/``bool`` fields;
7
+ * one exact field-copying ``__init__`` and no other methods;
8
+ * fresh automatic locals constructed by a direct function-body assignment;
9
+ * immutable, statically bound field reads and no aliasing or escape.
10
+
11
+ Anything outside that closed profile is rejected before lowering.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ from collections import defaultdict
18
+ from collections.abc import Mapping, Sequence
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ from pycforge.converter.analysis.model import ValueCategory
23
+ from pycforge.converter.analysis.symbols import PythonIRIndex, SymbolScopeAnalyzer
24
+ from pycforge.converter.ir.python_ir import python_ir_reference_ids
25
+
26
+ from .model import (
27
+ MAX_RECORD_FIELDS,
28
+ RecordAnalysisCanceled,
29
+ RecordAnalysisError,
30
+ RecordDefinitionFact,
31
+ RecordFieldAccessFact,
32
+ RecordFieldFact,
33
+ RecordInitializerFact,
34
+ RecordInstanceBindingFact,
35
+ RecordInstanceFact,
36
+ RecordValueCategory,
37
+ StaticRecordAnalysis,
38
+ )
39
+
40
+
41
+ _FIELD_CATEGORIES = {
42
+ "int": ValueCategory.INTEGER,
43
+ "float": ValueCategory.FLOAT,
44
+ "bool": ValueCategory.BOOLEAN,
45
+ }
46
+ _STORE_PARENT_SLOTS = {
47
+ ("Assign", "targets"),
48
+ ("AnnAssign", "target"),
49
+ ("AugAssign", "target"),
50
+ ("Delete", "targets"),
51
+ ("NamedExpr", "target"),
52
+ ("For", "target"),
53
+ ("AsyncFor", "target"),
54
+ ("comprehension", "target"),
55
+ ("withitem", "optional_vars"),
56
+ }
57
+ _DYNAMIC_ATTRIBUTE_BUILTINS = {"getattr", "setattr", "delattr", "hasattr"}
58
+
59
+
60
+ def _stable_id(prefix: str, *parts: str) -> str:
61
+ return prefix + hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest()[:20]
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class _Identity:
66
+ module_id: str
67
+ document_id: str
68
+ logical_name: str
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class _RecordSpec:
73
+ definition: RecordDefinitionFact
74
+ fields: tuple[RecordFieldFact, ...]
75
+ initializer: RecordInitializerFact
76
+
77
+
78
+ class StaticRecordAnalyzer:
79
+ """Prove and publish the bounded immutable static-record profile.
80
+
81
+ ``module_records`` is keyed by ``ClassDef`` node ID. A record may contain
82
+ ``source_name``, ``flattened_name``, ``module_id``, ``document_id``, and
83
+ ``logical_name`` (or ``logical_source_name``). This allows the Phase 12
84
+ bundle resolver to qualify class names without losing source identity.
85
+
86
+ ``function_records`` supplies equivalent owner-module metadata keyed by
87
+ top-level ``FunctionDef`` node ID. ``bindings`` and ``categories`` are the
88
+ already-published cumulative analysis values; both are optional so this
89
+ semantic gate remains independently testable on a single normalized file.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ module: dict[str, Any],
95
+ *,
96
+ module_records: Mapping[str, Mapping[str, Any]] | None = None,
97
+ function_records: Mapping[str, Mapping[str, Any]] | None = None,
98
+ bindings: Sequence[Mapping[str, Any]] = (),
99
+ categories: Mapping[str, ValueCategory | str] | None = None,
100
+ cancellation: Any | None = None,
101
+ default_module_id: str = "__main__",
102
+ default_logical_name: str = "<memory>",
103
+ ) -> None:
104
+ self.module = module
105
+ self.cancellation = cancellation
106
+ self._check_canceled()
107
+ self.index = PythonIRIndex(module)
108
+ self.module_records = {
109
+ key: dict(value) for key, value in (module_records or {}).items()
110
+ }
111
+ self.function_records = {
112
+ key: dict(value) for key, value in (function_records or {}).items()
113
+ }
114
+ supplied_bindings = tuple(dict(item) for item in bindings)
115
+ class_node_ids = {
116
+ item["node_id"] for item in module.get("nodes", ()) if item.get("kind") == "ClassDef"
117
+ }
118
+ supplied_record_declarations = {
119
+ item.get("declaration_node_id")
120
+ for item in supplied_bindings
121
+ if item.get("binding_kind") == "record-class"
122
+ }
123
+ if supplied_bindings and class_node_ids.issubset(supplied_record_declarations):
124
+ self.bindings = supplied_bindings
125
+ else:
126
+ # Keep the analyzer independently usable while retaining the same
127
+ # lexical-identity proof as the cumulative analysis stage. Older
128
+ # isolated callers may supply ordinary bindings produced without
129
+ # the Phase 13 record switch; those are intentionally recomputed.
130
+ _, inferred_bindings, _ = SymbolScopeAnalyzer().analyze(
131
+ module,
132
+ allow_records=True,
133
+ )
134
+ self.bindings = tuple(item.to_dict() for item in inferred_bindings)
135
+ self.categories = {
136
+ key: self._coerce_category(value)
137
+ for key, value in (categories or {}).items()
138
+ }
139
+ self.default_module_id = default_module_id
140
+ self.default_logical_name = default_logical_name
141
+ self.identity_by_document: dict[str, _Identity] = {}
142
+ for metadata in (
143
+ *(
144
+ self.module_records[key]
145
+ for key in sorted(self.module_records)
146
+ ),
147
+ *(
148
+ self.function_records[key]
149
+ for key in sorted(self.function_records)
150
+ ),
151
+ ):
152
+ self._check_canceled()
153
+ document_id = str(
154
+ metadata.get("document_id")
155
+ or metadata.get("source_document_id")
156
+ or ""
157
+ )
158
+ if not document_id:
159
+ continue
160
+ self.identity_by_document.setdefault(
161
+ document_id,
162
+ _Identity(
163
+ str(metadata.get("module_id") or default_module_id),
164
+ document_id,
165
+ str(
166
+ metadata.get("logical_name")
167
+ or metadata.get("logical_source_name")
168
+ or default_logical_name
169
+ ),
170
+ ),
171
+ )
172
+ self.binding_by_occurrence = {
173
+ occurrence: binding
174
+ for binding in self.bindings
175
+ for occurrence in binding.get("occurrence_node_ids", ())
176
+ if isinstance(occurrence, str)
177
+ }
178
+ self.binding_by_declaration = {
179
+ declaration: binding
180
+ for binding in self.bindings
181
+ for declaration in (binding.get("declaration_node_id"),)
182
+ if isinstance(declaration, str)
183
+ }
184
+ self.parents: dict[str, list[tuple[str, str]]] = defaultdict(list)
185
+ known_ids = self.index.nodes
186
+ for parent in self.index.nodes.values():
187
+ self._check_canceled()
188
+ for field_name, value in parent.get("fields", {}).items():
189
+ self._check_canceled()
190
+ for child_id in python_ir_reference_ids(
191
+ parent["kind"], field_name, value, known_ids
192
+ ):
193
+ self.parents[child_id].append((parent["node_id"], field_name))
194
+
195
+ def analyze(self) -> StaticRecordAnalysis:
196
+ self._check_canceled()
197
+ root = self.index.node(self.module["root_node_id"])
198
+ if root["kind"] != "Module":
199
+ self._reject("PYC3601", "Static-record analysis requires a Module root", root)
200
+ root_body = tuple(root["fields"].get("body", ()))
201
+ root_members = set(root_body)
202
+ class_nodes = tuple(
203
+ sorted(
204
+ (
205
+ node
206
+ for node in self._nodes()
207
+ if node["kind"] == "ClassDef"
208
+ ),
209
+ key=lambda item: self._ordinal(item["node_id"]),
210
+ )
211
+ )
212
+ for node in class_nodes:
213
+ self._check_canceled()
214
+ if node["node_id"] not in root_members:
215
+ self._reject(
216
+ "PYC3601",
217
+ "Static-record classes must be declared directly at module scope",
218
+ node,
219
+ )
220
+
221
+ specs_list: list[_RecordSpec] = []
222
+ for node in class_nodes:
223
+ self._check_canceled()
224
+ specs_list.append(self._definition(node))
225
+ specs = tuple(specs_list)
226
+ by_resolution_name: dict[str, _RecordSpec] = {}
227
+ for spec in specs:
228
+ self._check_canceled()
229
+ name = spec.definition.flattened_name
230
+ if name in by_resolution_name:
231
+ self._reject(
232
+ "PYC3601",
233
+ f"Static-record constructor name {name!r} is not unique",
234
+ self.index.node(spec.definition.class_node_id),
235
+ )
236
+ by_resolution_name[name] = spec
237
+
238
+ self._reject_cross_module_imports(specs)
239
+ self._reject_forward_object_annotations(specs)
240
+ by_class_binding = {
241
+ item.definition.class_binding_id: item for item in specs
242
+ }
243
+ by_record_id = {
244
+ item.definition.record_id: item for item in specs
245
+ }
246
+ if len(by_class_binding) != len(specs):
247
+ self._reject(
248
+ "PYC3601",
249
+ "Static-record class bindings are not unique",
250
+ self.index.node(specs[0].definition.class_node_id),
251
+ )
252
+ instances = self._instances(
253
+ by_resolution_name,
254
+ by_class_binding,
255
+ root_members,
256
+ )
257
+ bindings: list[RecordInstanceBindingFact] = []
258
+ accesses: list[RecordFieldAccessFact] = []
259
+ for instance in instances:
260
+ self._check_canceled()
261
+ binding, instance_accesses = self._validate_instance_uses(
262
+ instance, by_record_id
263
+ )
264
+ bindings.append(binding)
265
+ accesses.extend(instance_accesses)
266
+
267
+ return StaticRecordAnalysis(
268
+ definitions=tuple(item.definition for item in specs),
269
+ fields=tuple(field for item in specs for field in item.fields),
270
+ initializers=tuple(item.initializer for item in specs),
271
+ instances=tuple(instances),
272
+ bindings=tuple(bindings),
273
+ accesses=tuple(
274
+ sorted(accesses, key=lambda item: self._ordinal(item.access_node_id))
275
+ ),
276
+ )
277
+
278
+ def _definition(self, node: dict[str, Any]) -> _RecordSpec:
279
+ fields = node["fields"]
280
+ if (
281
+ fields.get("bases")
282
+ or fields.get("keywords")
283
+ or fields.get("decorator_list")
284
+ ):
285
+ self._reject(
286
+ "PYC3601",
287
+ "Static-record classes cannot have bases, keywords, metaclasses, or decorators",
288
+ node,
289
+ )
290
+
291
+ metadata = self.module_records.get(node["node_id"], {})
292
+ source_name = str(metadata.get("source_name", fields.get("name", "")))
293
+ flattened_name = str(metadata.get("flattened_name", fields.get("name", "")))
294
+ if not source_name or not flattened_name:
295
+ self._reject("PYC3601", "Static-record class name is missing", node)
296
+ if source_name in {"int", "float", "bool", "str"}:
297
+ self._reject(
298
+ "PYC3601",
299
+ "Static-record class names cannot shadow the closed annotation builtins",
300
+ node,
301
+ )
302
+ identity = self._identity(node, metadata)
303
+ record_id = _stable_id(
304
+ "record-", identity.module_id, node["node_id"], source_name
305
+ )
306
+ lexical_binding = self.binding_by_declaration.get(node["node_id"])
307
+ class_binding_id = str(
308
+ metadata.get("binding_id")
309
+ or (lexical_binding or {}).get("binding_id")
310
+ or _stable_id("bind-record-class-", record_id, flattened_name)
311
+ )
312
+ if lexical_binding is not None and (
313
+ lexical_binding.get("binding_kind") != "record-class"
314
+ or lexical_binding.get("source_name") != flattened_name
315
+ ):
316
+ self._reject(
317
+ "PYC3601",
318
+ "Static-record definition lacks its exact lexical class binding",
319
+ node,
320
+ identity,
321
+ )
322
+
323
+ body_ids = tuple(fields.get("body", ()))
324
+ body_nodes = tuple(self.index.node(item) for item in body_ids)
325
+ methods = tuple(item for item in body_nodes if item["kind"] == "FunctionDef")
326
+ for method in methods:
327
+ self._check_canceled()
328
+ if method["fields"].get("name") != "__init__":
329
+ self._reject(
330
+ "PYC3604",
331
+ "The first static-record slice supports __init__ only; other methods are deferred",
332
+ method,
333
+ identity,
334
+ )
335
+ if len(methods) != 1:
336
+ self._reject(
337
+ "PYC3603",
338
+ "A static-record class must declare exactly one __init__",
339
+ methods[1] if len(methods) > 1 else node,
340
+ identity,
341
+ )
342
+ initializer_node = methods[0]
343
+ field_nodes = tuple(item for item in body_nodes if item["kind"] == "AnnAssign")
344
+ unsupported = tuple(
345
+ item for item in body_nodes if item["kind"] not in {"AnnAssign", "FunctionDef"}
346
+ )
347
+ if unsupported:
348
+ self._reject(
349
+ "PYC3604",
350
+ "Static-record class bodies contain only value-less fields followed by __init__",
351
+ unsupported[0],
352
+ identity,
353
+ )
354
+ if body_nodes != field_nodes + (initializer_node,):
355
+ self._reject(
356
+ "PYC3604",
357
+ "Static-record fields must precede the sole __init__ declaration",
358
+ node,
359
+ identity,
360
+ )
361
+ if not field_nodes:
362
+ self._reject(
363
+ "PYC3602",
364
+ "A static-record class must declare at least one field",
365
+ node,
366
+ identity,
367
+ )
368
+ if len(field_nodes) > MAX_RECORD_FIELDS:
369
+ self._reject(
370
+ "PYC3602",
371
+ f"Static-record field count exceeds the fixed limit of {MAX_RECORD_FIELDS}",
372
+ field_nodes[MAX_RECORD_FIELDS],
373
+ identity,
374
+ )
375
+
376
+ field_facts: list[RecordFieldFact] = []
377
+ seen_names: set[str] = set()
378
+ for ordinal, field_node in enumerate(field_nodes):
379
+ self._check_canceled()
380
+ fact = self._field(
381
+ field_node,
382
+ record_id=record_id,
383
+ class_node_id=node["node_id"],
384
+ ordinal=ordinal,
385
+ identity=identity,
386
+ )
387
+ if fact.source_name in seen_names:
388
+ self._reject(
389
+ "PYC3602",
390
+ f"Duplicate static-record field {fact.source_name!r}",
391
+ field_node,
392
+ identity,
393
+ )
394
+ seen_names.add(fact.source_name)
395
+ field_facts.append(fact)
396
+
397
+ initializer = self._initializer(
398
+ initializer_node,
399
+ record_id=record_id,
400
+ fields=tuple(field_facts),
401
+ identity=identity,
402
+ )
403
+ definition = RecordDefinitionFact(
404
+ record_id=record_id,
405
+ class_node_id=node["node_id"],
406
+ class_binding_id=class_binding_id,
407
+ source_name=source_name,
408
+ flattened_name=flattened_name,
409
+ module_id=identity.module_id,
410
+ document_id=identity.document_id,
411
+ logical_name=identity.logical_name,
412
+ field_ids=tuple(item.field_id for item in field_facts),
413
+ initializer_id=initializer.initializer_id,
414
+ category=RecordValueCategory.DEFINITION,
415
+ storage_model="automatic-inline-record",
416
+ ownership_model="unique-lexical-owner",
417
+ lifetime_model="enclosing-function-activation",
418
+ aliasing_model="forbidden",
419
+ cleanup_model="none",
420
+ nullability_model="non-null-by-construction",
421
+ mutable=False,
422
+ )
423
+ return _RecordSpec(definition, tuple(field_facts), initializer)
424
+
425
+ def _field(
426
+ self,
427
+ node: dict[str, Any],
428
+ *,
429
+ record_id: str,
430
+ class_node_id: str,
431
+ ordinal: int,
432
+ identity: _Identity,
433
+ ) -> RecordFieldFact:
434
+ fields = node["fields"]
435
+ target = self.index.nodes.get(fields.get("target"))
436
+ annotation = self.index.nodes.get(fields.get("annotation"))
437
+ if (
438
+ target is None
439
+ or target["kind"] != "Name"
440
+ or fields.get("simple") != 1
441
+ or fields.get("value") is not None
442
+ ):
443
+ self._reject(
444
+ "PYC3602",
445
+ "Static-record fields must be simple value-less annotated names",
446
+ node,
447
+ identity,
448
+ )
449
+ annotation_name = (
450
+ annotation["fields"].get("id")
451
+ if annotation is not None and annotation["kind"] == "Name"
452
+ else None
453
+ )
454
+ if annotation_name not in _FIELD_CATEGORIES:
455
+ self._reject(
456
+ "PYC3602",
457
+ "Static-record fields require an exact int, float, or bool annotation",
458
+ annotation or node,
459
+ identity,
460
+ )
461
+ source_name = str(target["fields"].get("id", ""))
462
+ if (
463
+ not source_name
464
+ or source_name == "self"
465
+ or (source_name.startswith("__") and source_name.endswith("__"))
466
+ ):
467
+ self._reject(
468
+ "PYC3602",
469
+ "Static-record field name is unsupported; Python dunder descriptors are excluded",
470
+ target,
471
+ identity,
472
+ )
473
+ field_id = _stable_id("record-field-", record_id, source_name, str(ordinal))
474
+ return RecordFieldFact(
475
+ field_id=field_id,
476
+ record_id=record_id,
477
+ class_node_id=class_node_id,
478
+ declaration_node_id=node["node_id"],
479
+ target_node_id=target["node_id"],
480
+ annotation_node_id=annotation["node_id"],
481
+ source_name=source_name,
482
+ ordinal=ordinal,
483
+ category=_FIELD_CATEGORIES[annotation_name],
484
+ module_id=identity.module_id,
485
+ document_id=identity.document_id,
486
+ logical_name=identity.logical_name,
487
+ mutable=False,
488
+ )
489
+
490
+ def _initializer(
491
+ self,
492
+ node: dict[str, Any],
493
+ *,
494
+ record_id: str,
495
+ fields: tuple[RecordFieldFact, ...],
496
+ identity: _Identity,
497
+ ) -> RecordInitializerFact:
498
+ values = node["fields"]
499
+ if values.get("decorator_list") or values.get("type_comment"):
500
+ self._reject(
501
+ "PYC3603", "Static-record __init__ cannot be decorated or type-commented", node, identity
502
+ )
503
+ if not self._is_none_annotation(values.get("returns")):
504
+ target = self.index.nodes.get(values.get("returns"), node)
505
+ self._reject(
506
+ "PYC3603", "Static-record __init__ must declare -> None", target, identity
507
+ )
508
+ arguments = self.index.nodes.get(values.get("args"))
509
+ if arguments is None or arguments["kind"] != "arguments":
510
+ self._reject("PYC3603", "Static-record __init__ arguments are malformed", node, identity)
511
+ arg_fields = arguments["fields"]
512
+ positional = tuple(arg_fields.get("args", ()))
513
+ if (
514
+ arg_fields.get("posonlyargs")
515
+ or arg_fields.get("vararg") is not None
516
+ or arg_fields.get("kwonlyargs")
517
+ or arg_fields.get("kw_defaults")
518
+ or arg_fields.get("kwarg") is not None
519
+ or arg_fields.get("defaults")
520
+ or len(positional) != len(fields) + 1
521
+ ):
522
+ self._reject(
523
+ "PYC3603",
524
+ "Static-record __init__ is exact positional self plus one parameter per field",
525
+ arguments,
526
+ identity,
527
+ )
528
+ parameters = tuple(self.index.node(item) for item in positional)
529
+ self_parameter = parameters[0]
530
+ if (
531
+ self_parameter["kind"] != "arg"
532
+ or self_parameter["fields"].get("arg") != "self"
533
+ or self_parameter["fields"].get("annotation") is not None
534
+ or self_parameter["fields"].get("type_comment") is not None
535
+ ):
536
+ self._reject(
537
+ "PYC3603", "Static-record __init__ begins with unannotated self", self_parameter, identity
538
+ )
539
+ value_parameters = parameters[1:]
540
+ for parameter, field in zip(value_parameters, fields):
541
+ self._check_canceled()
542
+ annotation = self.index.nodes.get(parameter["fields"].get("annotation"))
543
+ expected = next(
544
+ name for name, category in _FIELD_CATEGORIES.items() if category is field.category
545
+ )
546
+ if (
547
+ parameter["kind"] != "arg"
548
+ or parameter["fields"].get("arg") != field.source_name
549
+ or annotation is None
550
+ or annotation["kind"] != "Name"
551
+ or annotation["fields"].get("id") != expected
552
+ or parameter["fields"].get("type_comment") is not None
553
+ ):
554
+ self._reject(
555
+ "PYC3603",
556
+ "Static-record __init__ parameters must match field names, order, and exact types",
557
+ parameter,
558
+ identity,
559
+ )
560
+
561
+ assignments = tuple(values.get("body", ()))
562
+ if len(assignments) != len(fields):
563
+ self._reject(
564
+ "PYC3603",
565
+ "Static-record __init__ assigns every field exactly once and performs no other work",
566
+ node,
567
+ identity,
568
+ )
569
+ for assignment_id, field, parameter in zip(assignments, fields, value_parameters):
570
+ self._check_canceled()
571
+ assignment = self.index.node(assignment_id)
572
+ assign_fields = assignment["fields"]
573
+ targets = tuple(assign_fields.get("targets", ()))
574
+ target = self.index.nodes.get(targets[0]) if len(targets) == 1 else None
575
+ receiver = (
576
+ self.index.nodes.get(target["fields"].get("value"))
577
+ if target is not None and target["kind"] == "Attribute"
578
+ else None
579
+ )
580
+ source = self.index.nodes.get(assign_fields.get("value"))
581
+ if (
582
+ assignment["kind"] != "Assign"
583
+ or assign_fields.get("type_comment") is not None
584
+ or target is None
585
+ or target["kind"] != "Attribute"
586
+ or target["fields"].get("attr") != field.source_name
587
+ or receiver is None
588
+ or receiver["kind"] != "Name"
589
+ or receiver["fields"].get("id") != "self"
590
+ or source is None
591
+ or source["kind"] != "Name"
592
+ or source["fields"].get("id") != parameter["fields"].get("arg")
593
+ ):
594
+ self._reject(
595
+ "PYC3603",
596
+ "Static-record __init__ body is the ordered direct copy self.field = field",
597
+ assignment,
598
+ identity,
599
+ )
600
+
601
+ initializer_id = _stable_id("record-init-", record_id, node["node_id"])
602
+ return RecordInitializerFact(
603
+ initializer_id=initializer_id,
604
+ record_id=record_id,
605
+ function_node_id=node["node_id"],
606
+ arguments_node_id=arguments["node_id"],
607
+ self_parameter_node_id=self_parameter["node_id"],
608
+ parameter_node_ids=tuple(item["node_id"] for item in value_parameters),
609
+ assignment_node_ids=assignments,
610
+ field_ids=tuple(item.field_id for item in fields),
611
+ module_id=identity.module_id,
612
+ document_id=identity.document_id,
613
+ logical_name=identity.logical_name,
614
+ receiver_model="direct-addressed-initialization-receiver",
615
+ evaluation_order="field-declaration-order-left-to-right-once",
616
+ initialization_completeness="all-fields-exactly-once",
617
+ )
618
+
619
+ def _instances(
620
+ self,
621
+ by_resolution_name: Mapping[str, _RecordSpec],
622
+ by_class_binding: Mapping[str, _RecordSpec],
623
+ root_members: set[str],
624
+ ) -> tuple[RecordInstanceFact, ...]:
625
+ recognized_constructor_names: set[str] = set()
626
+ instances: list[RecordInstanceFact] = []
627
+ used_bindings: dict[tuple[str, str], str] = {}
628
+ known_instance_fields: dict[tuple[str, str], dict[str, ValueCategory]] = {}
629
+ record_constructor_spellings = {
630
+ name
631
+ for item in by_resolution_name.values()
632
+ for name in (
633
+ item.definition.source_name,
634
+ item.definition.flattened_name,
635
+ )
636
+ }
637
+ for call in sorted(
638
+ (item for item in self._nodes() if item["kind"] == "Call"),
639
+ key=lambda item: self._ordinal(item["node_id"]),
640
+ ):
641
+ self._check_canceled()
642
+ function = self.index.nodes.get(call["fields"].get("func"))
643
+ if function is None or function["kind"] != "Name":
644
+ continue
645
+ callee_binding = self.binding_by_occurrence.get(function["node_id"])
646
+ spec = (
647
+ by_class_binding.get(str(callee_binding.get("binding_id")))
648
+ if callee_binding is not None
649
+ else None
650
+ )
651
+ if spec is None:
652
+ if function["fields"].get("id") in record_constructor_spellings:
653
+ self._reject(
654
+ "PYC3606",
655
+ "Static-record constructor call does not resolve to its lexical class binding",
656
+ function,
657
+ )
658
+ continue
659
+ recognized_constructor_names.add(function["node_id"])
660
+ parent = self._one_parent(call["node_id"])
661
+ assignment = self.index.nodes.get(parent[0]) if parent else None
662
+ if (
663
+ assignment is None
664
+ or assignment["kind"] != "Assign"
665
+ or parent[1] != "value"
666
+ or len(tuple(assignment["fields"].get("targets", ()))) != 1
667
+ or assignment["fields"].get("type_comment") is not None
668
+ ):
669
+ self._reject(
670
+ "PYC3605",
671
+ "Static-record construction must be the complete value of one direct local assignment",
672
+ call,
673
+ )
674
+ target = self.index.nodes.get(assignment["fields"]["targets"][0])
675
+ if target is None or target["kind"] != "Name":
676
+ self._reject(
677
+ "PYC3605", "Static-record construction target must be one fresh local name", assignment
678
+ )
679
+ owner_id = self._enclosing_function(assignment["node_id"])
680
+ owner = self.index.nodes.get(owner_id)
681
+ if (
682
+ owner is None
683
+ or owner_id not in root_members
684
+ or assignment["node_id"] not in tuple(owner["fields"].get("body", ()))
685
+ ):
686
+ self._reject(
687
+ "PYC3605",
688
+ "Static-record construction is allowed only as a direct top-level-function-body statement",
689
+ assignment,
690
+ )
691
+ self._require_same_module(spec.definition, owner)
692
+ arguments = tuple(call["fields"].get("args", ()))
693
+ if call["fields"].get("keywords") or len(arguments) != len(spec.fields):
694
+ self._reject(
695
+ "PYC3605",
696
+ "Static-record construction uses exact positional arity and no keywords",
697
+ call,
698
+ )
699
+ for argument_id, field in zip(arguments, spec.fields):
700
+ self._check_canceled()
701
+ category = self._argument_category(
702
+ argument_id,
703
+ owner,
704
+ known_instance_fields,
705
+ )
706
+ if category is not field.category:
707
+ self._reject(
708
+ "PYC3605",
709
+ f"Constructor argument for {field.source_name!r} must be exactly {field.category.value}",
710
+ self.index.node(argument_id),
711
+ )
712
+
713
+ target_name = str(target["fields"].get("id", ""))
714
+ if target_name in self._parameter_names(owner):
715
+ self._reject(
716
+ "PYC3606", "A static-record instance cannot replace a function parameter", target
717
+ )
718
+ source_binding = self.binding_by_occurrence.get(target["node_id"])
719
+ if (
720
+ source_binding is None
721
+ or source_binding.get("binding_kind") != "local"
722
+ or source_binding.get("declaration_node_id") != target["node_id"]
723
+ or source_binding.get("source_name") != target_name
724
+ ):
725
+ self._reject(
726
+ "PYC3606",
727
+ "A static-record instance requires one fresh ordinary function-local binding",
728
+ target,
729
+ )
730
+ conflict = self._owner_binding_conflict(
731
+ owner,
732
+ target_name,
733
+ target["node_id"],
734
+ )
735
+ if conflict is not None:
736
+ self._reject(
737
+ "PYC3606",
738
+ "A static-record instance binding cannot be redirected, rebound, or deleted",
739
+ conflict,
740
+ )
741
+ binding_id = str(
742
+ source_binding.get("binding_id")
743
+ )
744
+ binding_key = (owner_id, binding_id)
745
+ if binding_key in used_bindings:
746
+ self._reject(
747
+ "PYC3606",
748
+ "A static-record instance binding is single-assignment and cannot be rebound",
749
+ target,
750
+ )
751
+ instance_id = _stable_id(
752
+ "record-instance-", spec.definition.record_id, owner_id, call["node_id"], binding_id
753
+ )
754
+ used_bindings[binding_key] = instance_id
755
+ identity = self._identity(owner, self.function_records.get(owner_id, {}))
756
+ instances.append(
757
+ RecordInstanceFact(
758
+ instance_id=instance_id,
759
+ record_id=spec.definition.record_id,
760
+ class_node_id=spec.definition.class_node_id,
761
+ owner_function_node_id=owner_id,
762
+ construction_node_id=call["node_id"],
763
+ assignment_node_id=assignment["node_id"],
764
+ target_node_id=target["node_id"],
765
+ binding_id=binding_id,
766
+ source_name=target_name,
767
+ argument_node_ids=arguments,
768
+ module_id=identity.module_id,
769
+ document_id=identity.document_id,
770
+ logical_name=identity.logical_name,
771
+ category=RecordValueCategory.INSTANCE,
772
+ storage_model="automatic-inline-record",
773
+ ownership_model="unique-lexical-owner",
774
+ lifetime_model="enclosing-function-activation",
775
+ aliasing_model="forbidden",
776
+ cleanup_model="none",
777
+ nullability_model="non-null-by-construction",
778
+ allocation_model="none",
779
+ mutable=False,
780
+ )
781
+ )
782
+ known_instance_fields[(owner_id, binding_id)] = {
783
+ field.source_name: field.category for field in spec.fields
784
+ }
785
+
786
+ record_class_binding_ids = set(by_class_binding)
787
+ for node in sorted(
788
+ (
789
+ item
790
+ for item in self._nodes()
791
+ if item["kind"] == "Name"
792
+ and (
793
+ self.binding_by_occurrence.get(item["node_id"], {}).get("binding_id")
794
+ in record_class_binding_ids
795
+ )
796
+ ),
797
+ key=lambda item: self._ordinal(item["node_id"]),
798
+ ):
799
+ self._check_canceled()
800
+ if node["node_id"] not in recognized_constructor_names:
801
+ self._reject(
802
+ "PYC3606",
803
+ "Static-record class values, object annotations, and indirect constructors are unsupported",
804
+ node,
805
+ )
806
+ return tuple(instances)
807
+
808
+ def _validate_instance_uses(
809
+ self,
810
+ instance: RecordInstanceFact,
811
+ specs_by_record_id: Mapping[str, _RecordSpec],
812
+ ) -> tuple[RecordInstanceBindingFact, tuple[RecordFieldAccessFact, ...]]:
813
+ self._check_canceled()
814
+ spec = specs_by_record_id.get(instance.record_id)
815
+ if spec is None:
816
+ self._reject(
817
+ "PYC3606",
818
+ "Static-record instance does not resolve to its proved definition",
819
+ self.index.node(instance.construction_node_id),
820
+ )
821
+ fields = {item.source_name: item for item in spec.fields}
822
+ source_binding = self.binding_by_occurrence.get(instance.target_node_id)
823
+ if source_binding is not None:
824
+ occurrence_ids = tuple(
825
+ item
826
+ for item in source_binding.get("occurrence_node_ids", ())
827
+ if item in self.index.nodes
828
+ )
829
+ else:
830
+ occurrence_ids = self._function_name_occurrences(
831
+ instance.owner_function_node_id, instance.source_name
832
+ )
833
+ if instance.target_node_id not in occurrence_ids:
834
+ occurrence_ids = (instance.target_node_id,) + occurrence_ids
835
+ occurrence_ids = tuple(
836
+ sorted(set(occurrence_ids), key=self._ordinal)
837
+ )
838
+ access_facts: list[RecordFieldAccessFact] = []
839
+ for occurrence_id in occurrence_ids:
840
+ self._check_canceled()
841
+ if occurrence_id == instance.target_node_id:
842
+ continue
843
+ occurrence = self.index.node(occurrence_id)
844
+ if (
845
+ self._enclosing_function(occurrence_id) != instance.owner_function_node_id
846
+ or self._crosses_nested_scope(occurrence_id, instance.owner_function_node_id)
847
+ ):
848
+ self._reject(
849
+ "PYC3606",
850
+ "Static-record instances cannot be captured by a nested scope",
851
+ occurrence,
852
+ )
853
+ parent_slot = self._one_parent(occurrence_id)
854
+ parent = self.index.nodes.get(parent_slot[0]) if parent_slot else None
855
+ if (
856
+ parent is not None
857
+ and parent["kind"] == "Attribute"
858
+ and parent_slot[1] == "value"
859
+ ):
860
+ occurrence_position = self._owner_body_position(
861
+ occurrence_id,
862
+ instance.owner_function_node_id,
863
+ )
864
+ construction_position = self._owner_body_position(
865
+ instance.assignment_node_id,
866
+ instance.owner_function_node_id,
867
+ )
868
+ if (
869
+ occurrence_position is None
870
+ or construction_position is None
871
+ or occurrence_position <= construction_position
872
+ ):
873
+ self._reject(
874
+ "PYC3606",
875
+ "Static-record fields cannot be read before construction completes",
876
+ parent,
877
+ )
878
+ field_name = parent["fields"].get("attr")
879
+ field = fields.get(field_name)
880
+ if field is None:
881
+ self._reject(
882
+ "PYC3607",
883
+ f"Static-record field {field_name!r} is not declared",
884
+ parent,
885
+ )
886
+ if self._is_store_target(parent["node_id"]):
887
+ self._reject(
888
+ "PYC3607",
889
+ "Static-record fields are immutable after construction",
890
+ parent,
891
+ )
892
+ outer = self._one_parent(parent["node_id"])
893
+ if outer and self.index.node(outer[0])["kind"] == "Attribute":
894
+ self._reject(
895
+ "PYC3607", "Dynamic or chained attribute lookup is unsupported", self.index.node(outer[0])
896
+ )
897
+ identity = self._identity(
898
+ occurrence,
899
+ self.function_records.get(instance.owner_function_node_id, {}),
900
+ )
901
+ access_facts.append(
902
+ RecordFieldAccessFact(
903
+ access_node_id=parent["node_id"],
904
+ instance_id=instance.instance_id,
905
+ binding_id=instance.binding_id,
906
+ record_id=instance.record_id,
907
+ field_id=field.field_id,
908
+ field_name=field.source_name,
909
+ field_category=field.category,
910
+ owner_function_node_id=instance.owner_function_node_id,
911
+ module_id=identity.module_id,
912
+ document_id=identity.document_id,
913
+ logical_name=identity.logical_name,
914
+ access_mode="read",
915
+ statically_bound=True,
916
+ )
917
+ )
918
+ continue
919
+ if self._is_dynamic_attribute_use(occurrence_id):
920
+ self._reject(
921
+ "PYC3607", "Dynamic attribute access on static records is unsupported", occurrence
922
+ )
923
+ self._reject(
924
+ "PYC3606",
925
+ "Static-record instances cannot be aliased, rebound, escaped, compared by identity, tested for truth, passed, returned, or stored in containers",
926
+ occurrence,
927
+ )
928
+ identity = self._identity(
929
+ self.index.node(instance.owner_function_node_id),
930
+ self.function_records.get(instance.owner_function_node_id, {}),
931
+ )
932
+ binding = RecordInstanceBindingFact(
933
+ binding_id=instance.binding_id,
934
+ instance_id=instance.instance_id,
935
+ record_id=instance.record_id,
936
+ source_name=instance.source_name,
937
+ declaration_node_id=instance.target_node_id,
938
+ occurrence_node_ids=occurrence_ids,
939
+ allowed_field_access_node_ids=tuple(
940
+ item.access_node_id for item in access_facts
941
+ ),
942
+ owner_function_node_id=instance.owner_function_node_id,
943
+ module_id=identity.module_id,
944
+ document_id=identity.document_id,
945
+ logical_name=identity.logical_name,
946
+ category=RecordValueCategory.INSTANCE,
947
+ single_assignment=True,
948
+ noalias=True,
949
+ escapes=False,
950
+ )
951
+ return binding, tuple(access_facts)
952
+
953
+ def _reject_cross_module_imports(self, specs: tuple[_RecordSpec, ...]) -> None:
954
+ by_source = defaultdict(list)
955
+ for spec in specs:
956
+ self._check_canceled()
957
+ by_source[spec.definition.source_name].append(spec)
958
+ for node in sorted(
959
+ (item for item in self._nodes() if item["kind"] == "ImportFrom"),
960
+ key=lambda item: self._ordinal(item["node_id"]),
961
+ ):
962
+ self._check_canceled()
963
+ imported_module = node["fields"].get("module")
964
+ for alias_id in node["fields"].get("names", ()):
965
+ self._check_canceled()
966
+ alias = self.index.nodes.get(alias_id)
967
+ if alias is None:
968
+ continue
969
+ for spec in by_source.get(alias["fields"].get("name"), ()):
970
+ if imported_module == spec.definition.module_id:
971
+ self._reject(
972
+ "PYC3608",
973
+ "Cross-module static-record imports are deferred; records remain module-local",
974
+ alias,
975
+ )
976
+
977
+ def _reject_forward_object_annotations(self, specs: tuple[_RecordSpec, ...]) -> None:
978
+ names = {
979
+ name
980
+ for spec in specs
981
+ for name in (
982
+ spec.definition.source_name,
983
+ spec.definition.flattened_name,
984
+ )
985
+ }
986
+ for function in sorted(
987
+ (
988
+ item
989
+ for item in self._nodes()
990
+ if item["kind"] in {"FunctionDef", "AsyncFunctionDef"}
991
+ ),
992
+ key=lambda item: self._ordinal(item["node_id"]),
993
+ ):
994
+ self._check_canceled()
995
+ arguments = self.index.nodes.get(function["fields"].get("args"), {})
996
+ parameter_ids = tuple(arguments.get("fields", {}).get("posonlyargs", ())) + tuple(
997
+ arguments.get("fields", {}).get("args", ())
998
+ ) + tuple(arguments.get("fields", {}).get("kwonlyargs", ()))
999
+ annotation_ids = [
1000
+ self.index.node(item)["fields"].get("annotation")
1001
+ for item in parameter_ids
1002
+ ]
1003
+ annotation_ids.append(function["fields"].get("returns"))
1004
+ for annotation_id in annotation_ids:
1005
+ self._check_canceled()
1006
+ annotation = self.index.nodes.get(annotation_id)
1007
+ if annotation and (
1008
+ (
1009
+ annotation["kind"] == "Constant"
1010
+ and annotation["fields"].get("value") in names
1011
+ )
1012
+ or (
1013
+ annotation["kind"] == "Name"
1014
+ and annotation["fields"].get("id") in names
1015
+ )
1016
+ ):
1017
+ self._reject(
1018
+ "PYC3606",
1019
+ "Static-record parameters and returns are unsupported, including forward annotations",
1020
+ annotation,
1021
+ )
1022
+
1023
+ def _require_same_module(
1024
+ self, definition: RecordDefinitionFact, owner: dict[str, Any]
1025
+ ) -> None:
1026
+ metadata = self.function_records.get(owner["node_id"])
1027
+ if metadata and metadata.get("module_id") is not None:
1028
+ if str(metadata["module_id"]) != definition.module_id:
1029
+ self._reject(
1030
+ "PYC3608",
1031
+ "Static-record construction is restricted to the defining module",
1032
+ owner,
1033
+ )
1034
+ return
1035
+ owner_document = self._node_document_id(owner)
1036
+ if owner_document and definition.document_id and owner_document != definition.document_id:
1037
+ self._reject(
1038
+ "PYC3608",
1039
+ "Static-record construction is restricted to the defining source document",
1040
+ owner,
1041
+ )
1042
+
1043
+ def _argument_category(
1044
+ self,
1045
+ node_id: str,
1046
+ owner: dict[str, Any],
1047
+ known_instance_fields: Mapping[tuple[str, str], Mapping[str, ValueCategory]] | None = None,
1048
+ ) -> ValueCategory:
1049
+ known = self.categories.get(node_id, ValueCategory.UNKNOWN)
1050
+ if known not in {ValueCategory.UNKNOWN, ValueCategory.CONTRADICTORY}:
1051
+ return known
1052
+ node = self.index.node(node_id)
1053
+ if node["kind"] == "Constant":
1054
+ value = node["fields"].get("value")
1055
+ if isinstance(value, bool):
1056
+ return ValueCategory.BOOLEAN
1057
+ if isinstance(value, int):
1058
+ return ValueCategory.INTEGER
1059
+ if isinstance(value, float):
1060
+ return ValueCategory.FLOAT
1061
+ return ValueCategory.UNKNOWN
1062
+ if node["kind"] == "Name":
1063
+ name = node["fields"].get("id")
1064
+ arguments = self.index.nodes.get(owner["fields"].get("args"), {})
1065
+ for argument_id in tuple(arguments.get("fields", {}).get("posonlyargs", ())) + tuple(
1066
+ arguments.get("fields", {}).get("args", ())
1067
+ ) + tuple(arguments.get("fields", {}).get("kwonlyargs", ())):
1068
+ self._check_canceled()
1069
+ argument = self.index.node(argument_id)
1070
+ if argument["fields"].get("arg") != name:
1071
+ continue
1072
+ annotation = self.index.nodes.get(argument["fields"].get("annotation"))
1073
+ if annotation and annotation["kind"] == "Name":
1074
+ return _FIELD_CATEGORIES.get(
1075
+ annotation["fields"].get("id"), ValueCategory.UNKNOWN
1076
+ )
1077
+ if node["kind"] == "Attribute" and known_instance_fields:
1078
+ receiver = self.index.nodes.get(node["fields"].get("value"))
1079
+ source_binding = (
1080
+ self.binding_by_occurrence.get(receiver["node_id"])
1081
+ if receiver is not None and receiver["kind"] == "Name"
1082
+ else None
1083
+ )
1084
+ fields = known_instance_fields.get(
1085
+ (owner["node_id"], str(source_binding.get("binding_id")))
1086
+ ) if source_binding else None
1087
+ if fields is not None:
1088
+ return fields.get(node["fields"].get("attr"), ValueCategory.UNKNOWN)
1089
+ if node["kind"] == "UnaryOp":
1090
+ return self._argument_category(
1091
+ node["fields"].get("operand"), owner, known_instance_fields
1092
+ )
1093
+ if node["kind"] == "BinOp":
1094
+ left = self._argument_category(
1095
+ node["fields"].get("left"), owner, known_instance_fields
1096
+ )
1097
+ right = self._argument_category(
1098
+ node["fields"].get("right"), owner, known_instance_fields
1099
+ )
1100
+ if left is right and left in {ValueCategory.INTEGER, ValueCategory.FLOAT}:
1101
+ operator = self.index.nodes.get(node["fields"].get("op"), {})
1102
+ if operator.get("kind") != "Div" or left is ValueCategory.FLOAT:
1103
+ return left
1104
+ return ValueCategory.UNKNOWN
1105
+
1106
+ def _parameter_names(self, function: dict[str, Any]) -> set[str]:
1107
+ arguments = self.index.nodes.get(function["fields"].get("args"), {})
1108
+ ids = tuple(arguments.get("fields", {}).get("posonlyargs", ())) + tuple(
1109
+ arguments.get("fields", {}).get("args", ())
1110
+ ) + tuple(arguments.get("fields", {}).get("kwonlyargs", ()))
1111
+ return {
1112
+ str(self.index.node(item)["fields"].get("arg", "")) for item in ids
1113
+ }
1114
+
1115
+ def _function_name_occurrences(
1116
+ self, function_node_id: str, source_name: str
1117
+ ) -> tuple[str, ...]:
1118
+ found: list[str] = []
1119
+ stack = list(reversed(self.index.node(function_node_id)["fields"].get("body", ())))
1120
+ while stack:
1121
+ self._check_canceled()
1122
+ node_id = stack.pop()
1123
+ node = self.index.node(node_id)
1124
+ if node["kind"] == "Name" and node["fields"].get("id") == source_name:
1125
+ found.append(node_id)
1126
+ if node["kind"] in {"FunctionDef", "AsyncFunctionDef", "ClassDef"}:
1127
+ continue
1128
+ stack.extend(reversed(self.index.child_ids(node)))
1129
+ return tuple(found)
1130
+
1131
+ def _owner_binding_conflict(
1132
+ self,
1133
+ owner: dict[str, Any],
1134
+ source_name: str,
1135
+ declaration_node_id: str,
1136
+ ) -> dict[str, Any] | None:
1137
+ """Return any second owner-scope binding/unbinding form.
1138
+
1139
+ Python binds names through more syntax than ordinary assignments. The
1140
+ general symbol facts deliberately serve the cumulative language, so
1141
+ the record gate independently closes its stricter one-declaration
1142
+ proof over exception aliases, patterns, imports, nested definitions,
1143
+ scope directives, context-manager targets, and deletion as well.
1144
+ """
1145
+
1146
+ owner_id = owner["node_id"]
1147
+ for node in sorted(self._nodes(), key=lambda item: self._ordinal(item["node_id"])):
1148
+ self._check_canceled()
1149
+ node_id = node["node_id"]
1150
+ if node_id == declaration_node_id:
1151
+ continue
1152
+ if self._enclosing_function(node_id) != owner_id:
1153
+ continue
1154
+ if self._crosses_class_or_lambda_scope(node_id, owner_id):
1155
+ continue
1156
+ kind = node["kind"]
1157
+ fields = node["fields"]
1158
+ if kind == "Name" and fields.get("id") == source_name:
1159
+ if self._is_comprehension_iteration_target(node_id):
1160
+ continue
1161
+ if self._is_store_target(node_id):
1162
+ return node
1163
+ continue
1164
+ if kind in {"Global", "Nonlocal"} and source_name in tuple(fields.get("names", ())):
1165
+ return node
1166
+ if kind == "ExceptHandler" and fields.get("name") == source_name:
1167
+ return node
1168
+ if kind in {"MatchAs", "MatchStar"} and fields.get("name") == source_name:
1169
+ return node
1170
+ if kind == "MatchMapping" and fields.get("rest") == source_name:
1171
+ return node
1172
+ if (
1173
+ kind in {"FunctionDef", "AsyncFunctionDef", "ClassDef"}
1174
+ and node_id != owner_id
1175
+ and fields.get("name") == source_name
1176
+ ):
1177
+ return node
1178
+ if kind == "alias":
1179
+ imported = str(fields.get("name") or "")
1180
+ bound_name = str(fields.get("asname") or imported.split(".", 1)[0])
1181
+ if bound_name == source_name:
1182
+ return node
1183
+ return None
1184
+
1185
+ def _owner_body_position(
1186
+ self,
1187
+ node_id: str,
1188
+ owner_function_node_id: str,
1189
+ ) -> int | None:
1190
+ owner = self.index.nodes.get(owner_function_node_id)
1191
+ if owner is None:
1192
+ return None
1193
+ body = tuple(owner["fields"].get("body", ()))
1194
+ current = node_id
1195
+ seen: set[str] = set()
1196
+ while current not in seen:
1197
+ self._check_canceled()
1198
+ seen.add(current)
1199
+ parent = self._one_parent(current)
1200
+ if not parent:
1201
+ return None
1202
+ if parent[0] == owner_function_node_id and parent[1] == "body":
1203
+ try:
1204
+ return body.index(current)
1205
+ except ValueError:
1206
+ return None
1207
+ current = parent[0]
1208
+ return None
1209
+
1210
+ def _crosses_class_or_lambda_scope(
1211
+ self,
1212
+ node_id: str,
1213
+ owner_function_node_id: str,
1214
+ ) -> bool:
1215
+ current = node_id
1216
+ seen: set[str] = set()
1217
+ while current not in seen:
1218
+ self._check_canceled()
1219
+ seen.add(current)
1220
+ parent = self._one_parent(current)
1221
+ if not parent or parent[0] == owner_function_node_id:
1222
+ return False
1223
+ parent_node = self.index.node(parent[0])
1224
+ if parent_node["kind"] in {"ClassDef", "Lambda"}:
1225
+ return True
1226
+ current = parent[0]
1227
+ return True
1228
+
1229
+ def _is_comprehension_iteration_target(self, node_id: str) -> bool:
1230
+ current = node_id
1231
+ seen: set[str] = set()
1232
+ while current not in seen:
1233
+ self._check_canceled()
1234
+ seen.add(current)
1235
+ parent = self._one_parent(current)
1236
+ if not parent:
1237
+ return False
1238
+ parent_node = self.index.node(parent[0])
1239
+ if parent_node["kind"] == "comprehension":
1240
+ return parent[1] == "target"
1241
+ if parent_node["kind"] not in {"Tuple", "List", "Starred"}:
1242
+ return False
1243
+ current = parent[0]
1244
+ return False
1245
+
1246
+ def _is_dynamic_attribute_use(self, occurrence_id: str) -> bool:
1247
+ parent_slot = self._one_parent(occurrence_id)
1248
+ if not parent_slot:
1249
+ return False
1250
+ parent = self.index.node(parent_slot[0])
1251
+ if parent["kind"] != "Call" or parent_slot[1] not in {"args", "keywords"}:
1252
+ return False
1253
+ function = self.index.nodes.get(parent["fields"].get("func"))
1254
+ return bool(
1255
+ function
1256
+ and function["kind"] == "Name"
1257
+ and function["fields"].get("id") in _DYNAMIC_ATTRIBUTE_BUILTINS
1258
+ )
1259
+
1260
+ def _is_store_target(self, node_id: str) -> bool:
1261
+ current = node_id
1262
+ while True:
1263
+ self._check_canceled()
1264
+ parent_slot = self._one_parent(current)
1265
+ if not parent_slot:
1266
+ return False
1267
+ parent = self.index.node(parent_slot[0])
1268
+ if (parent["kind"], parent_slot[1]) in _STORE_PARENT_SLOTS:
1269
+ return True
1270
+ if parent["kind"] in {"Tuple", "List"} and parent_slot[1] == "elts":
1271
+ current = parent["node_id"]
1272
+ continue
1273
+ return False
1274
+
1275
+ def _enclosing_function(self, node_id: str) -> str | None:
1276
+ current = node_id
1277
+ seen: set[str] = set()
1278
+ while current not in seen:
1279
+ self._check_canceled()
1280
+ seen.add(current)
1281
+ parent = self._one_parent(current)
1282
+ if not parent:
1283
+ return None
1284
+ node = self.index.node(parent[0])
1285
+ if node["kind"] in {"FunctionDef", "AsyncFunctionDef"}:
1286
+ return node["node_id"]
1287
+ current = node["node_id"]
1288
+ return None
1289
+
1290
+ def _crosses_nested_scope(self, node_id: str, owner_function_node_id: str) -> bool:
1291
+ current = node_id
1292
+ seen: set[str] = set()
1293
+ nested_scope_kinds = {"Lambda", "ListComp", "SetComp", "DictComp", "GeneratorExp"}
1294
+ while current not in seen:
1295
+ self._check_canceled()
1296
+ seen.add(current)
1297
+ parent = self._one_parent(current)
1298
+ if not parent:
1299
+ return False
1300
+ parent_id = parent[0]
1301
+ if parent_id == owner_function_node_id:
1302
+ return False
1303
+ node = self.index.node(parent_id)
1304
+ if node["kind"] in nested_scope_kinds:
1305
+ return True
1306
+ current = parent_id
1307
+ return True
1308
+
1309
+ def _one_parent(self, node_id: str) -> tuple[str, str] | None:
1310
+ values = self.parents.get(node_id, ())
1311
+ if not values:
1312
+ return None
1313
+ return min(values, key=lambda item: (self._ordinal(item[0]), item[1]))
1314
+
1315
+ def _identity(
1316
+ self, node: dict[str, Any], metadata: Mapping[str, Any]
1317
+ ) -> _Identity:
1318
+ node_document_id = self._node_document_id(node)
1319
+ document_identity = self.identity_by_document.get(node_document_id)
1320
+ document_id = str(
1321
+ metadata.get("document_id")
1322
+ or metadata.get("source_document_id")
1323
+ or node_document_id
1324
+ or self.module.get("document_id", "")
1325
+ )
1326
+ module_id = str(
1327
+ metadata.get("module_id")
1328
+ or (document_identity.module_id if document_identity else None)
1329
+ or self.default_module_id
1330
+ )
1331
+ logical_name = str(
1332
+ metadata.get("logical_name")
1333
+ or metadata.get("logical_source_name")
1334
+ or (document_identity.logical_name if document_identity else None)
1335
+ or self.default_logical_name
1336
+ )
1337
+ return _Identity(module_id, document_id, logical_name)
1338
+
1339
+ @staticmethod
1340
+ def _coerce_category(value: ValueCategory | str) -> ValueCategory:
1341
+ if isinstance(value, ValueCategory):
1342
+ return value
1343
+ try:
1344
+ return ValueCategory(value)
1345
+ except ValueError:
1346
+ return ValueCategory.UNKNOWN
1347
+
1348
+ def _is_none_annotation(self, node_id: Any) -> bool:
1349
+ node = self.index.nodes.get(node_id)
1350
+ return bool(
1351
+ node
1352
+ and node["kind"] == "Constant"
1353
+ and node["fields"].get("value") is None
1354
+ )
1355
+
1356
+ @staticmethod
1357
+ def _node_document_id(node: Mapping[str, Any]) -> str:
1358
+ span = node.get("provenance", {}).get("source_span") or {}
1359
+ return str(span.get("document_id") or "")
1360
+
1361
+ def _ordinal(self, node_id: str) -> int:
1362
+ return self.index.ordinals.get(node_id, 2**63 - 1)
1363
+
1364
+ def _nodes(self):
1365
+ """Yield normalized nodes with a bounded cancellation safe point."""
1366
+
1367
+ for node in self.index.nodes.values():
1368
+ self._check_canceled()
1369
+ yield node
1370
+
1371
+ def _reject(
1372
+ self,
1373
+ code: str,
1374
+ message: str,
1375
+ node: dict[str, Any],
1376
+ identity: _Identity | None = None,
1377
+ ) -> None:
1378
+ self._check_canceled()
1379
+ actual = identity or self._identity(node, {})
1380
+ raise RecordAnalysisError(
1381
+ code,
1382
+ message,
1383
+ node_id=node["node_id"],
1384
+ module_id=actual.module_id,
1385
+ document_id=actual.document_id,
1386
+ logical_name=actual.logical_name,
1387
+ source_span=node.get("provenance", {}).get("source_span"),
1388
+ )
1389
+
1390
+ def _check_canceled(self) -> None:
1391
+ if self.cancellation is not None and bool(
1392
+ getattr(self.cancellation, "is_canceled", False)
1393
+ ):
1394
+ raise RecordAnalysisCanceled