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.
Files changed (221) hide show
  1. pycforge/__init__.py +8 -0
  2. pycforge/__main__.py +2 -0
  3. pycforge/_version.py +1 -0
  4. pycforge/converter/__init__.py +0 -0
  5. pycforge/converter/analysis/__init__.py +5 -0
  6. pycforge/converter/analysis/functions.py +723 -0
  7. pycforge/converter/analysis/model.py +158 -0
  8. pycforge/converter/analysis/planning.py +464 -0
  9. pycforge/converter/analysis/stage.py +1976 -0
  10. pycforge/converter/analysis/symbols.py +237 -0
  11. pycforge/converter/analysis/validation.py +2166 -0
  12. pycforge/converter/c_output/__init__.py +2 -0
  13. pycforge/converter/c_output/conformance.py +297 -0
  14. pycforge/converter/c_output/renderer.py +220 -0
  15. pycforge/converter/conditional_regions/__init__.py +71 -0
  16. pycforge/converter/conditional_regions/analysis.py +481 -0
  17. pycforge/converter/conditional_regions/lowering.py +579 -0
  18. pycforge/converter/conditional_regions/model.py +243 -0
  19. pycforge/converter/conditional_regions/validation.py +690 -0
  20. pycforge/converter/containers/__init__.py +19 -0
  21. pycforge/converter/containers/analysis.py +542 -0
  22. pycforge/converter/containers/lowering.py +290 -0
  23. pycforge/converter/containers/model.py +146 -0
  24. pycforge/converter/contracts/__init__.py +0 -0
  25. pycforge/converter/contracts/configuration.py +178 -0
  26. pycforge/converter/contracts/identifiers.py +68 -0
  27. pycforge/converter/contracts/versions.py +71 -0
  28. pycforge/converter/core/__init__.py +0 -0
  29. pycforge/converter/core/artifact_io.py +87 -0
  30. pycforge/converter/core/cancellation.py +7 -0
  31. pycforge/converter/core/canonicalization.py +254 -0
  32. pycforge/converter/core/context.py +13 -0
  33. pycforge/converter/core/diagnostics.py +95 -0
  34. pycforge/converter/core/enums.py +23 -0
  35. pycforge/converter/core/fingerprint.py +22 -0
  36. pycforge/converter/core/pipeline.py +8 -0
  37. pycforge/converter/core/progress.py +25 -0
  38. pycforge/converter/core/request.py +50 -0
  39. pycforge/converter/core/resource_policy.py +31 -0
  40. pycforge/converter/core/result.py +23 -0
  41. pycforge/converter/core/serialization.py +36 -0
  42. pycforge/converter/core/stage.py +10 -0
  43. pycforge/converter/core/stage_artifact.py +80 -0
  44. pycforge/converter/core/stage_outcome.py +15 -0
  45. pycforge/converter/decision_trace/__init__.py +0 -0
  46. pycforge/converter/decision_trace/recorder.py +52 -0
  47. pycforge/converter/facade.py +459 -0
  48. pycforge/converter/frontend/__init__.py +4 -0
  49. pycforge/converter/frontend/normalizer.py +59 -0
  50. pycforge/converter/frontend/parser.py +20 -0
  51. pycforge/converter/frontend/source_document.py +93 -0
  52. pycforge/converter/frontend/stages.py +518 -0
  53. pycforge/converter/frontend/validation.py +76 -0
  54. pycforge/converter/io/__init__.py +0 -0
  55. pycforge/converter/io/atomic_writer.py +33 -0
  56. pycforge/converter/ir/__init__.py +1 -0
  57. pycforge/converter/ir/c_ir/__init__.py +4 -0
  58. pycforge/converter/ir/c_ir/builder.py +26 -0
  59. pycforge/converter/ir/c_ir/model.py +318 -0
  60. pycforge/converter/ir/c_ir/serialization.py +88 -0
  61. pycforge/converter/ir/c_ir/validation.py +834 -0
  62. pycforge/converter/ir/python_ir/__init__.py +17 -0
  63. pycforge/converter/ir/python_ir/nodes.py +86 -0
  64. pycforge/converter/keyword_calls/__init__.py +68 -0
  65. pycforge/converter/keyword_calls/analysis.py +458 -0
  66. pycforge/converter/keyword_calls/lowering.py +271 -0
  67. pycforge/converter/keyword_calls/model.py +304 -0
  68. pycforge/converter/keyword_calls/validation.py +802 -0
  69. pycforge/converter/keyword_only_calls/__init__.py +68 -0
  70. pycforge/converter/keyword_only_calls/analysis.py +640 -0
  71. pycforge/converter/keyword_only_calls/lowering.py +376 -0
  72. pycforge/converter/keyword_only_calls/model.py +328 -0
  73. pycforge/converter/keyword_only_calls/validation.py +1357 -0
  74. pycforge/converter/lowering.py +1000 -0
  75. pycforge/converter/modules/__init__.py +13 -0
  76. pycforge/converter/modules/analysis.py +1250 -0
  77. pycforge/converter/modules/lowering.py +200 -0
  78. pycforge/converter/modules/model.py +73 -0
  79. pycforge/converter/modules/stage.py +192 -0
  80. pycforge/converter/numeric_semantics/__init__.py +23 -0
  81. pycforge/converter/numeric_semantics/analysis.py +420 -0
  82. pycforge/converter/numeric_semantics/lowering.py +153 -0
  83. pycforge/converter/numeric_semantics/model.py +123 -0
  84. pycforge/converter/records/__init__.py +34 -0
  85. pycforge/converter/records/analysis.py +1394 -0
  86. pycforge/converter/records/lowering.py +393 -0
  87. pycforge/converter/records/model.py +319 -0
  88. pycforge/converter/rules/__init__.py +3 -0
  89. pycforge/converter/support_templates/__init__.py +49 -0
  90. pycforge/converter/support_templates/assembly.py +106 -0
  91. pycforge/converter/support_templates/factories.py +348 -0
  92. pycforge/converter/support_templates/model.py +159 -0
  93. pycforge/converter/support_templates/registry.py +388 -0
  94. pycforge/converter/telemetry/__init__.py +0 -0
  95. pycforge/converter/telemetry/sink.py +39 -0
  96. pycforge/ide/__init__.py +19 -0
  97. pycforge/ide/__main__.py +5 -0
  98. pycforge/ide/_worker_event_codec.py +276 -0
  99. pycforge/ide/_worker_protocol_json.py +197 -0
  100. pycforge/ide/_worker_protocol_types.py +90 -0
  101. pycforge/ide/_worker_request_codec.py +200 -0
  102. pycforge/ide/_worker_request_data.py +260 -0
  103. pycforge/ide/_worker_result_data.py +343 -0
  104. pycforge/ide/action_contract.py +469 -0
  105. pycforge/ide/action_validation.py +229 -0
  106. pycforge/ide/command_palette.py +223 -0
  107. pycforge/ide/controller.py +904 -0
  108. pycforge/ide/controller_conversion.py +546 -0
  109. pycforge/ide/controller_io.py +205 -0
  110. pycforge/ide/editor.py +951 -0
  111. pycforge/ide/editor_commands_qt.py +274 -0
  112. pycforge/ide/editor_lexical.py +43 -0
  113. pycforge/ide/editor_sidebars.py +236 -0
  114. pycforge/ide/editor_syntax.py +243 -0
  115. pycforge/ide/find_replace.py +535 -0
  116. pycforge/ide/icons.py +91 -0
  117. pycforge/ide/io_service.py +196 -0
  118. pycforge/ide/model.py +164 -0
  119. pycforge/ide/panels.py +903 -0
  120. pycforge/ide/positions.py +220 -0
  121. pycforge/ide/process_worker.py +130 -0
  122. pycforge/ide/qt.py +230 -0
  123. pycforge/ide/qt_actions.py +418 -0
  124. pycforge/ide/qt_close.py +169 -0
  125. pycforge/ide/qt_command_palette.py +297 -0
  126. pycforge/ide/qt_contract.py +220 -0
  127. pycforge/ide/qt_documents.py +590 -0
  128. pycforge/ide/qt_editor_buffers.py +113 -0
  129. pycforge/ide/qt_editor_surfaces.py +112 -0
  130. pycforge/ide/qt_menus.py +324 -0
  131. pycforge/ide/qt_projection.py +500 -0
  132. pycforge/ide/qt_shell.py +348 -0
  133. pycforge/ide/qt_shell_interactions.py +320 -0
  134. pycforge/ide/qt_state.py +393 -0
  135. pycforge/ide/qt_workspace_features.py +398 -0
  136. pycforge/ide/qt_workspace_navigation.py +208 -0
  137. pycforge/ide/qt_workspace_observers.py +173 -0
  138. pycforge/ide/qt_workspace_panels.py +545 -0
  139. pycforge/ide/qt_workspace_widgets.py +228 -0
  140. pycforge/ide/resources/icons/about.svg +1 -0
  141. pycforge/ide/resources/icons/add-document.svg +1 -0
  142. pycforge/ide/resources/icons/brand-mark.svg +1 -0
  143. pycforge/ide/resources/icons/bundle-search.svg +1 -0
  144. pycforge/ide/resources/icons/cancel.svg +1 -0
  145. pycforge/ide/resources/icons/check.svg +1 -0
  146. pycforge/ide/resources/icons/chevron-right.svg +1 -0
  147. pycforge/ide/resources/icons/close.svg +1 -0
  148. pycforge/ide/resources/icons/collapse-all.svg +1 -0
  149. pycforge/ide/resources/icons/command-palette.svg +1 -0
  150. pycforge/ide/resources/icons/convert.svg +1 -0
  151. pycforge/ide/resources/icons/copy.svg +1 -0
  152. pycforge/ide/resources/icons/cut.svg +1 -0
  153. pycforge/ide/resources/icons/decision-trace.svg +1 -0
  154. pycforge/ide/resources/icons/details.svg +1 -0
  155. pycforge/ide/resources/icons/diagnostics.svg +1 -0
  156. pycforge/ide/resources/icons/duplicate-line.svg +1 -0
  157. pycforge/ide/resources/icons/exit.svg +1 -0
  158. pycforge/ide/resources/icons/expand-all.svg +1 -0
  159. pycforge/ide/resources/icons/export.svg +1 -0
  160. pycforge/ide/resources/icons/find.svg +1 -0
  161. pycforge/ide/resources/icons/go-to-line.svg +1 -0
  162. pycforge/ide/resources/icons/go-to-output.svg +1 -0
  163. pycforge/ide/resources/icons/go-to-source.svg +1 -0
  164. pycforge/ide/resources/icons/history.svg +1 -0
  165. pycforge/ide/resources/icons/indent.svg +1 -0
  166. pycforge/ide/resources/icons/link-c.svg +1 -0
  167. pycforge/ide/resources/icons/mappings.svg +1 -0
  168. pycforge/ide/resources/icons/module.svg +1 -0
  169. pycforge/ide/resources/icons/move-down.svg +1 -0
  170. pycforge/ide/resources/icons/move-line-down.svg +1 -0
  171. pycforge/ide/resources/icons/move-line-up.svg +1 -0
  172. pycforge/ide/resources/icons/move-up.svg +1 -0
  173. pycforge/ide/resources/icons/next.svg +1 -0
  174. pycforge/ide/resources/icons/open.svg +1 -0
  175. pycforge/ide/resources/icons/outdent.svg +1 -0
  176. pycforge/ide/resources/icons/outline.svg +1 -0
  177. pycforge/ide/resources/icons/paste.svg +1 -0
  178. pycforge/ide/resources/icons/previous.svg +1 -0
  179. pycforge/ide/resources/icons/primary-module.svg +1 -0
  180. pycforge/ide/resources/icons/redo.svg +1 -0
  181. pycforge/ide/resources/icons/remove-document.svg +1 -0
  182. pycforge/ide/resources/icons/replace.svg +1 -0
  183. pycforge/ide/resources/icons/save-as.svg +1 -0
  184. pycforge/ide/resources/icons/save.svg +1 -0
  185. pycforge/ide/resources/icons/select-all.svg +1 -0
  186. pycforge/ide/resources/icons/settings.svg +1 -0
  187. pycforge/ide/resources/icons/show-c.svg +1 -0
  188. pycforge/ide/resources/icons/split-view.svg +1 -0
  189. pycforge/ide/resources/icons/summary.svg +1 -0
  190. pycforge/ide/resources/icons/telemetry.svg +1 -0
  191. pycforge/ide/resources/icons/toggle-comment.svg +1 -0
  192. pycforge/ide/resources/icons/toggle-fold.svg +1 -0
  193. pycforge/ide/resources/icons/undo.svg +1 -0
  194. pycforge/ide/resources/icons/whitespace.svg +1 -0
  195. pycforge/ide/revisions.py +400 -0
  196. pycforge/ide/search_service.py +280 -0
  197. pycforge/ide/session_history.py +150 -0
  198. pycforge/ide/source_editing.py +417 -0
  199. pycforge/ide/source_structure.py +558 -0
  200. pycforge/ide/source_structure_async.py +191 -0
  201. pycforge/ide/supervisor.py +503 -0
  202. pycforge/ide/theme.py +119 -0
  203. pycforge/ide/theme_stylesheet.py +598 -0
  204. pycforge/ide/theme_workspace_stylesheet.py +155 -0
  205. pycforge/ide/visual_tokens.py +142 -0
  206. pycforge/ide/worker_protocol.py +80 -0
  207. pycforge/ide/workspace_search.py +462 -0
  208. pycforge/ide/workspace_session.py +256 -0
  209. pycforge/laboratory/__init__.py +6 -0
  210. pycforge/laboratory/audits.py +2244 -0
  211. pycforge/laboratory/checkpoint_e.py +2685 -0
  212. pycforge/laboratory/cli.py +119 -0
  213. pycforge/laboratory/exit_codes.py +11 -0
  214. pycforge/laboratory/keyword_audit.py +661 -0
  215. pycforge/laboratory/keyword_only_audit.py +481 -0
  216. pycforge-0.15.2.dist-info/METADATA +209 -0
  217. pycforge-0.15.2.dist-info/RECORD +221 -0
  218. pycforge-0.15.2.dist-info/WHEEL +5 -0
  219. pycforge-0.15.2.dist-info/entry_points.txt +5 -0
  220. pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
  221. 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]}