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,459 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, replace
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from .contracts.configuration import (
|
|
5
|
+
supports_conditional_regions,
|
|
6
|
+
supports_keyword_calls,
|
|
7
|
+
supports_keyword_only_calls,
|
|
8
|
+
supports_numeric,
|
|
9
|
+
supports_records,
|
|
10
|
+
)
|
|
11
|
+
from .contracts.versions import (
|
|
12
|
+
CONVERTER_CONTRACT_VERSION,
|
|
13
|
+
CONVERSION_SUMMARY_SCHEMA,
|
|
14
|
+
DECISION_TRACE_SCHEMA,
|
|
15
|
+
PHASE14_CONVERSION_SUMMARY_SCHEMA,
|
|
16
|
+
PHASE14_DECISION_TRACE_SCHEMA,
|
|
17
|
+
PHASE14B_CONVERSION_SUMMARY_SCHEMA,
|
|
18
|
+
PHASE14B_DECISION_TRACE_SCHEMA,
|
|
19
|
+
PHASE14C_CONVERSION_SUMMARY_SCHEMA,
|
|
20
|
+
PHASE14C_DECISION_TRACE_SCHEMA,
|
|
21
|
+
PHASE13_CONVERSION_SUMMARY_SCHEMA,
|
|
22
|
+
PHASE13_DECISION_TRACE_SCHEMA,
|
|
23
|
+
PHASE12_CONVERSION_SUMMARY_SCHEMA,
|
|
24
|
+
PHASE12_DECISION_TRACE_SCHEMA,
|
|
25
|
+
)
|
|
26
|
+
from .core.canonicalization import canonicalize
|
|
27
|
+
from .core.context import ConversionContext
|
|
28
|
+
from .core.diagnostics import DiagnosticCollector, Diagnostic
|
|
29
|
+
from .core.cancellation import CancellationToken
|
|
30
|
+
from .core.enums import ResultStatus,StageTerminal,Severity
|
|
31
|
+
from .core.fingerprint import fingerprint
|
|
32
|
+
from .core.pipeline import Pipeline
|
|
33
|
+
from .core.progress import ConversionProgress
|
|
34
|
+
from .core.request import ConversionRequest,ObservationOptions
|
|
35
|
+
from .core.result import ConversionResult
|
|
36
|
+
from .core.stage_artifact import StageArtifact, freeze_value
|
|
37
|
+
from .decision_trace.recorder import DecisionTraceRecorder
|
|
38
|
+
from .frontend.source_document import SourceDocument
|
|
39
|
+
from .telemetry.sink import TelemetrySink
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class _Services:
|
|
42
|
+
context:ConversionContext; trace:DecisionTraceRecorder; telemetry:TelemetrySink
|
|
43
|
+
class PythonToCConverter:
|
|
44
|
+
def __init__(self,pipeline:Pipeline|None=None)->None:self.pipeline=pipeline or Pipeline()
|
|
45
|
+
def convert(self,request:ConversionRequest,*,observation:ObservationOptions|None=None,cancellation:CancellationToken|None=None,progress:Callable[[ConversionProgress],None]|None=None,inject_trace_failure:bool=False,inject_telemetry_failure:bool=False)->ConversionResult:
|
|
46
|
+
progress_failed=False
|
|
47
|
+
def report_progress(event:ConversionProgress)->None:
|
|
48
|
+
nonlocal progress_failed
|
|
49
|
+
if progress is None or progress_failed:
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
progress(event)
|
|
53
|
+
except Exception:
|
|
54
|
+
progress_failed=True
|
|
55
|
+
if observation is not None and (
|
|
56
|
+
not isinstance(observation, ObservationOptions)
|
|
57
|
+
or observation.trace_level not in {"None", "Summary", "Decisions", "Full"}
|
|
58
|
+
or not isinstance(observation.telemetry_enabled, bool)
|
|
59
|
+
):
|
|
60
|
+
diagnostic=Diagnostic("PYC1012",Severity.ERROR,"request","observation must use a declared trace level and Boolean telemetry flag")
|
|
61
|
+
return ConversionResult(ResultStatus.REJECTED,None,(diagnostic,),None,None,None,None,())
|
|
62
|
+
if cancellation is not None and not isinstance(cancellation, CancellationToken):
|
|
63
|
+
diagnostic=Diagnostic("PYC1013",Severity.ERROR,"request","cancellation must be CancellationToken")
|
|
64
|
+
return ConversionResult(ResultStatus.REJECTED,None,(diagnostic,),None,None,None,None,())
|
|
65
|
+
observation=observation or ObservationOptions()
|
|
66
|
+
try:
|
|
67
|
+
canonical,initial_diags=canonicalize(request)
|
|
68
|
+
except Exception:
|
|
69
|
+
diagnostic=Diagnostic("PYC1000",Severity.ERROR,"request","Request could not be canonicalized")
|
|
70
|
+
return ConversionResult(ResultStatus.REJECTED,None,(diagnostic,),None,None,None,None,())
|
|
71
|
+
if canonical is None:
|
|
72
|
+
return ConversionResult(ResultStatus.REJECTED,None,initial_diags,None,None,None,None,())
|
|
73
|
+
request=canonical.request
|
|
74
|
+
source_inputs=(request.source_bundle.primary,)+request.source_bundle.companions
|
|
75
|
+
document_order=tuple(SourceDocument.create(item.logical_name,item.text).document_id for item in source_inputs)
|
|
76
|
+
source_metadata={document_id:(item.module_id,item.logical_name) for document_id,item in zip(document_order,source_inputs)}
|
|
77
|
+
collector=DiagnosticCollector(request.resource_policy.max_diagnostics,document_order)
|
|
78
|
+
token=cancellation or CancellationToken()
|
|
79
|
+
trace=DecisionTraceRecorder(observation.trace_level,request.resource_policy.max_trace_events,inject_trace_failure)
|
|
80
|
+
telemetry=TelemetrySink(observation.telemetry_enabled,request.resource_policy.max_telemetry_events,inject_telemetry_failure)
|
|
81
|
+
services=_Services(ConversionContext(canonical,collector,token),trace,telemetry)
|
|
82
|
+
artifact=StageArtifact.initial(canonical.request_fingerprint.value); last=None
|
|
83
|
+
total_stages=len(self.pipeline.stages)
|
|
84
|
+
report_progress(ConversionProgress("pipeline-ready",None,0,total_stages))
|
|
85
|
+
def add_diagnostic(diagnostic: Diagnostic) -> None:
|
|
86
|
+
span=diagnostic.source_span or {}
|
|
87
|
+
document_id=span.get("document_id") if isinstance(span,dict) else None
|
|
88
|
+
module_id,logical_name=source_metadata.get(document_id,(None,None))
|
|
89
|
+
collector.add(replace(
|
|
90
|
+
diagnostic,
|
|
91
|
+
target_contract=diagnostic.target_contract or request.target_contract,
|
|
92
|
+
semantic_policy=diagnostic.semantic_policy or request.semantic_policy,
|
|
93
|
+
source_module_id=diagnostic.source_module_id or module_id,
|
|
94
|
+
source_logical_name=diagnostic.source_logical_name or logical_name,
|
|
95
|
+
))
|
|
96
|
+
try:
|
|
97
|
+
for stage_ordinal,stage in enumerate(self.pipeline.stages):
|
|
98
|
+
report_progress(ConversionProgress("stage-entered",stage.stage_id,stage_ordinal,total_stages))
|
|
99
|
+
trace.record({"kind":"stage_enter","stage":stage.stage_id})
|
|
100
|
+
telemetry.record({"kind":"stage_enter","stage":stage.stage_id})
|
|
101
|
+
identity=f"{artifact.kind.replace('_','-')}/{artifact.schema_version}"
|
|
102
|
+
accepted=tuple(getattr(stage,"input_schemas",(stage.input_schema,)))
|
|
103
|
+
if identity not in accepted:
|
|
104
|
+
add_diagnostic(Diagnostic("PYC9004",Severity.INTERNAL_ERROR,stage.stage_id,f"Stage input contract mismatch: {identity}"))
|
|
105
|
+
return self._result(ResultStatus.INTERNAL_FAILURE,canonical,collector,artifact,last,trace,telemetry)
|
|
106
|
+
outcome=stage.run(artifact,services)
|
|
107
|
+
for d in outcome.diagnostics:add_diagnostic(d)
|
|
108
|
+
if not outcome.completed:
|
|
109
|
+
status={StageTerminal.REJECTED:ResultStatus.REJECTED,StageTerminal.INTERNAL_FAILURE:ResultStatus.INTERNAL_FAILURE,StageTerminal.CANCELED:ResultStatus.CANCELED}[outcome.terminal]
|
|
110
|
+
return self._result(status,canonical,collector,artifact,last,trace,telemetry)
|
|
111
|
+
if token.is_canceled:
|
|
112
|
+
add_diagnostic(Diagnostic("PYC1901",Severity.ERROR,stage.stage_id,"Conversion canceled before stage publication"))
|
|
113
|
+
return self._result(ResultStatus.CANCELED,canonical,collector,artifact,last,trace,telemetry)
|
|
114
|
+
valid,message=stage.validate(outcome.artifact,services)
|
|
115
|
+
if not valid:
|
|
116
|
+
add_diagnostic(Diagnostic("PYC9002",Severity.INTERNAL_ERROR,stage.stage_id,message))
|
|
117
|
+
return self._result(ResultStatus.INTERNAL_FAILURE,canonical,collector,artifact,last,trace,telemetry)
|
|
118
|
+
if token.is_canceled:
|
|
119
|
+
add_diagnostic(Diagnostic("PYC1901",Severity.ERROR,stage.stage_id,"Conversion canceled during stage validation"))
|
|
120
|
+
return self._result(ResultStatus.CANCELED,canonical,collector,artifact,last,trace,telemetry)
|
|
121
|
+
artifact=outcome.artifact; last=stage.stage_id
|
|
122
|
+
trace.record({"kind":"stage_completed","stage":stage.stage_id,"artifact":artifact.artifact_fingerprint.value})
|
|
123
|
+
report_progress(ConversionProgress("stage-completed",stage.stage_id,stage_ordinal+1,total_stages))
|
|
124
|
+
return self._result(ResultStatus.CONVERTED,canonical,collector,artifact,last,trace,telemetry)
|
|
125
|
+
except Exception:
|
|
126
|
+
add_diagnostic(Diagnostic("PYC9001",Severity.INTERNAL_ERROR,"facade","Internal converter invariant failed"))
|
|
127
|
+
return self._result(ResultStatus.INTERNAL_FAILURE,canonical,collector,artifact,last,trace,telemetry)
|
|
128
|
+
def _result(self,status,canonical,collector,artifact,last,trace,telemetry):
|
|
129
|
+
order=tuple(artifact.payload.get("stage_order",()))
|
|
130
|
+
publishable={ResultStatus.CONVERTED,ResultStatus.CONVERTED_WITH_WARNINGS,ResultStatus.CONVERTED_WITH_APPROXIMATIONS}
|
|
131
|
+
generated_c=artifact.payload.get("generated_c") if status in publishable else None
|
|
132
|
+
output_fp=fingerprint("generated-output",generated_c) if generated_c is not None else None
|
|
133
|
+
summary=freeze_value(self._conversion_summary(artifact))
|
|
134
|
+
diagnostics=collector.snapshot()
|
|
135
|
+
trace_snapshot=self._decision_trace_snapshot(canonical,artifact,diagnostics,output_fp,trace.snapshot())
|
|
136
|
+
telemetry_snapshot=self._telemetry_snapshot(canonical,telemetry.snapshot())
|
|
137
|
+
return ConversionResult(status,generated_c,diagnostics,canonical.request_fingerprint,canonical.resource_fingerprint,output_fp,last,order,freeze_value(trace_snapshot),freeze_value(telemetry_snapshot),artifact,summary)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _decision_trace_snapshot(canonical, artifact, diagnostics, output_fp, raw):
|
|
141
|
+
request=canonical.request
|
|
142
|
+
configuration={
|
|
143
|
+
"python_version":request.python_version,
|
|
144
|
+
"target_contract":request.target_contract,
|
|
145
|
+
"semantic_policy":request.semantic_policy,
|
|
146
|
+
"approximation_allowlist":list(request.approximation_allowlist),
|
|
147
|
+
"rule_set_version":request.rule_set_version,
|
|
148
|
+
"renderer_version":request.renderer_version,
|
|
149
|
+
"helper_policy_version":request.helper_policy_version,
|
|
150
|
+
"container_policy_version":request.container_policy_version,
|
|
151
|
+
"module_policy_version":request.module_policy_version,
|
|
152
|
+
**(
|
|
153
|
+
{"record_policy_version": request.record_policy_version}
|
|
154
|
+
if supports_records(request.rule_set_version)
|
|
155
|
+
else {}
|
|
156
|
+
),
|
|
157
|
+
**(
|
|
158
|
+
{"numeric_policy_version": request.numeric_policy_version}
|
|
159
|
+
if supports_numeric(request.rule_set_version)
|
|
160
|
+
else {}
|
|
161
|
+
),
|
|
162
|
+
}
|
|
163
|
+
events=list(raw["events"])
|
|
164
|
+
stage_summaries=[event for event in events if event.get("kind") in {"stage_enter","stage_completed"}]
|
|
165
|
+
rule_decisions=[event["plan"] for event in events if event.get("kind")=="rule_plan"]
|
|
166
|
+
trace_diagnostics=[item.to_dict() for item in diagnostics]
|
|
167
|
+
if raw["truncated"]:
|
|
168
|
+
trace_diagnostics.append(Diagnostic(
|
|
169
|
+
"PYC8001",Severity.WARNING,"observation","Decision trace deterministically truncated at its declared event budget",
|
|
170
|
+
effect_on_status=False,target_contract=request.target_contract,semantic_policy=request.semantic_policy,
|
|
171
|
+
).to_dict())
|
|
172
|
+
if raw["observer_failed"]:
|
|
173
|
+
trace_diagnostics.append(Diagnostic(
|
|
174
|
+
"PYC8002",Severity.WARNING,"observation","Decision trace observer failed without affecting conversion",
|
|
175
|
+
effect_on_status=False,target_contract=request.target_contract,semantic_policy=request.semantic_policy,
|
|
176
|
+
).to_dict())
|
|
177
|
+
payload=artifact.payload
|
|
178
|
+
fact_tables={item.get("table_id"):item for item in payload.get("fact_tables",())}
|
|
179
|
+
initialization_records=fact_tables.get("module-initialization-facts",{}).get("records",())
|
|
180
|
+
module_initialization=initialization_records[0]["value"] if initialization_records else None
|
|
181
|
+
module_order={module_id:ordinal for ordinal,module_id in enumerate((module_initialization or {}).get("module_order",()))}
|
|
182
|
+
module_manifest=sorted(
|
|
183
|
+
(record["value"] for record in fact_tables.get("module-identity-facts",{}).get("records",())),
|
|
184
|
+
key=lambda item:(module_order.get(item.get("module_id"),2**31-1),item.get("module_id","")),
|
|
185
|
+
)
|
|
186
|
+
module_imports=sorted(
|
|
187
|
+
(record["value"] for record in fact_tables.get("module-import-facts",{}).get("records",())),
|
|
188
|
+
key=lambda item:(module_order.get(item.get("importer_module_id"),2**31-1),item.get("source_ordinal",2**31-1),item.get("import_item_id","")),
|
|
189
|
+
)
|
|
190
|
+
record={
|
|
191
|
+
"schema_version":(
|
|
192
|
+
DECISION_TRACE_SCHEMA
|
|
193
|
+
if supports_keyword_only_calls(request.rule_set_version)
|
|
194
|
+
else PHASE14C_DECISION_TRACE_SCHEMA
|
|
195
|
+
if supports_keyword_calls(request.rule_set_version)
|
|
196
|
+
else PHASE14B_DECISION_TRACE_SCHEMA
|
|
197
|
+
if supports_conditional_regions(request.rule_set_version)
|
|
198
|
+
else PHASE14_DECISION_TRACE_SCHEMA
|
|
199
|
+
if supports_numeric(request.rule_set_version)
|
|
200
|
+
else PHASE13_DECISION_TRACE_SCHEMA
|
|
201
|
+
if supports_records(request.rule_set_version)
|
|
202
|
+
else PHASE12_DECISION_TRACE_SCHEMA
|
|
203
|
+
),
|
|
204
|
+
"converter_version":CONVERTER_CONTRACT_VERSION,
|
|
205
|
+
"trace_level":raw["level"],
|
|
206
|
+
"input_fingerprint":canonical.request_fingerprint.value,
|
|
207
|
+
"configuration_fingerprint":fingerprint("conversion-configuration",configuration).value,
|
|
208
|
+
"resource_policy_fingerprint":canonical.resource_fingerprint.value,
|
|
209
|
+
"stage_summaries":stage_summaries,
|
|
210
|
+
"rule_decisions":rule_decisions,
|
|
211
|
+
"diagnostics":trace_diagnostics,
|
|
212
|
+
"output_fingerprint":None if output_fp is None else output_fp.value,
|
|
213
|
+
"source_output_mappings":list(payload.get("source_output_mappings",())) if raw["level"]=="Full" else [],
|
|
214
|
+
"generated_artifact":{
|
|
215
|
+
"kind":artifact.kind,
|
|
216
|
+
"schema_version":artifact.schema_version,
|
|
217
|
+
"artifact_fingerprint":artifact.artifact_fingerprint.value,
|
|
218
|
+
"parent_fingerprint":None if artifact.parent_fingerprint is None else artifact.parent_fingerprint.value,
|
|
219
|
+
},
|
|
220
|
+
"target_contract":request.target_contract,
|
|
221
|
+
"semantic_policy":request.semantic_policy,
|
|
222
|
+
"approximation_allowlist":list(request.approximation_allowlist),
|
|
223
|
+
"rule_set_version":request.rule_set_version,
|
|
224
|
+
"renderer_version":request.renderer_version,
|
|
225
|
+
"helper_policy_version":request.helper_policy_version,
|
|
226
|
+
"container_policy_version":request.container_policy_version,
|
|
227
|
+
"module_policy_version":request.module_policy_version,
|
|
228
|
+
**(
|
|
229
|
+
{"record_policy_version": request.record_policy_version}
|
|
230
|
+
if supports_records(request.rule_set_version)
|
|
231
|
+
else {}
|
|
232
|
+
),
|
|
233
|
+
**(
|
|
234
|
+
{"numeric_policy_version": request.numeric_policy_version}
|
|
235
|
+
if supports_numeric(request.rule_set_version)
|
|
236
|
+
else {}
|
|
237
|
+
),
|
|
238
|
+
"rule_manifest_fingerprint":fingerprint("rule-registry-manifest",list(payload.get("rule_registry_manifest",()))).value,
|
|
239
|
+
"helper_registry_fingerprint":payload.get("helper_registry_fingerprint"),
|
|
240
|
+
"helper_manifest":list(payload.get("helper_manifest",())),
|
|
241
|
+
"helper_manifest_fingerprint":payload.get("helper_manifest_fingerprint") or fingerprint("helper-manifest",[]).value,
|
|
242
|
+
"module_manifest":module_manifest,
|
|
243
|
+
"module_imports":module_imports,
|
|
244
|
+
"module_initialization":module_initialization,
|
|
245
|
+
"completeness":"observer-failed" if raw["observer_failed"] else ("deterministically-truncated" if raw["truncated"] else "complete"),
|
|
246
|
+
"events":events,
|
|
247
|
+
"truncated":raw["truncated"],
|
|
248
|
+
"observer_failed":raw["observer_failed"],
|
|
249
|
+
}
|
|
250
|
+
record["trace_fingerprint"]=fingerprint("decision-trace",record).value
|
|
251
|
+
return record
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def _telemetry_snapshot(canonical, raw):
|
|
255
|
+
return {
|
|
256
|
+
"schema_version":"pycforge.telemetry/0.9",
|
|
257
|
+
"conversion_observation_id":fingerprint("conversion-observation",canonical.request_fingerprint.value).value,
|
|
258
|
+
"measurements":list(raw["events"]),
|
|
259
|
+
"drop_count":raw["dropped"],
|
|
260
|
+
"truncated":raw["dropped"]>0,
|
|
261
|
+
"observer_failed":raw["observer_failed"],
|
|
262
|
+
"events":list(raw["events"]),
|
|
263
|
+
"dropped":raw["dropped"],
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _conversion_summary(artifact: StageArtifact) -> dict[str, object] | None:
|
|
268
|
+
payload=artifact.payload
|
|
269
|
+
tables={item.get("table_id"):item for item in payload.get("fact_tables",())}
|
|
270
|
+
signature_table=tables.get("function-signature-facts")
|
|
271
|
+
call_table=tables.get("call-target-facts")
|
|
272
|
+
shape_table=tables.get("container-shape-facts")
|
|
273
|
+
container_binding_table=tables.get("container-binding-facts")
|
|
274
|
+
access_table=tables.get("container-access-facts")
|
|
275
|
+
iteration_table=tables.get("container-iteration-facts")
|
|
276
|
+
module_identity_table=tables.get("module-identity-facts")
|
|
277
|
+
module_import_table=tables.get("module-import-facts")
|
|
278
|
+
module_function_table=tables.get("module-function-facts")
|
|
279
|
+
module_initialization_table=tables.get("module-initialization-facts")
|
|
280
|
+
module_source_table=tables.get("module-source-facts")
|
|
281
|
+
record_definition_table=tables.get("record-definition-facts")
|
|
282
|
+
record_field_table=tables.get("record-field-facts")
|
|
283
|
+
record_initializer_table=tables.get("record-initializer-facts")
|
|
284
|
+
record_instance_table=tables.get("record-instance-facts")
|
|
285
|
+
record_binding_table=tables.get("record-binding-facts")
|
|
286
|
+
record_access_table=tables.get("record-access-facts")
|
|
287
|
+
numeric_operation_table=tables.get("numeric-operation-facts")
|
|
288
|
+
conditional_region_table=tables.get("conditional-region-facts")
|
|
289
|
+
keyword_call_table=tables.get("keyword-call-binding-facts")
|
|
290
|
+
keyword_only_call_table=tables.get("keyword-only-call-binding-facts")
|
|
291
|
+
if not signature_table:
|
|
292
|
+
return None
|
|
293
|
+
names={item["binding_id"]:item["generated_name"] for item in payload.get("generated_name_plans",())}
|
|
294
|
+
module_functions={record["value"].get("function_node_id"):record["value"] for record in (module_function_table or {}).get("records",())}
|
|
295
|
+
module_sources={record["value"].get("module_id"):record["value"] for record in (module_source_table or {}).get("records",())}
|
|
296
|
+
functions=[]
|
|
297
|
+
for record in signature_table["records"]:
|
|
298
|
+
value=record["value"]
|
|
299
|
+
module_function=module_functions.get(value["function_node_id"],{})
|
|
300
|
+
module_source=module_sources.get(module_function.get("module_id"),{})
|
|
301
|
+
functions.append({
|
|
302
|
+
"source_name":module_function.get("source_name",value["source_name"]),
|
|
303
|
+
"flattened_source_name":value["source_name"],
|
|
304
|
+
"generated_name":names.get(value.get("binding_id")),
|
|
305
|
+
"function_node_id":value["function_node_id"],
|
|
306
|
+
"module_id":module_function.get("module_id"),
|
|
307
|
+
"document_id":module_function.get("document_id"),
|
|
308
|
+
"logical_source_name":module_source.get("logical_name"),
|
|
309
|
+
"linkage":module_function.get("linkage"),
|
|
310
|
+
"parameters":[{
|
|
311
|
+
"source_name":item["source_name"],
|
|
312
|
+
"annotation":item["annotation_spelling"],
|
|
313
|
+
"annotation_node_id":item["annotation_node_id"],
|
|
314
|
+
"c_type":item["c_type"],
|
|
315
|
+
"passing":item["passing"],
|
|
316
|
+
"ownership":item["ownership"],
|
|
317
|
+
"lifetime":item["lifetime"],
|
|
318
|
+
} for item in value["parameters"]],
|
|
319
|
+
"return_annotation":value["return_annotation_spelling"],
|
|
320
|
+
"return_annotation_node_id":value["return_annotation_node_id"],
|
|
321
|
+
"return_c_type":value["return_c_type"],
|
|
322
|
+
"return_passing":value["return_passing"],
|
|
323
|
+
"return_ownership":value["return_ownership"],
|
|
324
|
+
"return_lifetime":value["return_lifetime"],
|
|
325
|
+
"eligible":value["eligible"],
|
|
326
|
+
})
|
|
327
|
+
functions.sort(key=lambda item:(module_functions.get(item["function_node_id"],{}).get("bundle_function_ordinal",2**31-1),item["function_node_id"]))
|
|
328
|
+
calls=[]
|
|
329
|
+
if call_table:
|
|
330
|
+
for record in call_table["records"]:
|
|
331
|
+
value=record["value"]
|
|
332
|
+
calls.append({
|
|
333
|
+
"call_node_id":value["call_node_id"],
|
|
334
|
+
"target_name":value["target_name"],
|
|
335
|
+
"resolution":value["resolution"],
|
|
336
|
+
"argument_categories":list(value["argument_categories"]),
|
|
337
|
+
"parameter_categories":list(value["parameter_categories"]),
|
|
338
|
+
"evaluation_order":list(value["evaluation_order"]),
|
|
339
|
+
"arguments_evaluated_once":value["arguments_evaluated_once"],
|
|
340
|
+
"ownership_boundary":list(value["ownership_boundary"]),
|
|
341
|
+
"annotation_evidence":list(value["annotation_evidence"]),
|
|
342
|
+
"supported":value["supported"],
|
|
343
|
+
"target_function_node_id":value.get("target_function_node_id"),
|
|
344
|
+
"target_module_id":module_functions.get(value.get("target_function_node_id"),{}).get("module_id"),
|
|
345
|
+
})
|
|
346
|
+
shapes = {
|
|
347
|
+
record["value"]["literal_node_id"]: record["value"]
|
|
348
|
+
for record in (shape_table or {}).get("records", ())
|
|
349
|
+
}
|
|
350
|
+
containers=[]
|
|
351
|
+
for record in (container_binding_table or {}).get("records", ()):
|
|
352
|
+
value=record["value"]
|
|
353
|
+
shape=shapes.get(value["literal_node_id"], {})
|
|
354
|
+
containers.append({
|
|
355
|
+
"binding_id":value["binding_id"],
|
|
356
|
+
"source_name":value["source_name"],
|
|
357
|
+
"container_kind":value["container_kind"],
|
|
358
|
+
"capacity":value["capacity"],
|
|
359
|
+
"element_category":value["element_category"],
|
|
360
|
+
"key_category":value["key_category"],
|
|
361
|
+
"value_category":value["value_category"],
|
|
362
|
+
"storage_model":shape.get("storage_model"),
|
|
363
|
+
"mutable":shape.get("mutable"),
|
|
364
|
+
"allocation":"none",
|
|
365
|
+
"cleanup":"not-required",
|
|
366
|
+
"valid":value["valid"],
|
|
367
|
+
})
|
|
368
|
+
accesses=[record["value"] for record in (access_table or {}).get("records", ())]
|
|
369
|
+
iterations=[record["value"] for record in (iteration_table or {}).get("records", ())]
|
|
370
|
+
initialization=(module_initialization_table or {}).get("records", ())
|
|
371
|
+
initialization_value=initialization[0]["value"] if initialization else None
|
|
372
|
+
module_order={module_id:ordinal for ordinal,module_id in enumerate((initialization_value or {}).get("module_order",()))}
|
|
373
|
+
modules=sorted(
|
|
374
|
+
(record["value"] for record in (module_identity_table or {}).get("records", ())),
|
|
375
|
+
key=lambda item:(module_order.get(item.get("module_id"),2**31-1),item.get("module_id","")),
|
|
376
|
+
)
|
|
377
|
+
imports=sorted(
|
|
378
|
+
(record["value"] for record in (module_import_table or {}).get("records", ())),
|
|
379
|
+
key=lambda item:(module_order.get(item.get("importer_module_id"),2**31-1),item.get("source_ordinal",2**31-1),item.get("import_item_id","")),
|
|
380
|
+
)
|
|
381
|
+
records=[record["value"] for record in (record_definition_table or {}).get("records", ())]
|
|
382
|
+
record_fields=[record["value"] for record in (record_field_table or {}).get("records", ())]
|
|
383
|
+
record_initializers=[record["value"] for record in (record_initializer_table or {}).get("records", ())]
|
|
384
|
+
record_instances=[record["value"] for record in (record_instance_table or {}).get("records", ())]
|
|
385
|
+
record_bindings=[record["value"] for record in (record_binding_table or {}).get("records", ())]
|
|
386
|
+
record_accesses=[record["value"] for record in (record_access_table or {}).get("records", ())]
|
|
387
|
+
numeric_operations=[record["value"] for record in (numeric_operation_table or {}).get("records", ())]
|
|
388
|
+
conditional_regions=[record["value"] for record in (conditional_region_table or {}).get("records", ())]
|
|
389
|
+
keyword_calls=[record["value"] for record in (keyword_call_table or {}).get("records", ())]
|
|
390
|
+
keyword_only_calls=[record["value"] for record in (keyword_only_call_table or {}).get("records", ())]
|
|
391
|
+
records_enabled=supports_records(payload.get("rule_set_version", ""))
|
|
392
|
+
numeric_enabled=supports_numeric(payload.get("rule_set_version", ""))
|
|
393
|
+
conditional_enabled=supports_conditional_regions(payload.get("rule_set_version", ""))
|
|
394
|
+
keyword_calls_enabled=supports_keyword_calls(payload.get("rule_set_version", ""))
|
|
395
|
+
keyword_only_calls_enabled=supports_keyword_only_calls(payload.get("rule_set_version", ""))
|
|
396
|
+
return {
|
|
397
|
+
"schema_version":CONVERSION_SUMMARY_SCHEMA if keyword_only_calls_enabled else PHASE14C_CONVERSION_SUMMARY_SCHEMA if keyword_calls_enabled else PHASE14B_CONVERSION_SUMMARY_SCHEMA if conditional_enabled else PHASE14_CONVERSION_SUMMARY_SCHEMA if numeric_enabled else PHASE13_CONVERSION_SUMMARY_SCHEMA if records_enabled else PHASE12_CONVERSION_SUMMARY_SCHEMA,
|
|
398
|
+
"target_contract":payload.get("target_contract"),
|
|
399
|
+
"semantic_policy":payload.get("semantic_policy"),
|
|
400
|
+
"rule_set_version":payload.get("rule_set_version"),
|
|
401
|
+
"renderer_version":payload.get("renderer_version"),
|
|
402
|
+
"helper_policy_version":payload.get("helper_policy_version"),
|
|
403
|
+
"container_policy_version":payload.get("container_policy_version"),
|
|
404
|
+
"module_policy_version":payload.get("module_policy_version"),
|
|
405
|
+
**(
|
|
406
|
+
{"record_policy_version": payload.get("record_policy_version")}
|
|
407
|
+
if records_enabled
|
|
408
|
+
else {}
|
|
409
|
+
),
|
|
410
|
+
**(
|
|
411
|
+
{"numeric_policy_version": payload.get("numeric_policy_version")}
|
|
412
|
+
if numeric_enabled
|
|
413
|
+
else {}
|
|
414
|
+
),
|
|
415
|
+
"helper_registry_fingerprint":payload.get("helper_registry_fingerprint"),
|
|
416
|
+
"helper_manifest_fingerprint":payload.get("helper_manifest_fingerprint"),
|
|
417
|
+
"helpers":list(payload.get("helper_manifest",())),
|
|
418
|
+
"functions":functions,
|
|
419
|
+
"calls":calls,
|
|
420
|
+
"containers":containers,
|
|
421
|
+
"container_accesses":accesses,
|
|
422
|
+
"container_iterations":iterations,
|
|
423
|
+
"modules":modules,
|
|
424
|
+
"module_imports":imports,
|
|
425
|
+
"module_initialization":initialization_value,
|
|
426
|
+
**(
|
|
427
|
+
{
|
|
428
|
+
"records": records,
|
|
429
|
+
"record_fields": record_fields,
|
|
430
|
+
"record_initializers": record_initializers,
|
|
431
|
+
"record_instances": record_instances,
|
|
432
|
+
"record_bindings": record_bindings,
|
|
433
|
+
"record_accesses": record_accesses,
|
|
434
|
+
}
|
|
435
|
+
if records_enabled
|
|
436
|
+
else {}
|
|
437
|
+
),
|
|
438
|
+
**(
|
|
439
|
+
{"numeric_operations": numeric_operations}
|
|
440
|
+
if numeric_enabled
|
|
441
|
+
else {}
|
|
442
|
+
),
|
|
443
|
+
**(
|
|
444
|
+
{"conditional_regions": conditional_regions}
|
|
445
|
+
if conditional_enabled
|
|
446
|
+
else {}
|
|
447
|
+
),
|
|
448
|
+
**(
|
|
449
|
+
{"keyword_calls": keyword_calls}
|
|
450
|
+
if keyword_calls_enabled
|
|
451
|
+
else {}
|
|
452
|
+
),
|
|
453
|
+
**(
|
|
454
|
+
{"keyword_only_calls": keyword_only_calls}
|
|
455
|
+
if keyword_only_calls_enabled
|
|
456
|
+
else {}
|
|
457
|
+
),
|
|
458
|
+
"translation_unit_count":1,
|
|
459
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
from .source_document import SourceDocument,SourceSpan,SourcePosition,TokenRecord
|
|
2
|
+
from .normalizer import PythonNormalizer
|
|
3
|
+
from .parser import Python311ParserAdapter
|
|
4
|
+
__all__=["SourceDocument","SourceSpan","SourcePosition","TokenRecord","PythonNormalizer","Python311ParserAdapter"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast, hashlib, json, math
|
|
3
|
+
from typing import Any
|
|
4
|
+
from .source_document import SourceDocument
|
|
5
|
+
from pycforge.converter.ir.python_ir.nodes import Provenance, PythonIRModule, PythonIRNode
|
|
6
|
+
|
|
7
|
+
# Host runtimes newer than the declared 3.11 grammar expose fields such as
|
|
8
|
+
# FunctionDef.type_params even when feature_version=(3, 11) makes them
|
|
9
|
+
# syntactically unavailable. Omit host-only fields so normalized Python IR is
|
|
10
|
+
# a projection of the declared grammar rather than the interpreter's AST shape.
|
|
11
|
+
_SKIP_FIELDS={"ctx","type_params"}
|
|
12
|
+
|
|
13
|
+
def _freeze(value:Any)->Any:
|
|
14
|
+
if isinstance(value,list): return tuple(_freeze(v) for v in value)
|
|
15
|
+
if isinstance(value,dict): return tuple((k,_freeze(value[k])) for k in sorted(value))
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
class PythonNormalizer:
|
|
19
|
+
schema_version="python-ir/0.3"
|
|
20
|
+
def normalize(self,tree:ast.AST,document:SourceDocument)->PythonIRModule:
|
|
21
|
+
nodes=[]; ids={}; counter=0
|
|
22
|
+
ordered=[]; seen=set()
|
|
23
|
+
for item in ast.walk(tree):
|
|
24
|
+
if isinstance(item,ast.expr_context) or id(item) in seen: continue
|
|
25
|
+
seen.add(id(item)); ordered.append(item)
|
|
26
|
+
for item in ordered:
|
|
27
|
+
if isinstance(item,ast.AST):
|
|
28
|
+
counter+=1
|
|
29
|
+
span=self._span(item,document)
|
|
30
|
+
seed={"document":document.document_id,"index":counter,"kind":type(item).__name__,"span":span}
|
|
31
|
+
ids[id(item)]="py-"+hashlib.sha256(json.dumps(seed,sort_keys=True,separators=(",",":"),ensure_ascii=False).encode()).hexdigest()[:20]
|
|
32
|
+
def convert_value(v:Any)->Any:
|
|
33
|
+
if isinstance(v,ast.AST): return ids[id(v)]
|
|
34
|
+
if isinstance(v,list): return tuple(convert_value(x) for x in v)
|
|
35
|
+
if isinstance(v,float) and not math.isfinite(v):
|
|
36
|
+
return ("unsupported-python-value","float-nonfinite",repr(v))
|
|
37
|
+
if isinstance(v,(str,int,float,bool,type(None))): return v
|
|
38
|
+
# Preserve the fact that bytes, complex, Ellipsis, and any future
|
|
39
|
+
# scalar are not strings. A plain repr() here would let an
|
|
40
|
+
# unsupported Python literal masquerade as a supported str value.
|
|
41
|
+
return ("unsupported-python-value", type(v).__name__, repr(v))
|
|
42
|
+
for item in ordered:
|
|
43
|
+
if not isinstance(item,ast.AST): continue
|
|
44
|
+
fields=[]
|
|
45
|
+
for name,value in ast.iter_fields(item):
|
|
46
|
+
if name in _SKIP_FIELDS: continue
|
|
47
|
+
fields.append((name,_freeze(convert_value(value))))
|
|
48
|
+
span=self._span(item,document)
|
|
49
|
+
nodes.append(PythonIRNode(ids[id(item)],type(item).__name__,tuple(fields),Provenance("source",span)))
|
|
50
|
+
return PythonIRModule(self.schema_version,document.document_id,ids[id(tree)],tuple(nodes))
|
|
51
|
+
def _span(self,node:ast.AST,document:SourceDocument)->dict[str,Any]|None:
|
|
52
|
+
if not hasattr(node,"lineno"): return None
|
|
53
|
+
return document.span_from_utf8_columns(node.lineno,node.col_offset,getattr(node,"end_lineno",node.lineno),getattr(node,"end_col_offset",node.col_offset)).to_dict()
|
|
54
|
+
|
|
55
|
+
def synthetic(self,kind:str,origin_node_ids:tuple[str,...],fields:dict[str,Any]|None=None)->PythonIRNode:
|
|
56
|
+
frozen=tuple((k,_freeze(v)) for k,v in sorted((fields or {}).items()))
|
|
57
|
+
seed={"kind":kind,"origins":origin_node_ids,"fields":frozen}
|
|
58
|
+
nid="syn-"+hashlib.sha256(repr(seed).encode()).hexdigest()[:20]
|
|
59
|
+
return PythonIRNode(nid,kind,frozen,Provenance("synthetic",None,origin_node_ids))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast, io, token, tokenize
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from .source_document import SourceDocument, TokenRecord
|
|
5
|
+
|
|
6
|
+
class ParserVersionError(ValueError): pass
|
|
7
|
+
|
|
8
|
+
class Python311ParserAdapter:
|
|
9
|
+
grammar_version="3.11"
|
|
10
|
+
def tokenize(self,document:SourceDocument)->SourceDocument:
|
|
11
|
+
records=[]
|
|
12
|
+
reader=io.StringIO(document.text).readline
|
|
13
|
+
for item in tokenize.generate_tokens(reader):
|
|
14
|
+
if item.type==token.ENDMARKER: continue
|
|
15
|
+
span=document.span(item.start[0],item.start[1],item.end[0],item.end[1])
|
|
16
|
+
records.append(TokenRecord(token.tok_name[item.type],item.string,span))
|
|
17
|
+
return replace(document,tokens=tuple(records))
|
|
18
|
+
def parse(self,document:SourceDocument,requested_version:str)->ast.Module:
|
|
19
|
+
if requested_version != self.grammar_version: raise ParserVersionError(f"unsupported grammar {requested_version}")
|
|
20
|
+
return ast.parse(document.text,filename=document.logical_name,mode="exec",feature_version=(3,11),type_comments=True)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class SourcePosition:
|
|
8
|
+
line: int
|
|
9
|
+
column: int
|
|
10
|
+
offset: int
|
|
11
|
+
def to_dict(self)->dict[str,int]: return {"line":self.line,"column":self.column,"offset":self.offset}
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class SourceSpan:
|
|
15
|
+
document_id: str
|
|
16
|
+
start: SourcePosition
|
|
17
|
+
end: SourcePosition
|
|
18
|
+
def to_dict(self)->dict[str,object]: return {"document_id":self.document_id,"start":self.start.to_dict(),"end":self.end.to_dict()}
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class TokenRecord:
|
|
22
|
+
kind: str
|
|
23
|
+
text: str
|
|
24
|
+
span: SourceSpan
|
|
25
|
+
def to_dict(self)->dict[str,object]: return {"kind":self.kind,"text":self.text,"span":self.span.to_dict()}
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class SourceDocument:
|
|
29
|
+
document_id: str
|
|
30
|
+
logical_name: str
|
|
31
|
+
text: str
|
|
32
|
+
utf8_sha256: str
|
|
33
|
+
line_starts: tuple[int,...]
|
|
34
|
+
newline_sequences: tuple[str,...]
|
|
35
|
+
tokens: tuple[TokenRecord,...]
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def create(cls, logical_name:str, text:str, tokens:Iterable[TokenRecord]=())->"SourceDocument":
|
|
39
|
+
raw=text.encode("utf-8")
|
|
40
|
+
document_id="src-"+sha256((logical_name+"\0").encode("utf-8")+raw).hexdigest()[:20]
|
|
41
|
+
starts=[0]; newlines=[]; i=0
|
|
42
|
+
while i < len(text):
|
|
43
|
+
if text.startswith("\r\n",i): newlines.append("\r\n"); i+=2; starts.append(i)
|
|
44
|
+
elif text[i] in "\r\n": newlines.append(text[i]); i+=1; starts.append(i)
|
|
45
|
+
else: i+=1
|
|
46
|
+
return cls(document_id,logical_name,text,sha256(raw).hexdigest(),tuple(starts),tuple(newlines),tuple(tokens))
|
|
47
|
+
|
|
48
|
+
def offset(self,line:int,column:int)->int:
|
|
49
|
+
if line < 1 or line > len(self.line_starts): raise ValueError("line outside source")
|
|
50
|
+
line_text = self._line_segment(line)
|
|
51
|
+
# ``tokenize`` can report one virtual end column after a terminal bare
|
|
52
|
+
# CR. Preserve that display column while clamping its concrete source
|
|
53
|
+
# offset to the document boundary.
|
|
54
|
+
if column < 0 or column > len(line_text)+1: raise ValueError("column outside source line")
|
|
55
|
+
return min(self.line_starts[line-1]+column,len(self.text))
|
|
56
|
+
|
|
57
|
+
def _line_segment(self, line: int) -> str:
|
|
58
|
+
start = self.line_starts[line-1]
|
|
59
|
+
end = self.line_starts[line] if line < len(self.line_starts) else len(self.text)
|
|
60
|
+
return self.text[start:end]
|
|
61
|
+
|
|
62
|
+
def _line_text(self, line: int) -> str:
|
|
63
|
+
value = self._line_segment(line)
|
|
64
|
+
if value.endswith("\r\n"):
|
|
65
|
+
return value[:-2]
|
|
66
|
+
if value.endswith(("\r", "\n")):
|
|
67
|
+
return value[:-1]
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
def span(self,start_line:int,start_col:int,end_line:int,end_col:int)->SourceSpan:
|
|
71
|
+
return SourceSpan(self.document_id,SourcePosition(start_line,start_col,self.offset(start_line,start_col)),SourcePosition(end_line,end_col,self.offset(end_line,end_col)))
|
|
72
|
+
|
|
73
|
+
def span_from_utf8_columns(self,start_line:int,start_col:int,end_line:int,end_col:int)->SourceSpan:
|
|
74
|
+
"""Convert CPython AST UTF-8 byte columns into source character columns."""
|
|
75
|
+
def character_column(line: int, byte_column: int) -> int:
|
|
76
|
+
if byte_column < 0:
|
|
77
|
+
raise ValueError("negative UTF-8 column")
|
|
78
|
+
raw = self._line_text(line).encode("utf-8")
|
|
79
|
+
if byte_column > len(raw):
|
|
80
|
+
raise ValueError("UTF-8 column outside source line")
|
|
81
|
+
try:
|
|
82
|
+
return len(raw[:byte_column].decode("utf-8"))
|
|
83
|
+
except UnicodeDecodeError as exc:
|
|
84
|
+
raise ValueError("UTF-8 column splits a source character") from exc
|
|
85
|
+
return self.span(
|
|
86
|
+
start_line,
|
|
87
|
+
character_column(start_line,start_col),
|
|
88
|
+
end_line,
|
|
89
|
+
character_column(end_line,end_col),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def to_dict(self)->dict[str,object]:
|
|
93
|
+
return {"document_id":self.document_id,"logical_name":self.logical_name,"text":self.text,"utf8_sha256":self.utf8_sha256,"line_starts":list(self.line_starts),"newline_sequences":list(self.newline_sequences),"tokens":[t.to_dict() for t in self.tokens]}
|