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,220 @@
|
|
|
1
|
+
"""Immutable text-position indexes for bounded workspace projection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from bisect import bisect_right
|
|
6
|
+
from concurrent.futures import CancelledError, Future
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from threading import Condition, Thread, current_thread
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class TextPositionIndex:
|
|
14
|
+
"""Line and UTF-16 facts built once for one exact immutable string."""
|
|
15
|
+
|
|
16
|
+
text_length: int
|
|
17
|
+
line_starts: tuple[int, ...]
|
|
18
|
+
utf16_line_starts: tuple[int, ...]
|
|
19
|
+
utf16_compatible: bool
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def line_count(self) -> int:
|
|
23
|
+
return len(self.line_starts)
|
|
24
|
+
|
|
25
|
+
def character_offset(self, line: int, column: int) -> int:
|
|
26
|
+
"""Translate a one-based line and zero-based column without rescanning."""
|
|
27
|
+
|
|
28
|
+
if not isinstance(line, int) or not isinstance(column, int):
|
|
29
|
+
return 0
|
|
30
|
+
if line < 1:
|
|
31
|
+
return 0
|
|
32
|
+
if line > len(self.line_starts):
|
|
33
|
+
return self.text_length
|
|
34
|
+
start = self.line_starts[line - 1]
|
|
35
|
+
end = (
|
|
36
|
+
self.line_starts[line]
|
|
37
|
+
if line < len(self.line_starts)
|
|
38
|
+
else self.text_length
|
|
39
|
+
)
|
|
40
|
+
return min(self.text_length, start + min(max(0, column), end - start))
|
|
41
|
+
|
|
42
|
+
def qt_position(self, text: str, offset: int) -> int:
|
|
43
|
+
"""Translate a Python code-point offset to a Qt UTF-16 position."""
|
|
44
|
+
|
|
45
|
+
clipped = min(self.text_length, max(0, offset))
|
|
46
|
+
if self.utf16_compatible:
|
|
47
|
+
return clipped
|
|
48
|
+
line_index = max(0, bisect_right(self.line_starts, clipped) - 1)
|
|
49
|
+
line_start = self.line_starts[line_index]
|
|
50
|
+
qt_start = self.utf16_line_starts[line_index]
|
|
51
|
+
return qt_start + len(
|
|
52
|
+
text[line_start:clipped].encode("utf-16-le")
|
|
53
|
+
) // 2
|
|
54
|
+
|
|
55
|
+
def qt_range(
|
|
56
|
+
self,
|
|
57
|
+
text: str,
|
|
58
|
+
start: int,
|
|
59
|
+
end: int,
|
|
60
|
+
) -> tuple[int, int]:
|
|
61
|
+
return self.qt_position(text, start), self.qt_position(text, end)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_text_position_index(
|
|
65
|
+
text: str,
|
|
66
|
+
*,
|
|
67
|
+
cancelled: Callable[[], bool] | None = None,
|
|
68
|
+
) -> TextPositionIndex:
|
|
69
|
+
"""Build source-proportional position facts outside the GUI thread."""
|
|
70
|
+
|
|
71
|
+
if not isinstance(text, str):
|
|
72
|
+
raise TypeError("position-index text must be a string")
|
|
73
|
+
line_starts = [0]
|
|
74
|
+
utf16_line_starts = [0]
|
|
75
|
+
utf16_offset = 0
|
|
76
|
+
utf16_compatible = True
|
|
77
|
+
for offset, character in enumerate(text):
|
|
78
|
+
if (
|
|
79
|
+
cancelled is not None
|
|
80
|
+
and offset % 65_536 == 0
|
|
81
|
+
and cancelled()
|
|
82
|
+
):
|
|
83
|
+
raise CancelledError("position index was superseded")
|
|
84
|
+
astral = ord(character) > 0xFFFF
|
|
85
|
+
if astral:
|
|
86
|
+
utf16_compatible = False
|
|
87
|
+
utf16_offset += 2 if astral else 1
|
|
88
|
+
if character == "\n":
|
|
89
|
+
line_starts.append(offset + 1)
|
|
90
|
+
utf16_line_starts.append(utf16_offset)
|
|
91
|
+
return TextPositionIndex(
|
|
92
|
+
text_length=len(text),
|
|
93
|
+
line_starts=tuple(line_starts),
|
|
94
|
+
utf16_line_starts=tuple(utf16_line_starts),
|
|
95
|
+
utf16_compatible=utf16_compatible,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
PositionIndexListener = Callable[[int, str, TextPositionIndex], None]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(slots=True)
|
|
103
|
+
class _IndexJob:
|
|
104
|
+
submission: int
|
|
105
|
+
generation: int
|
|
106
|
+
text: str
|
|
107
|
+
future: Future[TextPositionIndex]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TextPositionIndexService:
|
|
111
|
+
"""One-active/one-latest-pending text index builder."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
listener: PositionIndexListener | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
self._listener = listener
|
|
118
|
+
self._condition = Condition()
|
|
119
|
+
self._submission = 0
|
|
120
|
+
self._active: _IndexJob | None = None
|
|
121
|
+
self._pending: _IndexJob | None = None
|
|
122
|
+
self._closed = False
|
|
123
|
+
self._thread = Thread(
|
|
124
|
+
target=self._run,
|
|
125
|
+
name="pycforge-output-position-index",
|
|
126
|
+
daemon=True,
|
|
127
|
+
)
|
|
128
|
+
self._thread.start()
|
|
129
|
+
|
|
130
|
+
def submit(
|
|
131
|
+
self,
|
|
132
|
+
generation: int,
|
|
133
|
+
text: str,
|
|
134
|
+
) -> Future[TextPositionIndex]:
|
|
135
|
+
future: Future[TextPositionIndex] = Future()
|
|
136
|
+
with self._condition:
|
|
137
|
+
if self._closed:
|
|
138
|
+
raise RuntimeError("position index service is closed")
|
|
139
|
+
self._submission += 1
|
|
140
|
+
job = _IndexJob(self._submission, generation, text, future)
|
|
141
|
+
replaced = self._pending
|
|
142
|
+
self._pending = job
|
|
143
|
+
if replaced is not None:
|
|
144
|
+
replaced.future.cancel()
|
|
145
|
+
self._condition.notify()
|
|
146
|
+
return future
|
|
147
|
+
|
|
148
|
+
def close(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
wait: bool = False,
|
|
152
|
+
timeout: float | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
with self._condition:
|
|
155
|
+
if not self._closed:
|
|
156
|
+
self._closed = True
|
|
157
|
+
pending = self._pending
|
|
158
|
+
self._pending = None
|
|
159
|
+
if pending is not None:
|
|
160
|
+
pending.future.cancel()
|
|
161
|
+
self._condition.notify_all()
|
|
162
|
+
if wait and current_thread() is not self._thread:
|
|
163
|
+
self._thread.join(timeout)
|
|
164
|
+
|
|
165
|
+
def _superseded(self, submission: int) -> bool:
|
|
166
|
+
with self._condition:
|
|
167
|
+
return self._closed or submission != self._submission
|
|
168
|
+
|
|
169
|
+
def _run(self) -> None:
|
|
170
|
+
while True:
|
|
171
|
+
with self._condition:
|
|
172
|
+
while self._pending is None and not self._closed:
|
|
173
|
+
self._condition.wait()
|
|
174
|
+
if self._pending is None and self._closed:
|
|
175
|
+
return
|
|
176
|
+
job = self._pending
|
|
177
|
+
self._pending = None
|
|
178
|
+
self._active = job
|
|
179
|
+
assert job is not None
|
|
180
|
+
if not job.future.set_running_or_notify_cancel():
|
|
181
|
+
with self._condition:
|
|
182
|
+
if self._active is job:
|
|
183
|
+
self._active = None
|
|
184
|
+
self._condition.notify_all()
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
index = build_text_position_index(
|
|
188
|
+
job.text,
|
|
189
|
+
cancelled=lambda: self._superseded(job.submission),
|
|
190
|
+
)
|
|
191
|
+
except BaseException as exc:
|
|
192
|
+
if not job.future.done():
|
|
193
|
+
job.future.set_exception(exc)
|
|
194
|
+
index = None
|
|
195
|
+
with self._condition:
|
|
196
|
+
if self._active is job:
|
|
197
|
+
self._active = None
|
|
198
|
+
publishable = (
|
|
199
|
+
index is not None
|
|
200
|
+
and not self._closed
|
|
201
|
+
and job.submission == self._submission
|
|
202
|
+
and self._pending is None
|
|
203
|
+
)
|
|
204
|
+
self._condition.notify_all()
|
|
205
|
+
if index is None:
|
|
206
|
+
continue
|
|
207
|
+
if not job.future.done():
|
|
208
|
+
job.future.set_result(index)
|
|
209
|
+
if publishable and self._listener is not None:
|
|
210
|
+
try:
|
|
211
|
+
self._listener(job.generation, job.text, index)
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = [
|
|
217
|
+
"TextPositionIndex",
|
|
218
|
+
"TextPositionIndexService",
|
|
219
|
+
"build_text_position_index",
|
|
220
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Spawn-safe child-process entry point for one PyCForge conversion request."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from threading import Thread
|
|
6
|
+
|
|
7
|
+
from pycforge.converter.core.cancellation import CancellationToken
|
|
8
|
+
from pycforge.converter.core.progress import ConversionProgress
|
|
9
|
+
from pycforge.converter.facade import PythonToCConverter
|
|
10
|
+
|
|
11
|
+
from .worker_protocol import (
|
|
12
|
+
ByteConnection,
|
|
13
|
+
WorkerProtocolError,
|
|
14
|
+
WorkerRequest,
|
|
15
|
+
encode_failure,
|
|
16
|
+
encode_progress,
|
|
17
|
+
encode_terminal,
|
|
18
|
+
receive_control,
|
|
19
|
+
receive_request,
|
|
20
|
+
send_event,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def worker_main(
|
|
25
|
+
request_connection: ByteConnection,
|
|
26
|
+
event_connection: ByteConnection,
|
|
27
|
+
control_connection: ByteConnection,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Receive, convert, publish one terminal event, and exit.
|
|
30
|
+
|
|
31
|
+
Conversion runs only in this child process. The small daemon listener
|
|
32
|
+
translates the sole control command into the converter's existing local
|
|
33
|
+
``CancellationToken``; the converter facade itself remains unchanged.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
request: WorkerRequest | None = None
|
|
37
|
+
token = CancellationToken()
|
|
38
|
+
try:
|
|
39
|
+
request = receive_request(request_connection)
|
|
40
|
+
_close_quietly(request_connection)
|
|
41
|
+
Thread(
|
|
42
|
+
target=_listen_for_cancel,
|
|
43
|
+
args=(control_connection, request, token),
|
|
44
|
+
name=f"pycforge-cancel-{request.generation}",
|
|
45
|
+
daemon=True,
|
|
46
|
+
).start()
|
|
47
|
+
|
|
48
|
+
def report(progress: ConversionProgress) -> None:
|
|
49
|
+
send_event(event_connection, encode_progress(request, progress))
|
|
50
|
+
|
|
51
|
+
result = PythonToCConverter().convert(
|
|
52
|
+
request.request,
|
|
53
|
+
observation=request.observation,
|
|
54
|
+
cancellation=token,
|
|
55
|
+
progress=report,
|
|
56
|
+
)
|
|
57
|
+
send_event(event_connection, encode_terminal(request, result))
|
|
58
|
+
except WorkerProtocolError as exc:
|
|
59
|
+
_send_failure_quietly(
|
|
60
|
+
event_connection,
|
|
61
|
+
request,
|
|
62
|
+
"protocol-error",
|
|
63
|
+
str(exc),
|
|
64
|
+
)
|
|
65
|
+
except MemoryError:
|
|
66
|
+
_send_failure_quietly(
|
|
67
|
+
event_connection,
|
|
68
|
+
request,
|
|
69
|
+
"worker-resource-exhaustion",
|
|
70
|
+
"converter worker exhausted its bounded process resources",
|
|
71
|
+
)
|
|
72
|
+
except (EOFError, BrokenPipeError, OSError):
|
|
73
|
+
_send_failure_quietly(
|
|
74
|
+
event_connection,
|
|
75
|
+
request,
|
|
76
|
+
"request-transport-error",
|
|
77
|
+
"converter worker transport closed before terminal publication",
|
|
78
|
+
)
|
|
79
|
+
except Exception:
|
|
80
|
+
_send_failure_quietly(
|
|
81
|
+
event_connection,
|
|
82
|
+
request,
|
|
83
|
+
"worker-internal-error",
|
|
84
|
+
"converter worker failed before terminal publication",
|
|
85
|
+
)
|
|
86
|
+
finally:
|
|
87
|
+
_close_quietly(request_connection)
|
|
88
|
+
_close_quietly(control_connection)
|
|
89
|
+
_close_quietly(event_connection)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _listen_for_cancel(
|
|
93
|
+
connection: ByteConnection,
|
|
94
|
+
request: WorkerRequest,
|
|
95
|
+
token: CancellationToken,
|
|
96
|
+
) -> None:
|
|
97
|
+
try:
|
|
98
|
+
control = receive_control(connection)
|
|
99
|
+
if (
|
|
100
|
+
control.generation == request.generation
|
|
101
|
+
and control.bundle_fingerprint == request.bundle_fingerprint
|
|
102
|
+
and control.transport_fingerprint == request.transport_fingerprint
|
|
103
|
+
):
|
|
104
|
+
token.cancel()
|
|
105
|
+
except (WorkerProtocolError, EOFError, BrokenPipeError, OSError):
|
|
106
|
+
return
|
|
107
|
+
finally:
|
|
108
|
+
_close_quietly(connection)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _send_failure_quietly(
|
|
112
|
+
connection: ByteConnection,
|
|
113
|
+
request: WorkerRequest | None,
|
|
114
|
+
classification: str,
|
|
115
|
+
message: str,
|
|
116
|
+
) -> None:
|
|
117
|
+
try:
|
|
118
|
+
send_event(
|
|
119
|
+
connection,
|
|
120
|
+
encode_failure(request, classification, message),
|
|
121
|
+
)
|
|
122
|
+
except (WorkerProtocolError, EOFError, BrokenPipeError, OSError):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _close_quietly(connection: ByteConnection) -> None:
|
|
127
|
+
try:
|
|
128
|
+
connection.close()
|
|
129
|
+
except (BrokenPipeError, OSError):
|
|
130
|
+
pass
|
pycforge/ide/qt.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Professional PyQt5 workspace for the PyCForge facade.
|
|
2
|
+
|
|
3
|
+
The desktop surface remains presentation and orchestration only. It never
|
|
4
|
+
executes Python, compiles generated C, resolves imports through the host, or
|
|
5
|
+
changes converter contracts. The module is import-safe when PyQt5 is absent.
|
|
6
|
+
|
|
7
|
+
The implementation is decomposed into cohesive mixins so the active workspace
|
|
8
|
+
surface stays reviewable: shell construction, document actions, bounded
|
|
9
|
+
snapshot projection, settings/file observation, and guarded close behavior.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pycforge._version import __version__
|
|
17
|
+
|
|
18
|
+
from .controller import WorkspaceController
|
|
19
|
+
from .qt_contract import (
|
|
20
|
+
SETTINGS_APPLICATION,
|
|
21
|
+
SETTINGS_ORGANIZATION,
|
|
22
|
+
SETTINGS_SCHEMA_VERSION,
|
|
23
|
+
coerce_settings_schema_version,
|
|
24
|
+
diagnostic_character_range,
|
|
25
|
+
line_column_offset,
|
|
26
|
+
mapping_character_range,
|
|
27
|
+
python_offset_to_qt_position,
|
|
28
|
+
qt_position_to_python_offset,
|
|
29
|
+
qt_range,
|
|
30
|
+
)
|
|
31
|
+
from .theme import apply_pycforge_theme
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Preserve the original private helper spelling used by the workspace tests
|
|
35
|
+
# and downstream integrations.
|
|
36
|
+
_coerce_settings_schema_version = coerce_settings_schema_version
|
|
37
|
+
_qt_range = qt_range
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from PyQt5.QtCore import (
|
|
42
|
+
QFileSystemWatcher,
|
|
43
|
+
QSettings,
|
|
44
|
+
Qt,
|
|
45
|
+
QTimer,
|
|
46
|
+
pyqtSignal,
|
|
47
|
+
)
|
|
48
|
+
from PyQt5.QtWidgets import QApplication, QMainWindow
|
|
49
|
+
except (ImportError, ModuleNotFoundError) as exc:
|
|
50
|
+
QT_AVAILABLE = False
|
|
51
|
+
_QT_ERROR = exc
|
|
52
|
+
else:
|
|
53
|
+
QT_AVAILABLE = True
|
|
54
|
+
_QT_ERROR = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if QT_AVAILABLE:
|
|
58
|
+
from .qt_close import QtCloseMixin
|
|
59
|
+
from .qt_documents import QtDocumentActionsMixin
|
|
60
|
+
from .qt_projection import QtProjectionMixin
|
|
61
|
+
from .qt_shell import QtShellMixin
|
|
62
|
+
from .qt_state import QtStateMixin
|
|
63
|
+
from .qt_workspace_features import QtWorkspaceFeaturesMixin
|
|
64
|
+
|
|
65
|
+
class MainWindow(
|
|
66
|
+
QtShellMixin,
|
|
67
|
+
QtWorkspaceFeaturesMixin,
|
|
68
|
+
QtDocumentActionsMixin,
|
|
69
|
+
QtProjectionMixin,
|
|
70
|
+
QtStateMixin,
|
|
71
|
+
QtCloseMixin,
|
|
72
|
+
QMainWindow,
|
|
73
|
+
):
|
|
74
|
+
"""Python-first SourceBundle workspace with immutable C presentation."""
|
|
75
|
+
|
|
76
|
+
snapshot_ready = pyqtSignal(object)
|
|
77
|
+
structure_ready = pyqtSignal(object)
|
|
78
|
+
operation_error = pyqtSignal(str)
|
|
79
|
+
io_finished = pyqtSignal(object, object)
|
|
80
|
+
|
|
81
|
+
_STAGE_LABELS = {
|
|
82
|
+
"frontend.source_document": "Reading source bundle",
|
|
83
|
+
"frontend.parse": "Parsing Python documents",
|
|
84
|
+
"frontend.normalize": "Normalizing Python IR",
|
|
85
|
+
"modules.resolve": "Resolving module bundle",
|
|
86
|
+
"analysis.plan": "Analyzing and planning",
|
|
87
|
+
"lowering.first_slice": (
|
|
88
|
+
"Lowering, resolving helpers, and validating C"
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
_SUCCESS_STATES = {
|
|
92
|
+
"converted",
|
|
93
|
+
"warning",
|
|
94
|
+
"approximation",
|
|
95
|
+
"observer-incomplete",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
controller: WorkspaceController | None = None,
|
|
101
|
+
):
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.controller = controller or WorkspaceController()
|
|
104
|
+
self.settings = QSettings(
|
|
105
|
+
SETTINGS_ORGANIZATION,
|
|
106
|
+
SETTINGS_APPLICATION,
|
|
107
|
+
)
|
|
108
|
+
self._prepare_settings()
|
|
109
|
+
self._displayed_document_id: str | None = None
|
|
110
|
+
self._displayed_source_key: tuple[str, int] | None = None
|
|
111
|
+
self._displayed_output_key: (
|
|
112
|
+
tuple[int | None, int] | None
|
|
113
|
+
) = None
|
|
114
|
+
self._output_projection_key: (
|
|
115
|
+
tuple[int | None, int] | None
|
|
116
|
+
) = None
|
|
117
|
+
self._output_projection_offset = 0
|
|
118
|
+
self._output_projection_text = ""
|
|
119
|
+
self._navigator_key: tuple[Any, ...] | None = None
|
|
120
|
+
self._detail_projection_key: (
|
|
121
|
+
tuple[Any, ...] | None
|
|
122
|
+
) = None
|
|
123
|
+
self._source_marker_key: tuple[Any, ...] | None = None
|
|
124
|
+
self._output_marker_key: tuple[Any, ...] | None = None
|
|
125
|
+
self._last_snapshot = None
|
|
126
|
+
self._progress_sequence: int | None = None
|
|
127
|
+
self._closing = False
|
|
128
|
+
self._close_save_pending = 0
|
|
129
|
+
self._close_save_failed = False
|
|
130
|
+
self._applying_source_text = False
|
|
131
|
+
self._source_sync_pending = False
|
|
132
|
+
self._source_sync_document_id: str | None = None
|
|
133
|
+
self._source_sync_timer = QTimer(self)
|
|
134
|
+
self._source_sync_timer.setSingleShot(True)
|
|
135
|
+
self._source_sync_timer.setInterval(120)
|
|
136
|
+
self._source_sync_timer.timeout.connect(
|
|
137
|
+
self._flush_pending_source_sync
|
|
138
|
+
)
|
|
139
|
+
self._known_file_fingerprints: dict[str, str] = {}
|
|
140
|
+
self._watcher_projection_key: (
|
|
141
|
+
tuple[Any, ...] | None
|
|
142
|
+
) = None
|
|
143
|
+
self._recent_paths = self._load_recent_paths()
|
|
144
|
+
self._last_directory = self._setting_text(
|
|
145
|
+
"workspace/last_directory", ""
|
|
146
|
+
)
|
|
147
|
+
self._initialize_workspace_features()
|
|
148
|
+
|
|
149
|
+
self.setObjectName("PyCForgeMainWindow")
|
|
150
|
+
self.setWindowTitle(f"PyCForge {__version__} — Workspace")
|
|
151
|
+
self.resize(1420, 880)
|
|
152
|
+
self.setMinimumSize(960, 620)
|
|
153
|
+
|
|
154
|
+
self._build_workspace()
|
|
155
|
+
self._build_actions()
|
|
156
|
+
self._build_toolbar()
|
|
157
|
+
self._build_menus()
|
|
158
|
+
self._wire_workspace()
|
|
159
|
+
self._restore_workspace_state()
|
|
160
|
+
|
|
161
|
+
self.file_watcher = QFileSystemWatcher(self)
|
|
162
|
+
self.file_watcher.fileChanged.connect(
|
|
163
|
+
self._external_file_changed
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.snapshot_ready.connect(self._apply_snapshot)
|
|
167
|
+
self.operation_error.connect(
|
|
168
|
+
lambda message: self.toast.show_message(
|
|
169
|
+
message, "error"
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
self.io_finished.connect(
|
|
173
|
+
lambda callback, future: callback(future)
|
|
174
|
+
)
|
|
175
|
+
self._snapshot_listener = self.snapshot_ready.emit
|
|
176
|
+
self.controller.subscribe(self._snapshot_listener)
|
|
177
|
+
self._apply_snapshot(self.controller.snapshot)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Static integration vocabulary remains documented at the public module seam
|
|
181
|
+
# after decomposition. Runtime implementations live in the cohesive mixins:
|
|
182
|
+
# CodeEditor(language="python"), CodeEditor(language="c"), DocumentNavigator(),
|
|
183
|
+
# FindReplaceBar(), DiagnosticsView(), MappingsView(), QProgressBar;
|
|
184
|
+
# toolbar.setObjectName("PyCForgeToolbar");
|
|
185
|
+
# self.controller.save_generated_c_linked_async(), snapshot.can_save_c;
|
|
186
|
+
# self.output.setReadOnly(True), self.output.setVisible(False),
|
|
187
|
+
# self.tabs.setVisible(False); action label "Show C"; progress delay 180 ms.
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def run() -> int:
|
|
191
|
+
if not QT_AVAILABLE:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
"PyQt5 is required for the PyCForge workspace"
|
|
194
|
+
) from _QT_ERROR
|
|
195
|
+
if QApplication.instance() is None:
|
|
196
|
+
QApplication.setAttribute(
|
|
197
|
+
Qt.AA_EnableHighDpiScaling, True
|
|
198
|
+
)
|
|
199
|
+
QApplication.setAttribute(
|
|
200
|
+
Qt.AA_UseHighDpiPixmaps, True
|
|
201
|
+
)
|
|
202
|
+
app = QApplication.instance() or QApplication([])
|
|
203
|
+
app.setApplicationName("PyCForge")
|
|
204
|
+
app.setOrganizationName("PyCForge")
|
|
205
|
+
apply_pycforge_theme(app)
|
|
206
|
+
window = MainWindow()
|
|
207
|
+
window.show()
|
|
208
|
+
return app.exec_()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
__all__ = [
|
|
212
|
+
"MainWindow",
|
|
213
|
+
"QT_AVAILABLE",
|
|
214
|
+
"SETTINGS_SCHEMA_VERSION",
|
|
215
|
+
"diagnostic_character_range",
|
|
216
|
+
"line_column_offset",
|
|
217
|
+
"mapping_character_range",
|
|
218
|
+
"python_offset_to_qt_position",
|
|
219
|
+
"qt_position_to_python_offset",
|
|
220
|
+
"run",
|
|
221
|
+
] if QT_AVAILABLE else [
|
|
222
|
+
"QT_AVAILABLE",
|
|
223
|
+
"SETTINGS_SCHEMA_VERSION",
|
|
224
|
+
"diagnostic_character_range",
|
|
225
|
+
"line_column_offset",
|
|
226
|
+
"mapping_character_range",
|
|
227
|
+
"python_offset_to_qt_position",
|
|
228
|
+
"qt_position_to_python_offset",
|
|
229
|
+
"run",
|
|
230
|
+
]
|