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,1250 @@
1
+ """Closed-world analysis for explicit logical modules in a SourceBundle.
2
+
3
+ This module deliberately operates only on normalized in-memory Python IR. It
4
+ does not import, discover, open, or execute anything named by source imports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+ from pycforge.converter.contracts.identifiers import (
15
+ C11_EXTERNAL_IDENTIFIERS,
16
+ C_KEYWORDS,
17
+ TARGET_RESERVED_NAMES,
18
+ )
19
+ from pycforge.converter.ir.python_ir import python_ir_reference_ids
20
+
21
+ from .model import (
22
+ ModuleAnalysisCanceled,
23
+ ModuleResolutionError,
24
+ ModuleResolutionProduct,
25
+ ResolvedImport,
26
+ )
27
+
28
+
29
+ _DYNAMIC_IMPORT_ATTRIBUTES = frozenset(
30
+ {"find_spec", "import_module", "module_from_spec", "reload"}
31
+ )
32
+
33
+
34
+ def _stable_id(prefix: str, *parts: object) -> str:
35
+ seed = "\x1f".join(str(part) for part in parts).encode("utf-8")
36
+ return prefix + hashlib.sha256(seed).hexdigest()[:20]
37
+
38
+
39
+ def _thaw(value: Any) -> Any:
40
+ if isinstance(value, dict):
41
+ return {key: _thaw(item) for key, item in value.items()}
42
+ if isinstance(value, (list, tuple)):
43
+ return [_thaw(item) for item in value]
44
+ return value
45
+
46
+
47
+ def _legacy_function_token(source_name: str) -> str:
48
+ base = re.sub(r"[^A-Za-z0-9_]", "_", source_name)
49
+ if not base or not (base[0].isalpha() or base[0] == "_"):
50
+ base = "py_" + base
51
+ if base.startswith("_"):
52
+ base = "py" + base
53
+ if (
54
+ base in C_KEYWORDS
55
+ or base in TARGET_RESERVED_NAMES
56
+ or base in C11_EXTERNAL_IDENTIFIERS
57
+ or base.startswith(("pycf_", "pycm_"))
58
+ ):
59
+ base = "py_" + base
60
+ return base
61
+
62
+
63
+ def _module_escape(module_id: str) -> str:
64
+ return module_id.replace("_", "_u").replace(".", "_d")
65
+
66
+
67
+ def _qualified_function_name(module_id: str, source_name: str) -> str:
68
+ binding_identity = f"{module_id}.{source_name}"
69
+ digest = hashlib.sha256(binding_identity.encode("utf-8")).hexdigest()
70
+ # Put collision entropy inside C11's guaranteed 31 significant initial
71
+ # characters for external identifiers. The readable suffix is diagnostic
72
+ # context only; portable identity does not depend on its tail.
73
+ return f"pycm_{digest}__{_module_escape(module_id)}__{_legacy_function_token(source_name)}"
74
+
75
+
76
+ @dataclass(slots=True)
77
+ class _Module:
78
+ module_id: str
79
+ logical_name: str
80
+ bundle_ordinal: int
81
+ is_primary: bool
82
+ document_id: str
83
+ python_ir: dict[str, Any]
84
+ source: dict[str, Any]
85
+ nodes: dict[str, dict[str, Any]] = field(init=False)
86
+ root: dict[str, Any] = field(init=False)
87
+ body: tuple[str, ...] = field(init=False)
88
+ function_ids: tuple[str, ...] = ()
89
+ functions: dict[str, dict[str, Any]] = field(default_factory=dict)
90
+ record_ids: tuple[str, ...] = ()
91
+ records: dict[str, dict[str, Any]] = field(default_factory=dict)
92
+ ineligible_functions: dict[str, dict[str, Any]] = field(default_factory=dict)
93
+ deferred_top_level_node: dict[str, Any] | None = None
94
+ import_node_ids: tuple[str, ...] = ()
95
+ raw_imports: list[tuple[str, int, int, str, str, str]] = field(default_factory=list)
96
+ resolved_imports: list[ResolvedImport] = field(default_factory=list)
97
+ imported_bindings: dict[str, ResolvedImport] = field(default_factory=dict)
98
+ document_plan_node_id: str = ""
99
+
100
+ def __post_init__(self) -> None:
101
+ self.nodes = {node["node_id"]: node for node in self.python_ir["nodes"]}
102
+ self.root = self.nodes[self.python_ir["root_node_id"]]
103
+ self.body = tuple(self.root["fields"].get("body", ()))
104
+
105
+
106
+ class ExplicitModuleAnalyzer:
107
+ """Resolve the approved absolute-from-import module profile."""
108
+
109
+ stage_id = "modules.resolve"
110
+
111
+ def __init__(
112
+ self,
113
+ bundle: dict[str, Any],
114
+ source_bundle: dict[str, Any],
115
+ *,
116
+ max_import_edges: int,
117
+ cancellation: Any = None,
118
+ invalidation_dependency: str,
119
+ allow_records: bool = False,
120
+ allow_required_keyword_only: bool = False,
121
+ ) -> None:
122
+ self.bundle = bundle
123
+ self.source_bundle = source_bundle
124
+ self.max_import_edges = max_import_edges
125
+ self.cancellation = cancellation
126
+ self.invalidation_dependency = invalidation_dependency
127
+ self.allow_records = allow_records
128
+ self.allow_required_keyword_only = allow_required_keyword_only
129
+ sources = {
130
+ item["module_id"]: item for item in source_bundle.get("documents", ())
131
+ }
132
+ self.modules = tuple(
133
+ _Module(
134
+ module_id=item["module_id"],
135
+ logical_name=item["logical_name"],
136
+ bundle_ordinal=item["bundle_ordinal"],
137
+ is_primary=item["is_primary"],
138
+ document_id=item["document_id"],
139
+ python_ir=_thaw(item["python_ir"]),
140
+ source=_thaw(sources.get(item["module_id"], {})),
141
+ )
142
+ for item in bundle["documents"]
143
+ )
144
+ self.by_id = {item.module_id: item for item in self.modules}
145
+
146
+ def analyze(self) -> ModuleResolutionProduct:
147
+ for module in self.modules:
148
+ self._check_canceled()
149
+ self._analyze_document_shape(module)
150
+ import_count = sum(len(module.raw_imports) for module in self.modules)
151
+ if import_count > self.max_import_edges:
152
+ module = next(item for item in self.modules if item.raw_imports)
153
+ node = module.nodes[module.raw_imports[0][0]]
154
+ self._reject(
155
+ module,
156
+ "PYC3510",
157
+ "SourceBundle exceeds max_import_edges",
158
+ node,
159
+ )
160
+ for module in self.modules:
161
+ self._check_canceled()
162
+ self._resolve_imports(module)
163
+ for module in self.modules:
164
+ self._check_canceled()
165
+ self._validate_import_binding_uses(module)
166
+ for module in self.modules:
167
+ if module.deferred_top_level_node is not None:
168
+ self._reject(
169
+ module,
170
+ "PYC3509",
171
+ "Only eligible synchronous top-level functions may define a module namespace",
172
+ module.deferred_top_level_node,
173
+ )
174
+
175
+ dependency_edges = tuple(
176
+ sorted(
177
+ {
178
+ (item.module_id, resolved.target_module_id)
179
+ for item in self.modules
180
+ for resolved in item.resolved_imports
181
+ }
182
+ )
183
+ )
184
+ module_order = self._dependency_first_order(dependency_edges)
185
+ return self._publish(module_order, dependency_edges)
186
+
187
+ def _analyze_document_shape(self, module: _Module) -> None:
188
+ if module.root["kind"] != "Module":
189
+ self._reject(module, "PYC3509", "Module document lacks a module root", module.root)
190
+
191
+ body_set = set(module.body)
192
+ for node in sorted(module.nodes.values(), key=self._source_order_key):
193
+ self._check_canceled()
194
+ if node["kind"] in {"Import", "ImportFrom"} and node["node_id"] not in body_set:
195
+ self._reject(
196
+ module,
197
+ "PYC3504",
198
+ "Local and conditional imports are unsupported",
199
+ node,
200
+ )
201
+ if self._is_dynamic_import_call(module, node):
202
+ self._reject(
203
+ module,
204
+ "PYC3504",
205
+ "Dynamic import behavior is unsupported",
206
+ node,
207
+ )
208
+
209
+ imports: list[str] = []
210
+ functions: list[str] = []
211
+ records: list[str] = []
212
+ seen_function = False
213
+ function_names: set[str] = set()
214
+ raw_imports: list[tuple[str, int, int, str, str, str]] = []
215
+ import_item_ordinal = 0
216
+ for node_id in module.body:
217
+ self._check_canceled()
218
+ node = module.nodes[node_id]
219
+ kind = node["kind"]
220
+ if kind == "ImportFrom":
221
+ if seen_function:
222
+ self._reject(
223
+ module,
224
+ "PYC3504",
225
+ "Imports must form the module preamble before every function",
226
+ node,
227
+ )
228
+ imports.append(node_id)
229
+ target_module = node["fields"].get("module")
230
+ level = node["fields"].get("level")
231
+ aliases = node["fields"].get("names", ())
232
+ if level != 0 or not isinstance(target_module, str) or not target_module or not aliases:
233
+ self._reject(
234
+ module,
235
+ "PYC3504",
236
+ "Only nonempty absolute from-import declarations are supported",
237
+ node,
238
+ )
239
+ for alias_ordinal, alias_id in enumerate(aliases):
240
+ alias = module.nodes.get(alias_id)
241
+ if alias is None or alias.get("kind") != "alias":
242
+ self._reject(module, "PYC3504", "Import contains an invalid alias", node)
243
+ imported_name = alias["fields"].get("name")
244
+ asname = alias["fields"].get("asname")
245
+ if imported_name == "*" or not isinstance(imported_name, str) or not imported_name:
246
+ self._reject(
247
+ module,
248
+ "PYC3504",
249
+ "Star and malformed imports are unsupported",
250
+ alias,
251
+ )
252
+ if asname is not None and (not isinstance(asname, str) or not asname):
253
+ self._reject(module, "PYC3504", "Import alias is malformed", alias)
254
+ raw_imports.append(
255
+ (
256
+ node_id,
257
+ alias_ordinal,
258
+ import_item_ordinal,
259
+ target_module,
260
+ imported_name,
261
+ asname or imported_name,
262
+ )
263
+ )
264
+ import_item_ordinal += 1
265
+ elif kind == "FunctionDef":
266
+ seen_function = True
267
+ functions.append(node_id)
268
+ source_name = node["fields"].get("name")
269
+ if not isinstance(source_name, str) or not source_name:
270
+ self._reject(module, "PYC3506", "Function binding is malformed", node)
271
+ if source_name in function_names:
272
+ self._reject(
273
+ module,
274
+ "PYC3506",
275
+ f"Module function {source_name!r} is rebound",
276
+ node,
277
+ )
278
+ function_names.add(source_name)
279
+ module.functions[source_name] = node
280
+ elif kind == "ClassDef" and self.allow_records:
281
+ if seen_function:
282
+ self._reject(
283
+ module,
284
+ "PYC3601",
285
+ "Static record classes must precede every top-level function",
286
+ node,
287
+ )
288
+ source_name = node["fields"].get("name")
289
+ if not isinstance(source_name, str) or not source_name:
290
+ self._reject(module, "PYC3601", "Record class binding is malformed", node)
291
+ if source_name in function_names:
292
+ self._reject(
293
+ module,
294
+ "PYC3506",
295
+ f"Module binding {source_name!r} is rebound",
296
+ node,
297
+ )
298
+ function_names.add(source_name)
299
+ records.append(node_id)
300
+ module.records[source_name] = node
301
+ elif kind == "AsyncFunctionDef":
302
+ seen_function = True
303
+ source_name = node["fields"].get("name")
304
+ if not isinstance(source_name, str) or not source_name:
305
+ self._reject(module, "PYC3506", "Function binding is malformed", node)
306
+ if source_name in function_names:
307
+ self._reject(
308
+ module,
309
+ "PYC3506",
310
+ f"Module function {source_name!r} is rebound",
311
+ node,
312
+ )
313
+ function_names.add(source_name)
314
+ module.ineligible_functions[source_name] = node
315
+ module.deferred_top_level_node = module.deferred_top_level_node or node
316
+ elif kind == "Import":
317
+ self._reject(
318
+ module,
319
+ "PYC3504",
320
+ "Plain imports are unsupported",
321
+ node,
322
+ )
323
+ else:
324
+ imported_names = {item[5] for item in raw_imports}
325
+ rebound = sorted(imported_names & self._statement_bound_names(module, node))
326
+ if rebound:
327
+ self._reject(
328
+ module,
329
+ "PYC3506",
330
+ f"Imported binding {rebound[0]!r} is rebound at module scope",
331
+ node,
332
+ )
333
+ self._reject(
334
+ module,
335
+ "PYC3509",
336
+ "Executable module initialization and top-level state are unsupported",
337
+ node,
338
+ )
339
+
340
+ if not functions and not module.ineligible_functions:
341
+ self._reject(
342
+ module,
343
+ "PYC3509",
344
+ "Every module document must define at least one top-level function",
345
+ module.nodes[module.body[0]] if module.body else None,
346
+ )
347
+
348
+ module.function_ids = tuple(functions)
349
+ module.record_ids = tuple(records)
350
+ module.import_node_ids = tuple(imports)
351
+ module.raw_imports = raw_imports
352
+
353
+ def _statement_bound_names(
354
+ self,
355
+ module: _Module,
356
+ node: dict[str, Any],
357
+ ) -> set[str]:
358
+ fields = node["fields"]
359
+ target_ids: list[str] = []
360
+ if node["kind"] in {"Assign", "Delete"}:
361
+ target_ids.extend(fields.get("targets", ()))
362
+ elif node["kind"] in {"AnnAssign", "AugAssign", "For", "NamedExpr"}:
363
+ target_id = fields.get("target")
364
+ if isinstance(target_id, str):
365
+ target_ids.append(target_id)
366
+ names: set[str] = set()
367
+ stack = list(target_ids)
368
+ while stack:
369
+ target = module.nodes.get(stack.pop())
370
+ if not target:
371
+ continue
372
+ if target["kind"] == "Name":
373
+ name = target["fields"].get("id")
374
+ if isinstance(name, str):
375
+ names.add(name)
376
+ for field_name, value in target["fields"].items():
377
+ stack.extend(
378
+ python_ir_reference_ids(
379
+ target["kind"], field_name, value, module.nodes
380
+ )
381
+ )
382
+ return names
383
+
384
+ def _is_dynamic_import_call(self, module: _Module, node: dict[str, Any]) -> bool:
385
+ if node["kind"] != "Call":
386
+ return False
387
+ callee = module.nodes.get(node["fields"].get("func"))
388
+ if not callee:
389
+ return False
390
+ if callee["kind"] == "Name":
391
+ return callee["fields"].get("id") == "__import__"
392
+ if callee["kind"] == "Attribute":
393
+ path = self._attribute_path(module, callee)
394
+ return bool(
395
+ (path and path[0] == "importlib" and path[-1] in _DYNAMIC_IMPORT_ATTRIBUTES)
396
+ or path in {("builtins", "__import__"), ("__builtins__", "__import__")}
397
+ )
398
+ return False
399
+
400
+ def _attribute_path(
401
+ self,
402
+ module: _Module,
403
+ node: dict[str, Any],
404
+ ) -> tuple[str, ...] | None:
405
+ if node["kind"] == "Name":
406
+ name = node["fields"].get("id")
407
+ return (name,) if isinstance(name, str) else None
408
+ if node["kind"] != "Attribute":
409
+ return None
410
+ value = module.nodes.get(node["fields"].get("value"))
411
+ if value is None:
412
+ return None
413
+ prefix = self._attribute_path(module, value)
414
+ attribute = node["fields"].get("attr")
415
+ if prefix is None or not isinstance(attribute, str):
416
+ return None
417
+ return prefix + (attribute,)
418
+
419
+ @staticmethod
420
+ def _source_order_key(node: dict[str, Any]) -> tuple[int, str]:
421
+ span = node.get("provenance", {}).get("source_span") or {}
422
+ start = span.get("start", {}) if isinstance(span, dict) else {}
423
+ offset = start.get("offset") if isinstance(start, dict) else None
424
+ return (offset if isinstance(offset, int) else 2**63 - 1, node["node_id"])
425
+
426
+ def _resolve_imports(self, module: _Module) -> None:
427
+ occupied = set(module.functions) | set(module.records)
428
+ for node_id, alias_ordinal, source_ordinal, target_id, imported_name, local_name in module.raw_imports:
429
+ self._check_canceled()
430
+ import_node = module.nodes[node_id]
431
+ alias_id = import_node["fields"]["names"][alias_ordinal]
432
+ alias_node = module.nodes[alias_id]
433
+ target = self.by_id.get(target_id)
434
+ if target is None:
435
+ code = "PYC3508" if self._has_package_relation(target_id) else "PYC3503"
436
+ message = (
437
+ f"Import {target_id!r} requests unsupported package or implicit-parent behavior"
438
+ if code == "PYC3508"
439
+ else f"Import target module {target_id!r} is not present in SourceBundle"
440
+ )
441
+ self._reject(module, code, message, import_node)
442
+ if target_id == module.module_id:
443
+ self._reject(
444
+ module,
445
+ "PYC3507",
446
+ f"Module {module.module_id!r} imports itself",
447
+ import_node,
448
+ )
449
+ target_function = target.functions.get(imported_name)
450
+ if target_function is None:
451
+ if imported_name in target.records:
452
+ self._reject(
453
+ module,
454
+ "PYC3610",
455
+ f"Record class import {target_id}.{imported_name} is outside the module-private Phase 13 record boundary",
456
+ alias_node,
457
+ related=(self._span(target, target.records[imported_name]),),
458
+ )
459
+ if imported_name in target.ineligible_functions:
460
+ self._reject(
461
+ module,
462
+ "PYC3505",
463
+ f"Imported member {target_id}.{imported_name} is not an eligible synchronous function",
464
+ alias_node,
465
+ related=(
466
+ self._span(target, target.ineligible_functions[imported_name]),
467
+ ),
468
+ )
469
+ if imported_name in {item[5] for item in target.raw_imports}:
470
+ self._reject(
471
+ module,
472
+ "PYC3506",
473
+ f"Imported member {target_id}.{imported_name} would be a re-export",
474
+ alias_node,
475
+ )
476
+ if f"{target_id}.{imported_name}" in self.by_id:
477
+ self._reject(
478
+ module,
479
+ "PYC3506",
480
+ f"Imported member {target_id}.{imported_name} would be a module object",
481
+ alias_node,
482
+ )
483
+ self._reject(
484
+ module,
485
+ "PYC3505",
486
+ f"Imported member {target_id}.{imported_name} is not a direct top-level function",
487
+ alias_node,
488
+ )
489
+ if not self._importable_signature(target, target_function):
490
+ self._reject(
491
+ module,
492
+ "PYC3505",
493
+ f"Imported member {target_id}.{imported_name} is not an eligible direct function",
494
+ alias_node,
495
+ related=(self._span(target, target_function),),
496
+ )
497
+ if local_name == "range" or local_name in occupied:
498
+ self._reject(
499
+ module,
500
+ "PYC3506",
501
+ f"Imported binding {local_name!r} collides with another module binding",
502
+ alias_node,
503
+ )
504
+ occupied.add(local_name)
505
+ resolved = ResolvedImport(
506
+ import_item_id=(
507
+ f"module-import-{module.module_id}-{source_ordinal:04d}-"
508
+ + _stable_id(
509
+ "",
510
+ module.module_id,
511
+ node_id,
512
+ alias_ordinal,
513
+ target_id,
514
+ imported_name,
515
+ local_name,
516
+ )
517
+ ),
518
+ import_node_id=node_id,
519
+ alias_node_id=alias_id,
520
+ importer_module_id=module.module_id,
521
+ target_module_id=target_id,
522
+ imported_name=imported_name,
523
+ local_name=local_name,
524
+ target_function_node_id=target_function["node_id"],
525
+ source_ordinal=source_ordinal,
526
+ )
527
+ module.resolved_imports.append(resolved)
528
+ module.imported_bindings[local_name] = resolved
529
+
530
+ def _has_package_relation(self, target_id: str) -> bool:
531
+ return any(
532
+ candidate.startswith(target_id + ".") or target_id.startswith(candidate + ".")
533
+ for candidate in self.by_id
534
+ )
535
+
536
+ def _importable_signature(self, module: _Module, function: dict[str, Any]) -> bool:
537
+ fields = function["fields"]
538
+ if fields.get("decorator_list") or fields.get("type_params"):
539
+ return False
540
+ arguments = module.nodes.get(fields.get("args"), {})
541
+ if arguments.get("kind") != "arguments":
542
+ return False
543
+ args = arguments["fields"]
544
+ keyword_only_ids = tuple(args.get("kwonlyargs", ()))
545
+ keyword_defaults = tuple(args.get("kw_defaults", ()))
546
+ if (
547
+ args.get("vararg") is not None
548
+ or args.get("kwarg") is not None
549
+ or args.get("defaults")
550
+ or (
551
+ bool(keyword_only_ids)
552
+ and (
553
+ not self.allow_required_keyword_only
554
+ or len(keyword_defaults) != len(keyword_only_ids)
555
+ or any(item is not None for item in keyword_defaults)
556
+ )
557
+ )
558
+ ):
559
+ return False
560
+ parameter_ids = (
561
+ tuple(args.get("posonlyargs", ()))
562
+ + tuple(args.get("args", ()))
563
+ + keyword_only_ids
564
+ )
565
+ annotation_ids = [
566
+ module.nodes[parameter_id]["fields"].get("annotation")
567
+ for parameter_id in parameter_ids
568
+ ]
569
+ annotation_ids.append(fields.get("returns"))
570
+ for annotation_id in annotation_ids:
571
+ annotation = module.nodes.get(annotation_id)
572
+ if (
573
+ not annotation
574
+ or annotation["kind"] != "Name"
575
+ or annotation["fields"].get("id") not in {"bool", "float", "int", "str"}
576
+ ):
577
+ return False
578
+ rejected_body_kinds = {
579
+ "AsyncFunctionDef",
580
+ "Await",
581
+ "ClassDef",
582
+ "Global",
583
+ "Lambda",
584
+ "Nonlocal",
585
+ "Yield",
586
+ "YieldFrom",
587
+ }
588
+ stack = list(function["fields"].get("body", ()))
589
+ while stack:
590
+ node_id = stack.pop()
591
+ node = module.nodes[node_id]
592
+ if node["kind"] in rejected_body_kinds or node["kind"] == "FunctionDef":
593
+ return False
594
+ for field_name, value in node["fields"].items():
595
+ stack.extend(
596
+ python_ir_reference_ids(
597
+ node["kind"],
598
+ field_name,
599
+ value,
600
+ module.nodes,
601
+ )
602
+ )
603
+ return True
604
+
605
+ def _validate_import_binding_uses(self, module: _Module) -> None:
606
+ parents = self._parent_slots(module)
607
+ imported_names = set(module.imported_bindings)
608
+ owners = self._function_owners(module, parents)
609
+ shadowed = self._function_shadow_names(module)
610
+ foreign_function_names = {
611
+ source_name
612
+ for other in self.modules
613
+ if other.module_id != module.module_id
614
+ for source_name in set(other.functions) | set(other.ineligible_functions)
615
+ }
616
+ module_id_parts = {
617
+ tuple(item.module_id.split(".")) for item in self.modules
618
+ }
619
+ for node in module.nodes.values():
620
+ self._check_canceled()
621
+ fields = node["fields"]
622
+ if node["kind"] == "arg" and fields.get("arg") in imported_names:
623
+ self._reject(
624
+ module,
625
+ "PYC3506",
626
+ f"Imported binding {fields['arg']!r} is rebound by a parameter",
627
+ node,
628
+ )
629
+ if node["kind"] in {"ExceptHandler", "MatchAs", "MatchStar"}:
630
+ name = fields.get("name")
631
+ if name in imported_names:
632
+ self._reject(
633
+ module,
634
+ "PYC3506",
635
+ f"Imported binding {name!r} is rebound by a local declaration",
636
+ node,
637
+ )
638
+ if node["kind"] in {"FunctionDef", "AsyncFunctionDef"}:
639
+ name = fields.get("name")
640
+ if node["node_id"] not in module.function_ids and name in imported_names:
641
+ self._reject(
642
+ module,
643
+ "PYC3506",
644
+ f"Imported binding {name!r} is rebound by a nested definition",
645
+ node,
646
+ )
647
+ if node["kind"] == "Attribute":
648
+ parent_id, field_name = parents.get(node["node_id"], (None, None))
649
+ parent = module.nodes.get(parent_id)
650
+ if parent and parent["kind"] == "Attribute" and field_name == "value":
651
+ continue
652
+ path = self._attribute_path(module, node)
653
+ owner = owners.get(node["node_id"])
654
+ if (
655
+ path
656
+ and path[0] not in shadowed.get(owner, set())
657
+ and any(path[: len(parts)] == parts for parts in module_id_parts)
658
+ ):
659
+ self._reject(
660
+ module,
661
+ "PYC3506",
662
+ f"Logical module {'.'.join(path)!r} cannot be used as a runtime value",
663
+ node,
664
+ )
665
+ if node["kind"] != "Name" or fields.get("id") not in imported_names:
666
+ if node["kind"] != "Name":
667
+ continue
668
+ source_name = fields.get("id")
669
+ parent_id, field_name = parents.get(node["node_id"], (None, None))
670
+ parent = module.nodes.get(parent_id)
671
+ owner = owners.get(node["node_id"])
672
+ if (
673
+ (source_name,) in module_id_parts
674
+ and source_name not in module.functions
675
+ and source_name not in shadowed.get(owner, set())
676
+ ):
677
+ self._reject(
678
+ module,
679
+ "PYC3506",
680
+ f"Logical module {source_name!r} cannot be used as a runtime value",
681
+ node,
682
+ )
683
+ if (
684
+ parent
685
+ and parent["kind"] == "Call"
686
+ and field_name == "func"
687
+ and source_name in foreign_function_names
688
+ and source_name not in module.functions
689
+ and source_name not in imported_names
690
+ and source_name != "range"
691
+ and source_name not in shadowed.get(owner, set())
692
+ ):
693
+ self._reject(
694
+ module,
695
+ "PYC3506",
696
+ f"Foreign function {source_name!r} is used without an explicit import",
697
+ node,
698
+ )
699
+ continue
700
+ parent_id, field_name = parents.get(node["node_id"], (None, None))
701
+ parent = module.nodes.get(parent_id)
702
+ if not parent or parent["kind"] != "Call" or field_name != "func":
703
+ self._reject(
704
+ module,
705
+ "PYC3506",
706
+ f"Imported binding {fields['id']!r} may only be used as a direct call target",
707
+ node,
708
+ )
709
+
710
+ def _parent_slots(self, module: _Module) -> dict[str, tuple[str, str]]:
711
+ result: dict[str, tuple[str, str]] = {}
712
+ known = set(module.nodes)
713
+ for parent in module.nodes.values():
714
+ for field_name, value in parent["fields"].items():
715
+ for child_id in python_ir_reference_ids(parent["kind"], field_name, value, known):
716
+ result.setdefault(child_id, (parent["node_id"], field_name))
717
+ return result
718
+
719
+ def _dependency_first_order(
720
+ self, dependency_edges: tuple[tuple[str, str], ...]
721
+ ) -> tuple[str, ...]:
722
+ dependencies = {module_id: set() for module_id in self.by_id}
723
+ dependents = {module_id: set() for module_id in self.by_id}
724
+ for importer, target in dependency_edges:
725
+ dependencies[importer].add(target)
726
+ dependents[target].add(importer)
727
+ ready = sorted(module_id for module_id, values in dependencies.items() if not values)
728
+ order: list[str] = []
729
+ while ready:
730
+ self._check_canceled()
731
+ current = ready.pop(0)
732
+ order.append(current)
733
+ for dependent in sorted(dependents[current]):
734
+ dependencies[dependent].discard(current)
735
+ if not dependencies[dependent] and dependent not in ready and dependent not in order:
736
+ ready.append(dependent)
737
+ ready.sort()
738
+ if len(order) != len(self.by_id):
739
+ component = self._first_cyclic_component(dependency_edges)
740
+ cycle_items = sorted(
741
+ (
742
+ resolved.importer_module_id,
743
+ resolved.source_ordinal,
744
+ resolved.target_module_id,
745
+ module,
746
+ resolved,
747
+ )
748
+ for module in self.modules
749
+ for resolved in module.resolved_imports
750
+ if resolved.importer_module_id in component
751
+ and resolved.target_module_id in component
752
+ )
753
+ _, _, _, module, resolved = cycle_items[0]
754
+ related = tuple(
755
+ self._span(item_module, item_module.nodes[item.import_node_id])
756
+ for _, _, _, item_module, item in cycle_items[1:]
757
+ )
758
+ self._reject(
759
+ module,
760
+ "PYC3507",
761
+ "Module dependency graph contains a rejected import cycle",
762
+ module.nodes[resolved.import_node_id],
763
+ related=related,
764
+ )
765
+ return tuple(order)
766
+
767
+ def _first_cyclic_component(
768
+ self,
769
+ dependency_edges: tuple[tuple[str, str], ...],
770
+ ) -> frozenset[str]:
771
+ graph = {module_id: set() for module_id in self.by_id}
772
+ reverse = {module_id: set() for module_id in self.by_id}
773
+ for importer, target in dependency_edges:
774
+ graph[importer].add(target)
775
+ reverse[target].add(importer)
776
+
777
+ visited: set[str] = set()
778
+ finish: list[str] = []
779
+
780
+ def visit(node: str) -> None:
781
+ self._check_canceled()
782
+ visited.add(node)
783
+ for child in sorted(graph[node]):
784
+ if child not in visited:
785
+ visit(child)
786
+ finish.append(node)
787
+
788
+ for module_id in sorted(graph):
789
+ if module_id not in visited:
790
+ visit(module_id)
791
+
792
+ assigned: set[str] = set()
793
+ cyclic: list[frozenset[str]] = []
794
+
795
+ def collect(node: str, component: set[str]) -> None:
796
+ self._check_canceled()
797
+ assigned.add(node)
798
+ component.add(node)
799
+ for parent in sorted(reverse[node]):
800
+ if parent not in assigned:
801
+ collect(parent, component)
802
+
803
+ for module_id in reversed(finish):
804
+ if module_id in assigned:
805
+ continue
806
+ component: set[str] = set()
807
+ collect(module_id, component)
808
+ if len(component) > 1 or any(item in graph[item] for item in component):
809
+ cyclic.append(frozenset(component))
810
+ if not cyclic:
811
+ raise RuntimeError("topological ordering failed without a cyclic component")
812
+ return min(cyclic, key=lambda item: tuple(sorted(item)))
813
+
814
+ def _publish(
815
+ self,
816
+ module_order: tuple[str, ...],
817
+ dependency_edges: tuple[tuple[str, str], ...],
818
+ ) -> ModuleResolutionProduct:
819
+ qualify_names = len(self.modules) > 1
820
+ function_facts: dict[str, dict[str, Any]] = {}
821
+ record_facts: dict[str, dict[str, Any]] = {}
822
+ generated_by_node: dict[str, str] = {}
823
+ generated_record_by_node: dict[str, str] = {}
824
+ function_ordinal = 0
825
+ for module_id in module_order:
826
+ module = self.by_id[module_id]
827
+ for function_id in module.function_ids:
828
+ function = module.nodes[function_id]
829
+ source_name = function["fields"]["name"]
830
+ flattened_name = (
831
+ _qualified_function_name(module_id, source_name)
832
+ if qualify_names
833
+ else source_name
834
+ )
835
+ generated_by_node[function_id] = flattened_name
836
+ function_facts[function_id] = {
837
+ "function_node_id": function_id,
838
+ "module_id": module_id,
839
+ "document_id": module.document_id,
840
+ "source_name": source_name,
841
+ "flattened_name": flattened_name,
842
+ "bundle_function_ordinal": function_ordinal,
843
+ "linkage": "external",
844
+ "module_generated_name": qualify_names,
845
+ }
846
+ function_ordinal += 1
847
+ for record_ordinal, record_id in enumerate(module.record_ids):
848
+ record = module.nodes[record_id]
849
+ source_name = record["fields"]["name"]
850
+ flattened_name = (
851
+ _qualified_function_name(module_id, source_name)
852
+ if qualify_names
853
+ else source_name
854
+ )
855
+ generated_record_by_node[record_id] = flattened_name
856
+ record_facts[record_id] = {
857
+ "class_node_id": record_id,
858
+ "module_id": module_id,
859
+ "document_id": module.document_id,
860
+ "source_name": source_name,
861
+ "flattened_name": flattened_name,
862
+ "module_record_ordinal": record_ordinal,
863
+ "module_generated_name": qualify_names,
864
+ }
865
+
866
+ rewritten_nodes: dict[str, dict[str, Any]] = {}
867
+ flattened_body: list[str] = []
868
+ for module_id in module_order:
869
+ self._check_canceled()
870
+ module = self.by_id[module_id]
871
+ parents = self._parent_slots(module)
872
+ shadowed = self._function_shadow_names(module)
873
+ owner_by_node = self._function_owners(module, parents)
874
+ for original in module.python_ir["nodes"]:
875
+ if original["node_id"] == module.python_ir["root_node_id"]:
876
+ continue
877
+ node = _thaw(original)
878
+ node_id = node["node_id"]
879
+ if node["kind"] == "FunctionDef" and node_id in generated_by_node:
880
+ node["fields"]["name"] = generated_by_node[node_id]
881
+ elif node["kind"] == "ClassDef" and node_id in generated_record_by_node:
882
+ node["fields"]["name"] = generated_record_by_node[node_id]
883
+ elif node["kind"] == "Name":
884
+ parent_id, field_name = parents.get(node_id, (None, None))
885
+ parent = module.nodes.get(parent_id)
886
+ source_name = node["fields"].get("id")
887
+ owner = owner_by_node.get(node_id)
888
+ if (
889
+ source_name in module.records
890
+ and owner is not None
891
+ and source_name not in shadowed.get(owner, set())
892
+ ):
893
+ node["fields"]["id"] = generated_record_by_node[
894
+ module.records[source_name]["node_id"]
895
+ ]
896
+ if parent and parent["kind"] == "Call" and field_name == "func":
897
+ source_name = node["fields"].get("id")
898
+ if source_name in module.imported_bindings:
899
+ target_id = module.imported_bindings[source_name].target_function_node_id
900
+ node["fields"]["id"] = generated_by_node[target_id]
901
+ elif source_name in module.functions:
902
+ owner = owner_by_node.get(node_id)
903
+ if source_name not in shadowed.get(owner, set()):
904
+ node["fields"]["id"] = generated_by_node[
905
+ module.functions[source_name]["node_id"]
906
+ ]
907
+ rewritten_nodes[node_id] = node
908
+ flattened_body.extend(module.body)
909
+
910
+ document_nodes: list[dict[str, Any]] = []
911
+ for module in self.modules:
912
+ document_node_id = _stable_id(
913
+ "syn-", "ModuleDocument", module.module_id, module.document_id
914
+ )
915
+ module.document_plan_node_id = document_node_id
916
+ document_nodes.append(
917
+ self._synthetic_node(
918
+ document_node_id,
919
+ "ModuleDocument",
920
+ {
921
+ "module_id": module.module_id,
922
+ "bundle_ordinal": module.bundle_ordinal,
923
+ "is_primary": module.is_primary,
924
+ "import_node_ids": list(module.import_node_ids),
925
+ "function_node_ids": list(module.function_ids),
926
+ },
927
+ tuple(module.import_node_ids + module.record_ids + module.function_ids),
928
+ )
929
+ )
930
+
931
+ initialization_node_id = _stable_id(
932
+ "syn-", "ModuleInitialization", *module_order, *dependency_edges
933
+ )
934
+ initialization_node = self._synthetic_node(
935
+ initialization_node_id,
936
+ "ModuleInitialization",
937
+ {
938
+ "module_order": list(module_order),
939
+ "dependency_edges": [
940
+ {"importer_module_id": importer, "target_module_id": target}
941
+ for importer, target in dependency_edges
942
+ ],
943
+ "cycle_policy": "reject-all-cycles",
944
+ "runtime_initialization": "none",
945
+ },
946
+ tuple(module.document_plan_node_id for module in self.modules),
947
+ )
948
+ assembly_node_id = _stable_id(
949
+ "syn-", "ModuleBundleAssembly", initialization_node_id, *module_order
950
+ )
951
+ assembly_node = self._synthetic_node(
952
+ assembly_node_id,
953
+ "ModuleBundleAssembly",
954
+ {
955
+ "module_order": list(module_order),
956
+ "translation_unit_count": 1,
957
+ "runtime_initializers": 0,
958
+ },
959
+ (initialization_node_id,),
960
+ )
961
+ bundle_document_id = _stable_id(
962
+ "bundle-", self.bundle["primary_module_id"], *(
963
+ module.document_id for module in self.modules
964
+ )
965
+ )
966
+ root_node_id = _stable_id("syn-", "Module", bundle_document_id, *flattened_body)
967
+ root_node = self._synthetic_node(
968
+ root_node_id,
969
+ "Module",
970
+ {"body": flattened_body, "type_ignores": []},
971
+ tuple(module.document_plan_node_id for module in self.modules),
972
+ )
973
+ python_ir = {
974
+ "schema_version": "python-ir/0.4",
975
+ "document_id": bundle_document_id,
976
+ "root_node_id": root_node_id,
977
+ "nodes": [
978
+ root_node,
979
+ *(
980
+ rewritten_nodes[node["node_id"]]
981
+ for module_id in module_order
982
+ for node in self.by_id[module_id].python_ir["nodes"]
983
+ if node["node_id"] != self.by_id[module_id].python_ir["root_node_id"]
984
+ ),
985
+ *document_nodes,
986
+ initialization_node,
987
+ assembly_node,
988
+ ],
989
+ }
990
+
991
+ import_facts = [
992
+ resolved.to_fact()
993
+ for module_id in module_order
994
+ for resolved in self.by_id[module_id].resolved_imports
995
+ ]
996
+ identity_facts = [
997
+ {
998
+ "module_id": module.module_id,
999
+ "document_id": module.document_id,
1000
+ "logical_name": module.logical_name,
1001
+ "bundle_ordinal": module.bundle_ordinal,
1002
+ "is_primary": module.is_primary,
1003
+ "document_plan_node_id": module.document_plan_node_id,
1004
+ "import_node_ids": list(module.import_node_ids),
1005
+ "function_node_ids": list(module.function_ids),
1006
+ }
1007
+ for module in self.modules
1008
+ ]
1009
+ source_facts = [self._source_fact(module) for module in self.modules]
1010
+ namespace_facts = [
1011
+ {
1012
+ "module_id": module.module_id,
1013
+ "document_plan_node_id": module.document_plan_node_id,
1014
+ "import_node_ids": list(module.import_node_ids),
1015
+ "function_node_ids": list(module.function_ids),
1016
+ "local_function_names": [
1017
+ module.nodes[node_id]["fields"]["name"] for node_id in module.function_ids
1018
+ ],
1019
+ "imported_bindings": [
1020
+ {
1021
+ "local_name": item.local_name,
1022
+ "target_module_id": item.target_module_id,
1023
+ "target_function_node_id": item.target_function_node_id,
1024
+ }
1025
+ for item in module.resolved_imports
1026
+ ],
1027
+ "generated_function_names": [
1028
+ generated_by_node[node_id] for node_id in module.function_ids
1029
+ ],
1030
+ }
1031
+ for module in self.modules
1032
+ ]
1033
+ initialization_fact = {
1034
+ "initialization_node_id": initialization_node_id,
1035
+ "module_order": list(module_order),
1036
+ "dependency_edges": [
1037
+ {"importer_module_id": importer, "target_module_id": target}
1038
+ for importer, target in dependency_edges
1039
+ ],
1040
+ "cycle_policy": "reject-all-cycles",
1041
+ "runtime_initialization": "none",
1042
+ }
1043
+ tables = (
1044
+ self._fact_table("module-identity-facts", "module-id", identity_facts, "module_id"),
1045
+ self._fact_table("module-import-facts", "module-import-item-id", import_facts, "import_item_id"),
1046
+ self._fact_table("module-function-facts", "function-node-id", list(function_facts.values()), "function_node_id"),
1047
+ self._fact_table("module-initialization-facts", "initialization-node-id", [initialization_fact], "initialization_node_id"),
1048
+ self._fact_table("module-namespace-facts", "module-id", namespace_facts, "module_id"),
1049
+ self._fact_table("module-source-facts", "module-id", source_facts, "module_id"),
1050
+ )
1051
+ resolution = {
1052
+ "schema_version": "module-resolution/0.12",
1053
+ "primary_module_id": self.bundle["primary_module_id"],
1054
+ "source_module_order": [module.module_id for module in self.modules],
1055
+ "module_order": list(module_order),
1056
+ "dependency_edges": initialization_fact["dependency_edges"],
1057
+ "import_item_count": len(import_facts),
1058
+ "flattened_root_node_id": root_node_id,
1059
+ "bundle_document_id": bundle_document_id,
1060
+ "function_renames": [
1061
+ {
1062
+ "function_node_id": node_id,
1063
+ "source_name": fact["source_name"],
1064
+ "flattened_name": fact["flattened_name"],
1065
+ }
1066
+ for node_id, fact in sorted(
1067
+ function_facts.items(), key=lambda item: item[1]["bundle_function_ordinal"]
1068
+ )
1069
+ ],
1070
+ "initialization_node_id": initialization_node_id,
1071
+ "module_bundle_assembly_node_id": assembly_node_id,
1072
+ }
1073
+ return ModuleResolutionProduct(
1074
+ python_ir=python_ir,
1075
+ module_bundle=_thaw(self.bundle),
1076
+ module_resolution=resolution,
1077
+ module_fact_tables=tables,
1078
+ module_import_node_ids=tuple(
1079
+ node_id
1080
+ for module_id in module_order
1081
+ for node_id in self.by_id[module_id].import_node_ids
1082
+ ),
1083
+ module_function_by_node={
1084
+ key: value for key, value in sorted(function_facts.items())
1085
+ },
1086
+ module_record_by_node={
1087
+ key: value for key, value in sorted(record_facts.items())
1088
+ },
1089
+ module_bundle_assembly_node_id=assembly_node_id,
1090
+ )
1091
+
1092
+ def _function_shadow_names(self, module: _Module) -> dict[str, set[str]]:
1093
+ result: dict[str, set[str]] = {node_id: set() for node_id in module.function_ids}
1094
+ owners = self._function_owners(module, self._parent_slots(module))
1095
+ store_parent_fields = {"targets", "target"}
1096
+ parents = self._parent_slots(module)
1097
+ for node in module.nodes.values():
1098
+ owner = owners.get(node["node_id"])
1099
+ if owner not in result:
1100
+ continue
1101
+ if node["kind"] == "arg":
1102
+ result[owner].add(node["fields"].get("arg", ""))
1103
+ elif node["kind"] == "Name":
1104
+ _, field_name = parents.get(node["node_id"], (None, None))
1105
+ if field_name in store_parent_fields:
1106
+ result[owner].add(node["fields"].get("id", ""))
1107
+ elif node["kind"] in {"FunctionDef", "AsyncFunctionDef"} and node["node_id"] != owner:
1108
+ result[owner].add(node["fields"].get("name", ""))
1109
+ return result
1110
+
1111
+ def _function_owners(
1112
+ self,
1113
+ module: _Module,
1114
+ parents: dict[str, tuple[str, str]],
1115
+ ) -> dict[str, str]:
1116
+ functions = set(module.function_ids)
1117
+ result: dict[str, str] = {}
1118
+ for node_id in module.nodes:
1119
+ current = node_id
1120
+ seen: set[str] = set()
1121
+ while current not in seen:
1122
+ seen.add(current)
1123
+ if current in functions:
1124
+ result[node_id] = current
1125
+ break
1126
+ parent_id, _ = parents.get(current, (None, None))
1127
+ if parent_id is None:
1128
+ break
1129
+ current = parent_id
1130
+ return result
1131
+
1132
+ def _source_fact(self, module: _Module) -> dict[str, Any]:
1133
+ source_document = module.source.get("source_document", {})
1134
+ return {
1135
+ "module_id": module.module_id,
1136
+ "source_document_id": module.document_id,
1137
+ "logical_source_name": module.logical_name,
1138
+ "document_id": module.document_id,
1139
+ "logical_name": module.logical_name,
1140
+ "bundle_ordinal": module.bundle_ordinal,
1141
+ "is_primary": module.is_primary,
1142
+ "document_plan_node_id": module.document_plan_node_id,
1143
+ "import_node_ids": list(module.import_node_ids),
1144
+ "function_node_ids": list(module.function_ids),
1145
+ "content_fingerprint": source_document.get("utf8_sha256"),
1146
+ "eligible": True,
1147
+ "diagnostic_code": None,
1148
+ "reason": None,
1149
+ }
1150
+
1151
+ def _fact_table(
1152
+ self,
1153
+ table_id: str,
1154
+ key_domain: str,
1155
+ values: list[dict[str, Any]],
1156
+ key_field: str,
1157
+ ) -> dict[str, Any]:
1158
+ records = []
1159
+ for value in sorted(values, key=lambda item: item[key_field]):
1160
+ source_ids: list[str] = []
1161
+ for field_name in (
1162
+ "document_plan_node_id",
1163
+ "import_node_id",
1164
+ "alias_node_id",
1165
+ "target_function_node_id",
1166
+ "function_node_id",
1167
+ "initialization_node_id",
1168
+ ):
1169
+ item = value.get(field_name)
1170
+ if isinstance(item, str):
1171
+ source_ids.append(item)
1172
+ for field_name in ("import_node_ids", "function_node_ids"):
1173
+ source_ids.extend(
1174
+ item for item in value.get(field_name, ()) if isinstance(item, str)
1175
+ )
1176
+ for binding in value.get("imported_bindings", ()):
1177
+ target = binding.get("target_function_node_id") if isinstance(binding, dict) else None
1178
+ if isinstance(target, str):
1179
+ source_ids.append(target)
1180
+ source_ids = list(dict.fromkeys(source_ids))
1181
+ records.append(
1182
+ {
1183
+ "key": value[key_field],
1184
+ "value": value,
1185
+ "provenance": {
1186
+ "source_node_ids": source_ids,
1187
+ "evidence": ["explicit-sourcebundle-closed-world", "deterministic-module-resolution"],
1188
+ },
1189
+ }
1190
+ )
1191
+ return {
1192
+ "schema_version": "fact-table/0.12",
1193
+ "table_id": table_id,
1194
+ "producer_stage": self.stage_id,
1195
+ "key_domain": key_domain,
1196
+ "completeness": "complete",
1197
+ "invalidation_dependencies": [self.invalidation_dependency],
1198
+ "records": records,
1199
+ }
1200
+
1201
+ @staticmethod
1202
+ def _synthetic_node(
1203
+ node_id: str,
1204
+ kind: str,
1205
+ fields: dict[str, Any],
1206
+ origins: tuple[str, ...],
1207
+ ) -> dict[str, Any]:
1208
+ return {
1209
+ "node_id": node_id,
1210
+ "kind": kind,
1211
+ "fields": fields,
1212
+ "provenance": {
1213
+ "origin_kind": "synthetic",
1214
+ "source_span": None,
1215
+ "origin_node_ids": list(origins),
1216
+ },
1217
+ }
1218
+
1219
+ def _span(self, module: _Module, node: dict[str, Any] | None) -> dict[str, Any]:
1220
+ if node is not None:
1221
+ span = node.get("provenance", {}).get("source_span")
1222
+ if isinstance(span, dict):
1223
+ return _thaw(span)
1224
+ return {
1225
+ "document_id": module.document_id,
1226
+ "start": {"line": 1, "column": 0, "offset": 0},
1227
+ "end": {"line": 1, "column": 0, "offset": 0},
1228
+ }
1229
+
1230
+ def _reject(
1231
+ self,
1232
+ module: _Module,
1233
+ code: str,
1234
+ message: str,
1235
+ node: dict[str, Any] | None,
1236
+ *,
1237
+ related: tuple[dict[str, Any], ...] = (),
1238
+ ) -> None:
1239
+ raise ModuleResolutionError(
1240
+ code,
1241
+ message,
1242
+ module_id=module.module_id,
1243
+ logical_name=module.logical_name,
1244
+ source_span=self._span(module, node),
1245
+ related_spans=related,
1246
+ )
1247
+
1248
+ def _check_canceled(self) -> None:
1249
+ if self.cancellation is not None and self.cancellation.is_canceled:
1250
+ raise ModuleAnalysisCanceled