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,802 @@
|
|
|
1
|
+
"""Independent reconstruction of exact direct keyword-call binding facts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from pycforge.converter.ir.python_ir import python_ir_reference_ids
|
|
8
|
+
|
|
9
|
+
from .model import (
|
|
10
|
+
CUMULATIVE_KEYWORD_TARGET_DIAGNOSTIC_CODE,
|
|
11
|
+
CUMULATIVE_KEYWORD_TARGET_REASON,
|
|
12
|
+
KEYWORD_CALL_FACT_SCHEMA,
|
|
13
|
+
KEYWORD_CALL_KEY_DOMAIN,
|
|
14
|
+
KEYWORD_CALL_LOWERING_SHAPE,
|
|
15
|
+
KEYWORD_CALL_OBLIGATIONS,
|
|
16
|
+
KEYWORD_CALL_PROVENANCE_EVIDENCE,
|
|
17
|
+
KEYWORD_CALL_RULE_ID,
|
|
18
|
+
KEYWORD_CALL_RULE_VERSION,
|
|
19
|
+
KEYWORD_CALL_TABLE_DEPENDENCIES,
|
|
20
|
+
KEYWORD_CALL_TABLE_ID,
|
|
21
|
+
KeywordCallValidationCanceled,
|
|
22
|
+
keyword_call_binding_id,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _MalformedKeywordEvidence(ValueError):
|
|
27
|
+
"""Internal sentinel for hostile or structurally invalid evidence."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sequence(value: Any) -> bool:
|
|
31
|
+
return isinstance(value, (list, tuple))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check(cancellation: Any) -> None:
|
|
35
|
+
if cancellation is not None and bool(getattr(cancellation, "is_canceled", False)):
|
|
36
|
+
raise KeywordCallValidationCanceled
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _category(value: Any) -> str:
|
|
40
|
+
return value.value if hasattr(value, "value") else str(value or "unknown")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate_keyword_call_binding_facts(
|
|
44
|
+
payload: Mapping[str, Any],
|
|
45
|
+
tables: list[dict[str, Any]] | tuple[dict[str, Any], ...] | None = None,
|
|
46
|
+
*,
|
|
47
|
+
expected_fact_schema: str = KEYWORD_CALL_FACT_SCHEMA,
|
|
48
|
+
require_table: bool = True,
|
|
49
|
+
require_plans: bool = True,
|
|
50
|
+
ignored_call_node_ids: frozenset[str] = frozenset(),
|
|
51
|
+
cumulatively_ineligible_target_function_node_ids: frozenset[str] = frozenset(),
|
|
52
|
+
cancellation: Any = None,
|
|
53
|
+
) -> tuple[bool, str]:
|
|
54
|
+
"""Require exact table coverage, independently rebuilt facts, and plans."""
|
|
55
|
+
|
|
56
|
+
_check(cancellation)
|
|
57
|
+
raw_tables = tables if tables is not None else payload.get("fact_tables")
|
|
58
|
+
if not _sequence(raw_tables) or any(not isinstance(item, dict) for item in raw_tables):
|
|
59
|
+
return False, "keyword-call fact tables are malformed"
|
|
60
|
+
table_values = tuple(raw_tables)
|
|
61
|
+
table = next((item for item in table_values if item.get("table_id") == KEYWORD_CALL_TABLE_ID), None)
|
|
62
|
+
if table is None:
|
|
63
|
+
return (False, "keyword-call binding fact table is absent") if require_table else (True, "")
|
|
64
|
+
dependencies = table.get("invalidation_dependencies")
|
|
65
|
+
if (
|
|
66
|
+
table.get("schema_version") != expected_fact_schema
|
|
67
|
+
or table.get("producer_stage") != "analysis.plan"
|
|
68
|
+
or table.get("key_domain") != KEYWORD_CALL_KEY_DOMAIN
|
|
69
|
+
or table.get("completeness") != "complete"
|
|
70
|
+
or not _sequence(dependencies)
|
|
71
|
+
or tuple(dependencies) != KEYWORD_CALL_TABLE_DEPENDENCIES
|
|
72
|
+
):
|
|
73
|
+
return False, "keyword-call binding fact schema is invalid"
|
|
74
|
+
|
|
75
|
+
module = payload.get("python_ir")
|
|
76
|
+
node_values = module.get("nodes") if isinstance(module, dict) else None
|
|
77
|
+
if not isinstance(node_values, list):
|
|
78
|
+
return False, "keyword-call validation lacks normalized Python IR"
|
|
79
|
+
nodes: dict[str, dict[str, Any]] = {}
|
|
80
|
+
node_order: dict[str, int] = {}
|
|
81
|
+
for ordinal, node in enumerate(node_values):
|
|
82
|
+
_check(cancellation)
|
|
83
|
+
node_id = node.get("node_id") if isinstance(node, dict) else None
|
|
84
|
+
if not isinstance(node_id, str) or node_id in nodes:
|
|
85
|
+
return False, "keyword-call Python IR identities are invalid"
|
|
86
|
+
nodes[node_id] = node
|
|
87
|
+
node_order[node_id] = ordinal
|
|
88
|
+
|
|
89
|
+
table_map: dict[str, dict[str, Any]] = {}
|
|
90
|
+
for item in table_values:
|
|
91
|
+
table_id = item.get("table_id")
|
|
92
|
+
if not isinstance(table_id, str) or table_id in table_map:
|
|
93
|
+
return False, "keyword-call fact-table identities are malformed"
|
|
94
|
+
table_map[table_id] = item
|
|
95
|
+
required = {
|
|
96
|
+
"binding-facts",
|
|
97
|
+
"function-signature-facts",
|
|
98
|
+
"value-category-facts",
|
|
99
|
+
"call-target-facts",
|
|
100
|
+
}
|
|
101
|
+
if not required.issubset(table_map):
|
|
102
|
+
return False, "keyword-call proof dependencies are absent"
|
|
103
|
+
|
|
104
|
+
def record_values(table_id: str) -> tuple[dict[str, Any], ...]:
|
|
105
|
+
result: list[dict[str, Any]] = []
|
|
106
|
+
records = table_map[table_id].get("records")
|
|
107
|
+
if not isinstance(records, list):
|
|
108
|
+
raise _MalformedKeywordEvidence(
|
|
109
|
+
f"keyword-call dependency {table_id!r} records are malformed"
|
|
110
|
+
)
|
|
111
|
+
for record in records:
|
|
112
|
+
_check(cancellation)
|
|
113
|
+
value = record.get("value") if isinstance(record, dict) else None
|
|
114
|
+
if not isinstance(value, dict):
|
|
115
|
+
raise _MalformedKeywordEvidence(
|
|
116
|
+
f"keyword-call dependency {table_id!r} contains a malformed record"
|
|
117
|
+
)
|
|
118
|
+
result.append(value)
|
|
119
|
+
return tuple(result)
|
|
120
|
+
|
|
121
|
+
occurrence_bindings: dict[str, str] = {}
|
|
122
|
+
for binding in record_values("binding-facts"):
|
|
123
|
+
_check(cancellation)
|
|
124
|
+
binding_id = binding.get("binding_id")
|
|
125
|
+
occurrences = binding.get("occurrence_node_ids")
|
|
126
|
+
if (
|
|
127
|
+
not isinstance(binding_id, str)
|
|
128
|
+
or not _sequence(occurrences)
|
|
129
|
+
or any(not isinstance(item, str) for item in occurrences)
|
|
130
|
+
):
|
|
131
|
+
raise _MalformedKeywordEvidence("keyword-call binding evidence is malformed")
|
|
132
|
+
for occurrence in occurrences:
|
|
133
|
+
existing = occurrence_bindings.get(occurrence)
|
|
134
|
+
if existing is not None and existing != binding_id:
|
|
135
|
+
raise _MalformedKeywordEvidence(
|
|
136
|
+
"keyword-call occurrence has conflicting binding evidence"
|
|
137
|
+
)
|
|
138
|
+
occurrence_bindings[occurrence] = binding_id
|
|
139
|
+
signatures: dict[str, dict[str, Any]] = {}
|
|
140
|
+
for item in record_values("function-signature-facts"):
|
|
141
|
+
binding_id = item.get("binding_id")
|
|
142
|
+
parameters = item.get("parameters")
|
|
143
|
+
if (
|
|
144
|
+
not isinstance(binding_id, str)
|
|
145
|
+
or not _sequence(parameters)
|
|
146
|
+
or binding_id in signatures
|
|
147
|
+
):
|
|
148
|
+
raise _MalformedKeywordEvidence("keyword-call signature evidence is malformed")
|
|
149
|
+
signatures[binding_id] = item
|
|
150
|
+
categories: dict[Any, str] = {}
|
|
151
|
+
category_records = table_map["value-category-facts"].get("records")
|
|
152
|
+
if not isinstance(category_records, list):
|
|
153
|
+
raise _MalformedKeywordEvidence("keyword-call category evidence is malformed")
|
|
154
|
+
for record in category_records:
|
|
155
|
+
_check(cancellation)
|
|
156
|
+
if not isinstance(record, dict) or not isinstance(record.get("key"), str):
|
|
157
|
+
raise _MalformedKeywordEvidence("keyword-call category record is malformed")
|
|
158
|
+
categories[record["key"]] = _category(record.get("value"))
|
|
159
|
+
call_targets: dict[str, dict[str, Any]] = {}
|
|
160
|
+
for item in record_values("call-target-facts"):
|
|
161
|
+
call_id = item.get("call_node_id")
|
|
162
|
+
if not isinstance(call_id, str) or call_id in call_targets:
|
|
163
|
+
raise _MalformedKeywordEvidence("keyword-call target evidence is malformed")
|
|
164
|
+
call_targets[call_id] = item
|
|
165
|
+
|
|
166
|
+
reconstruction = _IndependentKeywordReconstruction(
|
|
167
|
+
node_values,
|
|
168
|
+
nodes=nodes,
|
|
169
|
+
node_order=node_order,
|
|
170
|
+
occurrence_bindings=occurrence_bindings,
|
|
171
|
+
signatures=signatures,
|
|
172
|
+
categories=categories,
|
|
173
|
+
ignored_call_node_ids=ignored_call_node_ids,
|
|
174
|
+
cancellation=cancellation,
|
|
175
|
+
)
|
|
176
|
+
expected = reconstruction.expected()
|
|
177
|
+
for call_id, fact in tuple(expected.items()):
|
|
178
|
+
_check(cancellation)
|
|
179
|
+
if (
|
|
180
|
+
fact.get("supported") is True
|
|
181
|
+
and fact.get("target_function_node_id")
|
|
182
|
+
in cumulatively_ineligible_target_function_node_ids
|
|
183
|
+
):
|
|
184
|
+
expected[call_id] = {
|
|
185
|
+
**fact,
|
|
186
|
+
"runtime_binding_failure": "compile-time-rejected",
|
|
187
|
+
"supported": False,
|
|
188
|
+
"diagnostic_code": CUMULATIVE_KEYWORD_TARGET_DIAGNOSTIC_CODE,
|
|
189
|
+
"reason": CUMULATIVE_KEYWORD_TARGET_REASON,
|
|
190
|
+
"rejection_node_id": fact["target_function_node_id"],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
records = table.get("records")
|
|
194
|
+
if not isinstance(records, list):
|
|
195
|
+
return False, "keyword-call binding records are malformed"
|
|
196
|
+
keys = [record.get("key") if isinstance(record, dict) else None for record in records]
|
|
197
|
+
if (
|
|
198
|
+
any(not isinstance(key, str) for key in keys)
|
|
199
|
+
or any(keys[index - 1] >= keys[index] for index in range(1, len(keys)))
|
|
200
|
+
):
|
|
201
|
+
return False, "keyword-call binding keys are not unique and sorted"
|
|
202
|
+
found: dict[str, dict[str, Any]] = {}
|
|
203
|
+
provenance: dict[str, Any] = {}
|
|
204
|
+
for record in records:
|
|
205
|
+
_check(cancellation)
|
|
206
|
+
key, value = record.get("key"), record.get("value")
|
|
207
|
+
if not isinstance(value, dict) or value.get("call_node_id") != key:
|
|
208
|
+
return False, "keyword-call binding record identity is malformed"
|
|
209
|
+
found[key] = value
|
|
210
|
+
provenance[key] = record.get("provenance")
|
|
211
|
+
if set(found) != set(expected):
|
|
212
|
+
return False, "keyword-call facts do not exactly cover eligible direct keyword calls"
|
|
213
|
+
for call_id, fact in expected.items():
|
|
214
|
+
_check(cancellation)
|
|
215
|
+
if found[call_id] != fact:
|
|
216
|
+
return False, "keyword-call fact disagrees with independent reconstruction"
|
|
217
|
+
proof = provenance.get(call_id)
|
|
218
|
+
if (
|
|
219
|
+
not isinstance(proof, dict)
|
|
220
|
+
or proof.get("source_node_ids") != _provenance_ids(fact)
|
|
221
|
+
or proof.get("evidence") != list(KEYWORD_CALL_PROVENANCE_EVIDENCE)
|
|
222
|
+
):
|
|
223
|
+
return False, "keyword-call fact provenance is incomplete"
|
|
224
|
+
target = call_targets.get(call_id)
|
|
225
|
+
if not isinstance(target, dict) or any(
|
|
226
|
+
target.get(name) != value
|
|
227
|
+
for name, value in {
|
|
228
|
+
"callee_node_id": fact["callee_node_id"],
|
|
229
|
+
"target_function_node_id": fact["target_function_node_id"],
|
|
230
|
+
"target_binding_id": fact["target_binding_id"],
|
|
231
|
+
"argument_node_ids": fact["source_argument_node_ids"],
|
|
232
|
+
"argument_categories": fact["source_argument_categories"],
|
|
233
|
+
"evaluation_order": fact["evaluation_order"],
|
|
234
|
+
"arguments_evaluated_once": True,
|
|
235
|
+
}.items()
|
|
236
|
+
):
|
|
237
|
+
return False, "keyword-call fact disagrees with call-target evidence"
|
|
238
|
+
recursive_override = bool(
|
|
239
|
+
fact["supported"]
|
|
240
|
+
and target.get("resolution") == "recursive-target"
|
|
241
|
+
and target.get("supported") is False
|
|
242
|
+
and target.get("diagnostic_code") == "PYC2920"
|
|
243
|
+
and target.get("reason")
|
|
244
|
+
== "Direct and mutual recursion are unsupported in Phase 9"
|
|
245
|
+
)
|
|
246
|
+
if not recursive_override and any(
|
|
247
|
+
target.get(name) != value
|
|
248
|
+
for name, value in {
|
|
249
|
+
"resolution": "understood-source-function" if fact["supported"] else "ineligible-source-function",
|
|
250
|
+
"supported": fact["supported"],
|
|
251
|
+
"diagnostic_code": fact["diagnostic_code"],
|
|
252
|
+
"reason": fact["reason"],
|
|
253
|
+
}.items()
|
|
254
|
+
):
|
|
255
|
+
return False, "keyword-call fact disagrees with call-target support state"
|
|
256
|
+
|
|
257
|
+
if require_plans:
|
|
258
|
+
valid, reason = _validate_plans(payload, expected, cancellation)
|
|
259
|
+
if not valid:
|
|
260
|
+
return valid, reason
|
|
261
|
+
return True, ""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _provenance_ids(fact: dict[str, Any]) -> list[str]:
|
|
265
|
+
values = (
|
|
266
|
+
fact["call_node_id"], fact["callee_node_id"], fact["target_function_node_id"],
|
|
267
|
+
*fact["parameter_node_ids"], *fact["positional_argument_node_ids"],
|
|
268
|
+
*fact["keyword_node_ids"], *fact["keyword_value_node_ids"],
|
|
269
|
+
)
|
|
270
|
+
return list(dict.fromkeys(values))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class _IndependentKeywordReconstruction:
|
|
274
|
+
"""A producer-independent implementation of the closed binding algorithm."""
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
node_values: list[dict[str, Any]],
|
|
279
|
+
*,
|
|
280
|
+
nodes: Mapping[str, dict[str, Any]],
|
|
281
|
+
node_order: Mapping[str, int],
|
|
282
|
+
occurrence_bindings: Mapping[str, str],
|
|
283
|
+
signatures: Mapping[str, dict[str, Any]],
|
|
284
|
+
categories: Mapping[str, str],
|
|
285
|
+
ignored_call_node_ids: frozenset[str],
|
|
286
|
+
cancellation: Any,
|
|
287
|
+
) -> None:
|
|
288
|
+
self.node_values = node_values
|
|
289
|
+
self.nodes = nodes
|
|
290
|
+
self.node_order = node_order
|
|
291
|
+
self.occurrence_bindings = occurrence_bindings
|
|
292
|
+
self.signatures = signatures
|
|
293
|
+
self.categories = categories
|
|
294
|
+
self.ignored = ignored_call_node_ids
|
|
295
|
+
self.cancellation = cancellation
|
|
296
|
+
|
|
297
|
+
def expected(self) -> dict[str, dict[str, Any]]:
|
|
298
|
+
result: dict[str, dict[str, Any]] = {}
|
|
299
|
+
for call in self.node_values:
|
|
300
|
+
_check(self.cancellation)
|
|
301
|
+
if call.get("kind") != "Call" or call.get("node_id") in self.ignored:
|
|
302
|
+
continue
|
|
303
|
+
fields = call.get("fields")
|
|
304
|
+
if not isinstance(fields, dict) or not _sequence(fields.get("keywords")):
|
|
305
|
+
raise _MalformedKeywordEvidence("keyword-call Python IR fields are malformed")
|
|
306
|
+
if not fields["keywords"]:
|
|
307
|
+
continue
|
|
308
|
+
callee_id = fields.get("func")
|
|
309
|
+
callee = self.nodes.get(callee_id)
|
|
310
|
+
binding_id = self.occurrence_bindings.get(callee_id)
|
|
311
|
+
signature = self.signatures.get(binding_id)
|
|
312
|
+
if (
|
|
313
|
+
not callee or callee.get("kind") != "Name"
|
|
314
|
+
or not isinstance(signature, dict)
|
|
315
|
+
or signature.get("eligible") is not True
|
|
316
|
+
):
|
|
317
|
+
continue
|
|
318
|
+
target = self.nodes.get(signature.get("function_node_id"))
|
|
319
|
+
target_fields = target.get("fields") if isinstance(target, dict) else None
|
|
320
|
+
arguments = (
|
|
321
|
+
self.nodes.get(target_fields.get("args"))
|
|
322
|
+
if isinstance(target_fields, dict)
|
|
323
|
+
else None
|
|
324
|
+
)
|
|
325
|
+
argument_fields = (
|
|
326
|
+
arguments.get("fields") if isinstance(arguments, dict) else None
|
|
327
|
+
)
|
|
328
|
+
if (
|
|
329
|
+
isinstance(argument_fields, dict)
|
|
330
|
+
and argument_fields.get("kwonlyargs")
|
|
331
|
+
):
|
|
332
|
+
continue
|
|
333
|
+
result[call["node_id"]] = self._fact(call, signature, str(binding_id))
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
def _fact(self, call: dict[str, Any], signature: dict[str, Any], target_binding_id: str) -> dict[str, Any]:
|
|
337
|
+
_check(self.cancellation)
|
|
338
|
+
fields, call_id = call.get("fields"), call.get("node_id")
|
|
339
|
+
if not isinstance(fields, dict) or not isinstance(call_id, str):
|
|
340
|
+
raise _MalformedKeywordEvidence("keyword-call Python IR call is malformed")
|
|
341
|
+
target_id = signature.get("function_node_id")
|
|
342
|
+
target = self.nodes.get(target_id)
|
|
343
|
+
target_fields = target.get("fields") if isinstance(target, dict) else None
|
|
344
|
+
if (
|
|
345
|
+
not isinstance(target_id, str)
|
|
346
|
+
or not isinstance(target, dict)
|
|
347
|
+
or target.get("kind") != "FunctionDef"
|
|
348
|
+
or not isinstance(target_fields, dict)
|
|
349
|
+
):
|
|
350
|
+
raise _MalformedKeywordEvidence("keyword-call target evidence is malformed")
|
|
351
|
+
arguments = self.nodes.get(target_fields.get("args"))
|
|
352
|
+
argument_fields = arguments.get("fields") if isinstance(arguments, dict) else None
|
|
353
|
+
parameters_value = signature.get("parameters")
|
|
354
|
+
if (
|
|
355
|
+
not isinstance(arguments, dict)
|
|
356
|
+
or arguments.get("kind") != "arguments"
|
|
357
|
+
or not isinstance(argument_fields, dict)
|
|
358
|
+
or not _sequence(argument_fields.get("posonlyargs"))
|
|
359
|
+
or not _sequence(parameters_value)
|
|
360
|
+
):
|
|
361
|
+
raise _MalformedKeywordEvidence("keyword-call signature evidence is malformed")
|
|
362
|
+
parameters = tuple(parameters_value)
|
|
363
|
+
if any(not isinstance(item, dict) for item in parameters):
|
|
364
|
+
raise _MalformedKeywordEvidence("keyword-call parameters are malformed")
|
|
365
|
+
parameter_node_ids = tuple(str(item.get("parameter_node_id", "")) for item in parameters)
|
|
366
|
+
parameter_names = tuple(str(item.get("source_name", "")) for item in parameters)
|
|
367
|
+
parameter_categories = tuple(_category(item.get("category")) for item in parameters)
|
|
368
|
+
if (
|
|
369
|
+
any(not item for item in parameter_node_ids)
|
|
370
|
+
or any(not item for item in parameter_names)
|
|
371
|
+
or len(parameter_names) != len(set(parameter_names))
|
|
372
|
+
or any(item not in self.nodes for item in parameter_node_ids)
|
|
373
|
+
):
|
|
374
|
+
raise _MalformedKeywordEvidence("keyword-call parameter identities are malformed")
|
|
375
|
+
positional_only_count = len(argument_fields["posonlyargs"])
|
|
376
|
+
if positional_only_count > len(parameters):
|
|
377
|
+
raise _MalformedKeywordEvidence("keyword-call positional-only evidence is malformed")
|
|
378
|
+
if not _sequence(fields.get("args")) or not _sequence(fields.get("keywords")):
|
|
379
|
+
raise _MalformedKeywordEvidence("keyword-call argument sequences are malformed")
|
|
380
|
+
positional_ids = tuple(fields["args"])
|
|
381
|
+
keyword_ids = tuple(fields["keywords"])
|
|
382
|
+
if any(not isinstance(item, str) for item in (*positional_ids, *keyword_ids)):
|
|
383
|
+
raise _MalformedKeywordEvidence("keyword-call argument identities are malformed")
|
|
384
|
+
keyword_nodes: list[dict[str, Any]] = []
|
|
385
|
+
for keyword_id in keyword_ids:
|
|
386
|
+
keyword = self.nodes.get(keyword_id)
|
|
387
|
+
keyword_fields = keyword.get("fields") if isinstance(keyword, dict) else None
|
|
388
|
+
if (
|
|
389
|
+
not isinstance(keyword, dict)
|
|
390
|
+
or keyword.get("kind") != "keyword"
|
|
391
|
+
or not isinstance(keyword_fields, dict)
|
|
392
|
+
or not isinstance(keyword_fields.get("value"), str)
|
|
393
|
+
or keyword_fields["value"] not in self.nodes
|
|
394
|
+
):
|
|
395
|
+
raise _MalformedKeywordEvidence("keyword-call keyword evidence is malformed")
|
|
396
|
+
keyword_nodes.append(keyword)
|
|
397
|
+
|
|
398
|
+
positional_entries: list[dict[str, Any]] = []
|
|
399
|
+
for position, argument_id in enumerate(positional_ids):
|
|
400
|
+
_check(self.cancellation)
|
|
401
|
+
if argument_id not in self.nodes:
|
|
402
|
+
raise _MalformedKeywordEvidence("keyword-call argument node is absent")
|
|
403
|
+
positional_entries.append({"kind": "positional", "position": position, "argument": argument_id, "keyword": None, "name": None, "order_node_id": argument_id})
|
|
404
|
+
keyword_entries: list[dict[str, Any]] = []
|
|
405
|
+
for position, keyword in enumerate(keyword_nodes):
|
|
406
|
+
_check(self.cancellation)
|
|
407
|
+
keyword_fields = keyword["fields"]
|
|
408
|
+
keyword_entries.append({"kind": "keyword", "position": position, "argument": keyword_fields["value"], "keyword": keyword["node_id"], "name": keyword_fields.get("arg"), "order_node_id": keyword["node_id"]})
|
|
409
|
+
entries = self._merge_source_entries(positional_entries, keyword_entries)
|
|
410
|
+
for ordinal, entry in enumerate(entries):
|
|
411
|
+
entry["ordinal"] = ordinal
|
|
412
|
+
by_position = {(item["kind"], item["position"]): item for item in entries}
|
|
413
|
+
|
|
414
|
+
parameter_by_name = {name: ordinal for ordinal, name in enumerate(parameter_names)}
|
|
415
|
+
parameter_arguments: list[str | None] = [None] * len(parameters)
|
|
416
|
+
parameter_sources: list[int | None] = [None] * len(parameters)
|
|
417
|
+
source_parameters: dict[tuple[str, int], int | None] = {}
|
|
418
|
+
unpacking: list[tuple[int, str]] = []
|
|
419
|
+
excess: list[tuple[int, str]] = []
|
|
420
|
+
problems: list[tuple[str, str]] = []
|
|
421
|
+
saw_starred = False
|
|
422
|
+
for position, argument_id in enumerate(positional_ids):
|
|
423
|
+
_check(self.cancellation)
|
|
424
|
+
entry = by_position[("positional", position)]
|
|
425
|
+
if self.nodes.get(argument_id, {}).get("kind") == "Starred":
|
|
426
|
+
saw_starred = True
|
|
427
|
+
source_parameters[("positional", position)] = None
|
|
428
|
+
unpacking.append((entry["ordinal"], argument_id))
|
|
429
|
+
elif saw_starred:
|
|
430
|
+
source_parameters[("positional", position)] = None
|
|
431
|
+
elif position >= len(parameters):
|
|
432
|
+
source_parameters[("positional", position)] = None
|
|
433
|
+
excess.append((entry["ordinal"], argument_id))
|
|
434
|
+
else:
|
|
435
|
+
source_parameters[("positional", position)] = position
|
|
436
|
+
parameter_arguments[position] = argument_id
|
|
437
|
+
parameter_sources[position] = entry["ordinal"]
|
|
438
|
+
for position, keyword in enumerate(keyword_nodes):
|
|
439
|
+
_check(self.cancellation)
|
|
440
|
+
entry = by_position[("keyword", position)]
|
|
441
|
+
keyword_id, name = keyword["node_id"], keyword.get("fields", {}).get("arg")
|
|
442
|
+
if name is None:
|
|
443
|
+
source_parameters[("keyword", position)] = None
|
|
444
|
+
unpacking.append((entry["ordinal"], keyword_id))
|
|
445
|
+
continue
|
|
446
|
+
parameter_ordinal = parameter_by_name.get(name)
|
|
447
|
+
if parameter_ordinal is None:
|
|
448
|
+
source_parameters[("keyword", position)] = None
|
|
449
|
+
problems.append((keyword_id, f"Unknown keyword argument: {name}"))
|
|
450
|
+
continue
|
|
451
|
+
source_parameters[("keyword", position)] = parameter_ordinal
|
|
452
|
+
if parameter_ordinal < positional_only_count:
|
|
453
|
+
problems.append((keyword_id, f"Positional-only parameter cannot be bound by keyword: {name}"))
|
|
454
|
+
elif parameter_arguments[parameter_ordinal] is not None:
|
|
455
|
+
prior = parameter_sources[parameter_ordinal]
|
|
456
|
+
prior_entry = entries[prior] if isinstance(prior, int) else None
|
|
457
|
+
reason = f"Keyword argument duplicates a positionally bound parameter: {name}" if prior_entry and prior_entry["kind"] == "positional" else f"Duplicate keyword argument: {name}"
|
|
458
|
+
problems.append((keyword_id, reason))
|
|
459
|
+
else:
|
|
460
|
+
parameter_arguments[parameter_ordinal] = keyword.get("fields", {}).get("value")
|
|
461
|
+
parameter_sources[parameter_ordinal] = entry["ordinal"]
|
|
462
|
+
|
|
463
|
+
argument_bindings: list[dict[str, Any]] = []
|
|
464
|
+
for entry in entries:
|
|
465
|
+
_check(self.cancellation)
|
|
466
|
+
parameter_ordinal = source_parameters.get((entry["kind"], entry["position"]))
|
|
467
|
+
valid = isinstance(parameter_ordinal, int) and 0 <= parameter_ordinal < len(parameters)
|
|
468
|
+
argument_id = entry["argument"]
|
|
469
|
+
argument_bindings.append({
|
|
470
|
+
"source_argument_node_id": argument_id,
|
|
471
|
+
"keyword_node_id": entry["keyword"], "keyword_name": entry["name"],
|
|
472
|
+
"source_ordinal": entry["ordinal"],
|
|
473
|
+
"parameter_node_id": parameter_node_ids[parameter_ordinal] if valid else None,
|
|
474
|
+
"parameter_name": parameter_names[parameter_ordinal] if valid else None,
|
|
475
|
+
"parameter_ordinal": parameter_ordinal,
|
|
476
|
+
"category": self.categories.get(argument_id, "unknown"),
|
|
477
|
+
"expected_category": parameter_categories[parameter_ordinal] if valid else None,
|
|
478
|
+
})
|
|
479
|
+
missing = [parameter_names[i] for i, value in enumerate(parameter_arguments) if value is None]
|
|
480
|
+
mismatches = [item for item in argument_bindings if item["parameter_ordinal"] is not None and item["expected_category"] is not None and item["category"] != item["expected_category"] and parameter_sources[item["parameter_ordinal"]] == item["source_ordinal"]]
|
|
481
|
+
coverage = bool(not unpacking and not excess and not problems and not missing and all(value is not None for value in parameter_arguments) and len(entries) == len(parameters))
|
|
482
|
+
|
|
483
|
+
code = reason = rejection = None
|
|
484
|
+
if unpacking:
|
|
485
|
+
_, rejection = min(unpacking, key=lambda item: item[0]); code, reason = "PYC2910", "Starred and unpacked call arguments are unsupported by the exact keyword profile"
|
|
486
|
+
elif excess:
|
|
487
|
+
_, rejection = min(excess, key=lambda item: item[0]); code, reason = "PYC2904", "Argument count exceeds the target signature"
|
|
488
|
+
elif problems:
|
|
489
|
+
rejection, reason = problems[0]; code = "PYC2912"
|
|
490
|
+
elif missing:
|
|
491
|
+
rejection, code, reason = call_id, "PYC2904", "Every target parameter must be supplied exactly once; missing: " + ", ".join(missing)
|
|
492
|
+
elif mismatches:
|
|
493
|
+
mismatch = mismatches[0]
|
|
494
|
+
rejection = mismatch["keyword_node_id"] or mismatch["source_argument_node_id"]
|
|
495
|
+
code, reason = "PYC2905", f"Argument representation does not match parameter {mismatch['parameter_name']}"
|
|
496
|
+
supported = code is None and coverage
|
|
497
|
+
if not supported and code is None:
|
|
498
|
+
rejection, code, reason = call_id, "PYC2904", "Keyword arguments do not exactly cover the target signature"
|
|
499
|
+
source_ids = tuple(item["source_argument_node_id"] for item in argument_bindings)
|
|
500
|
+
source_categories = tuple(item["category"] for item in argument_bindings)
|
|
501
|
+
return {
|
|
502
|
+
"binding_id": keyword_call_binding_id(call_id, target_binding_id),
|
|
503
|
+
"call_node_id": call_id, "callee_node_id": fields["func"],
|
|
504
|
+
"target_function_node_id": target_id, "target_binding_id": target_binding_id,
|
|
505
|
+
"target_name": str(signature.get("source_name", target_fields.get("name", ""))),
|
|
506
|
+
"positional_only_parameter_count": positional_only_count,
|
|
507
|
+
"parameter_node_ids": list(parameter_node_ids), "parameter_names": list(parameter_names),
|
|
508
|
+
"parameter_categories": list(parameter_categories),
|
|
509
|
+
"positional_argument_node_ids": list(positional_ids), "keyword_node_ids": list(keyword_ids),
|
|
510
|
+
"keyword_names": [item.get("fields", {}).get("arg") for item in keyword_nodes],
|
|
511
|
+
"keyword_value_node_ids": [item.get("fields", {}).get("value") for item in keyword_nodes],
|
|
512
|
+
"argument_bindings": argument_bindings,
|
|
513
|
+
"source_argument_node_ids": list(source_ids), "source_argument_categories": list(source_categories),
|
|
514
|
+
"source_to_parameter_ordinals": [item["parameter_ordinal"] for item in argument_bindings],
|
|
515
|
+
"parameter_argument_node_ids": list(parameter_arguments), "parameter_to_source_ordinals": list(parameter_sources),
|
|
516
|
+
"evaluation_order": list(source_ids), "arguments_evaluated_once": True,
|
|
517
|
+
"parameter_coverage_exact": coverage, "lowering_shape": KEYWORD_CALL_LOWERING_SHAPE,
|
|
518
|
+
"allocation_model": "none", "cleanup_model": "none",
|
|
519
|
+
"runtime_binding_failure": "proved-absent" if supported else "compile-time-rejected",
|
|
520
|
+
"supported": supported, "diagnostic_code": code, "reason": reason, "rejection_node_id": rejection,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def _merge_source_entries(
|
|
524
|
+
self,
|
|
525
|
+
positional: list[dict[str, Any]],
|
|
526
|
+
keywords: list[dict[str, Any]],
|
|
527
|
+
) -> list[dict[str, Any]]:
|
|
528
|
+
"""Linearly merge normalized source-ordered positional/keyword lists."""
|
|
529
|
+
|
|
530
|
+
merged: list[dict[str, Any]] = []
|
|
531
|
+
positional_index = 0
|
|
532
|
+
keyword_index = 0
|
|
533
|
+
while positional_index < len(positional) and keyword_index < len(keywords):
|
|
534
|
+
_check(self.cancellation)
|
|
535
|
+
positional_item = positional[positional_index]
|
|
536
|
+
keyword_item = keywords[keyword_index]
|
|
537
|
+
if self._source_key(positional_item["order_node_id"]) <= self._source_key(
|
|
538
|
+
keyword_item["order_node_id"]
|
|
539
|
+
):
|
|
540
|
+
merged.append(positional_item)
|
|
541
|
+
positional_index += 1
|
|
542
|
+
else:
|
|
543
|
+
merged.append(keyword_item)
|
|
544
|
+
keyword_index += 1
|
|
545
|
+
merged.extend(positional[positional_index:])
|
|
546
|
+
merged.extend(keywords[keyword_index:])
|
|
547
|
+
return merged
|
|
548
|
+
|
|
549
|
+
def _source_key(self, node_id: str) -> tuple[int, int]:
|
|
550
|
+
node = self.nodes.get(node_id, {})
|
|
551
|
+
provenance = node.get("provenance")
|
|
552
|
+
span = provenance.get("source_span") if isinstance(provenance, dict) else None
|
|
553
|
+
start = span.get("start") if isinstance(span, dict) else None
|
|
554
|
+
offset = start.get("offset") if isinstance(start, dict) else None
|
|
555
|
+
return (
|
|
556
|
+
offset if isinstance(offset, int) else 2**63 - 1,
|
|
557
|
+
self.node_order.get(node_id, 2**63 - 1),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
def _validate_plans(payload: Mapping[str, Any], expected: dict[str, dict[str, Any]], cancellation: Any) -> tuple[bool, str]:
|
|
561
|
+
plan_values = payload.get("rule_plans")
|
|
562
|
+
if not isinstance(plan_values, list):
|
|
563
|
+
return False, "keyword-call RulePlans are malformed"
|
|
564
|
+
plans_by_source: dict[str, dict[str, Any]] = {}
|
|
565
|
+
for item in plan_values:
|
|
566
|
+
_check(cancellation)
|
|
567
|
+
if not isinstance(item, dict):
|
|
568
|
+
return False, "keyword-call RulePlans are malformed"
|
|
569
|
+
if item.get("rule_id") != KEYWORD_CALL_RULE_ID:
|
|
570
|
+
continue
|
|
571
|
+
source_id = item.get("source_node_id")
|
|
572
|
+
if not isinstance(source_id, str) or source_id in plans_by_source:
|
|
573
|
+
return False, "keyword-call RulePlans have duplicate or invalid sources"
|
|
574
|
+
plans_by_source[source_id] = item
|
|
575
|
+
|
|
576
|
+
table_values = payload.get("fact_tables")
|
|
577
|
+
if not isinstance(table_values, list):
|
|
578
|
+
return False, "keyword-call RulePlan dependencies are malformed"
|
|
579
|
+
tables: dict[str, dict[str, Any]] = {}
|
|
580
|
+
for item in table_values:
|
|
581
|
+
if not isinstance(item, dict) or not isinstance(item.get("table_id"), str):
|
|
582
|
+
return False, "keyword-call RulePlan dependencies are malformed"
|
|
583
|
+
table_id = item["table_id"]
|
|
584
|
+
if table_id in tables:
|
|
585
|
+
return False, "keyword-call RulePlan dependencies have duplicate identities"
|
|
586
|
+
tables[table_id] = item
|
|
587
|
+
|
|
588
|
+
def values(table_id: str) -> tuple[dict[str, Any], ...]:
|
|
589
|
+
table = tables.get(table_id)
|
|
590
|
+
records = table.get("records") if isinstance(table, dict) else None
|
|
591
|
+
if not isinstance(records, list):
|
|
592
|
+
raise _MalformedKeywordEvidence(
|
|
593
|
+
f"keyword-call RulePlan dependency {table_id!r} is malformed"
|
|
594
|
+
)
|
|
595
|
+
result: list[dict[str, Any]] = []
|
|
596
|
+
for record in records:
|
|
597
|
+
value = record.get("value") if isinstance(record, dict) else None
|
|
598
|
+
if not isinstance(value, dict):
|
|
599
|
+
raise _MalformedKeywordEvidence(
|
|
600
|
+
f"keyword-call RulePlan dependency {table_id!r} is malformed"
|
|
601
|
+
)
|
|
602
|
+
result.append(value)
|
|
603
|
+
return tuple(result)
|
|
604
|
+
|
|
605
|
+
call_targets = {item.get("call_node_id"): item for item in values("call-target-facts")}
|
|
606
|
+
supported = {
|
|
607
|
+
key: value
|
|
608
|
+
for key, value in expected.items()
|
|
609
|
+
if value["supported"] and call_targets.get(key, {}).get("supported") is True
|
|
610
|
+
}
|
|
611
|
+
if set(plans_by_source) != set(supported):
|
|
612
|
+
return False, "keyword-call RulePlans do not exactly cover cumulatively supported facts"
|
|
613
|
+
decision_values = payload.get("support_decisions")
|
|
614
|
+
if not isinstance(decision_values, list):
|
|
615
|
+
return False, "keyword-call support decisions are malformed"
|
|
616
|
+
decisions: dict[str, dict[str, Any]] = {}
|
|
617
|
+
for item in decision_values:
|
|
618
|
+
node_id = item.get("node_id") if isinstance(item, dict) else None
|
|
619
|
+
if not isinstance(node_id, str) or node_id in decisions:
|
|
620
|
+
return False, "keyword-call support decisions are malformed"
|
|
621
|
+
decisions[node_id] = item
|
|
622
|
+
category_records = tables.get("value-category-facts", {}).get("records")
|
|
623
|
+
if not isinstance(category_records, list):
|
|
624
|
+
return False, "keyword-call RulePlan categories are malformed"
|
|
625
|
+
categories = {
|
|
626
|
+
record.get("key"): _category(record.get("value"))
|
|
627
|
+
for record in category_records
|
|
628
|
+
if isinstance(record, dict)
|
|
629
|
+
}
|
|
630
|
+
module_functions = {item.get("function_node_id"): item for item in values("module-function-facts")}
|
|
631
|
+
module = payload.get("python_ir")
|
|
632
|
+
node_values = module.get("nodes") if isinstance(module, dict) else None
|
|
633
|
+
if not isinstance(node_values, list):
|
|
634
|
+
return False, "keyword-call RulePlan Python IR is malformed"
|
|
635
|
+
nodes = {
|
|
636
|
+
item.get("node_id"): item
|
|
637
|
+
for item in node_values
|
|
638
|
+
if isinstance(item, dict) and isinstance(item.get("node_id"), str)
|
|
639
|
+
}
|
|
640
|
+
parents: dict[str, list[str]] = {}
|
|
641
|
+
for parent in node_values:
|
|
642
|
+
_check(cancellation)
|
|
643
|
+
fields = parent.get("fields") if isinstance(parent, dict) else None
|
|
644
|
+
if (
|
|
645
|
+
not isinstance(fields, dict)
|
|
646
|
+
or not isinstance(parent.get("kind"), str)
|
|
647
|
+
or not isinstance(parent.get("node_id"), str)
|
|
648
|
+
):
|
|
649
|
+
return False, "keyword-call RulePlan Python IR is malformed"
|
|
650
|
+
for field, value in fields.items():
|
|
651
|
+
for child in python_ir_reference_ids(parent["kind"], field, value, nodes):
|
|
652
|
+
parents.setdefault(child, []).append(parent["node_id"])
|
|
653
|
+
def owner(node_id: str) -> str | None:
|
|
654
|
+
seen: set[str] = set()
|
|
655
|
+
current = node_id
|
|
656
|
+
while current not in seen:
|
|
657
|
+
seen.add(current)
|
|
658
|
+
candidates = parents.get(current, ())
|
|
659
|
+
if not candidates:
|
|
660
|
+
return None
|
|
661
|
+
current = candidates[0]
|
|
662
|
+
if nodes.get(current, {}).get("kind") == "FunctionDef":
|
|
663
|
+
return current
|
|
664
|
+
return None
|
|
665
|
+
for call_id, fact in supported.items():
|
|
666
|
+
_check(cancellation)
|
|
667
|
+
plan = plans_by_source[call_id]
|
|
668
|
+
feature_facts = {
|
|
669
|
+
f"keyword-call-binding:{fact['binding_id']}", f"keyword-call:{call_id}",
|
|
670
|
+
f"keyword-call-target:{fact['target_binding_id']}",
|
|
671
|
+
f"keyword-source-argument-count:{len(fact['source_argument_node_ids'])}",
|
|
672
|
+
f"keyword-parameter-count:{len(fact['parameter_node_ids'])}",
|
|
673
|
+
f"keyword-call-lowering-shape:{fact['lowering_shape']}",
|
|
674
|
+
}
|
|
675
|
+
facts_used = plan.get("facts_used")
|
|
676
|
+
if not isinstance(facts_used, list) or any(
|
|
677
|
+
not isinstance(item, str) for item in facts_used
|
|
678
|
+
):
|
|
679
|
+
return False, "keyword-call RulePlan facts are malformed"
|
|
680
|
+
actual_feature_facts = {
|
|
681
|
+
item for item in facts_used if item.startswith("keyword-")
|
|
682
|
+
}
|
|
683
|
+
call = call_targets.get(call_id, {})
|
|
684
|
+
annotation_evidence = call.get("annotation_evidence")
|
|
685
|
+
argument_categories = call.get("argument_categories")
|
|
686
|
+
if not _sequence(annotation_evidence) or not _sequence(argument_categories):
|
|
687
|
+
return False, "keyword-call RulePlan call-target evidence is malformed"
|
|
688
|
+
expected_facts = {
|
|
689
|
+
f"value-category:{categories.get(call_id, 'unknown')}",
|
|
690
|
+
f"call-target:{fact['target_binding_id']}",
|
|
691
|
+
*(f"annotation-evidence:{item}" for item in annotation_evidence),
|
|
692
|
+
*(f"argument-category:{item}" for item in argument_categories),
|
|
693
|
+
*feature_facts,
|
|
694
|
+
}
|
|
695
|
+
expected_obligations = [
|
|
696
|
+
*KEYWORD_CALL_OBLIGATIONS,
|
|
697
|
+
"call-result-representation-known",
|
|
698
|
+
"source-output-call-mapping-required",
|
|
699
|
+
]
|
|
700
|
+
expected_explanation = [
|
|
701
|
+
"selected", KEYWORD_CALL_RULE_ID, "for", "Call",
|
|
702
|
+
"resolved-target", fact["target_name"], "argument-order",
|
|
703
|
+
*fact["evaluation_order"],
|
|
704
|
+
]
|
|
705
|
+
caller = module_functions.get(owner(call_id), {})
|
|
706
|
+
callee = module_functions.get(fact["target_function_node_id"], {})
|
|
707
|
+
caller_module = caller.get("module_id")
|
|
708
|
+
callee_module = callee.get("module_id")
|
|
709
|
+
if caller_module and callee_module and caller_module != callee_module:
|
|
710
|
+
expected_facts.update({
|
|
711
|
+
f"caller-module:{caller_module}",
|
|
712
|
+
f"callee-module:{callee_module}",
|
|
713
|
+
f"target-function:{fact['target_function_node_id']}",
|
|
714
|
+
})
|
|
715
|
+
expected_obligations.extend((
|
|
716
|
+
"cross-module-call-uses-target-binding",
|
|
717
|
+
"bundle-wide-recursion-policy-explicit",
|
|
718
|
+
))
|
|
719
|
+
expected_explanation.extend((
|
|
720
|
+
"cross-module-direct-call", str(caller_module), "to", str(callee_module),
|
|
721
|
+
))
|
|
722
|
+
expected_explanation.extend((
|
|
723
|
+
"keyword-call-binding", fact["target_name"],
|
|
724
|
+
"source-order-arguments", str(len(fact["source_argument_node_ids"])),
|
|
725
|
+
"formal-order-parameters", str(len(fact["parameter_node_ids"])),
|
|
726
|
+
"lowered-as", fact["lowering_shape"],
|
|
727
|
+
))
|
|
728
|
+
decision = decisions.get(call_id)
|
|
729
|
+
facts_are_sorted_unique = all(
|
|
730
|
+
facts_used[index - 1] < facts_used[index]
|
|
731
|
+
for index in range(1, len(facts_used))
|
|
732
|
+
)
|
|
733
|
+
if (
|
|
734
|
+
plan.get("rule_version") != KEYWORD_CALL_RULE_VERSION
|
|
735
|
+
or plan.get("support_state") != "SupportedDirect"
|
|
736
|
+
or plan.get("helper_requirements") != []
|
|
737
|
+
or plan.get("unresolved_obligations") != []
|
|
738
|
+
or actual_feature_facts != feature_facts
|
|
739
|
+
or not facts_are_sorted_unique
|
|
740
|
+
or set(facts_used) != expected_facts
|
|
741
|
+
or plan.get("semantic_obligations") != expected_obligations
|
|
742
|
+
or plan.get("resolved_obligations") != expected_obligations
|
|
743
|
+
or plan.get("explanation_tokens") != expected_explanation
|
|
744
|
+
or not isinstance(decision, dict)
|
|
745
|
+
or decision.get("state") != "SupportedDirect"
|
|
746
|
+
or decision.get("rule_plan_id") != plan.get("plan_id")
|
|
747
|
+
):
|
|
748
|
+
return False, "keyword-call RulePlan does not close its exact proof"
|
|
749
|
+
return True, ""
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def validate_keyword_call_binding_facts(
|
|
753
|
+
payload: Mapping[str, Any],
|
|
754
|
+
tables: list[dict[str, Any]] | tuple[dict[str, Any], ...] | None = None,
|
|
755
|
+
*,
|
|
756
|
+
expected_fact_schema: str = KEYWORD_CALL_FACT_SCHEMA,
|
|
757
|
+
require_table: bool = True,
|
|
758
|
+
require_plans: bool = True,
|
|
759
|
+
ignored_call_node_ids: frozenset[str] = frozenset(),
|
|
760
|
+
cumulatively_ineligible_target_function_node_ids: frozenset[str] = frozenset(),
|
|
761
|
+
cancellation: Any = None,
|
|
762
|
+
) -> tuple[bool, str]:
|
|
763
|
+
"""Fail closed on malformed evidence while preserving cancellation."""
|
|
764
|
+
|
|
765
|
+
_check(cancellation)
|
|
766
|
+
if not isinstance(payload, Mapping):
|
|
767
|
+
return False, "keyword-call validation payload is malformed"
|
|
768
|
+
if tables is not None and not _sequence(tables):
|
|
769
|
+
return False, "keyword-call fact tables are malformed"
|
|
770
|
+
if (
|
|
771
|
+
not isinstance(ignored_call_node_ids, (set, frozenset))
|
|
772
|
+
or any(not isinstance(item, str) for item in ignored_call_node_ids)
|
|
773
|
+
or not isinstance(
|
|
774
|
+
cumulatively_ineligible_target_function_node_ids,
|
|
775
|
+
(set, frozenset),
|
|
776
|
+
)
|
|
777
|
+
or any(
|
|
778
|
+
not isinstance(item, str)
|
|
779
|
+
for item in cumulatively_ineligible_target_function_node_ids
|
|
780
|
+
)
|
|
781
|
+
):
|
|
782
|
+
return False, "keyword-call validation exclusions are malformed"
|
|
783
|
+
try:
|
|
784
|
+
return _validate_keyword_call_binding_facts(
|
|
785
|
+
payload,
|
|
786
|
+
tables,
|
|
787
|
+
expected_fact_schema=expected_fact_schema,
|
|
788
|
+
require_table=require_table,
|
|
789
|
+
require_plans=require_plans,
|
|
790
|
+
ignored_call_node_ids=frozenset(ignored_call_node_ids),
|
|
791
|
+
cumulatively_ineligible_target_function_node_ids=frozenset(
|
|
792
|
+
cumulatively_ineligible_target_function_node_ids
|
|
793
|
+
),
|
|
794
|
+
cancellation=cancellation,
|
|
795
|
+
)
|
|
796
|
+
except _MalformedKeywordEvidence as exc:
|
|
797
|
+
return False, str(exc) or "keyword-call evidence is malformed"
|
|
798
|
+
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
|
|
799
|
+
return False, "keyword-call evidence is malformed"
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
__all__ = ["validate_keyword_call_binding_facts"]
|