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,2166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from collections import deque
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pycforge.converter.contracts.configuration import (
|
|
8
|
+
DEFAULT_MODULE_POLICY,
|
|
9
|
+
DEFAULT_NUMERIC_POLICY,
|
|
10
|
+
DEFAULT_RECORD_POLICY,
|
|
11
|
+
PHASE12_MODULE_POLICY,
|
|
12
|
+
)
|
|
13
|
+
from pycforge.converter.contracts.identifiers import (
|
|
14
|
+
C11_EXTERNAL_IDENTIFIERS,
|
|
15
|
+
C_KEYWORDS,
|
|
16
|
+
TARGET_RESERVED_NAMES,
|
|
17
|
+
)
|
|
18
|
+
from pycforge.converter.contracts.versions import (
|
|
19
|
+
CONDITIONAL_FACT_SCHEMA,
|
|
20
|
+
CONVERSION_PLAN_SCHEMA,
|
|
21
|
+
KEYWORD_CALL_FACT_SCHEMA,
|
|
22
|
+
KEYWORD_ONLY_CALL_FACT_SCHEMA,
|
|
23
|
+
MODULE_FACT_SCHEMA,
|
|
24
|
+
NUMERIC_FACT_SCHEMA,
|
|
25
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
26
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
27
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
28
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
29
|
+
PHASE12_CONVERSION_PLAN_SCHEMA,
|
|
30
|
+
RECORD_FACT_SCHEMA,
|
|
31
|
+
)
|
|
32
|
+
from pycforge.converter.conditional_regions.validation import (
|
|
33
|
+
validate_conditional_region_facts,
|
|
34
|
+
)
|
|
35
|
+
from pycforge.converter.keyword_calls.validation import (
|
|
36
|
+
validate_keyword_call_binding_facts,
|
|
37
|
+
)
|
|
38
|
+
from pycforge.converter.keyword_calls.model import KeywordCallValidationCanceled
|
|
39
|
+
from pycforge.converter.keyword_only_calls.validation import (
|
|
40
|
+
validate_keyword_only_call_binding_facts,
|
|
41
|
+
)
|
|
42
|
+
from pycforge.converter.ir.python_ir import python_ir_reference_ids
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_HELPER_REQUIREMENT = re.compile(
|
|
46
|
+
r"^pycf(?:\.[a-z][a-z0-9_]*)+@(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$"
|
|
47
|
+
)
|
|
48
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
49
|
+
|
|
50
|
+
_RECORD_TABLE_CONTRACTS = {
|
|
51
|
+
"record-definition-facts": ("record-id", "record_id"),
|
|
52
|
+
"record-field-facts": ("record-field-id", "field_id"),
|
|
53
|
+
"record-initializer-facts": ("record-initializer-id", "initializer_id"),
|
|
54
|
+
"record-instance-facts": ("record-instance-id", "instance_id"),
|
|
55
|
+
"record-binding-facts": ("binding-id", "binding_id"),
|
|
56
|
+
"record-access-facts": ("attribute-node-id", "access_node_id"),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_RECORD_FIELD_CATEGORIES = {
|
|
60
|
+
"integer-like": "int",
|
|
61
|
+
"floating-like": "float",
|
|
62
|
+
"boolean-like": "bool",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _cumulatively_ineligible_target_function_node_ids(
|
|
67
|
+
payload: dict[str, Any],
|
|
68
|
+
tables: list[dict[str, Any]],
|
|
69
|
+
cancellation: Any = None,
|
|
70
|
+
) -> frozenset[str]:
|
|
71
|
+
def check_cancellation() -> None:
|
|
72
|
+
if cancellation is not None and bool(
|
|
73
|
+
getattr(cancellation, "is_canceled", False)
|
|
74
|
+
):
|
|
75
|
+
raise KeywordCallValidationCanceled
|
|
76
|
+
|
|
77
|
+
check_cancellation()
|
|
78
|
+
table_map = {
|
|
79
|
+
table.get("table_id"): table
|
|
80
|
+
for table in tables
|
|
81
|
+
if isinstance(table, dict) and isinstance(table.get("table_id"), str)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def values(table_id: str) -> tuple[dict[str, Any], ...]:
|
|
85
|
+
return tuple(
|
|
86
|
+
record["value"]
|
|
87
|
+
for record in table_map.get(table_id, {}).get("records", ())
|
|
88
|
+
if isinstance(record, dict) and isinstance(record.get("value"), dict)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
signatures = {
|
|
92
|
+
item.get("function_node_id"): item
|
|
93
|
+
for item in values("function-signature-facts")
|
|
94
|
+
if isinstance(item.get("function_node_id"), str)
|
|
95
|
+
}
|
|
96
|
+
returns = {
|
|
97
|
+
item.get("function_node_id"): item
|
|
98
|
+
for item in values("return-path-facts")
|
|
99
|
+
if isinstance(item.get("function_node_id"), str)
|
|
100
|
+
}
|
|
101
|
+
locals_by_function = {
|
|
102
|
+
item.get("function_node_id"): item
|
|
103
|
+
for item in values("local-declaration-facts")
|
|
104
|
+
if isinstance(item.get("function_node_id"), str)
|
|
105
|
+
}
|
|
106
|
+
calls = values("call-target-facts")
|
|
107
|
+
graph_values = values("call-graph-facts")
|
|
108
|
+
recursive_functions = set(
|
|
109
|
+
graph_values[0].get("recursive_function_node_ids", ())
|
|
110
|
+
if graph_values
|
|
111
|
+
else ()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
module = payload.get("python_ir")
|
|
115
|
+
node_values = module.get("nodes") if isinstance(module, dict) else None
|
|
116
|
+
if not isinstance(node_values, list):
|
|
117
|
+
return frozenset(signatures)
|
|
118
|
+
nodes = {
|
|
119
|
+
node.get("node_id"): node
|
|
120
|
+
for node in node_values
|
|
121
|
+
if isinstance(node, dict) and isinstance(node.get("node_id"), str)
|
|
122
|
+
}
|
|
123
|
+
parent_by_node: dict[str, str] = {}
|
|
124
|
+
for parent in node_values:
|
|
125
|
+
check_cancellation()
|
|
126
|
+
if not isinstance(parent, dict) or parent.get("node_id") not in nodes:
|
|
127
|
+
continue
|
|
128
|
+
for field, value in parent.get("fields", {}).items():
|
|
129
|
+
check_cancellation()
|
|
130
|
+
for child in python_ir_reference_ids(
|
|
131
|
+
parent.get("kind"),
|
|
132
|
+
field,
|
|
133
|
+
value,
|
|
134
|
+
nodes,
|
|
135
|
+
):
|
|
136
|
+
parent_by_node.setdefault(child, parent["node_id"])
|
|
137
|
+
|
|
138
|
+
owner_cache: dict[str, str | None] = {}
|
|
139
|
+
|
|
140
|
+
def owner(node_id: str) -> str | None:
|
|
141
|
+
cached = owner_cache.get(node_id)
|
|
142
|
+
if cached is not None or node_id in owner_cache:
|
|
143
|
+
return cached
|
|
144
|
+
current = node_id
|
|
145
|
+
path: list[str] = []
|
|
146
|
+
seen: set[str] = set()
|
|
147
|
+
while current not in seen:
|
|
148
|
+
check_cancellation()
|
|
149
|
+
seen.add(current)
|
|
150
|
+
path.append(current)
|
|
151
|
+
if current in owner_cache:
|
|
152
|
+
result = owner_cache[current]
|
|
153
|
+
break
|
|
154
|
+
parent_id = parent_by_node.get(current)
|
|
155
|
+
if parent_id is None:
|
|
156
|
+
result = None
|
|
157
|
+
break
|
|
158
|
+
if nodes.get(parent_id, {}).get("kind") == "FunctionDef":
|
|
159
|
+
result = parent_id
|
|
160
|
+
break
|
|
161
|
+
current = parent_id
|
|
162
|
+
else:
|
|
163
|
+
result = None
|
|
164
|
+
for item in path:
|
|
165
|
+
owner_cache[item] = result
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
calls_by_owner: dict[str, list[dict[str, Any]]] = {}
|
|
169
|
+
callers_by_target: dict[str, list[str]] = {}
|
|
170
|
+
for call in calls:
|
|
171
|
+
check_cancellation()
|
|
172
|
+
call_node_id = call.get("call_node_id")
|
|
173
|
+
if not isinstance(call_node_id, str):
|
|
174
|
+
continue
|
|
175
|
+
function_id = owner(call_node_id)
|
|
176
|
+
if function_id is not None:
|
|
177
|
+
calls_by_owner.setdefault(function_id, []).append(call)
|
|
178
|
+
target_function_id = call.get("target_function_node_id")
|
|
179
|
+
if target_function_id in signatures:
|
|
180
|
+
callers_by_target.setdefault(target_function_id, []).append(
|
|
181
|
+
function_id
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
local_failure_fields = (
|
|
185
|
+
"use_before_binding_node_ids",
|
|
186
|
+
"loop_target_escape_node_ids",
|
|
187
|
+
"first_definitions_in_control_node_ids",
|
|
188
|
+
"representation_conflict_node_ids",
|
|
189
|
+
"loop_target_rebind_node_ids",
|
|
190
|
+
"loop_target_mutation_node_ids",
|
|
191
|
+
)
|
|
192
|
+
eligible: dict[str, bool] = {}
|
|
193
|
+
for function_id, signature in signatures.items():
|
|
194
|
+
check_cancellation()
|
|
195
|
+
path = returns.get(function_id, {})
|
|
196
|
+
local = locals_by_function.get(function_id, {})
|
|
197
|
+
eligible[function_id] = bool(
|
|
198
|
+
signature.get("eligible") is True
|
|
199
|
+
and path.get("compatible") is True
|
|
200
|
+
and path.get("fallthrough_possible") is False
|
|
201
|
+
and local.get("valid") is True
|
|
202
|
+
and not any(local.get(field) for field in local_failure_fields)
|
|
203
|
+
and function_id not in recursive_functions
|
|
204
|
+
and all(
|
|
205
|
+
call.get("supported") is True
|
|
206
|
+
for call in calls_by_owner.get(function_id, ())
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
pending = deque(
|
|
211
|
+
function_id
|
|
212
|
+
for function_id, supported in eligible.items()
|
|
213
|
+
if not supported
|
|
214
|
+
)
|
|
215
|
+
while pending:
|
|
216
|
+
check_cancellation()
|
|
217
|
+
target = pending.popleft()
|
|
218
|
+
for caller in callers_by_target.get(target, ()):
|
|
219
|
+
if eligible.get(caller, False):
|
|
220
|
+
eligible[caller] = False
|
|
221
|
+
pending.append(caller)
|
|
222
|
+
return frozenset(
|
|
223
|
+
function_id for function_id, supported in eligible.items() if not supported
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _module_function_name(module_id: str, source_name: str) -> str:
|
|
228
|
+
base = re.sub(r"[^A-Za-z0-9_]", "_", source_name)
|
|
229
|
+
if not base or not (base[0].isalpha() or base[0] == "_"):
|
|
230
|
+
base = "py_" + base
|
|
231
|
+
if base.startswith("_"):
|
|
232
|
+
base = "py" + base
|
|
233
|
+
if (
|
|
234
|
+
base in C_KEYWORDS
|
|
235
|
+
or base in TARGET_RESERVED_NAMES
|
|
236
|
+
or base in C11_EXTERNAL_IDENTIFIERS
|
|
237
|
+
or base.startswith(("pycf_", "pycm_"))
|
|
238
|
+
):
|
|
239
|
+
base = "py_" + base
|
|
240
|
+
digest = hashlib.sha256(f"{module_id}.{source_name}".encode("utf-8")).hexdigest()
|
|
241
|
+
module_token = module_id.replace("_", "_u").replace(".", "_d")
|
|
242
|
+
return f"pycm_{digest}__{module_token}__{base}"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _dependency_first_order(
|
|
246
|
+
module_ids: set[str],
|
|
247
|
+
edges: set[tuple[str, str]],
|
|
248
|
+
) -> list[str] | None:
|
|
249
|
+
dependencies = {module_id: set() for module_id in module_ids}
|
|
250
|
+
dependents = {module_id: set() for module_id in module_ids}
|
|
251
|
+
for importer, target in edges:
|
|
252
|
+
if importer not in dependencies or target not in dependencies or importer == target:
|
|
253
|
+
return None
|
|
254
|
+
dependencies[importer].add(target)
|
|
255
|
+
dependents[target].add(importer)
|
|
256
|
+
ready = sorted(
|
|
257
|
+
(module_id for module_id, values in dependencies.items() if not values),
|
|
258
|
+
key=lambda item: item.encode("utf-8"),
|
|
259
|
+
)
|
|
260
|
+
order: list[str] = []
|
|
261
|
+
while ready:
|
|
262
|
+
current = ready.pop(0)
|
|
263
|
+
order.append(current)
|
|
264
|
+
for dependent in sorted(dependents[current], key=lambda item: item.encode("utf-8")):
|
|
265
|
+
dependencies[dependent].discard(current)
|
|
266
|
+
if not dependencies[dependent] and dependent not in ready and dependent not in order:
|
|
267
|
+
ready.append(dependent)
|
|
268
|
+
ready.sort(key=lambda item: item.encode("utf-8"))
|
|
269
|
+
return order if len(order) == len(module_ids) else None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _validate_record_fact_tables(
|
|
273
|
+
payload: dict[str, Any],
|
|
274
|
+
tables: list[dict[str, Any]],
|
|
275
|
+
) -> tuple[bool, str]:
|
|
276
|
+
"""Validate the closed Phase 13 immutable static-record evidence.
|
|
277
|
+
|
|
278
|
+
This validator intentionally consumes only serialized facts and normalized
|
|
279
|
+
Python IR. It therefore acts as an independent boundary check between
|
|
280
|
+
analysis and lowering rather than trusting analyzer implementation types.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
if payload.get("record_policy_version") != DEFAULT_RECORD_POLICY:
|
|
284
|
+
return False, "Phase 13 conversion plan has an unknown record_policy_version"
|
|
285
|
+
|
|
286
|
+
table_by_id = {table.get("table_id"): table for table in tables}
|
|
287
|
+
if not set(_RECORD_TABLE_CONTRACTS).issubset(table_by_id):
|
|
288
|
+
return False, "Phase 13 conversion plan omits required static-record fact tables"
|
|
289
|
+
record_tables = {
|
|
290
|
+
table_id: table_by_id[table_id] for table_id in _RECORD_TABLE_CONTRACTS
|
|
291
|
+
}
|
|
292
|
+
for table_id, (key_domain, key_field) in _RECORD_TABLE_CONTRACTS.items():
|
|
293
|
+
table = record_tables[table_id]
|
|
294
|
+
if (
|
|
295
|
+
table.get("schema_version") != RECORD_FACT_SCHEMA
|
|
296
|
+
or table.get("producer_stage") != "analysis.plan"
|
|
297
|
+
or table.get("key_domain") != key_domain
|
|
298
|
+
or table.get("completeness") != "complete"
|
|
299
|
+
):
|
|
300
|
+
return False, f"Phase 13 {table_id} contract identity is invalid"
|
|
301
|
+
for record in table.get("records", ()):
|
|
302
|
+
value = record.get("value")
|
|
303
|
+
if (
|
|
304
|
+
not isinstance(value, dict)
|
|
305
|
+
or not isinstance(record.get("key"), str)
|
|
306
|
+
or not record["key"]
|
|
307
|
+
or record["key"] != value.get(key_field)
|
|
308
|
+
):
|
|
309
|
+
return False, f"Phase 13 {table_id} key disagrees with its value identity"
|
|
310
|
+
|
|
311
|
+
python_ir = payload.get("python_ir")
|
|
312
|
+
node_values = python_ir.get("nodes") if isinstance(python_ir, dict) else None
|
|
313
|
+
if not isinstance(node_values, list):
|
|
314
|
+
return False, "Phase 13 conversion plan lacks flattened Python IR"
|
|
315
|
+
nodes = {
|
|
316
|
+
node.get("node_id"): node
|
|
317
|
+
for node in node_values
|
|
318
|
+
if isinstance(node, dict) and isinstance(node.get("node_id"), str)
|
|
319
|
+
}
|
|
320
|
+
if len(nodes) != len(node_values):
|
|
321
|
+
return False, "Phase 13 flattened Python IR has missing or duplicate node identities"
|
|
322
|
+
|
|
323
|
+
node_ordinals = {
|
|
324
|
+
node["node_id"]: ordinal for ordinal, node in enumerate(node_values)
|
|
325
|
+
}
|
|
326
|
+
parents: dict[str, list[tuple[str, str]]] = {}
|
|
327
|
+
for parent in node_values:
|
|
328
|
+
for field_name, field_value in parent.get("fields", {}).items():
|
|
329
|
+
for child_id in python_ir_reference_ids(
|
|
330
|
+
parent["kind"], field_name, field_value, nodes
|
|
331
|
+
):
|
|
332
|
+
parents.setdefault(child_id, []).append(
|
|
333
|
+
(parent["node_id"], field_name)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _parent(node_id: str) -> tuple[str, str] | None:
|
|
337
|
+
values = parents.get(node_id, ())
|
|
338
|
+
if not values:
|
|
339
|
+
return None
|
|
340
|
+
return min(
|
|
341
|
+
values,
|
|
342
|
+
key=lambda item: (node_ordinals.get(item[0], 2**63 - 1), item[1]),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def _enclosing_function(node_id: str) -> str | None:
|
|
346
|
+
current = node_id
|
|
347
|
+
seen: set[str] = set()
|
|
348
|
+
while current not in seen:
|
|
349
|
+
seen.add(current)
|
|
350
|
+
parent = _parent(current)
|
|
351
|
+
if parent is None:
|
|
352
|
+
return None
|
|
353
|
+
candidate = nodes[parent[0]]
|
|
354
|
+
if candidate.get("kind") in {"FunctionDef", "AsyncFunctionDef"}:
|
|
355
|
+
return parent[0]
|
|
356
|
+
current = parent[0]
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def _owner_body_position(node_id: str, owner_id: str) -> int | None:
|
|
360
|
+
body = nodes.get(owner_id, {}).get("fields", {}).get("body")
|
|
361
|
+
if not isinstance(body, list):
|
|
362
|
+
return None
|
|
363
|
+
current = node_id
|
|
364
|
+
seen: set[str] = set()
|
|
365
|
+
while current not in seen:
|
|
366
|
+
seen.add(current)
|
|
367
|
+
parent = _parent(current)
|
|
368
|
+
if parent is None:
|
|
369
|
+
return None
|
|
370
|
+
if parent == (owner_id, "body"):
|
|
371
|
+
try:
|
|
372
|
+
return body.index(current)
|
|
373
|
+
except ValueError:
|
|
374
|
+
return None
|
|
375
|
+
current = parent[0]
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
store_parent_slots = {
|
|
379
|
+
("Assign", "targets"),
|
|
380
|
+
("AnnAssign", "target"),
|
|
381
|
+
("AugAssign", "target"),
|
|
382
|
+
("Delete", "targets"),
|
|
383
|
+
("NamedExpr", "target"),
|
|
384
|
+
("For", "target"),
|
|
385
|
+
("AsyncFor", "target"),
|
|
386
|
+
("comprehension", "target"),
|
|
387
|
+
("withitem", "optional_vars"),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
def _is_store_target(node_id: str) -> bool:
|
|
391
|
+
current = node_id
|
|
392
|
+
while True:
|
|
393
|
+
parent = _parent(current)
|
|
394
|
+
if parent is None:
|
|
395
|
+
return False
|
|
396
|
+
parent_node = nodes[parent[0]]
|
|
397
|
+
if (parent_node.get("kind"), parent[1]) in store_parent_slots:
|
|
398
|
+
return True
|
|
399
|
+
if (
|
|
400
|
+
parent_node.get("kind") in {"Tuple", "List", "Starred"}
|
|
401
|
+
and parent[1] in {"elts", "value"}
|
|
402
|
+
):
|
|
403
|
+
current = parent[0]
|
|
404
|
+
continue
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
def _crosses_class_or_lambda(node_id: str, owner_id: str) -> bool:
|
|
408
|
+
current = node_id
|
|
409
|
+
seen: set[str] = set()
|
|
410
|
+
while current not in seen:
|
|
411
|
+
seen.add(current)
|
|
412
|
+
parent = _parent(current)
|
|
413
|
+
if parent is None or parent[0] == owner_id:
|
|
414
|
+
return False
|
|
415
|
+
if nodes[parent[0]].get("kind") in {"ClassDef", "Lambda"}:
|
|
416
|
+
return True
|
|
417
|
+
current = parent[0]
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
def _comprehension_target(node_id: str) -> bool:
|
|
421
|
+
current = node_id
|
|
422
|
+
seen: set[str] = set()
|
|
423
|
+
while current not in seen:
|
|
424
|
+
seen.add(current)
|
|
425
|
+
parent = _parent(current)
|
|
426
|
+
if parent is None:
|
|
427
|
+
return False
|
|
428
|
+
parent_node = nodes[parent[0]]
|
|
429
|
+
if parent_node.get("kind") == "comprehension":
|
|
430
|
+
return parent[1] == "target"
|
|
431
|
+
if parent_node.get("kind") not in {"Tuple", "List", "Starred"}:
|
|
432
|
+
return False
|
|
433
|
+
current = parent[0]
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
def _owner_binding_conflict(
|
|
437
|
+
owner_id: str,
|
|
438
|
+
source_name: str,
|
|
439
|
+
declaration_node_id: str,
|
|
440
|
+
) -> bool:
|
|
441
|
+
for node in node_values:
|
|
442
|
+
node_id = node["node_id"]
|
|
443
|
+
if node_id == declaration_node_id:
|
|
444
|
+
continue
|
|
445
|
+
if _enclosing_function(node_id) != owner_id:
|
|
446
|
+
continue
|
|
447
|
+
if _crosses_class_or_lambda(node_id, owner_id):
|
|
448
|
+
continue
|
|
449
|
+
kind = node.get("kind")
|
|
450
|
+
fields = node.get("fields", {})
|
|
451
|
+
if kind == "Name" and fields.get("id") == source_name:
|
|
452
|
+
if not _comprehension_target(node_id) and _is_store_target(node_id):
|
|
453
|
+
return True
|
|
454
|
+
elif kind in {"Global", "Nonlocal"} and source_name in fields.get("names", ()):
|
|
455
|
+
return True
|
|
456
|
+
elif kind == "ExceptHandler" and fields.get("name") == source_name:
|
|
457
|
+
return True
|
|
458
|
+
elif kind in {"MatchAs", "MatchStar"} and fields.get("name") == source_name:
|
|
459
|
+
return True
|
|
460
|
+
elif kind == "MatchMapping" and fields.get("rest") == source_name:
|
|
461
|
+
return True
|
|
462
|
+
elif (
|
|
463
|
+
kind in {"FunctionDef", "AsyncFunctionDef", "ClassDef"}
|
|
464
|
+
and node_id != owner_id
|
|
465
|
+
and fields.get("name") == source_name
|
|
466
|
+
):
|
|
467
|
+
return True
|
|
468
|
+
elif kind == "alias":
|
|
469
|
+
imported = str(fields.get("name") or "")
|
|
470
|
+
bound_name = str(fields.get("asname") or imported.split(".", 1)[0])
|
|
471
|
+
if bound_name == source_name:
|
|
472
|
+
return True
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
def _table_values(table_id: str, key_field: str) -> dict[str, dict[str, Any]]:
|
|
476
|
+
table = table_by_id.get(table_id, {})
|
|
477
|
+
result: dict[str, dict[str, Any]] = {}
|
|
478
|
+
for record in table.get("records", ()):
|
|
479
|
+
value = record.get("value")
|
|
480
|
+
if isinstance(value, dict) and isinstance(value.get(key_field), str):
|
|
481
|
+
result[value[key_field]] = value
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
lexical_bindings = _table_values("binding-facts", "binding_id")
|
|
485
|
+
lexical_scopes = _table_values("scope-facts", "scope_id")
|
|
486
|
+
module_identities = _table_values("module-identity-facts", "module_id")
|
|
487
|
+
module_sources = _table_values("module-source-facts", "module_id")
|
|
488
|
+
module_functions = _table_values("module-function-facts", "function_node_id")
|
|
489
|
+
value_categories = {
|
|
490
|
+
record.get("key"): record.get("value")
|
|
491
|
+
for record in table_by_id.get("value-category-facts", {}).get("records", ())
|
|
492
|
+
if isinstance(record.get("key"), str)
|
|
493
|
+
}
|
|
494
|
+
module_records = payload.get("module_record_by_node")
|
|
495
|
+
if (
|
|
496
|
+
not lexical_bindings
|
|
497
|
+
or not lexical_scopes
|
|
498
|
+
or not module_identities
|
|
499
|
+
or not module_sources
|
|
500
|
+
or not isinstance(module_records, dict)
|
|
501
|
+
):
|
|
502
|
+
return False, "Phase 13 record proof lacks lexical or module identity evidence"
|
|
503
|
+
|
|
504
|
+
required_provenance: dict[tuple[str, str], set[str]] = {}
|
|
505
|
+
for table_id, table in record_tables.items():
|
|
506
|
+
for record in table["records"]:
|
|
507
|
+
provenance = record.get("provenance")
|
|
508
|
+
source_node_ids = (
|
|
509
|
+
provenance.get("source_node_ids")
|
|
510
|
+
if isinstance(provenance, dict)
|
|
511
|
+
else None
|
|
512
|
+
)
|
|
513
|
+
if (
|
|
514
|
+
not isinstance(source_node_ids, list)
|
|
515
|
+
or not source_node_ids
|
|
516
|
+
or any(
|
|
517
|
+
not isinstance(node_id, str) or node_id not in nodes
|
|
518
|
+
for node_id in source_node_ids
|
|
519
|
+
)
|
|
520
|
+
):
|
|
521
|
+
return False, "Phase 13 record fact provenance references an absent node"
|
|
522
|
+
required_provenance[(table_id, record["key"])] = set(source_node_ids)
|
|
523
|
+
|
|
524
|
+
values = {
|
|
525
|
+
table_id: {
|
|
526
|
+
record["key"]: record["value"] for record in table["records"]
|
|
527
|
+
}
|
|
528
|
+
for table_id, table in record_tables.items()
|
|
529
|
+
}
|
|
530
|
+
definitions = values["record-definition-facts"]
|
|
531
|
+
fields = values["record-field-facts"]
|
|
532
|
+
initializers = values["record-initializer-facts"]
|
|
533
|
+
instances = values["record-instance-facts"]
|
|
534
|
+
bindings = values["record-binding-facts"]
|
|
535
|
+
accesses = values["record-access-facts"]
|
|
536
|
+
generated_names = payload.get("generated_name_plans")
|
|
537
|
+
if not isinstance(generated_names, list) or any(
|
|
538
|
+
not isinstance(item, dict)
|
|
539
|
+
or not isinstance(item.get("binding_id"), str)
|
|
540
|
+
or not item["binding_id"]
|
|
541
|
+
or not isinstance(item.get("generated_name"), str)
|
|
542
|
+
or not item["generated_name"]
|
|
543
|
+
for item in generated_names
|
|
544
|
+
):
|
|
545
|
+
return False, "Phase 13 generated-name plans are malformed"
|
|
546
|
+
generated_name_ids = [item["binding_id"] for item in generated_names]
|
|
547
|
+
if len(generated_name_ids) != len(set(generated_name_ids)):
|
|
548
|
+
return False, "Phase 13 generated-name plan bindings are not unique"
|
|
549
|
+
required_generated_name_ids = {
|
|
550
|
+
item.get("class_binding_id") for item in definitions.values()
|
|
551
|
+
} | set(fields) | set(bindings)
|
|
552
|
+
if not required_generated_name_ids.issubset(generated_name_ids):
|
|
553
|
+
return False, "Phase 13 record lowering lacks a required generated-name plan"
|
|
554
|
+
|
|
555
|
+
def _node_is(node_id: Any, kind: str) -> bool:
|
|
556
|
+
return isinstance(node_id, str) and nodes.get(node_id, {}).get("kind") == kind
|
|
557
|
+
|
|
558
|
+
def _identity(value: dict[str, Any]) -> tuple[Any, Any, Any]:
|
|
559
|
+
return value.get("module_id"), value.get("document_id"), value.get("logical_name")
|
|
560
|
+
|
|
561
|
+
class_node_ids = {
|
|
562
|
+
node_id for node_id, node in nodes.items() if node.get("kind") == "ClassDef"
|
|
563
|
+
}
|
|
564
|
+
if {item.get("class_node_id") for item in definitions.values()} != class_node_ids:
|
|
565
|
+
return False, "Phase 13 record definitions do not completely cover flattened classes"
|
|
566
|
+
|
|
567
|
+
definition_class_bindings: set[str] = set()
|
|
568
|
+
definition_names: set[tuple[str, str]] = set()
|
|
569
|
+
for record_id, definition in definitions.items():
|
|
570
|
+
field_ids = definition.get("field_ids")
|
|
571
|
+
class_node_id = definition.get("class_node_id")
|
|
572
|
+
class_binding_id = definition.get("class_binding_id")
|
|
573
|
+
identity = _identity(definition)
|
|
574
|
+
module_record = module_records.get(class_node_id)
|
|
575
|
+
module_identity = module_identities.get(identity[0])
|
|
576
|
+
module_source = module_sources.get(identity[0])
|
|
577
|
+
lexical_class = lexical_bindings.get(class_binding_id)
|
|
578
|
+
if (
|
|
579
|
+
not _node_is(class_node_id, "ClassDef")
|
|
580
|
+
or not isinstance(class_binding_id, str)
|
|
581
|
+
or not class_binding_id
|
|
582
|
+
or class_binding_id in definition_class_bindings
|
|
583
|
+
or not all(isinstance(item, str) and item for item in identity)
|
|
584
|
+
or not isinstance(definition.get("source_name"), str)
|
|
585
|
+
or not definition["source_name"]
|
|
586
|
+
or definition["source_name"] in {"int", "float", "bool", "str"}
|
|
587
|
+
or not isinstance(definition.get("flattened_name"), str)
|
|
588
|
+
or not definition["flattened_name"]
|
|
589
|
+
or (identity[0], definition["flattened_name"]) in definition_names
|
|
590
|
+
or not isinstance(field_ids, list)
|
|
591
|
+
or not 1 <= len(field_ids) <= 64
|
|
592
|
+
or len(field_ids) != len(set(field_ids))
|
|
593
|
+
or not isinstance(definition.get("initializer_id"), str)
|
|
594
|
+
or definition.get("category") != "immutable-static-record-definition"
|
|
595
|
+
or definition.get("storage_model") != "automatic-inline-record"
|
|
596
|
+
or definition.get("ownership_model") != "unique-lexical-owner"
|
|
597
|
+
or definition.get("lifetime_model") != "enclosing-function-activation"
|
|
598
|
+
or definition.get("aliasing_model") != "forbidden"
|
|
599
|
+
or definition.get("cleanup_model") != "none"
|
|
600
|
+
or definition.get("nullability_model") != "non-null-by-construction"
|
|
601
|
+
or definition.get("mutable") is not False
|
|
602
|
+
or not isinstance(module_record, dict)
|
|
603
|
+
or module_record.get("class_node_id") != class_node_id
|
|
604
|
+
or module_record.get("module_id") != identity[0]
|
|
605
|
+
or module_record.get("document_id") != identity[1]
|
|
606
|
+
or module_record.get("source_name") != definition.get("source_name")
|
|
607
|
+
or module_record.get("flattened_name") != definition.get("flattened_name")
|
|
608
|
+
or not isinstance(module_identity, dict)
|
|
609
|
+
or module_identity.get("document_id") != identity[1]
|
|
610
|
+
or module_identity.get("logical_name") != identity[2]
|
|
611
|
+
or not isinstance(module_source, dict)
|
|
612
|
+
or module_source.get("document_id") != identity[1]
|
|
613
|
+
or module_source.get("logical_name") != identity[2]
|
|
614
|
+
or not isinstance(lexical_class, dict)
|
|
615
|
+
or lexical_class.get("binding_kind") != "record-class"
|
|
616
|
+
or lexical_class.get("declaration_node_id") != class_node_id
|
|
617
|
+
or lexical_class.get("source_name") != definition.get("flattened_name")
|
|
618
|
+
):
|
|
619
|
+
return False, "Phase 13 record definition violates the closed representation contract"
|
|
620
|
+
class_node = nodes[class_node_id]
|
|
621
|
+
class_fields = class_node.get("fields", {})
|
|
622
|
+
if (
|
|
623
|
+
class_fields.get("name") != definition["flattened_name"]
|
|
624
|
+
or class_fields.get("bases")
|
|
625
|
+
or class_fields.get("keywords")
|
|
626
|
+
or class_fields.get("decorator_list")
|
|
627
|
+
):
|
|
628
|
+
return False, "Phase 13 record definition name disagrees with flattened Python IR"
|
|
629
|
+
if class_node_id not in required_provenance[("record-definition-facts", record_id)]:
|
|
630
|
+
return False, "Phase 13 record definition provenance is incomplete"
|
|
631
|
+
definition_class_bindings.add(class_binding_id)
|
|
632
|
+
definition_names.add((identity[0], definition["flattened_name"]))
|
|
633
|
+
|
|
634
|
+
fields_by_record: dict[str, list[dict[str, Any]]] = {
|
|
635
|
+
record_id: [] for record_id in definitions
|
|
636
|
+
}
|
|
637
|
+
for field_id, field in fields.items():
|
|
638
|
+
definition = definitions.get(field.get("record_id"))
|
|
639
|
+
declaration_id = field.get("declaration_node_id")
|
|
640
|
+
target_id = field.get("target_node_id")
|
|
641
|
+
annotation_id = field.get("annotation_node_id")
|
|
642
|
+
ordinal = field.get("ordinal")
|
|
643
|
+
category = field.get("category")
|
|
644
|
+
if (
|
|
645
|
+
definition is None
|
|
646
|
+
or field.get("class_node_id") != definition.get("class_node_id")
|
|
647
|
+
or _identity(field) != _identity(definition)
|
|
648
|
+
or not _node_is(declaration_id, "AnnAssign")
|
|
649
|
+
or not _node_is(target_id, "Name")
|
|
650
|
+
or not _node_is(annotation_id, "Name")
|
|
651
|
+
or not isinstance(ordinal, int)
|
|
652
|
+
or isinstance(ordinal, bool)
|
|
653
|
+
or ordinal < 0
|
|
654
|
+
or category not in _RECORD_FIELD_CATEGORIES
|
|
655
|
+
or not isinstance(field.get("source_name"), str)
|
|
656
|
+
or not field["source_name"]
|
|
657
|
+
or (
|
|
658
|
+
field["source_name"].startswith("__")
|
|
659
|
+
and field["source_name"].endswith("__")
|
|
660
|
+
)
|
|
661
|
+
or field.get("mutable") is not False
|
|
662
|
+
):
|
|
663
|
+
return False, "Phase 13 record field violates the closed field contract"
|
|
664
|
+
declaration = nodes[declaration_id]
|
|
665
|
+
target = nodes[target_id]
|
|
666
|
+
annotation = nodes[annotation_id]
|
|
667
|
+
if (
|
|
668
|
+
declaration.get("fields", {}).get("target") != target_id
|
|
669
|
+
or declaration.get("fields", {}).get("annotation") != annotation_id
|
|
670
|
+
or declaration.get("fields", {}).get("value") is not None
|
|
671
|
+
or declaration.get("fields", {}).get("simple") != 1
|
|
672
|
+
or target.get("fields", {}).get("id") != field["source_name"]
|
|
673
|
+
or annotation.get("fields", {}).get("id") != _RECORD_FIELD_CATEGORIES[category]
|
|
674
|
+
):
|
|
675
|
+
return False, "Phase 13 record field disagrees with flattened Python IR"
|
|
676
|
+
if not {declaration_id, target_id, annotation_id}.issubset(
|
|
677
|
+
required_provenance[("record-field-facts", field_id)]
|
|
678
|
+
):
|
|
679
|
+
return False, "Phase 13 record field provenance is incomplete"
|
|
680
|
+
fields_by_record[field["record_id"]].append(field)
|
|
681
|
+
|
|
682
|
+
for record_id, definition in definitions.items():
|
|
683
|
+
ordered_fields = sorted(fields_by_record[record_id], key=lambda item: item["ordinal"])
|
|
684
|
+
if (
|
|
685
|
+
[item["ordinal"] for item in ordered_fields] != list(range(len(ordered_fields)))
|
|
686
|
+
or [item["field_id"] for item in ordered_fields] != definition["field_ids"]
|
|
687
|
+
or len({item["source_name"] for item in ordered_fields}) != len(ordered_fields)
|
|
688
|
+
):
|
|
689
|
+
return False, "Phase 13 record field coverage, identity, or ordinal order is incomplete"
|
|
690
|
+
|
|
691
|
+
if {item.get("initializer_id") for item in definitions.values()} != set(initializers):
|
|
692
|
+
return False, "Phase 13 record initializer coverage is not exactly one per definition"
|
|
693
|
+
for initializer_id, initializer in initializers.items():
|
|
694
|
+
definition = definitions.get(initializer.get("record_id"))
|
|
695
|
+
function_id = initializer.get("function_node_id")
|
|
696
|
+
arguments_id = initializer.get("arguments_node_id")
|
|
697
|
+
self_id = initializer.get("self_parameter_node_id")
|
|
698
|
+
parameter_ids = initializer.get("parameter_node_ids")
|
|
699
|
+
assignment_ids = initializer.get("assignment_node_ids")
|
|
700
|
+
if (
|
|
701
|
+
definition is None
|
|
702
|
+
or definition.get("initializer_id") != initializer_id
|
|
703
|
+
or _identity(initializer) != _identity(definition)
|
|
704
|
+
or not _node_is(function_id, "FunctionDef")
|
|
705
|
+
or not _node_is(arguments_id, "arguments")
|
|
706
|
+
or not _node_is(self_id, "arg")
|
|
707
|
+
or not isinstance(parameter_ids, list)
|
|
708
|
+
or not isinstance(assignment_ids, list)
|
|
709
|
+
or initializer.get("field_ids") != definition.get("field_ids")
|
|
710
|
+
or len(parameter_ids) != len(definition["field_ids"])
|
|
711
|
+
or len(assignment_ids) != len(definition["field_ids"])
|
|
712
|
+
or len(parameter_ids) != len(set(parameter_ids))
|
|
713
|
+
or len(assignment_ids) != len(set(assignment_ids))
|
|
714
|
+
or any(not _node_is(item, "arg") for item in parameter_ids)
|
|
715
|
+
or any(not _node_is(item, "Assign") for item in assignment_ids)
|
|
716
|
+
or initializer.get("receiver_model") != "direct-addressed-initialization-receiver"
|
|
717
|
+
or initializer.get("evaluation_order")
|
|
718
|
+
!= "field-declaration-order-left-to-right-once"
|
|
719
|
+
or initializer.get("initialization_completeness") != "all-fields-exactly-once"
|
|
720
|
+
):
|
|
721
|
+
return False, "Phase 13 record initializer violates exact field coverage"
|
|
722
|
+
function = nodes[function_id]
|
|
723
|
+
arguments = nodes[arguments_id]
|
|
724
|
+
ordered_fields = sorted(
|
|
725
|
+
fields_by_record[initializer["record_id"]], key=lambda item: item["ordinal"]
|
|
726
|
+
)
|
|
727
|
+
return_annotation = nodes.get(function.get("fields", {}).get("returns"), {})
|
|
728
|
+
if (
|
|
729
|
+
function.get("fields", {}).get("name") != "__init__"
|
|
730
|
+
or function.get("fields", {}).get("args") != arguments_id
|
|
731
|
+
or function.get("fields", {}).get("body") != assignment_ids
|
|
732
|
+
or function.get("fields", {}).get("decorator_list")
|
|
733
|
+
or function.get("fields", {}).get("type_comment") is not None
|
|
734
|
+
or arguments.get("fields", {}).get("args") != [self_id] + parameter_ids
|
|
735
|
+
or any(
|
|
736
|
+
arguments.get("fields", {}).get(field)
|
|
737
|
+
for field in ("posonlyargs", "kwonlyargs", "kw_defaults", "defaults")
|
|
738
|
+
)
|
|
739
|
+
or arguments.get("fields", {}).get("vararg") is not None
|
|
740
|
+
or arguments.get("fields", {}).get("kwarg") is not None
|
|
741
|
+
or nodes[self_id].get("fields", {}).get("arg") != "self"
|
|
742
|
+
or nodes[self_id].get("fields", {}).get("annotation") is not None
|
|
743
|
+
or nodes[self_id].get("fields", {}).get("type_comment") is not None
|
|
744
|
+
or return_annotation.get("kind") != "Constant"
|
|
745
|
+
or return_annotation.get("fields", {}).get("value") is not None
|
|
746
|
+
):
|
|
747
|
+
return False, "Phase 13 record initializer disagrees with flattened Python IR"
|
|
748
|
+
for parameter_id, assignment_id, field in zip(
|
|
749
|
+
parameter_ids, assignment_ids, ordered_fields
|
|
750
|
+
):
|
|
751
|
+
parameter = nodes[parameter_id]
|
|
752
|
+
parameter_annotation = nodes.get(
|
|
753
|
+
parameter.get("fields", {}).get("annotation"), {}
|
|
754
|
+
)
|
|
755
|
+
assignment = nodes[assignment_id]
|
|
756
|
+
assignment_targets = assignment.get("fields", {}).get("targets")
|
|
757
|
+
target = (
|
|
758
|
+
nodes.get(assignment_targets[0], {})
|
|
759
|
+
if isinstance(assignment_targets, list) and len(assignment_targets) == 1
|
|
760
|
+
else {}
|
|
761
|
+
)
|
|
762
|
+
receiver = nodes.get(target.get("fields", {}).get("value"), {})
|
|
763
|
+
source = nodes.get(assignment.get("fields", {}).get("value"), {})
|
|
764
|
+
if (
|
|
765
|
+
parameter.get("fields", {}).get("arg") != field["source_name"]
|
|
766
|
+
or parameter.get("fields", {}).get("type_comment") is not None
|
|
767
|
+
or parameter_annotation.get("kind") != "Name"
|
|
768
|
+
or parameter_annotation.get("fields", {}).get("id")
|
|
769
|
+
!= _RECORD_FIELD_CATEGORIES[field["category"]]
|
|
770
|
+
or target.get("kind") != "Attribute"
|
|
771
|
+
or target.get("fields", {}).get("attr") != field["source_name"]
|
|
772
|
+
or receiver.get("kind") != "Name"
|
|
773
|
+
or receiver.get("fields", {}).get("id") != "self"
|
|
774
|
+
or source.get("kind") != "Name"
|
|
775
|
+
or source.get("fields", {}).get("id") != field["source_name"]
|
|
776
|
+
or assignment.get("fields", {}).get("type_comment") is not None
|
|
777
|
+
):
|
|
778
|
+
return False, "Phase 13 record initializer field copies are not exact"
|
|
779
|
+
class_body = nodes[definition["class_node_id"]].get("fields", {}).get("body")
|
|
780
|
+
if class_body != [item["declaration_node_id"] for item in ordered_fields] + [function_id]:
|
|
781
|
+
return False, "Phase 13 record class body exceeds the closed record surface"
|
|
782
|
+
initializer_nodes = {
|
|
783
|
+
function_id,
|
|
784
|
+
arguments_id,
|
|
785
|
+
self_id,
|
|
786
|
+
*parameter_ids,
|
|
787
|
+
*assignment_ids,
|
|
788
|
+
}
|
|
789
|
+
if not initializer_nodes.issubset(
|
|
790
|
+
required_provenance[("record-initializer-facts", initializer_id)]
|
|
791
|
+
):
|
|
792
|
+
return False, "Phase 13 record initializer provenance is incomplete"
|
|
793
|
+
|
|
794
|
+
if {item.get("binding_id") for item in instances.values()} != set(bindings):
|
|
795
|
+
return False, "Phase 13 record instance bindings are not one-to-one"
|
|
796
|
+
if {item.get("instance_id") for item in bindings.values()} != set(instances):
|
|
797
|
+
return False, "Phase 13 record instance coverage is not one-to-one"
|
|
798
|
+
for instance_id, instance in instances.items():
|
|
799
|
+
definition = definitions.get(instance.get("record_id"))
|
|
800
|
+
binding = bindings.get(instance.get("binding_id"))
|
|
801
|
+
construction_id = instance.get("construction_node_id")
|
|
802
|
+
assignment_id = instance.get("assignment_node_id")
|
|
803
|
+
target_id = instance.get("target_node_id")
|
|
804
|
+
owner_id = instance.get("owner_function_node_id")
|
|
805
|
+
argument_ids = instance.get("argument_node_ids")
|
|
806
|
+
lexical_instance = lexical_bindings.get(instance.get("binding_id"))
|
|
807
|
+
lexical_scope = (
|
|
808
|
+
lexical_scopes.get(lexical_instance.get("scope_id"))
|
|
809
|
+
if isinstance(lexical_instance, dict)
|
|
810
|
+
else None
|
|
811
|
+
)
|
|
812
|
+
module_function = module_functions.get(owner_id)
|
|
813
|
+
module_identity = module_identities.get(instance.get("module_id"))
|
|
814
|
+
module_source = module_sources.get(instance.get("module_id"))
|
|
815
|
+
if (
|
|
816
|
+
definition is None
|
|
817
|
+
or binding is None
|
|
818
|
+
or instance.get("class_node_id") != definition.get("class_node_id")
|
|
819
|
+
or _identity(instance) != _identity(definition)
|
|
820
|
+
or not _node_is(owner_id, "FunctionDef")
|
|
821
|
+
or not _node_is(construction_id, "Call")
|
|
822
|
+
or not _node_is(assignment_id, "Assign")
|
|
823
|
+
or not _node_is(target_id, "Name")
|
|
824
|
+
or not isinstance(argument_ids, list)
|
|
825
|
+
or len(argument_ids) != len(definition["field_ids"])
|
|
826
|
+
or any(item not in nodes for item in argument_ids)
|
|
827
|
+
or instance.get("category") != "immutable-static-record-like"
|
|
828
|
+
or instance.get("storage_model") != "automatic-inline-record"
|
|
829
|
+
or instance.get("ownership_model") != "unique-lexical-owner"
|
|
830
|
+
or instance.get("lifetime_model") != "enclosing-function-activation"
|
|
831
|
+
or instance.get("aliasing_model") != "forbidden"
|
|
832
|
+
or instance.get("cleanup_model") != "none"
|
|
833
|
+
or instance.get("nullability_model") != "non-null-by-construction"
|
|
834
|
+
or instance.get("allocation_model") != "none"
|
|
835
|
+
or instance.get("mutable") is not False
|
|
836
|
+
or not isinstance(lexical_instance, dict)
|
|
837
|
+
or lexical_instance.get("binding_kind") != "local"
|
|
838
|
+
or lexical_instance.get("declaration_node_id") != target_id
|
|
839
|
+
or lexical_instance.get("source_name") != instance.get("source_name")
|
|
840
|
+
or not isinstance(lexical_scope, dict)
|
|
841
|
+
or lexical_scope.get("scope_kind") != "function"
|
|
842
|
+
or lexical_scope.get("owner_node_id") != owner_id
|
|
843
|
+
or not isinstance(module_function, dict)
|
|
844
|
+
or module_function.get("module_id") != instance.get("module_id")
|
|
845
|
+
or module_function.get("document_id") != instance.get("document_id")
|
|
846
|
+
or not isinstance(module_identity, dict)
|
|
847
|
+
or owner_id not in module_identity.get("function_node_ids", ())
|
|
848
|
+
or module_identity.get("document_id") != instance.get("document_id")
|
|
849
|
+
or module_identity.get("logical_name") != instance.get("logical_name")
|
|
850
|
+
or not isinstance(module_source, dict)
|
|
851
|
+
or owner_id not in module_source.get("function_node_ids", ())
|
|
852
|
+
or module_source.get("document_id") != instance.get("document_id")
|
|
853
|
+
or module_source.get("logical_name") != instance.get("logical_name")
|
|
854
|
+
):
|
|
855
|
+
return False, "Phase 13 record instance violates automatic immutable ownership"
|
|
856
|
+
construction = nodes[construction_id]
|
|
857
|
+
assignment = nodes[assignment_id]
|
|
858
|
+
target = nodes[target_id]
|
|
859
|
+
constructor_name = nodes.get(construction.get("fields", {}).get("func"), {})
|
|
860
|
+
lexical_class = lexical_bindings.get(definition.get("class_binding_id"), {})
|
|
861
|
+
ordered_fields = sorted(
|
|
862
|
+
fields_by_record[instance["record_id"]], key=lambda item: item["ordinal"]
|
|
863
|
+
)
|
|
864
|
+
if (
|
|
865
|
+
construction.get("fields", {}).get("args") != argument_ids
|
|
866
|
+
or construction.get("fields", {}).get("keywords")
|
|
867
|
+
or constructor_name.get("kind") != "Name"
|
|
868
|
+
or constructor_name.get("fields", {}).get("id")
|
|
869
|
+
!= definition.get("flattened_name")
|
|
870
|
+
or constructor_name.get("node_id")
|
|
871
|
+
not in lexical_class.get("occurrence_node_ids", ())
|
|
872
|
+
or assignment.get("fields", {}).get("value") != construction_id
|
|
873
|
+
or assignment.get("fields", {}).get("targets") != [target_id]
|
|
874
|
+
or assignment.get("fields", {}).get("type_comment") is not None
|
|
875
|
+
or target.get("fields", {}).get("id") != instance.get("source_name")
|
|
876
|
+
or assignment_id not in nodes[owner_id].get("fields", {}).get("body", ())
|
|
877
|
+
or [value_categories.get(item) for item in argument_ids]
|
|
878
|
+
!= [item["category"] for item in ordered_fields]
|
|
879
|
+
or _owner_binding_conflict(
|
|
880
|
+
owner_id,
|
|
881
|
+
instance.get("source_name"),
|
|
882
|
+
target_id,
|
|
883
|
+
)
|
|
884
|
+
):
|
|
885
|
+
return False, "Phase 13 record construction disagrees with flattened Python IR"
|
|
886
|
+
instance_nodes = {construction_id, assignment_id, target_id, *argument_ids}
|
|
887
|
+
if not instance_nodes.issubset(
|
|
888
|
+
required_provenance[("record-instance-facts", instance_id)]
|
|
889
|
+
):
|
|
890
|
+
return False, "Phase 13 record instance provenance is incomplete"
|
|
891
|
+
|
|
892
|
+
for definition in definitions.values():
|
|
893
|
+
lexical_class = lexical_bindings[definition["class_binding_id"]]
|
|
894
|
+
expected_constructor_names = {
|
|
895
|
+
nodes[instance["construction_node_id"]]["fields"]["func"]
|
|
896
|
+
for instance in instances.values()
|
|
897
|
+
if instance.get("record_id") == definition.get("record_id")
|
|
898
|
+
}
|
|
899
|
+
if set(lexical_class.get("occurrence_node_ids", ())) != expected_constructor_names:
|
|
900
|
+
return False, "Phase 13 record class binding has an indirect or unproved use"
|
|
901
|
+
|
|
902
|
+
accesses_by_binding: dict[str, set[str]] = {binding_id: set() for binding_id in bindings}
|
|
903
|
+
for access_id, access in accesses.items():
|
|
904
|
+
instance = instances.get(access.get("instance_id"))
|
|
905
|
+
binding = bindings.get(access.get("binding_id"))
|
|
906
|
+
definition = definitions.get(access.get("record_id"))
|
|
907
|
+
field = fields.get(access.get("field_id"))
|
|
908
|
+
if (
|
|
909
|
+
instance is None
|
|
910
|
+
or binding is None
|
|
911
|
+
or definition is None
|
|
912
|
+
or field is None
|
|
913
|
+
or access.get("instance_id") != binding.get("instance_id")
|
|
914
|
+
or access.get("binding_id") != instance.get("binding_id")
|
|
915
|
+
or access.get("record_id") != instance.get("record_id")
|
|
916
|
+
or field.get("record_id") != access.get("record_id")
|
|
917
|
+
or access.get("field_name") != field.get("source_name")
|
|
918
|
+
or access.get("field_category") != field.get("category")
|
|
919
|
+
or access.get("owner_function_node_id") != instance.get("owner_function_node_id")
|
|
920
|
+
or _identity(access) != _identity(instance)
|
|
921
|
+
or not _node_is(access_id, "Attribute")
|
|
922
|
+
or access.get("access_mode") != "read"
|
|
923
|
+
or access.get("statically_bound") is not True
|
|
924
|
+
):
|
|
925
|
+
return False, "Phase 13 record field access lacks an exact static relationship"
|
|
926
|
+
access_node = nodes[access_id]
|
|
927
|
+
receiver_id = access_node.get("fields", {}).get("value")
|
|
928
|
+
access_position = _owner_body_position(
|
|
929
|
+
access_id,
|
|
930
|
+
instance["owner_function_node_id"],
|
|
931
|
+
)
|
|
932
|
+
construction_position = _owner_body_position(
|
|
933
|
+
instance["assignment_node_id"],
|
|
934
|
+
instance["owner_function_node_id"],
|
|
935
|
+
)
|
|
936
|
+
if (
|
|
937
|
+
access_node.get("fields", {}).get("attr") != field["source_name"]
|
|
938
|
+
or not _node_is(receiver_id, "Name")
|
|
939
|
+
or receiver_id not in binding.get("occurrence_node_ids", ())
|
|
940
|
+
or nodes[receiver_id].get("fields", {}).get("id")
|
|
941
|
+
!= binding.get("source_name")
|
|
942
|
+
or _is_store_target(access_id)
|
|
943
|
+
or access_position is None
|
|
944
|
+
or construction_position is None
|
|
945
|
+
or access_position <= construction_position
|
|
946
|
+
):
|
|
947
|
+
return False, "Phase 13 record field access disagrees with flattened Python IR"
|
|
948
|
+
if access_id not in required_provenance[("record-access-facts", access_id)]:
|
|
949
|
+
return False, "Phase 13 record access provenance is incomplete"
|
|
950
|
+
accesses_by_binding[access["binding_id"]].add(access_id)
|
|
951
|
+
|
|
952
|
+
for binding_id, binding in bindings.items():
|
|
953
|
+
instance = instances.get(binding.get("instance_id"))
|
|
954
|
+
occurrence_ids = binding.get("occurrence_node_ids")
|
|
955
|
+
allowed_access_ids = binding.get("allowed_field_access_node_ids")
|
|
956
|
+
if (
|
|
957
|
+
instance is None
|
|
958
|
+
or binding.get("record_id") != instance.get("record_id")
|
|
959
|
+
or binding.get("source_name") != instance.get("source_name")
|
|
960
|
+
or binding.get("declaration_node_id") != instance.get("target_node_id")
|
|
961
|
+
or binding.get("owner_function_node_id") != instance.get("owner_function_node_id")
|
|
962
|
+
or _identity(binding) != _identity(instance)
|
|
963
|
+
or binding.get("category") != "immutable-static-record-like"
|
|
964
|
+
or binding.get("single_assignment") is not True
|
|
965
|
+
or binding.get("noalias") is not True
|
|
966
|
+
or binding.get("escapes") is not False
|
|
967
|
+
or not isinstance(occurrence_ids, list)
|
|
968
|
+
or not occurrence_ids
|
|
969
|
+
or len(occurrence_ids) != len(set(occurrence_ids))
|
|
970
|
+
or instance.get("target_node_id") not in occurrence_ids
|
|
971
|
+
or any(not _node_is(item, "Name") for item in occurrence_ids)
|
|
972
|
+
or not isinstance(allowed_access_ids, list)
|
|
973
|
+
or len(allowed_access_ids) != len(set(allowed_access_ids))
|
|
974
|
+
or set(allowed_access_ids) != accesses_by_binding[binding_id]
|
|
975
|
+
or set(occurrence_ids)
|
|
976
|
+
!= {
|
|
977
|
+
instance.get("target_node_id"),
|
|
978
|
+
*(
|
|
979
|
+
nodes[access_id].get("fields", {}).get("value")
|
|
980
|
+
for access_id in allowed_access_ids
|
|
981
|
+
),
|
|
982
|
+
}
|
|
983
|
+
):
|
|
984
|
+
return False, "Phase 13 record binding violates no-alias/no-escape ownership"
|
|
985
|
+
lexical_binding = lexical_bindings.get(binding_id)
|
|
986
|
+
if (
|
|
987
|
+
not isinstance(lexical_binding, dict)
|
|
988
|
+
or lexical_binding.get("binding_kind") != "local"
|
|
989
|
+
or lexical_binding.get("declaration_node_id")
|
|
990
|
+
!= binding.get("declaration_node_id")
|
|
991
|
+
or lexical_binding.get("source_name") != binding.get("source_name")
|
|
992
|
+
or set(lexical_binding.get("occurrence_node_ids", ()))
|
|
993
|
+
!= set(occurrence_ids)
|
|
994
|
+
):
|
|
995
|
+
return False, "Phase 13 record binding disagrees with lexical binding facts"
|
|
996
|
+
binding_nodes = {
|
|
997
|
+
binding["declaration_node_id"],
|
|
998
|
+
*occurrence_ids,
|
|
999
|
+
*allowed_access_ids,
|
|
1000
|
+
}
|
|
1001
|
+
if not binding_nodes.issubset(
|
|
1002
|
+
required_provenance[("record-binding-facts", binding_id)]
|
|
1003
|
+
):
|
|
1004
|
+
return False, "Phase 13 record binding provenance is incomplete"
|
|
1005
|
+
|
|
1006
|
+
return True, ""
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _validate_numeric_fact_table(
|
|
1010
|
+
payload: dict[str, Any],
|
|
1011
|
+
tables: list[dict[str, Any]],
|
|
1012
|
+
) -> tuple[bool, str]:
|
|
1013
|
+
"""Re-prove Phase 14A facts from serialized Python IR and plan data."""
|
|
1014
|
+
|
|
1015
|
+
if payload.get("numeric_policy_version") != DEFAULT_NUMERIC_POLICY:
|
|
1016
|
+
return False, "Phase 14 conversion plan has an unknown numeric_policy_version"
|
|
1017
|
+
if payload.get("target_contract") != "c11-portable-fixed-v1":
|
|
1018
|
+
return False, "Phase 14 numeric proof has an inexact target contract"
|
|
1019
|
+
table = next(
|
|
1020
|
+
(item for item in tables if item.get("table_id") == "numeric-operation-facts"),
|
|
1021
|
+
None,
|
|
1022
|
+
)
|
|
1023
|
+
if not isinstance(table, dict):
|
|
1024
|
+
return False, "Phase 14 conversion plan omits numeric-operation-facts"
|
|
1025
|
+
if (
|
|
1026
|
+
table.get("schema_version") != NUMERIC_FACT_SCHEMA
|
|
1027
|
+
or table.get("producer_stage") != "analysis.plan"
|
|
1028
|
+
or table.get("key_domain") != "binop-node-id"
|
|
1029
|
+
or table.get("completeness") != "complete"
|
|
1030
|
+
or table.get("invalidation_dependencies")
|
|
1031
|
+
!= ["value-category-facts", "evaluation-order-facts"]
|
|
1032
|
+
):
|
|
1033
|
+
return False, "Phase 14 numeric fact-table contract identity is invalid"
|
|
1034
|
+
|
|
1035
|
+
python_ir = payload.get("python_ir")
|
|
1036
|
+
node_values = python_ir.get("nodes") if isinstance(python_ir, dict) else None
|
|
1037
|
+
if not isinstance(node_values, list):
|
|
1038
|
+
return False, "Phase 14 numeric proof lacks flattened Python IR"
|
|
1039
|
+
nodes = {
|
|
1040
|
+
item.get("node_id"): item
|
|
1041
|
+
for item in node_values
|
|
1042
|
+
if isinstance(item, dict) and isinstance(item.get("node_id"), str)
|
|
1043
|
+
}
|
|
1044
|
+
if len(nodes) != len(node_values):
|
|
1045
|
+
return False, "Phase 14 numeric proof has duplicate or absent Python node IDs"
|
|
1046
|
+
ordinals = {item["node_id"]: index for index, item in enumerate(node_values)}
|
|
1047
|
+
parents: dict[str, list[tuple[str, str]]] = {}
|
|
1048
|
+
for parent in node_values:
|
|
1049
|
+
for field_name, value in parent.get("fields", {}).items():
|
|
1050
|
+
for child_id in python_ir_reference_ids(
|
|
1051
|
+
parent["kind"], field_name, value, nodes
|
|
1052
|
+
):
|
|
1053
|
+
parents.setdefault(child_id, []).append(
|
|
1054
|
+
(parent["node_id"], field_name)
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
module_functions = {
|
|
1058
|
+
record.get("value", {}).get("function_node_id"): record.get("value")
|
|
1059
|
+
for module_table in tables
|
|
1060
|
+
if module_table.get("table_id") == "module-function-facts"
|
|
1061
|
+
for record in module_table.get("records", ())
|
|
1062
|
+
if isinstance(record.get("value"), dict)
|
|
1063
|
+
}
|
|
1064
|
+
module_identities = {
|
|
1065
|
+
record.get("value", {}).get("module_id"): record.get("value")
|
|
1066
|
+
for module_table in tables
|
|
1067
|
+
if module_table.get("table_id") == "module-identity-facts"
|
|
1068
|
+
for record in module_table.get("records", ())
|
|
1069
|
+
if isinstance(record.get("value"), dict)
|
|
1070
|
+
}
|
|
1071
|
+
module_sources = {
|
|
1072
|
+
record.get("value", {}).get("module_id"): record.get("value")
|
|
1073
|
+
for module_table in tables
|
|
1074
|
+
if module_table.get("table_id") == "module-source-facts"
|
|
1075
|
+
for record in module_table.get("records", ())
|
|
1076
|
+
if isinstance(record.get("value"), dict)
|
|
1077
|
+
}
|
|
1078
|
+
prohibited_contexts = {
|
|
1079
|
+
"AsyncFor",
|
|
1080
|
+
"AsyncFunctionDef",
|
|
1081
|
+
"AsyncWith",
|
|
1082
|
+
"Await",
|
|
1083
|
+
"DictComp",
|
|
1084
|
+
"FormattedValue",
|
|
1085
|
+
"GeneratorExp",
|
|
1086
|
+
"JoinedStr",
|
|
1087
|
+
"Lambda",
|
|
1088
|
+
"ListComp",
|
|
1089
|
+
"Match",
|
|
1090
|
+
"NamedExpr",
|
|
1091
|
+
"SetComp",
|
|
1092
|
+
"Try",
|
|
1093
|
+
"TryStar",
|
|
1094
|
+
"With",
|
|
1095
|
+
"Yield",
|
|
1096
|
+
"YieldFrom",
|
|
1097
|
+
"comprehension",
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
def approved_context(node_id: str) -> str | None:
|
|
1101
|
+
current = node_id
|
|
1102
|
+
seen: set[str] = set()
|
|
1103
|
+
while current not in seen:
|
|
1104
|
+
seen.add(current)
|
|
1105
|
+
candidates = parents.get(current, ())
|
|
1106
|
+
if not candidates:
|
|
1107
|
+
return None
|
|
1108
|
+
parent_id, field_name = min(
|
|
1109
|
+
candidates,
|
|
1110
|
+
key=lambda item: (ordinals.get(item[0], 2**63 - 1), item[1]),
|
|
1111
|
+
)
|
|
1112
|
+
parent = nodes[parent_id]
|
|
1113
|
+
if parent.get("kind") == "FunctionDef":
|
|
1114
|
+
return parent_id if field_name == "body" and parent_id in module_functions else None
|
|
1115
|
+
if parent.get("kind") in prohibited_contexts or parent.get("kind") == "ClassDef":
|
|
1116
|
+
return None
|
|
1117
|
+
current = parent_id
|
|
1118
|
+
return None
|
|
1119
|
+
|
|
1120
|
+
def signed_literal(node: dict[str, Any]) -> tuple[int, tuple[str, ...], str] | None:
|
|
1121
|
+
if node.get("kind") == "Constant":
|
|
1122
|
+
value = node.get("fields", {}).get("value")
|
|
1123
|
+
if (
|
|
1124
|
+
isinstance(value, int)
|
|
1125
|
+
and not isinstance(value, bool)
|
|
1126
|
+
and 0 <= value <= 2**63 - 1
|
|
1127
|
+
):
|
|
1128
|
+
return value, (node["node_id"],), "constant"
|
|
1129
|
+
return None
|
|
1130
|
+
if node.get("kind") != "UnaryOp":
|
|
1131
|
+
return None
|
|
1132
|
+
operator = nodes.get(node.get("fields", {}).get("op"), {})
|
|
1133
|
+
operand = nodes.get(node.get("fields", {}).get("operand"), {})
|
|
1134
|
+
value = operand.get("fields", {}).get("value")
|
|
1135
|
+
if (
|
|
1136
|
+
operator.get("kind") not in {"UAdd", "USub"}
|
|
1137
|
+
or operand.get("kind") != "Constant"
|
|
1138
|
+
or not isinstance(value, int)
|
|
1139
|
+
or isinstance(value, bool)
|
|
1140
|
+
or not 0 <= value <= 2**63 - 1
|
|
1141
|
+
):
|
|
1142
|
+
return None
|
|
1143
|
+
return (
|
|
1144
|
+
-value if operator["kind"] == "USub" else value,
|
|
1145
|
+
(node["node_id"], operator["node_id"], operand["node_id"]),
|
|
1146
|
+
"unary-minus" if operator["kind"] == "USub" else "unary-plus",
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
category_table = next(
|
|
1150
|
+
(item for item in tables if item.get("table_id") == "value-category-facts"),
|
|
1151
|
+
{},
|
|
1152
|
+
)
|
|
1153
|
+
categories = {
|
|
1154
|
+
record.get("key"): record.get("value")
|
|
1155
|
+
for record in category_table.get("records", ())
|
|
1156
|
+
}
|
|
1157
|
+
numeric_nodes: dict[str, tuple[dict[str, Any], dict[str, Any]]] = {}
|
|
1158
|
+
for node in node_values:
|
|
1159
|
+
if node.get("kind") != "BinOp":
|
|
1160
|
+
continue
|
|
1161
|
+
operator = nodes.get(node.get("fields", {}).get("op"), {})
|
|
1162
|
+
if operator.get("kind") in {"FloorDiv", "Mod"}:
|
|
1163
|
+
numeric_nodes[node["node_id"]] = (node, operator)
|
|
1164
|
+
|
|
1165
|
+
supported_calls = {
|
|
1166
|
+
record.get("value", {}).get("call_node_id")
|
|
1167
|
+
for candidate in tables
|
|
1168
|
+
if candidate.get("table_id") == "call-target-facts"
|
|
1169
|
+
for record in candidate.get("records", ())
|
|
1170
|
+
if isinstance(record.get("value"), dict)
|
|
1171
|
+
and record["value"].get("supported") is True
|
|
1172
|
+
}
|
|
1173
|
+
supported_container_accesses = {
|
|
1174
|
+
record.get("value", {}).get("subscript_node_id")
|
|
1175
|
+
for candidate in tables
|
|
1176
|
+
if candidate.get("table_id") == "container-access-facts"
|
|
1177
|
+
for record in candidate.get("records", ())
|
|
1178
|
+
if isinstance(record.get("value"), dict)
|
|
1179
|
+
and record["value"].get("supported") is True
|
|
1180
|
+
}
|
|
1181
|
+
supported_record_accesses = {
|
|
1182
|
+
record.get("value", {}).get("access_node_id")
|
|
1183
|
+
for candidate in tables
|
|
1184
|
+
if candidate.get("table_id") == "record-access-facts"
|
|
1185
|
+
for record in candidate.get("records", ())
|
|
1186
|
+
if isinstance(record.get("value"), dict)
|
|
1187
|
+
}
|
|
1188
|
+
integer_expression_cache: dict[str, bool] = {}
|
|
1189
|
+
|
|
1190
|
+
def approved_integer_expression(
|
|
1191
|
+
node_id: str,
|
|
1192
|
+
active: frozenset[str] = frozenset(),
|
|
1193
|
+
) -> bool:
|
|
1194
|
+
if node_id in integer_expression_cache:
|
|
1195
|
+
return integer_expression_cache[node_id]
|
|
1196
|
+
if node_id in active or categories.get(node_id) != "integer-like":
|
|
1197
|
+
return False
|
|
1198
|
+
node = nodes[node_id]
|
|
1199
|
+
kind = node.get("kind")
|
|
1200
|
+
fields = node.get("fields", {})
|
|
1201
|
+
next_active = active | {node_id}
|
|
1202
|
+
accepted = False
|
|
1203
|
+
if kind == "Name":
|
|
1204
|
+
accepted = True
|
|
1205
|
+
elif kind == "Constant":
|
|
1206
|
+
value = fields.get("value")
|
|
1207
|
+
accepted = (
|
|
1208
|
+
isinstance(value, int)
|
|
1209
|
+
and not isinstance(value, bool)
|
|
1210
|
+
and 0 <= value <= 2**63 - 1
|
|
1211
|
+
)
|
|
1212
|
+
elif kind == "UnaryOp":
|
|
1213
|
+
operator = nodes.get(fields.get("op"), {})
|
|
1214
|
+
operand_id = fields.get("operand")
|
|
1215
|
+
accepted = (
|
|
1216
|
+
operator.get("kind") in {"UAdd", "USub"}
|
|
1217
|
+
and isinstance(operand_id, str)
|
|
1218
|
+
and operand_id in nodes
|
|
1219
|
+
and approved_integer_expression(operand_id, next_active)
|
|
1220
|
+
)
|
|
1221
|
+
elif kind == "BinOp":
|
|
1222
|
+
operator = nodes.get(fields.get("op"), {})
|
|
1223
|
+
left_id = fields.get("left")
|
|
1224
|
+
right_id = fields.get("right")
|
|
1225
|
+
if operator.get("kind") in {"FloorDiv", "Mod"}:
|
|
1226
|
+
accepted = node_id in numeric_nodes
|
|
1227
|
+
else:
|
|
1228
|
+
accepted = (
|
|
1229
|
+
operator.get("kind") in {"Add", "Sub", "Mult"}
|
|
1230
|
+
and isinstance(left_id, str)
|
|
1231
|
+
and isinstance(right_id, str)
|
|
1232
|
+
and left_id in nodes
|
|
1233
|
+
and right_id in nodes
|
|
1234
|
+
and approved_integer_expression(left_id, next_active)
|
|
1235
|
+
and approved_integer_expression(right_id, next_active)
|
|
1236
|
+
)
|
|
1237
|
+
elif kind == "Call":
|
|
1238
|
+
accepted = node_id in supported_calls
|
|
1239
|
+
elif kind == "Subscript":
|
|
1240
|
+
accepted = node_id in supported_container_accesses
|
|
1241
|
+
elif kind == "Attribute":
|
|
1242
|
+
accepted = node_id in supported_record_accesses
|
|
1243
|
+
integer_expression_cache[node_id] = accepted
|
|
1244
|
+
return accepted
|
|
1245
|
+
|
|
1246
|
+
records = table.get("records")
|
|
1247
|
+
if not isinstance(records, list):
|
|
1248
|
+
return False, "Phase 14 numeric fact table records are malformed"
|
|
1249
|
+
facts: dict[str, dict[str, Any]] = {}
|
|
1250
|
+
provenance_by_node: dict[str, set[str]] = {}
|
|
1251
|
+
for record in records:
|
|
1252
|
+
value = record.get("value")
|
|
1253
|
+
key = record.get("key")
|
|
1254
|
+
provenance = record.get("provenance")
|
|
1255
|
+
source_ids = provenance.get("source_node_ids") if isinstance(provenance, dict) else None
|
|
1256
|
+
if (
|
|
1257
|
+
not isinstance(key, str)
|
|
1258
|
+
or not isinstance(value, dict)
|
|
1259
|
+
or value.get("binop_node_id") != key
|
|
1260
|
+
or not isinstance(source_ids, list)
|
|
1261
|
+
or any(item not in nodes for item in source_ids)
|
|
1262
|
+
):
|
|
1263
|
+
return False, "Phase 14 numeric fact identity or provenance is invalid"
|
|
1264
|
+
facts[key] = value
|
|
1265
|
+
provenance_by_node[key] = set(source_ids)
|
|
1266
|
+
if set(facts) != set(numeric_nodes):
|
|
1267
|
+
return False, "Phase 14 numeric facts do not exactly cover // and % nodes"
|
|
1268
|
+
|
|
1269
|
+
plans = payload.get("rule_plans", [])
|
|
1270
|
+
support = {
|
|
1271
|
+
item.get("node_id"): item
|
|
1272
|
+
for item in payload.get("support_decisions", [])
|
|
1273
|
+
if isinstance(item, dict)
|
|
1274
|
+
}
|
|
1275
|
+
numeric_helpers = {
|
|
1276
|
+
"pycf.i64.floor_div@1.0.0",
|
|
1277
|
+
"pycf.i64.floor_mod@1.0.0",
|
|
1278
|
+
}
|
|
1279
|
+
numeric_obligations = [
|
|
1280
|
+
"exact-int64-operands-proved",
|
|
1281
|
+
"direct-safe-divisor-literal-proved",
|
|
1282
|
+
"left-before-right-evaluation-once",
|
|
1283
|
+
"helper-preconditions-proved",
|
|
1284
|
+
"python-floor-semantics-preserved",
|
|
1285
|
+
"no-runtime-failure-channel",
|
|
1286
|
+
"scalar-value-ownership-by-value",
|
|
1287
|
+
"allocation-and-cleanup-absent",
|
|
1288
|
+
"source-provenance-anchored",
|
|
1289
|
+
"cancellation-safe-points-honored",
|
|
1290
|
+
"target-contract-exact",
|
|
1291
|
+
"operands-materialized-left-to-right-once",
|
|
1292
|
+
"helper-result-materialized-once",
|
|
1293
|
+
"zero-and-negative-one-divisors-excluded",
|
|
1294
|
+
]
|
|
1295
|
+
for plan in plans:
|
|
1296
|
+
if plan.get("rule_id") != "phase14.numeric.floor_arithmetic" and numeric_helpers.intersection(
|
|
1297
|
+
plan.get("helper_requirements", ())
|
|
1298
|
+
):
|
|
1299
|
+
return False, "A non-numeric RulePlan claims a floor-arithmetic helper"
|
|
1300
|
+
|
|
1301
|
+
for node_id, (node, operator) in sorted(numeric_nodes.items()):
|
|
1302
|
+
fact = facts[node_id]
|
|
1303
|
+
left_id = node.get("fields", {}).get("left")
|
|
1304
|
+
right_id = node.get("fields", {}).get("right")
|
|
1305
|
+
function_node_id = approved_context(node_id)
|
|
1306
|
+
if (
|
|
1307
|
+
left_id not in nodes
|
|
1308
|
+
or right_id not in nodes
|
|
1309
|
+
or function_node_id is None
|
|
1310
|
+
or categories.get(left_id) != "integer-like"
|
|
1311
|
+
or categories.get(right_id) != "integer-like"
|
|
1312
|
+
or categories.get(node_id) != "integer-like"
|
|
1313
|
+
or not approved_integer_expression(left_id)
|
|
1314
|
+
):
|
|
1315
|
+
return False, "Phase 14 numeric fact lacks exact category or context proof"
|
|
1316
|
+
literal = signed_literal(nodes[right_id])
|
|
1317
|
+
if literal is None or literal[0] in {0, -1}:
|
|
1318
|
+
return False, "Phase 14 numeric divisor proof is unsafe"
|
|
1319
|
+
divisor, literal_ids, literal_shape = literal
|
|
1320
|
+
is_division = operator["kind"] == "FloorDiv"
|
|
1321
|
+
operation_kind = "floor-divide" if is_division else "floor-modulo"
|
|
1322
|
+
helper = (
|
|
1323
|
+
"pycf.i64.floor_div@1.0.0"
|
|
1324
|
+
if is_division
|
|
1325
|
+
else "pycf.i64.floor_mod@1.0.0"
|
|
1326
|
+
)
|
|
1327
|
+
operation_id = "numeric-op-" + hashlib.sha256(
|
|
1328
|
+
f"{node_id}\x1f{operation_kind}".encode("utf-8")
|
|
1329
|
+
).hexdigest()[:20]
|
|
1330
|
+
owner = module_functions.get(function_node_id, {})
|
|
1331
|
+
identity = module_identities.get(owner.get("module_id"), {})
|
|
1332
|
+
source = module_sources.get(owner.get("module_id"), {})
|
|
1333
|
+
if (
|
|
1334
|
+
not isinstance(owner.get("module_id"), str)
|
|
1335
|
+
or not owner.get("module_id")
|
|
1336
|
+
or owner.get("document_id") != identity.get("document_id")
|
|
1337
|
+
or owner.get("document_id") != source.get("document_id")
|
|
1338
|
+
or identity.get("logical_name") != source.get("logical_name")
|
|
1339
|
+
or function_node_id not in identity.get("function_node_ids", ())
|
|
1340
|
+
or function_node_id not in source.get("function_node_ids", ())
|
|
1341
|
+
):
|
|
1342
|
+
return False, "Phase 14 numeric ownership evidence is inconsistent"
|
|
1343
|
+
expected = {
|
|
1344
|
+
"operation_id": operation_id,
|
|
1345
|
+
"binop_node_id": node_id,
|
|
1346
|
+
"function_node_id": function_node_id,
|
|
1347
|
+
"module_id": owner.get("module_id"),
|
|
1348
|
+
"document_id": owner.get("document_id"),
|
|
1349
|
+
"logical_name": source.get("logical_name"),
|
|
1350
|
+
"operator_node_id": operator["node_id"],
|
|
1351
|
+
"operator_kind": operation_kind,
|
|
1352
|
+
"left_node_id": left_id,
|
|
1353
|
+
"right_node_id": right_id,
|
|
1354
|
+
"left_category": "integer-like",
|
|
1355
|
+
"right_category": "integer-like",
|
|
1356
|
+
"result_category": "integer-like",
|
|
1357
|
+
"left_c_type": "int64_t",
|
|
1358
|
+
"right_c_type": "int64_t",
|
|
1359
|
+
"result_c_type": "int64_t",
|
|
1360
|
+
"divisor_value": divisor,
|
|
1361
|
+
"divisor_literal_node_ids": list(literal_ids),
|
|
1362
|
+
"literal_shape": literal_shape,
|
|
1363
|
+
"divisor_in_admitted_domain": True,
|
|
1364
|
+
"divisor_nonzero_proved": True,
|
|
1365
|
+
"negative_one_divisor_excluded": True,
|
|
1366
|
+
"minimum_signed_divisor_excluded": True,
|
|
1367
|
+
"helper_requirement": helper,
|
|
1368
|
+
"evaluation_order": [left_id, right_id],
|
|
1369
|
+
"operands_evaluated_once": True,
|
|
1370
|
+
"c_type": "int64_t",
|
|
1371
|
+
"failure_policy": "caller-proved-no-runtime-failure-channel",
|
|
1372
|
+
"support_state": "SupportedWithHelper",
|
|
1373
|
+
"parameter_ownership": "scalar-values-by-value",
|
|
1374
|
+
"result_ownership": "scalar-value-by-value",
|
|
1375
|
+
"allocation_model": "none",
|
|
1376
|
+
"cleanup_model": "none",
|
|
1377
|
+
"runtime_failure_channel": "none",
|
|
1378
|
+
"target_contract": "c11-portable-fixed-v1",
|
|
1379
|
+
}
|
|
1380
|
+
if fact != expected:
|
|
1381
|
+
return False, "Phase 14 numeric fact disagrees with independent proof"
|
|
1382
|
+
if not ({node_id, function_node_id, operator["node_id"], left_id, *literal_ids}).issubset(
|
|
1383
|
+
provenance_by_node[node_id]
|
|
1384
|
+
):
|
|
1385
|
+
return False, "Phase 14 numeric fact provenance is incomplete"
|
|
1386
|
+
node_plans = [item for item in plans if item.get("source_node_id") == node_id]
|
|
1387
|
+
if len(node_plans) != 1:
|
|
1388
|
+
return False, "Phase 14 numeric operation lacks exactly one RulePlan"
|
|
1389
|
+
plan = node_plans[0]
|
|
1390
|
+
decision = support.get(node_id)
|
|
1391
|
+
expected_facts = {
|
|
1392
|
+
f"numeric-operation:{node_id}",
|
|
1393
|
+
f"numeric-divisor:{divisor}",
|
|
1394
|
+
f"numeric-helper:{helper}",
|
|
1395
|
+
"numeric-target:c11-portable-fixed-v1",
|
|
1396
|
+
"value-category:integer-like",
|
|
1397
|
+
}
|
|
1398
|
+
expected_explanation = [
|
|
1399
|
+
"selected",
|
|
1400
|
+
"phase14.numeric.floor_arithmetic",
|
|
1401
|
+
"for",
|
|
1402
|
+
"BinOp",
|
|
1403
|
+
operation_kind,
|
|
1404
|
+
"via",
|
|
1405
|
+
helper,
|
|
1406
|
+
]
|
|
1407
|
+
if (
|
|
1408
|
+
plan.get("rule_id") != "phase14.numeric.floor_arithmetic"
|
|
1409
|
+
or plan.get("rule_version") != "0.14"
|
|
1410
|
+
or plan.get("support_state") != "SupportedWithHelper"
|
|
1411
|
+
or plan.get("helper_requirements") != [helper]
|
|
1412
|
+
or set(plan.get("facts_used", ())) != expected_facts
|
|
1413
|
+
or plan.get("semantic_obligations") != numeric_obligations
|
|
1414
|
+
or plan.get("resolved_obligations") != numeric_obligations
|
|
1415
|
+
or plan.get("unresolved_obligations") != []
|
|
1416
|
+
or plan.get("explanation_tokens") != expected_explanation
|
|
1417
|
+
or not isinstance(decision, dict)
|
|
1418
|
+
or decision.get("state") != "SupportedWithHelper"
|
|
1419
|
+
or decision.get("rule_plan_id") != plan.get("plan_id")
|
|
1420
|
+
):
|
|
1421
|
+
return False, "Phase 14 numeric RulePlan does not close its helper proof"
|
|
1422
|
+
return True, ""
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def validate_analysis_payload(
|
|
1426
|
+
payload: dict[str, Any],
|
|
1427
|
+
cancellation: Any = None,
|
|
1428
|
+
) -> tuple[bool, str]:
|
|
1429
|
+
if not isinstance(payload, dict):
|
|
1430
|
+
return False, "analysis payload is malformed"
|
|
1431
|
+
schema = payload.get("schema_version")
|
|
1432
|
+
if schema not in {
|
|
1433
|
+
"conversion-plan/0.5",
|
|
1434
|
+
"conversion-plan/0.9",
|
|
1435
|
+
"conversion-plan/0.11",
|
|
1436
|
+
PHASE12_CONVERSION_PLAN_SCHEMA,
|
|
1437
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
1438
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
1439
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
1440
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
1441
|
+
CONVERSION_PLAN_SCHEMA,
|
|
1442
|
+
}:
|
|
1443
|
+
return False, "unsupported conversion-plan schema"
|
|
1444
|
+
tables = payload.get("fact_tables")
|
|
1445
|
+
if not isinstance(tables, list) or any(
|
|
1446
|
+
not isinstance(table, dict) for table in tables
|
|
1447
|
+
):
|
|
1448
|
+
return False, "fact tables are malformed"
|
|
1449
|
+
table_ids = [table.get("table_id") for table in tables]
|
|
1450
|
+
if any(not isinstance(table_id, str) or not table_id for table_id in table_ids):
|
|
1451
|
+
return False, "fact-table IDs are malformed"
|
|
1452
|
+
if len(table_ids) != len(set(table_ids)):
|
|
1453
|
+
return False, "duplicate fact-table IDs"
|
|
1454
|
+
for table in tables:
|
|
1455
|
+
required = {"schema_version", "table_id", "producer_stage", "key_domain", "completeness", "invalidation_dependencies", "records"}
|
|
1456
|
+
if not required.issubset(table):
|
|
1457
|
+
return False, "fact table omits required contract fields"
|
|
1458
|
+
records = table["records"]
|
|
1459
|
+
if (
|
|
1460
|
+
not all(
|
|
1461
|
+
isinstance(table.get(field), str) and table.get(field)
|
|
1462
|
+
for field in (
|
|
1463
|
+
"schema_version",
|
|
1464
|
+
"producer_stage",
|
|
1465
|
+
"key_domain",
|
|
1466
|
+
"completeness",
|
|
1467
|
+
)
|
|
1468
|
+
)
|
|
1469
|
+
or not isinstance(table.get("invalidation_dependencies"), list)
|
|
1470
|
+
):
|
|
1471
|
+
return False, "fact-table contract fields are malformed"
|
|
1472
|
+
if not isinstance(records, list) or any(
|
|
1473
|
+
not isinstance(record, dict) for record in records
|
|
1474
|
+
):
|
|
1475
|
+
return False, "fact-table records are malformed"
|
|
1476
|
+
keys = [record.get("key") for record in records]
|
|
1477
|
+
if any(not isinstance(key, str) or not key for key in keys):
|
|
1478
|
+
return False, "fact record keys are malformed"
|
|
1479
|
+
if any(left >= right for left, right in zip(keys, keys[1:])):
|
|
1480
|
+
return False, f"fact table {table['table_id']} keys are not unique and sorted"
|
|
1481
|
+
if any(
|
|
1482
|
+
not isinstance(record.get("provenance"), dict) for record in records
|
|
1483
|
+
):
|
|
1484
|
+
return False, "fact record without provenance"
|
|
1485
|
+
plans = payload.get("rule_plans")
|
|
1486
|
+
if not isinstance(plans, list) or any(
|
|
1487
|
+
not isinstance(plan, dict) for plan in plans
|
|
1488
|
+
):
|
|
1489
|
+
return False, "RulePlans are malformed"
|
|
1490
|
+
plan_ids = [plan.get("plan_id") for plan in plans]
|
|
1491
|
+
if any(not isinstance(plan_id, str) or not plan_id for plan_id in plan_ids):
|
|
1492
|
+
return False, "RulePlan IDs are malformed"
|
|
1493
|
+
if len(plan_ids) != len(set(plan_ids)):
|
|
1494
|
+
return False, "duplicate RulePlan IDs"
|
|
1495
|
+
plan_by_id = {plan["plan_id"]: plan for plan in plans}
|
|
1496
|
+
decisions = payload.get("support_decisions")
|
|
1497
|
+
if not isinstance(decisions, list) or any(
|
|
1498
|
+
not isinstance(item, dict) for item in decisions
|
|
1499
|
+
):
|
|
1500
|
+
return False, "support decisions are malformed"
|
|
1501
|
+
decision_keys = [item.get("decision_key") for item in decisions]
|
|
1502
|
+
if any(not isinstance(key, str) or not key for key in decision_keys):
|
|
1503
|
+
return False, "support decision keys are malformed"
|
|
1504
|
+
if len(decision_keys) != len(set(decision_keys)):
|
|
1505
|
+
return False, "duplicate support decision keys"
|
|
1506
|
+
for decision in decisions:
|
|
1507
|
+
state = decision.get("state")
|
|
1508
|
+
if not isinstance(state, str):
|
|
1509
|
+
return False, "support decision state is malformed"
|
|
1510
|
+
plan_id = decision.get("rule_plan_id")
|
|
1511
|
+
if plan_id is not None and not isinstance(plan_id, str):
|
|
1512
|
+
return False, "support decision RulePlan identity is malformed"
|
|
1513
|
+
if state.startswith("Supported") and plan_id not in plan_by_id:
|
|
1514
|
+
return False, "supported decision lacks exactly one RulePlan"
|
|
1515
|
+
if not state.startswith("Supported") and plan_id is not None:
|
|
1516
|
+
return False, "unsupported decision references RulePlan"
|
|
1517
|
+
if plan_id is not None:
|
|
1518
|
+
plan = plan_by_id[plan_id]
|
|
1519
|
+
if plan.get("decision_key") != decision.get("decision_key") or plan.get("source_node_id") != decision.get("node_id"):
|
|
1520
|
+
return False, "support decision and RulePlan identity disagree"
|
|
1521
|
+
for plan in plans:
|
|
1522
|
+
unresolved = plan.get("unresolved_obligations")
|
|
1523
|
+
semantic = plan.get("semantic_obligations")
|
|
1524
|
+
resolved = plan.get("resolved_obligations")
|
|
1525
|
+
if not all(
|
|
1526
|
+
isinstance(value, list) for value in (unresolved, semantic, resolved)
|
|
1527
|
+
) or any(
|
|
1528
|
+
not isinstance(item, str)
|
|
1529
|
+
for value in (unresolved, semantic, resolved)
|
|
1530
|
+
for item in value
|
|
1531
|
+
):
|
|
1532
|
+
return False, "RulePlan obligations are malformed"
|
|
1533
|
+
if unresolved:
|
|
1534
|
+
return False, "published RulePlan has unresolved obligations"
|
|
1535
|
+
if sorted(semantic) != sorted(resolved):
|
|
1536
|
+
return False, "RulePlan obligations are not closed"
|
|
1537
|
+
plan_helpers = plan.get("helper_requirements", [])
|
|
1538
|
+
if not isinstance(plan_helpers, list) or any(
|
|
1539
|
+
not isinstance(item, str) for item in plan_helpers
|
|
1540
|
+
):
|
|
1541
|
+
return False, "RulePlan helper requirements are malformed"
|
|
1542
|
+
if any(
|
|
1543
|
+
left >= right
|
|
1544
|
+
for left, right in zip(plan_helpers, plan_helpers[1:])
|
|
1545
|
+
) or any(
|
|
1546
|
+
not isinstance(item, str) or not _HELPER_REQUIREMENT.fullmatch(item)
|
|
1547
|
+
for item in plan_helpers
|
|
1548
|
+
):
|
|
1549
|
+
return False, "RulePlan helper requirements are not exact, unique, and sorted"
|
|
1550
|
+
if plan.get("support_state") == "SupportedWithHelper" and not plan_helpers:
|
|
1551
|
+
return False, "helper-backed RulePlan lacks a helper requirement"
|
|
1552
|
+
helper_requirements = payload.get("helper_requirements")
|
|
1553
|
+
if not isinstance(helper_requirements, list) or any(
|
|
1554
|
+
not isinstance(item, str) for item in helper_requirements
|
|
1555
|
+
):
|
|
1556
|
+
return False, "conversion-plan helper requirements are malformed"
|
|
1557
|
+
expected_helpers = sorted(
|
|
1558
|
+
{
|
|
1559
|
+
item
|
|
1560
|
+
for plan in plans
|
|
1561
|
+
for item in plan.get("helper_requirements", [])
|
|
1562
|
+
}
|
|
1563
|
+
)
|
|
1564
|
+
if helper_requirements != expected_helpers:
|
|
1565
|
+
return False, "conversion-plan helper requirements disagree with RulePlans"
|
|
1566
|
+
generated_names = payload.get("generated_name_plans")
|
|
1567
|
+
if not isinstance(generated_names, list) or any(
|
|
1568
|
+
not isinstance(item, dict)
|
|
1569
|
+
or not isinstance(item.get("generated_name"), str)
|
|
1570
|
+
for item in generated_names
|
|
1571
|
+
):
|
|
1572
|
+
return False, "generated-name plans are malformed"
|
|
1573
|
+
names = [item["generated_name"] for item in generated_names]
|
|
1574
|
+
if len(names) != len(set(names)):
|
|
1575
|
+
return False, "generated name collision"
|
|
1576
|
+
representation_values = payload.get("representation_plans")
|
|
1577
|
+
if not isinstance(representation_values, list) or any(
|
|
1578
|
+
not isinstance(item, dict)
|
|
1579
|
+
or not isinstance(item.get("decision_key"), str)
|
|
1580
|
+
for item in representation_values
|
|
1581
|
+
):
|
|
1582
|
+
return False, "representation plans are malformed"
|
|
1583
|
+
representations = {
|
|
1584
|
+
item["decision_key"]: item for item in representation_values
|
|
1585
|
+
}
|
|
1586
|
+
non_value_rules = {
|
|
1587
|
+
"phase8.if.control",
|
|
1588
|
+
"phase8.while.control",
|
|
1589
|
+
"phase8.for.range",
|
|
1590
|
+
"phase8.break.control",
|
|
1591
|
+
"phase8.continue.control",
|
|
1592
|
+
"phase8.range.bound",
|
|
1593
|
+
"phase11.container.for.bounded",
|
|
1594
|
+
"phase12.module.import_from",
|
|
1595
|
+
"phase12.module.document",
|
|
1596
|
+
"phase12.module.imported_binding",
|
|
1597
|
+
"phase12.module.initialization",
|
|
1598
|
+
"phase12.module.bundle_assembly",
|
|
1599
|
+
"phase13.record.class",
|
|
1600
|
+
"phase13.record.field",
|
|
1601
|
+
"phase13.record.initializer",
|
|
1602
|
+
"phase13.record.binding",
|
|
1603
|
+
}
|
|
1604
|
+
for decision in decisions:
|
|
1605
|
+
if decision["state"].startswith("Supported"):
|
|
1606
|
+
representation = representations.get(decision["decision_key"])
|
|
1607
|
+
plan = plan_by_id[decision["rule_plan_id"]]
|
|
1608
|
+
if not representation or (plan.get("rule_id") not in non_value_rules and (representation.get("c_type") is None or representation.get("unresolved_obligations"))):
|
|
1609
|
+
return False, "supported decision lacks a closed representation plan"
|
|
1610
|
+
if schema in {
|
|
1611
|
+
"conversion-plan/0.9",
|
|
1612
|
+
"conversion-plan/0.11",
|
|
1613
|
+
PHASE12_CONVERSION_PLAN_SCHEMA,
|
|
1614
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
1615
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
1616
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
1617
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
1618
|
+
CONVERSION_PLAN_SCHEMA,
|
|
1619
|
+
}:
|
|
1620
|
+
required_tables = {"function-signature-facts", "call-target-facts", "return-path-facts", "local-declaration-facts", "call-graph-facts"}
|
|
1621
|
+
if not required_tables.issubset(table_ids):
|
|
1622
|
+
return False, "Phase 9 conversion plan omits required function/call fact tables"
|
|
1623
|
+
for field in ("target_contract", "semantic_policy", "rule_set_version", "renderer_version", "helper_policy_version"):
|
|
1624
|
+
if not isinstance(payload.get(field), str) or not payload[field]:
|
|
1625
|
+
return False, f"Phase 9 conversion plan omits {field}"
|
|
1626
|
+
if schema in {
|
|
1627
|
+
"conversion-plan/0.11",
|
|
1628
|
+
PHASE12_CONVERSION_PLAN_SCHEMA,
|
|
1629
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
1630
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
1631
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
1632
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
1633
|
+
CONVERSION_PLAN_SCHEMA,
|
|
1634
|
+
}:
|
|
1635
|
+
required_tables = {
|
|
1636
|
+
"container-shape-facts",
|
|
1637
|
+
"container-binding-facts",
|
|
1638
|
+
"container-access-facts",
|
|
1639
|
+
"container-iteration-facts",
|
|
1640
|
+
}
|
|
1641
|
+
if not required_tables.issubset(table_ids):
|
|
1642
|
+
return False, "Phase 11 conversion plan omits required container fact tables"
|
|
1643
|
+
if not isinstance(payload.get("container_policy_version"), str) or not payload["container_policy_version"]:
|
|
1644
|
+
return False, "Phase 11 conversion plan omits container_policy_version"
|
|
1645
|
+
records = {
|
|
1646
|
+
table["table_id"]: {
|
|
1647
|
+
record["key"]: record["value"]
|
|
1648
|
+
for record in table["records"]
|
|
1649
|
+
}
|
|
1650
|
+
for table in tables
|
|
1651
|
+
if table["table_id"] in required_tables
|
|
1652
|
+
}
|
|
1653
|
+
bindings = records["container-binding-facts"]
|
|
1654
|
+
for access in records["container-access-facts"].values():
|
|
1655
|
+
binding = bindings.get(access.get("binding_id"))
|
|
1656
|
+
if access.get("supported") and binding is None:
|
|
1657
|
+
return False, "supported container access references an absent binding fact"
|
|
1658
|
+
if access.get("supported") and not binding.get("valid"):
|
|
1659
|
+
return False, "supported container access references an invalid binding fact"
|
|
1660
|
+
if access.get("supported") and not isinstance(access.get("resolved_offset"), int):
|
|
1661
|
+
return False, "supported container access lacks a resolved offset"
|
|
1662
|
+
for iteration in records["container-iteration-facts"].values():
|
|
1663
|
+
binding = bindings.get(iteration.get("binding_id"))
|
|
1664
|
+
if iteration.get("supported") and binding is None:
|
|
1665
|
+
return False, "supported container iteration references an absent binding fact"
|
|
1666
|
+
if iteration.get("supported") and not binding.get("valid"):
|
|
1667
|
+
return False, "supported container iteration references an invalid binding fact"
|
|
1668
|
+
if iteration.get("supported") and (
|
|
1669
|
+
not isinstance(iteration.get("capacity"), int)
|
|
1670
|
+
or iteration["capacity"] <= 0
|
|
1671
|
+
):
|
|
1672
|
+
return False, "supported container iteration lacks a positive fixed bound"
|
|
1673
|
+
if schema in {
|
|
1674
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
1675
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
1676
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
1677
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
1678
|
+
CONVERSION_PLAN_SCHEMA,
|
|
1679
|
+
}:
|
|
1680
|
+
valid, reason = _validate_record_fact_tables(payload, tables)
|
|
1681
|
+
if not valid:
|
|
1682
|
+
return valid, reason
|
|
1683
|
+
if schema in {PHASE14A_CONVERSION_PLAN_SCHEMA, PHASE14B_CONVERSION_PLAN_SCHEMA, PHASE14C_CONVERSION_PLAN_SCHEMA, CONVERSION_PLAN_SCHEMA}:
|
|
1684
|
+
valid, reason = _validate_numeric_fact_table(payload, tables)
|
|
1685
|
+
if not valid:
|
|
1686
|
+
return valid, reason
|
|
1687
|
+
if schema in {PHASE14B_CONVERSION_PLAN_SCHEMA, PHASE14C_CONVERSION_PLAN_SCHEMA, CONVERSION_PLAN_SCHEMA}:
|
|
1688
|
+
valid, reason = validate_conditional_region_facts(
|
|
1689
|
+
payload,
|
|
1690
|
+
tables,
|
|
1691
|
+
expected_fact_schema=CONDITIONAL_FACT_SCHEMA,
|
|
1692
|
+
require_table=True,
|
|
1693
|
+
cancellation=cancellation,
|
|
1694
|
+
)
|
|
1695
|
+
if not valid:
|
|
1696
|
+
return valid, reason
|
|
1697
|
+
if schema in {PHASE14C_CONVERSION_PLAN_SCHEMA, CONVERSION_PLAN_SCHEMA}:
|
|
1698
|
+
cumulatively_ineligible_targets = (
|
|
1699
|
+
_cumulatively_ineligible_target_function_node_ids(
|
|
1700
|
+
payload,
|
|
1701
|
+
tables,
|
|
1702
|
+
cancellation,
|
|
1703
|
+
)
|
|
1704
|
+
)
|
|
1705
|
+
valid, reason = validate_keyword_call_binding_facts(
|
|
1706
|
+
payload,
|
|
1707
|
+
tables,
|
|
1708
|
+
expected_fact_schema=KEYWORD_CALL_FACT_SCHEMA,
|
|
1709
|
+
require_table=True,
|
|
1710
|
+
require_plans=True,
|
|
1711
|
+
cumulatively_ineligible_target_function_node_ids=(
|
|
1712
|
+
cumulatively_ineligible_targets
|
|
1713
|
+
),
|
|
1714
|
+
cancellation=cancellation,
|
|
1715
|
+
)
|
|
1716
|
+
if not valid:
|
|
1717
|
+
return valid, reason
|
|
1718
|
+
if schema == CONVERSION_PLAN_SCHEMA:
|
|
1719
|
+
valid, reason = validate_keyword_only_call_binding_facts(
|
|
1720
|
+
payload,
|
|
1721
|
+
tables,
|
|
1722
|
+
expected_fact_schema=KEYWORD_ONLY_CALL_FACT_SCHEMA,
|
|
1723
|
+
require_table=True,
|
|
1724
|
+
require_plans=True,
|
|
1725
|
+
cumulatively_ineligible_target_function_node_ids=(
|
|
1726
|
+
cumulatively_ineligible_targets
|
|
1727
|
+
),
|
|
1728
|
+
cancellation=cancellation,
|
|
1729
|
+
)
|
|
1730
|
+
if not valid:
|
|
1731
|
+
return valid, reason
|
|
1732
|
+
if schema in {
|
|
1733
|
+
PHASE12_CONVERSION_PLAN_SCHEMA,
|
|
1734
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
1735
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
1736
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
1737
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
1738
|
+
CONVERSION_PLAN_SCHEMA,
|
|
1739
|
+
}:
|
|
1740
|
+
required_module_tables = {
|
|
1741
|
+
"module-identity-facts",
|
|
1742
|
+
"module-import-facts",
|
|
1743
|
+
"module-function-facts",
|
|
1744
|
+
"module-initialization-facts",
|
|
1745
|
+
"module-namespace-facts",
|
|
1746
|
+
"module-source-facts",
|
|
1747
|
+
}
|
|
1748
|
+
if not required_module_tables.issubset(table_ids):
|
|
1749
|
+
return False, "Phase 12 conversion plan omits required module fact tables"
|
|
1750
|
+
expected_module_policy = (
|
|
1751
|
+
DEFAULT_MODULE_POLICY
|
|
1752
|
+
if schema in {
|
|
1753
|
+
PHASE13_CONVERSION_PLAN_SCHEMA,
|
|
1754
|
+
PHASE14A_CONVERSION_PLAN_SCHEMA,
|
|
1755
|
+
PHASE14B_CONVERSION_PLAN_SCHEMA,
|
|
1756
|
+
PHASE14C_CONVERSION_PLAN_SCHEMA,
|
|
1757
|
+
CONVERSION_PLAN_SCHEMA,
|
|
1758
|
+
}
|
|
1759
|
+
else PHASE12_MODULE_POLICY
|
|
1760
|
+
)
|
|
1761
|
+
if payload.get("module_policy_version") != expected_module_policy:
|
|
1762
|
+
return False, "module conversion plan has an unknown module_policy_version"
|
|
1763
|
+
module_tables = {table["table_id"]: table for table in tables if table["table_id"] in required_module_tables}
|
|
1764
|
+
if any(
|
|
1765
|
+
table.get("schema_version") != MODULE_FACT_SCHEMA
|
|
1766
|
+
or table.get("completeness") != "complete"
|
|
1767
|
+
or table.get("producer_stage") != "modules.resolve"
|
|
1768
|
+
for table in module_tables.values()
|
|
1769
|
+
):
|
|
1770
|
+
return False, "Phase 12 module fact table contract identity is invalid"
|
|
1771
|
+
module_key_fields = {
|
|
1772
|
+
"module-identity-facts": "module_id",
|
|
1773
|
+
"module-import-facts": "import_item_id",
|
|
1774
|
+
"module-function-facts": "function_node_id",
|
|
1775
|
+
"module-initialization-facts": "initialization_node_id",
|
|
1776
|
+
"module-namespace-facts": "module_id",
|
|
1777
|
+
"module-source-facts": "module_id",
|
|
1778
|
+
}
|
|
1779
|
+
for table_id, table in module_tables.items():
|
|
1780
|
+
key_field = module_key_fields[table_id]
|
|
1781
|
+
if any(
|
|
1782
|
+
not isinstance(record.get("value"), dict)
|
|
1783
|
+
or record.get("key") != record["value"].get(key_field)
|
|
1784
|
+
for record in table["records"]
|
|
1785
|
+
):
|
|
1786
|
+
return False, "Phase 12 module fact record key disagrees with its value identity"
|
|
1787
|
+
values = {
|
|
1788
|
+
table_id: [record["value"] for record in table["records"]]
|
|
1789
|
+
for table_id, table in module_tables.items()
|
|
1790
|
+
}
|
|
1791
|
+
identities = values["module-identity-facts"]
|
|
1792
|
+
module_id_values = [item.get("module_id") for item in identities]
|
|
1793
|
+
module_ids = set(module_id_values)
|
|
1794
|
+
if (
|
|
1795
|
+
not identities
|
|
1796
|
+
or any(not isinstance(item, str) or not item for item in module_id_values)
|
|
1797
|
+
or len(module_ids) != len(identities)
|
|
1798
|
+
):
|
|
1799
|
+
return False, "Phase 12 module identities are incomplete or duplicated"
|
|
1800
|
+
bundle_ordinal_values = [item.get("bundle_ordinal") for item in identities]
|
|
1801
|
+
if any(
|
|
1802
|
+
not isinstance(item, int) or isinstance(item, bool) or item < 0
|
|
1803
|
+
for item in bundle_ordinal_values
|
|
1804
|
+
):
|
|
1805
|
+
return False, "Phase 12 bundle ordinals or primary identity are invalid"
|
|
1806
|
+
bundle_ordinals = sorted(bundle_ordinal_values)
|
|
1807
|
+
if (
|
|
1808
|
+
bundle_ordinals != list(range(len(identities)))
|
|
1809
|
+
or any(not isinstance(item.get("is_primary"), bool) for item in identities)
|
|
1810
|
+
or any(
|
|
1811
|
+
item.get("is_primary") is not (item.get("bundle_ordinal") == 0)
|
|
1812
|
+
for item in identities
|
|
1813
|
+
)
|
|
1814
|
+
):
|
|
1815
|
+
return False, "Phase 12 bundle ordinals or primary identity are invalid"
|
|
1816
|
+
identity_by_module = {item["module_id"]: item for item in identities}
|
|
1817
|
+
primary_module_id = next(
|
|
1818
|
+
item["module_id"] for item in identities if item["is_primary"]
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
module_bundle = payload.get("module_bundle")
|
|
1822
|
+
bundle_documents = (
|
|
1823
|
+
module_bundle.get("documents") if isinstance(module_bundle, dict) else None
|
|
1824
|
+
)
|
|
1825
|
+
if (
|
|
1826
|
+
not isinstance(module_bundle, dict)
|
|
1827
|
+
or module_bundle.get("schema_version") != "python-ir-bundle/0.4"
|
|
1828
|
+
or module_bundle.get("primary_module_id") != primary_module_id
|
|
1829
|
+
or not isinstance(bundle_documents, list)
|
|
1830
|
+
or len(bundle_documents) != len(identities)
|
|
1831
|
+
):
|
|
1832
|
+
return False, "Phase 12 module identities disagree with the closed source bundle"
|
|
1833
|
+
source_module_order = [item.get("module_id") for item in bundle_documents]
|
|
1834
|
+
expected_source_module_order = [
|
|
1835
|
+
item["module_id"]
|
|
1836
|
+
for item in sorted(identities, key=lambda item: item["bundle_ordinal"])
|
|
1837
|
+
]
|
|
1838
|
+
if source_module_order != expected_source_module_order:
|
|
1839
|
+
return False, "Phase 12 source-bundle order disagrees with bundle ordinals"
|
|
1840
|
+
bundle_by_module = {
|
|
1841
|
+
item.get("module_id"): item
|
|
1842
|
+
for item in bundle_documents
|
|
1843
|
+
if isinstance(item, dict)
|
|
1844
|
+
}
|
|
1845
|
+
if set(bundle_by_module) != module_ids:
|
|
1846
|
+
return False, "Phase 12 module identities disagree with the closed source bundle"
|
|
1847
|
+
|
|
1848
|
+
original_nodes_by_module: dict[str, dict[str, dict[str, Any]]] = {}
|
|
1849
|
+
original_import_items: dict[str, list[dict[str, Any]]] = {}
|
|
1850
|
+
for module_id in expected_source_module_order:
|
|
1851
|
+
identity = identity_by_module[module_id]
|
|
1852
|
+
document = bundle_by_module[module_id]
|
|
1853
|
+
if any(
|
|
1854
|
+
document.get(field) != identity.get(field)
|
|
1855
|
+
for field in (
|
|
1856
|
+
"document_id",
|
|
1857
|
+
"logical_name",
|
|
1858
|
+
"bundle_ordinal",
|
|
1859
|
+
"is_primary",
|
|
1860
|
+
)
|
|
1861
|
+
):
|
|
1862
|
+
return False, "Phase 12 module identity fields disagree with the closed source bundle"
|
|
1863
|
+
python_ir = document.get("python_ir")
|
|
1864
|
+
if (
|
|
1865
|
+
not isinstance(python_ir, dict)
|
|
1866
|
+
or python_ir.get("document_id") != identity.get("document_id")
|
|
1867
|
+
or not isinstance(python_ir.get("nodes"), list)
|
|
1868
|
+
):
|
|
1869
|
+
return False, "Phase 12 module identity lacks its exact source Python IR"
|
|
1870
|
+
node_values = python_ir["nodes"]
|
|
1871
|
+
nodes = {
|
|
1872
|
+
node.get("node_id"): node
|
|
1873
|
+
for node in node_values
|
|
1874
|
+
if isinstance(node, dict) and isinstance(node.get("node_id"), str)
|
|
1875
|
+
}
|
|
1876
|
+
if len(nodes) != len(node_values):
|
|
1877
|
+
return False, "Phase 12 source Python IR has missing or duplicate node identities"
|
|
1878
|
+
root = nodes.get(python_ir.get("root_node_id"))
|
|
1879
|
+
body = root.get("fields", {}).get("body") if root else None
|
|
1880
|
+
if not isinstance(body, list):
|
|
1881
|
+
return False, "Phase 12 source Python IR lacks an ordered module body"
|
|
1882
|
+
import_node_ids = [
|
|
1883
|
+
node_id for node_id in body if nodes.get(node_id, {}).get("kind") == "ImportFrom"
|
|
1884
|
+
]
|
|
1885
|
+
function_node_ids = [
|
|
1886
|
+
node_id for node_id in body if nodes.get(node_id, {}).get("kind") == "FunctionDef"
|
|
1887
|
+
]
|
|
1888
|
+
if (
|
|
1889
|
+
not function_node_ids
|
|
1890
|
+
or identity.get("import_node_ids") != import_node_ids
|
|
1891
|
+
or identity.get("function_node_ids") != function_node_ids
|
|
1892
|
+
):
|
|
1893
|
+
return False, "Phase 12 module ownership lists disagree with source order"
|
|
1894
|
+
original_nodes_by_module[module_id] = nodes
|
|
1895
|
+
import_items: list[dict[str, Any]] = []
|
|
1896
|
+
source_ordinal = 0
|
|
1897
|
+
for import_node_id in import_node_ids:
|
|
1898
|
+
import_node = nodes[import_node_id]
|
|
1899
|
+
target_module_id = import_node.get("fields", {}).get("module")
|
|
1900
|
+
alias_ids = import_node.get("fields", {}).get("names")
|
|
1901
|
+
if not isinstance(target_module_id, str) or not isinstance(alias_ids, list):
|
|
1902
|
+
return False, "Phase 12 source import lacks an exact ordered alias list"
|
|
1903
|
+
for alias_node_id in alias_ids:
|
|
1904
|
+
alias = nodes.get(alias_node_id)
|
|
1905
|
+
if alias is None or alias.get("kind") != "alias":
|
|
1906
|
+
return False, "Phase 12 source import references an absent alias node"
|
|
1907
|
+
imported_name = alias.get("fields", {}).get("name")
|
|
1908
|
+
asname = alias.get("fields", {}).get("asname")
|
|
1909
|
+
if not isinstance(imported_name, str) or not imported_name:
|
|
1910
|
+
return False, "Phase 12 source import has an invalid imported spelling"
|
|
1911
|
+
import_items.append(
|
|
1912
|
+
{
|
|
1913
|
+
"import_node_id": import_node_id,
|
|
1914
|
+
"alias_node_id": alias_node_id,
|
|
1915
|
+
"target_module_id": target_module_id,
|
|
1916
|
+
"imported_name": imported_name,
|
|
1917
|
+
"local_name": asname or imported_name,
|
|
1918
|
+
"source_ordinal": source_ordinal,
|
|
1919
|
+
}
|
|
1920
|
+
)
|
|
1921
|
+
source_ordinal += 1
|
|
1922
|
+
original_import_items[module_id] = import_items
|
|
1923
|
+
|
|
1924
|
+
functions = values["module-function-facts"]
|
|
1925
|
+
function_by_node = {item.get("function_node_id"): item for item in functions}
|
|
1926
|
+
flattened_names = [item.get("flattened_name") for item in functions]
|
|
1927
|
+
significant_external_prefixes = [str(name)[:31] for name in flattened_names]
|
|
1928
|
+
function_ordinal_values = [item.get("bundle_function_ordinal") for item in functions]
|
|
1929
|
+
if any(
|
|
1930
|
+
not isinstance(item, int) or isinstance(item, bool) or item < 0
|
|
1931
|
+
for item in function_ordinal_values
|
|
1932
|
+
):
|
|
1933
|
+
return False, "Phase 12 module function identities, names, or order are invalid"
|
|
1934
|
+
function_ordinals = sorted(function_ordinal_values)
|
|
1935
|
+
if (
|
|
1936
|
+
not functions
|
|
1937
|
+
or None in function_by_node
|
|
1938
|
+
or len(function_by_node) != len(functions)
|
|
1939
|
+
or None in flattened_names
|
|
1940
|
+
or len(flattened_names) != len(set(flattened_names))
|
|
1941
|
+
or len(significant_external_prefixes) != len(set(significant_external_prefixes))
|
|
1942
|
+
or function_ordinals != list(range(len(functions)))
|
|
1943
|
+
):
|
|
1944
|
+
return False, "Phase 12 module function identities, names, or order are invalid"
|
|
1945
|
+
multi_document = len(module_ids) > 1
|
|
1946
|
+
for item in functions:
|
|
1947
|
+
identity = identity_by_module.get(item.get("module_id"))
|
|
1948
|
+
source_node = original_nodes_by_module.get(item.get("module_id"), {}).get(
|
|
1949
|
+
item.get("function_node_id")
|
|
1950
|
+
)
|
|
1951
|
+
source_name = (
|
|
1952
|
+
source_node.get("fields", {}).get("name") if source_node else None
|
|
1953
|
+
)
|
|
1954
|
+
expected_name = (
|
|
1955
|
+
_module_function_name(item["module_id"], source_name)
|
|
1956
|
+
if multi_document and identity is not None and isinstance(source_name, str)
|
|
1957
|
+
else source_name
|
|
1958
|
+
)
|
|
1959
|
+
if (
|
|
1960
|
+
identity is None
|
|
1961
|
+
or item.get("document_id") != identity.get("document_id")
|
|
1962
|
+
or source_node is None
|
|
1963
|
+
or source_node.get("kind") != "FunctionDef"
|
|
1964
|
+
or item.get("function_node_id") not in identity.get("function_node_ids", ())
|
|
1965
|
+
or item.get("source_name") != source_name
|
|
1966
|
+
or item.get("flattened_name") != expected_name
|
|
1967
|
+
or item.get("linkage") != "external"
|
|
1968
|
+
or item.get("module_generated_name") is not multi_document
|
|
1969
|
+
):
|
|
1970
|
+
return False, "Phase 12 module function linkage or namespace contract is invalid"
|
|
1971
|
+
|
|
1972
|
+
imports = values["module-import-facts"]
|
|
1973
|
+
import_edges: set[tuple[str, str]] = set()
|
|
1974
|
+
source_ordinals: dict[str, list[int]] = {}
|
|
1975
|
+
imports_by_module: dict[str, list[dict[str, Any]]] = {
|
|
1976
|
+
module_id: [] for module_id in module_ids
|
|
1977
|
+
}
|
|
1978
|
+
for value in imports:
|
|
1979
|
+
target = function_by_node.get(value.get("target_function_node_id"))
|
|
1980
|
+
importer = value.get("importer_module_id")
|
|
1981
|
+
target_module = value.get("target_module_id")
|
|
1982
|
+
if (
|
|
1983
|
+
value.get("supported") is not True
|
|
1984
|
+
or importer not in module_ids
|
|
1985
|
+
or target_module not in module_ids
|
|
1986
|
+
or importer == target_module
|
|
1987
|
+
or not value.get("import_node_id")
|
|
1988
|
+
or not value.get("alias_node_id")
|
|
1989
|
+
or not value.get("import_item_id")
|
|
1990
|
+
or not value.get("imported_name")
|
|
1991
|
+
or not value.get("local_name")
|
|
1992
|
+
or target is None
|
|
1993
|
+
or target.get("module_id") != target_module
|
|
1994
|
+
or target.get("source_name") != value.get("imported_name")
|
|
1995
|
+
):
|
|
1996
|
+
return False, "supported module import lacks an exact direct target"
|
|
1997
|
+
ordinal = value.get("source_ordinal")
|
|
1998
|
+
if not isinstance(ordinal, int) or isinstance(ordinal, bool) or ordinal < 0:
|
|
1999
|
+
return False, "supported module import lacks a normalized source ordinal"
|
|
2000
|
+
source_ordinals.setdefault(importer, []).append(ordinal)
|
|
2001
|
+
imports_by_module[importer].append(value)
|
|
2002
|
+
import_edges.add((importer, target_module))
|
|
2003
|
+
if any(sorted(ordinals) != list(range(len(ordinals))) for ordinals in source_ordinals.values()):
|
|
2004
|
+
return False, "Phase 12 import ordinals are not contiguous within the importer"
|
|
2005
|
+
for module_id in module_ids:
|
|
2006
|
+
published = sorted(
|
|
2007
|
+
imports_by_module[module_id], key=lambda item: item["source_ordinal"]
|
|
2008
|
+
)
|
|
2009
|
+
expected = original_import_items[module_id]
|
|
2010
|
+
if len(published) != len(expected):
|
|
2011
|
+
return False, "Phase 12 import facts do not cover every normalized source alias"
|
|
2012
|
+
for value, source_item in zip(published, expected):
|
|
2013
|
+
if any(value.get(field) != source_item[field] for field in source_item):
|
|
2014
|
+
return False, "Phase 12 import fact disagrees with its source alias"
|
|
2015
|
+
|
|
2016
|
+
initialization = module_tables["module-initialization-facts"]
|
|
2017
|
+
if len(initialization["records"]) != 1:
|
|
2018
|
+
return False, "Phase 12 conversion plan lacks one closed initialization-order fact"
|
|
2019
|
+
init_value = initialization["records"][0]["value"]
|
|
2020
|
+
if init_value.get("cycle_policy") != "reject-all-cycles":
|
|
2021
|
+
return False, "Phase 12 initialization fact lacks the closed cycle policy"
|
|
2022
|
+
if init_value.get("runtime_initialization") != "none":
|
|
2023
|
+
return False, "Phase 12 module profile cannot publish runtime initialization"
|
|
2024
|
+
module_order = init_value.get("module_order")
|
|
2025
|
+
if not isinstance(module_order, list) or len(module_order) != len(module_ids) or set(module_order) != module_ids:
|
|
2026
|
+
return False, "Phase 12 initialization order is not a permutation of bundle modules"
|
|
2027
|
+
declared_edges = init_value.get("dependency_edges")
|
|
2028
|
+
if not isinstance(declared_edges, list):
|
|
2029
|
+
return False, "Phase 12 initialization fact omits dependency edges"
|
|
2030
|
+
declared_edge_values = [
|
|
2031
|
+
(item.get("importer_module_id"), item.get("target_module_id"))
|
|
2032
|
+
for item in declared_edges
|
|
2033
|
+
if isinstance(item, dict)
|
|
2034
|
+
]
|
|
2035
|
+
edge_values = set(declared_edge_values)
|
|
2036
|
+
expected_edge_values = sorted(import_edges)
|
|
2037
|
+
if (
|
|
2038
|
+
len(declared_edge_values) != len(declared_edges)
|
|
2039
|
+
or declared_edge_values != expected_edge_values
|
|
2040
|
+
):
|
|
2041
|
+
return False, "Phase 12 initialization edges disagree with resolved imports"
|
|
2042
|
+
expected_module_order = _dependency_first_order(module_ids, edge_values)
|
|
2043
|
+
if expected_module_order is None or module_order != expected_module_order:
|
|
2044
|
+
return False, "Phase 12 initialization order violates deterministic dependency ordering"
|
|
2045
|
+
|
|
2046
|
+
expected_function_ids = [
|
|
2047
|
+
function_node_id
|
|
2048
|
+
for module_id in module_order
|
|
2049
|
+
for function_node_id in identity_by_module[module_id]["function_node_ids"]
|
|
2050
|
+
]
|
|
2051
|
+
published_function_ids = [
|
|
2052
|
+
item["function_node_id"]
|
|
2053
|
+
for item in sorted(functions, key=lambda item: item["bundle_function_ordinal"])
|
|
2054
|
+
]
|
|
2055
|
+
if published_function_ids != expected_function_ids:
|
|
2056
|
+
return False, "Phase 12 bundle function order disagrees with module and source order"
|
|
2057
|
+
|
|
2058
|
+
sources = values["module-source-facts"]
|
|
2059
|
+
source_by_module = {item.get("module_id"): item for item in sources}
|
|
2060
|
+
namespaces = values["module-namespace-facts"]
|
|
2061
|
+
namespace_by_module = {item.get("module_id"): item for item in namespaces}
|
|
2062
|
+
if (
|
|
2063
|
+
len(source_by_module) != len(sources)
|
|
2064
|
+
or len(namespace_by_module) != len(namespaces)
|
|
2065
|
+
or set(source_by_module) != module_ids
|
|
2066
|
+
or set(namespace_by_module) != module_ids
|
|
2067
|
+
):
|
|
2068
|
+
return False, "Phase 12 source or namespace coverage is incomplete"
|
|
2069
|
+
for module_id, identity in identity_by_module.items():
|
|
2070
|
+
source = source_by_module[module_id]
|
|
2071
|
+
namespace = namespace_by_module[module_id]
|
|
2072
|
+
module_functions = [function_by_node[node_id] for node_id in identity["function_node_ids"]]
|
|
2073
|
+
module_imports = sorted(
|
|
2074
|
+
imports_by_module[module_id], key=lambda item: item["source_ordinal"]
|
|
2075
|
+
)
|
|
2076
|
+
if (
|
|
2077
|
+
source.get("source_document_id") != identity.get("document_id")
|
|
2078
|
+
or source.get("logical_source_name") != identity.get("logical_name")
|
|
2079
|
+
or source.get("document_id") != identity.get("document_id")
|
|
2080
|
+
or source.get("logical_name") != identity.get("logical_name")
|
|
2081
|
+
or source.get("bundle_ordinal") != identity.get("bundle_ordinal")
|
|
2082
|
+
or source.get("is_primary") is not identity.get("is_primary")
|
|
2083
|
+
or source.get("document_plan_node_id") != identity.get("document_plan_node_id")
|
|
2084
|
+
or source.get("import_node_ids") != identity.get("import_node_ids")
|
|
2085
|
+
or source.get("function_node_ids") != identity.get("function_node_ids")
|
|
2086
|
+
or not isinstance(source.get("content_fingerprint"), str)
|
|
2087
|
+
or not _SHA256.fullmatch(source["content_fingerprint"])
|
|
2088
|
+
or source.get("eligible") is not True
|
|
2089
|
+
or source.get("diagnostic_code") is not None
|
|
2090
|
+
or source.get("reason") is not None
|
|
2091
|
+
):
|
|
2092
|
+
return False, "Phase 12 source facts disagree with module identity"
|
|
2093
|
+
expected_imported_bindings = [
|
|
2094
|
+
{
|
|
2095
|
+
"local_name": item["local_name"],
|
|
2096
|
+
"target_module_id": item["target_module_id"],
|
|
2097
|
+
"target_function_node_id": item["target_function_node_id"],
|
|
2098
|
+
}
|
|
2099
|
+
for item in module_imports
|
|
2100
|
+
]
|
|
2101
|
+
if (
|
|
2102
|
+
namespace.get("document_plan_node_id") != identity.get("document_plan_node_id")
|
|
2103
|
+
or namespace.get("import_node_ids") != identity.get("import_node_ids")
|
|
2104
|
+
or namespace.get("function_node_ids") != identity.get("function_node_ids")
|
|
2105
|
+
or namespace.get("local_function_names")
|
|
2106
|
+
!= [item["source_name"] for item in module_functions]
|
|
2107
|
+
or namespace.get("imported_bindings") != expected_imported_bindings
|
|
2108
|
+
or namespace.get("generated_function_names")
|
|
2109
|
+
!= [item["flattened_name"] for item in module_functions]
|
|
2110
|
+
):
|
|
2111
|
+
return False, "Phase 12 namespace facts disagree with owned functions or imports"
|
|
2112
|
+
|
|
2113
|
+
flattened_python_ir = payload.get("python_ir")
|
|
2114
|
+
flattened_nodes = (
|
|
2115
|
+
flattened_python_ir.get("nodes")
|
|
2116
|
+
if isinstance(flattened_python_ir, dict)
|
|
2117
|
+
else None
|
|
2118
|
+
)
|
|
2119
|
+
if not isinstance(flattened_nodes, list):
|
|
2120
|
+
return False, "Phase 12 conversion plan lacks flattened Python IR"
|
|
2121
|
+
flattened_node_ids = {
|
|
2122
|
+
item.get("node_id") for item in flattened_nodes if isinstance(item, dict)
|
|
2123
|
+
}
|
|
2124
|
+
if None in flattened_node_ids or len(flattened_node_ids) != len(flattened_nodes):
|
|
2125
|
+
return False, "Phase 12 flattened Python IR has invalid node identities"
|
|
2126
|
+
if any(
|
|
2127
|
+
not record.get("provenance", {}).get("source_node_ids")
|
|
2128
|
+
or any(
|
|
2129
|
+
node_id not in flattened_node_ids
|
|
2130
|
+
for node_id in record["provenance"]["source_node_ids"]
|
|
2131
|
+
)
|
|
2132
|
+
for table in module_tables.values()
|
|
2133
|
+
for record in table["records"]
|
|
2134
|
+
):
|
|
2135
|
+
return False, "Phase 12 module fact provenance references an absent node"
|
|
2136
|
+
|
|
2137
|
+
resolution = payload.get("module_resolution")
|
|
2138
|
+
expected_renames = [
|
|
2139
|
+
{
|
|
2140
|
+
"function_node_id": function_by_node[node_id]["function_node_id"],
|
|
2141
|
+
"source_name": function_by_node[node_id]["source_name"],
|
|
2142
|
+
"flattened_name": function_by_node[node_id]["flattened_name"],
|
|
2143
|
+
}
|
|
2144
|
+
for node_id in expected_function_ids
|
|
2145
|
+
]
|
|
2146
|
+
expected_edges = [
|
|
2147
|
+
{"importer_module_id": importer, "target_module_id": target}
|
|
2148
|
+
for importer, target in expected_edge_values
|
|
2149
|
+
]
|
|
2150
|
+
if (
|
|
2151
|
+
not isinstance(resolution, dict)
|
|
2152
|
+
or resolution.get("schema_version") != "module-resolution/0.12"
|
|
2153
|
+
or resolution.get("primary_module_id") != primary_module_id
|
|
2154
|
+
or resolution.get("source_module_order") != expected_source_module_order
|
|
2155
|
+
or resolution.get("module_order") != module_order
|
|
2156
|
+
or resolution.get("dependency_edges") != expected_edges
|
|
2157
|
+
or resolution.get("import_item_count") != len(imports)
|
|
2158
|
+
or resolution.get("function_renames") != expected_renames
|
|
2159
|
+
or resolution.get("initialization_node_id") != init_value.get("initialization_node_id")
|
|
2160
|
+
or resolution.get("flattened_root_node_id") != flattened_python_ir.get("root_node_id")
|
|
2161
|
+
or resolution.get("bundle_document_id") != flattened_python_ir.get("document_id")
|
|
2162
|
+
or resolution.get("module_bundle_assembly_node_id")
|
|
2163
|
+
!= payload.get("module_bundle_assembly_node_id")
|
|
2164
|
+
):
|
|
2165
|
+
return False, "Phase 12 resolution summary disagrees with closed module facts"
|
|
2166
|
+
return True, ""
|