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,481 @@
|
|
|
1
|
+
"""Closed, linear proof pass for conditional temporary placement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from pycforge.converter.analysis.model import ValueCategory
|
|
8
|
+
from pycforge.converter.ir.python_ir import python_ir_reference_ids
|
|
9
|
+
|
|
10
|
+
from .model import (
|
|
11
|
+
CONDITIONAL_REGION_LOWERING_SHAPE,
|
|
12
|
+
ConditionalEvaluationMode,
|
|
13
|
+
ConditionalGuardPolarity,
|
|
14
|
+
ConditionalOperandPlacementFact,
|
|
15
|
+
ConditionalRegionAnalysis,
|
|
16
|
+
ConditionalRegionAnalysisCanceled,
|
|
17
|
+
ConditionalRegionAnalysisError,
|
|
18
|
+
ConditionalRegionFact,
|
|
19
|
+
ConditionalRegionKind,
|
|
20
|
+
conditional_region_id,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_SCALAR_CATEGORIES = frozenset(
|
|
25
|
+
{
|
|
26
|
+
ValueCategory.INTEGER.value,
|
|
27
|
+
ValueCategory.FLOAT.value,
|
|
28
|
+
ValueCategory.BOOLEAN.value,
|
|
29
|
+
ValueCategory.STRING.value,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
_COMPARABLE_CATEGORIES = frozenset(
|
|
33
|
+
{
|
|
34
|
+
ValueCategory.INTEGER.value,
|
|
35
|
+
ValueCategory.FLOAT.value,
|
|
36
|
+
ValueCategory.BOOLEAN.value,
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
_COMPARISON_OPERATORS = frozenset({"Eq", "NotEq", "Lt", "LtE", "Gt", "GtE"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _category_value(value: ValueCategory | str | None) -> str:
|
|
43
|
+
return value.value if isinstance(value, ValueCategory) else str(value or "unknown")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _PrerequisiteClosure:
|
|
47
|
+
"""Persistent, ordered prerequisite rope.
|
|
48
|
+
|
|
49
|
+
Building one closure costs only the number of direct child expressions.
|
|
50
|
+
Flattening is delayed until a published fact needs the sequence, making
|
|
51
|
+
nested wrapper analysis linear plus the size of serialized fact output.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
__slots__ = ("parts", "node_id", "nonempty")
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
parts: tuple["_PrerequisiteClosure", ...] = (),
|
|
59
|
+
node_id: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.parts = tuple(part for part in parts if part.nonempty)
|
|
62
|
+
self.node_id = node_id
|
|
63
|
+
self.nonempty = bool(self.parts) or node_id is not None
|
|
64
|
+
|
|
65
|
+
def materialize(self, check_cancellation: Any) -> tuple[str, ...]:
|
|
66
|
+
if not self.nonempty:
|
|
67
|
+
return ()
|
|
68
|
+
result: list[str] = []
|
|
69
|
+
seen_node_ids: set[str] = set()
|
|
70
|
+
seen_closures: set[int] = set()
|
|
71
|
+
stack: list[_PrerequisiteClosure | str] = [self]
|
|
72
|
+
while stack:
|
|
73
|
+
check_cancellation()
|
|
74
|
+
item = stack.pop()
|
|
75
|
+
if isinstance(item, str):
|
|
76
|
+
if item not in seen_node_ids:
|
|
77
|
+
seen_node_ids.add(item)
|
|
78
|
+
result.append(item)
|
|
79
|
+
continue
|
|
80
|
+
identity = id(item)
|
|
81
|
+
if identity in seen_closures:
|
|
82
|
+
continue
|
|
83
|
+
seen_closures.add(identity)
|
|
84
|
+
if item.node_id is not None:
|
|
85
|
+
stack.append(item.node_id)
|
|
86
|
+
stack.extend(reversed(item.parts))
|
|
87
|
+
return tuple(result)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_EMPTY_PREREQUISITES = _PrerequisiteClosure()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _closure(
|
|
94
|
+
parts: tuple[_PrerequisiteClosure, ...] = (),
|
|
95
|
+
node_id: str | None = None,
|
|
96
|
+
) -> _PrerequisiteClosure:
|
|
97
|
+
if node_id is None and not any(part.nonempty for part in parts):
|
|
98
|
+
return _EMPTY_PREREQUISITES
|
|
99
|
+
return _PrerequisiteClosure(parts, node_id)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ConditionalRegionAnalyzer:
|
|
103
|
+
"""Prove statement-prerequisite placement for existing scalar expressions.
|
|
104
|
+
|
|
105
|
+
The pass is deliberately independent of C IR. It computes one postorder
|
|
106
|
+
staging-demand map and then selects only source forms rejected by the
|
|
107
|
+
predecessor's eager-prelude boundary.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
module: dict[str, Any],
|
|
113
|
+
*,
|
|
114
|
+
categories: Mapping[str, ValueCategory | str],
|
|
115
|
+
function_records: Mapping[str, Mapping[str, Any]],
|
|
116
|
+
owner_by_node: Mapping[str, str],
|
|
117
|
+
supported_call_node_ids: frozenset[str],
|
|
118
|
+
numeric_operation_node_ids: frozenset[str],
|
|
119
|
+
supported_container_access_node_ids: frozenset[str] = frozenset(),
|
|
120
|
+
supported_record_access_node_ids: frozenset[str] = frozenset(),
|
|
121
|
+
cancellation: Any,
|
|
122
|
+
target_contract: str = "c11-portable-fixed-v1",
|
|
123
|
+
) -> None:
|
|
124
|
+
self.module = module
|
|
125
|
+
self.cancellation = cancellation
|
|
126
|
+
self.nodes: dict[str, dict[str, Any]] = {}
|
|
127
|
+
self.ordinals: dict[str, int] = {}
|
|
128
|
+
for ordinal, node in enumerate(module["nodes"]):
|
|
129
|
+
self._check_cancellation()
|
|
130
|
+
self.nodes[node["node_id"]] = node
|
|
131
|
+
self.ordinals[node["node_id"]] = ordinal
|
|
132
|
+
self.categories = {
|
|
133
|
+
node_id: _category_value(category) for node_id, category in categories.items()
|
|
134
|
+
}
|
|
135
|
+
self.function_records = function_records
|
|
136
|
+
self.owner_by_node = owner_by_node
|
|
137
|
+
self.supported_calls = supported_call_node_ids
|
|
138
|
+
self.numeric_operations = numeric_operation_node_ids
|
|
139
|
+
self.supported_container_accesses = supported_container_access_node_ids
|
|
140
|
+
self.supported_record_accesses = supported_record_access_node_ids
|
|
141
|
+
self.target_contract = target_contract
|
|
142
|
+
self.children: dict[str, tuple[str, ...]] = {}
|
|
143
|
+
for node_id, node in self.nodes.items():
|
|
144
|
+
self._check_cancellation()
|
|
145
|
+
self.children[node_id] = self._children(node)
|
|
146
|
+
self.approved: dict[str, bool] = {}
|
|
147
|
+
self.prerequisites: dict[str, _PrerequisiteClosure] = {}
|
|
148
|
+
|
|
149
|
+
def analyze(self) -> ConditionalRegionAnalysis:
|
|
150
|
+
for node_id in self._postorder():
|
|
151
|
+
self._check_cancellation()
|
|
152
|
+
self.approved[node_id] = self._approved_expression(self.nodes[node_id])
|
|
153
|
+
self.prerequisites[node_id] = self._statement_prerequisites(
|
|
154
|
+
self.nodes[node_id]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
regions: list[ConditionalRegionFact] = []
|
|
158
|
+
for node in self.module["nodes"]:
|
|
159
|
+
self._check_cancellation()
|
|
160
|
+
if not self.approved.get(node["node_id"], False):
|
|
161
|
+
continue
|
|
162
|
+
if node["kind"] == "BoolOp" and self._boolean_requires_region(node):
|
|
163
|
+
regions.append(self._region(node, ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT))
|
|
164
|
+
elif node["kind"] == "Compare" and self._comparison_requires_region(node):
|
|
165
|
+
regions.append(self._region(node, ConditionalRegionKind.CHAINED_COMPARISON))
|
|
166
|
+
return ConditionalRegionAnalysis(
|
|
167
|
+
tuple(sorted(regions, key=lambda item: item.region_node_id))
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _children(self, node: dict[str, Any]) -> tuple[str, ...]:
|
|
171
|
+
found: list[str] = []
|
|
172
|
+
for field_name, value in node.get("fields", {}).items():
|
|
173
|
+
found.extend(
|
|
174
|
+
python_ir_reference_ids(node["kind"], field_name, value, self.nodes)
|
|
175
|
+
)
|
|
176
|
+
return tuple(found)
|
|
177
|
+
|
|
178
|
+
def _postorder(self) -> tuple[str, ...]:
|
|
179
|
+
"""Return a deterministic iterative postorder over the bounded IR graph."""
|
|
180
|
+
|
|
181
|
+
state: dict[str, int] = {}
|
|
182
|
+
result: list[str] = []
|
|
183
|
+
for start in sorted(self.nodes, key=self.ordinals.__getitem__):
|
|
184
|
+
if state.get(start) == 2:
|
|
185
|
+
continue
|
|
186
|
+
stack: list[tuple[str, bool]] = [(start, False)]
|
|
187
|
+
while stack:
|
|
188
|
+
self._check_cancellation()
|
|
189
|
+
node_id, expanded = stack.pop()
|
|
190
|
+
if expanded:
|
|
191
|
+
if state.get(node_id) != 2:
|
|
192
|
+
state[node_id] = 2
|
|
193
|
+
result.append(node_id)
|
|
194
|
+
continue
|
|
195
|
+
current = state.get(node_id, 0)
|
|
196
|
+
if current == 2:
|
|
197
|
+
continue
|
|
198
|
+
if current == 1:
|
|
199
|
+
self._error(
|
|
200
|
+
"PYC2950",
|
|
201
|
+
"Conditional-region analysis encountered a cyclic Python IR reference",
|
|
202
|
+
self.nodes[node_id],
|
|
203
|
+
)
|
|
204
|
+
state[node_id] = 1
|
|
205
|
+
stack.append((node_id, True))
|
|
206
|
+
for child_id in reversed(self.children[node_id]):
|
|
207
|
+
if state.get(child_id, 0) == 1:
|
|
208
|
+
self._error(
|
|
209
|
+
"PYC2950",
|
|
210
|
+
"Conditional-region analysis encountered a cyclic Python IR reference",
|
|
211
|
+
self.nodes[child_id],
|
|
212
|
+
)
|
|
213
|
+
if state.get(child_id, 0) != 2:
|
|
214
|
+
stack.append((child_id, False))
|
|
215
|
+
return tuple(result)
|
|
216
|
+
|
|
217
|
+
def _approved_expression(self, node: dict[str, Any]) -> bool:
|
|
218
|
+
kind = node["kind"]
|
|
219
|
+
fields = node.get("fields", {})
|
|
220
|
+
node_id = node["node_id"]
|
|
221
|
+
category = self.categories.get(node_id, "unknown")
|
|
222
|
+
if kind in {"Name", "Constant"}:
|
|
223
|
+
return category in _SCALAR_CATEGORIES
|
|
224
|
+
if kind == "Attribute":
|
|
225
|
+
return node_id in self.supported_record_accesses and category in _SCALAR_CATEGORIES
|
|
226
|
+
if kind == "Subscript":
|
|
227
|
+
return node_id in self.supported_container_accesses and category in _SCALAR_CATEGORIES
|
|
228
|
+
if kind == "Call":
|
|
229
|
+
arguments = tuple(fields.get("args", ()))
|
|
230
|
+
return (
|
|
231
|
+
node_id in self.supported_calls
|
|
232
|
+
and category in _SCALAR_CATEGORIES
|
|
233
|
+
and all(self.approved.get(item, False) for item in arguments)
|
|
234
|
+
)
|
|
235
|
+
if kind == "UnaryOp":
|
|
236
|
+
operator = self.nodes.get(fields.get("op"), {}).get("kind")
|
|
237
|
+
operand_id = fields.get("operand")
|
|
238
|
+
operand_category = self.categories.get(operand_id, "unknown")
|
|
239
|
+
if not isinstance(operand_id, str) or not self.approved.get(operand_id, False):
|
|
240
|
+
return False
|
|
241
|
+
if operator == "Not":
|
|
242
|
+
return category == ValueCategory.BOOLEAN.value and operand_category in {
|
|
243
|
+
ValueCategory.INTEGER.value,
|
|
244
|
+
ValueCategory.FLOAT.value,
|
|
245
|
+
ValueCategory.BOOLEAN.value,
|
|
246
|
+
}
|
|
247
|
+
return operator in {"UAdd", "USub"} and category in {
|
|
248
|
+
ValueCategory.INTEGER.value,
|
|
249
|
+
ValueCategory.FLOAT.value,
|
|
250
|
+
}
|
|
251
|
+
if kind == "BinOp":
|
|
252
|
+
left_id, right_id = fields.get("left"), fields.get("right")
|
|
253
|
+
operator = self.nodes.get(fields.get("op"), {}).get("kind")
|
|
254
|
+
if not (
|
|
255
|
+
isinstance(left_id, str)
|
|
256
|
+
and isinstance(right_id, str)
|
|
257
|
+
and self.approved.get(left_id, False)
|
|
258
|
+
and self.approved.get(right_id, False)
|
|
259
|
+
):
|
|
260
|
+
return False
|
|
261
|
+
if operator in {"FloorDiv", "Mod"}:
|
|
262
|
+
return node_id in self.numeric_operations
|
|
263
|
+
if operator == "Div":
|
|
264
|
+
return category == ValueCategory.FLOAT.value
|
|
265
|
+
return operator in {"Add", "Sub", "Mult"} and category in {
|
|
266
|
+
ValueCategory.INTEGER.value,
|
|
267
|
+
ValueCategory.FLOAT.value,
|
|
268
|
+
}
|
|
269
|
+
if kind == "BoolOp":
|
|
270
|
+
values = tuple(fields.get("values", ()))
|
|
271
|
+
operator = self.nodes.get(fields.get("op"), {}).get("kind")
|
|
272
|
+
return (
|
|
273
|
+
operator in {"And", "Or"}
|
|
274
|
+
and len(values) >= 2
|
|
275
|
+
and category == ValueCategory.BOOLEAN.value
|
|
276
|
+
and all(
|
|
277
|
+
self.categories.get(item) == ValueCategory.BOOLEAN.value
|
|
278
|
+
and self.approved.get(item, False)
|
|
279
|
+
for item in values
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
if kind == "Compare":
|
|
283
|
+
operands = (fields.get("left"), *tuple(fields.get("comparators", ())))
|
|
284
|
+
operators = tuple(fields.get("ops", ()))
|
|
285
|
+
operand_categories = tuple(self.categories.get(item, "unknown") for item in operands)
|
|
286
|
+
operator_kinds = tuple(
|
|
287
|
+
self.nodes.get(item, {}).get("kind") for item in operators
|
|
288
|
+
)
|
|
289
|
+
return (
|
|
290
|
+
bool(operators)
|
|
291
|
+
and len(operands) == len(operators) + 1
|
|
292
|
+
and all(isinstance(item, str) and self.approved.get(item, False) for item in operands)
|
|
293
|
+
and all(item in _COMPARISON_OPERATORS for item in operator_kinds)
|
|
294
|
+
and operand_categories
|
|
295
|
+
and operand_categories[0] in _COMPARABLE_CATEGORIES
|
|
296
|
+
and all(item == operand_categories[0] for item in operand_categories)
|
|
297
|
+
and category == ValueCategory.BOOLEAN.value
|
|
298
|
+
)
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def _statement_prerequisites(
|
|
302
|
+
self,
|
|
303
|
+
node: dict[str, Any],
|
|
304
|
+
) -> _PrerequisiteClosure:
|
|
305
|
+
if not self.approved.get(node["node_id"], False):
|
|
306
|
+
return _EMPTY_PREREQUISITES
|
|
307
|
+
kind = node["kind"]
|
|
308
|
+
fields = node.get("fields", {})
|
|
309
|
+
node_id = node["node_id"]
|
|
310
|
+
|
|
311
|
+
def merge(node_ids: tuple[str, ...] | list[str]) -> _PrerequisiteClosure:
|
|
312
|
+
return _closure(
|
|
313
|
+
tuple(
|
|
314
|
+
self.prerequisites.get(child_id, _EMPTY_PREREQUISITES)
|
|
315
|
+
for child_id in node_ids
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if kind == "Call":
|
|
320
|
+
return _closure((merge(tuple(fields.get("args", ()))),), node_id)
|
|
321
|
+
if kind == "UnaryOp":
|
|
322
|
+
operand_id = fields.get("operand")
|
|
323
|
+
return self.prerequisites.get(operand_id, _EMPTY_PREREQUISITES)
|
|
324
|
+
if kind == "BinOp":
|
|
325
|
+
children = (fields.get("left"), fields.get("right"))
|
|
326
|
+
inherited = merge(tuple(item for item in children if isinstance(item, str)))
|
|
327
|
+
if node_id in self.numeric_operations:
|
|
328
|
+
return _closure((inherited,), node_id)
|
|
329
|
+
return inherited
|
|
330
|
+
if kind == "BoolOp":
|
|
331
|
+
inherited = merge(tuple(fields.get("values", ())))
|
|
332
|
+
return (
|
|
333
|
+
_closure((inherited,), node_id)
|
|
334
|
+
if inherited.nonempty
|
|
335
|
+
else _EMPTY_PREREQUISITES
|
|
336
|
+
)
|
|
337
|
+
if kind == "Compare":
|
|
338
|
+
operands = (fields.get("left"), *tuple(fields.get("comparators", ())))
|
|
339
|
+
inherited = merge(tuple(item for item in operands if isinstance(item, str)))
|
|
340
|
+
if len(tuple(fields.get("ops", ()))) > 1:
|
|
341
|
+
return _closure((inherited,), node_id)
|
|
342
|
+
return inherited
|
|
343
|
+
return _EMPTY_PREREQUISITES
|
|
344
|
+
|
|
345
|
+
def _boolean_requires_region(self, node: dict[str, Any]) -> bool:
|
|
346
|
+
return any(
|
|
347
|
+
self.prerequisites.get(item, _EMPTY_PREREQUISITES).nonempty
|
|
348
|
+
for item in node.get("fields", {}).get("values", ())
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def _comparison_requires_region(self, node: dict[str, Any]) -> bool:
|
|
352
|
+
fields = node.get("fields", {})
|
|
353
|
+
operators = tuple(fields.get("ops", ()))
|
|
354
|
+
operands = (fields.get("left"), *tuple(fields.get("comparators", ())))
|
|
355
|
+
if len(operators) <= 1:
|
|
356
|
+
return False
|
|
357
|
+
return any(
|
|
358
|
+
self.nodes[item]["kind"] not in {"Name", "Constant"}
|
|
359
|
+
or self.prerequisites.get(item, _EMPTY_PREREQUISITES).nonempty
|
|
360
|
+
for item in operands[2:]
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _region(
|
|
364
|
+
self,
|
|
365
|
+
node: dict[str, Any],
|
|
366
|
+
kind: ConditionalRegionKind,
|
|
367
|
+
) -> ConditionalRegionFact:
|
|
368
|
+
fields = node["fields"]
|
|
369
|
+
if kind is ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT:
|
|
370
|
+
operand_ids = tuple(fields.get("values", ()))
|
|
371
|
+
operator_ids = (fields["op"],)
|
|
372
|
+
prefix_count = 1
|
|
373
|
+
boolean_operator = self.nodes[operator_ids[0]]["kind"]
|
|
374
|
+
guarded_polarity = (
|
|
375
|
+
ConditionalGuardPolarity.WHEN_RESULT_TRUE
|
|
376
|
+
if boolean_operator == "And"
|
|
377
|
+
else ConditionalGuardPolarity.WHEN_RESULT_FALSE
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
operand_ids = (fields["left"], *tuple(fields.get("comparators", ())))
|
|
381
|
+
operator_ids = tuple(fields.get("ops", ()))
|
|
382
|
+
prefix_count = 2
|
|
383
|
+
guarded_polarity = ConditionalGuardPolarity.WHEN_RESULT_TRUE
|
|
384
|
+
|
|
385
|
+
owner_id = self.owner_by_node.get(node["node_id"])
|
|
386
|
+
owner = self.function_records.get(owner_id or "")
|
|
387
|
+
if not isinstance(owner_id, str) or not isinstance(owner, Mapping):
|
|
388
|
+
self._error(
|
|
389
|
+
"PYC2950" if kind is ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT else "PYC2951",
|
|
390
|
+
"Conditional temporary placement requires an understood top-level function owner",
|
|
391
|
+
node,
|
|
392
|
+
)
|
|
393
|
+
module_id = owner.get("module_id")
|
|
394
|
+
document_id = owner.get("document_id")
|
|
395
|
+
logical_name = owner.get("logical_name")
|
|
396
|
+
if not all(isinstance(item, str) and item for item in (module_id, document_id, logical_name)):
|
|
397
|
+
self._error(
|
|
398
|
+
"PYC2950" if kind is ConditionalRegionKind.BOOLEAN_SHORT_CIRCUIT else "PYC2951",
|
|
399
|
+
"Conditional temporary placement lacks exact module or source ownership",
|
|
400
|
+
node,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
placements: list[ConditionalOperandPlacementFact] = []
|
|
404
|
+
for ordinal, operand_id in enumerate(operand_ids):
|
|
405
|
+
self._check_cancellation()
|
|
406
|
+
unconditional = ordinal < prefix_count
|
|
407
|
+
prerequisite_node_ids = self.prerequisites.get(
|
|
408
|
+
operand_id,
|
|
409
|
+
_EMPTY_PREREQUISITES,
|
|
410
|
+
).materialize(self._check_cancellation)
|
|
411
|
+
placements.append(
|
|
412
|
+
ConditionalOperandPlacementFact(
|
|
413
|
+
operand_node_id=operand_id,
|
|
414
|
+
ordinal=ordinal,
|
|
415
|
+
category=self.categories[operand_id],
|
|
416
|
+
evaluation_mode=(
|
|
417
|
+
ConditionalEvaluationMode.UNCONDITIONAL
|
|
418
|
+
if unconditional
|
|
419
|
+
else ConditionalEvaluationMode.GUARDED
|
|
420
|
+
),
|
|
421
|
+
guard_polarity=(
|
|
422
|
+
ConditionalGuardPolarity.NONE
|
|
423
|
+
if unconditional
|
|
424
|
+
else guarded_polarity
|
|
425
|
+
),
|
|
426
|
+
guard_after_operand_ordinal=None if unconditional else ordinal - 1,
|
|
427
|
+
requires_statement_prelude=bool(prerequisite_node_ids),
|
|
428
|
+
prerequisite_node_ids=prerequisite_node_ids,
|
|
429
|
+
legacy_direct_safe=self.nodes[operand_id]["kind"] in {"Name", "Constant"},
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
prerequisite_values: list[str] = []
|
|
433
|
+
prerequisite_seen: set[str] = set()
|
|
434
|
+
for placement in placements:
|
|
435
|
+
for prerequisite in placement.prerequisite_node_ids:
|
|
436
|
+
self._check_cancellation()
|
|
437
|
+
if prerequisite not in prerequisite_seen:
|
|
438
|
+
prerequisite_seen.add(prerequisite)
|
|
439
|
+
prerequisite_values.append(prerequisite)
|
|
440
|
+
prerequisites = tuple(prerequisite_values)
|
|
441
|
+
return ConditionalRegionFact(
|
|
442
|
+
region_id=conditional_region_id(node["node_id"], kind),
|
|
443
|
+
region_node_id=node["node_id"],
|
|
444
|
+
region_kind=kind,
|
|
445
|
+
function_node_id=owner_id,
|
|
446
|
+
module_id=module_id,
|
|
447
|
+
document_id=document_id,
|
|
448
|
+
logical_name=logical_name,
|
|
449
|
+
operator_node_ids=operator_ids,
|
|
450
|
+
operator_kinds=tuple(self.nodes[item]["kind"] for item in operator_ids),
|
|
451
|
+
operand_node_ids=operand_ids,
|
|
452
|
+
operand_categories=tuple(self.categories[item] for item in operand_ids),
|
|
453
|
+
unconditional_prefix_count=prefix_count,
|
|
454
|
+
placements=tuple(placements),
|
|
455
|
+
guarded_operand_node_ids=operand_ids[prefix_count:],
|
|
456
|
+
prerequisite_node_ids=prerequisites,
|
|
457
|
+
result_category=ValueCategory.BOOLEAN.value,
|
|
458
|
+
result_c_type="bool",
|
|
459
|
+
evaluation_order=operand_ids,
|
|
460
|
+
operands_evaluated_once=True,
|
|
461
|
+
lowering_shape=CONDITIONAL_REGION_LOWERING_SHAPE,
|
|
462
|
+
allocation_model="none",
|
|
463
|
+
cleanup_model="none",
|
|
464
|
+
runtime_failure_channel="unchanged",
|
|
465
|
+
target_contract=self.target_contract,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def _error(self, code: str, message: str, node: dict[str, Any]) -> None:
|
|
469
|
+
raise ConditionalRegionAnalysisError(
|
|
470
|
+
code,
|
|
471
|
+
message,
|
|
472
|
+
node["node_id"],
|
|
473
|
+
node.get("provenance", {}).get("source_span"),
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def _check_cancellation(self) -> None:
|
|
477
|
+
if bool(getattr(self.cancellation, "is_canceled", False)):
|
|
478
|
+
raise ConditionalRegionAnalysisCanceled
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
__all__ = ["ConditionalRegionAnalyzer"]
|