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