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,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
+ ]