python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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 (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,548 @@
1
+ """Process-tree memory measurement and escalation for wiki exports.
2
+
3
+ The guard is deliberately independent of the exporter: its callbacks change
4
+ execution strategy, while callers retain sole ownership of output payloads.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ from collections import defaultdict, deque
14
+ from collections.abc import Callable, Mapping
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+ from functools import lru_cache
18
+ from pathlib import Path
19
+ from time import monotonic
20
+
21
+ from .agent_cache import current_process_rss_bytes, parse_memory_size
22
+
23
+ GIB = 1024**3
24
+ DEFAULT_MEMORY_LIMIT_BYTES = 4 * GIB
25
+ WIKI_MAX_MEMORY_ENV = "DELPHI_LSP_WIKI_MAX_MEMORY"
26
+ _PROC_PIDTBSDINFO = 3
27
+ _PROC_PIDTASKINFO = 4
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ProcessMemorySnapshot:
32
+ """One process's parent relationship and resident memory usage."""
33
+
34
+ parent_pid: int
35
+ rss_bytes: int
36
+
37
+
38
+ class MemoryStage(str, Enum):
39
+ COMPACT = "compact"
40
+ REDUCE_WORKERS = "reduce_workers"
41
+ SEQUENTIAL = "sequential"
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class MemoryTransition:
46
+ """An escalation that callers may surface through progress reporting."""
47
+
48
+ stage: MemoryStage
49
+ phase: str
50
+ used_bytes: int
51
+ limit_bytes: int
52
+
53
+ @property
54
+ def utilization_percent(self) -> float:
55
+ return self.used_bytes * 100.0 / self.limit_bytes
56
+
57
+
58
+ class WikiMemoryLimitExceeded(RuntimeError):
59
+ """Raised when memory remains over budget after sequentialization."""
60
+
61
+ def __init__(self, *, phase: str, used_bytes: int, limit_bytes: int) -> None:
62
+ self.phase = phase
63
+ self.used_bytes = used_bytes
64
+ self.limit_bytes = limit_bytes
65
+ super().__init__(
66
+ f"Wiki export exceeded its {limit_bytes} byte memory limit during "
67
+ f"{phase} after switching to sequential processing ({used_bytes} bytes)."
68
+ )
69
+
70
+
71
+ def system_memory_bytes() -> int:
72
+ """Return physical memory without optional dependencies, or zero on failure."""
73
+ try:
74
+ if os.name == "nt":
75
+ return _windows_system_memory_bytes()
76
+ if sys.platform == "darwin":
77
+ return _darwin_system_memory_bytes()
78
+ if sys.platform.startswith("linux"):
79
+ return _linux_system_memory_bytes()
80
+ except Exception: # noqa: BLE001, S110 - probes must never disrupt export.
81
+ pass
82
+ return 0
83
+
84
+
85
+ def _linux_system_memory_bytes() -> int:
86
+ for line in Path("/proc/meminfo").read_text(encoding="ascii").splitlines():
87
+ field, _, value = line.partition(":")
88
+ if field == "MemTotal":
89
+ return int(value.split()[0]) * 1024
90
+ return 0
91
+
92
+
93
+ def _darwin_system_memory_bytes() -> int:
94
+ result = subprocess.run(
95
+ ["/usr/sbin/sysctl", "-n", "hw.memsize"],
96
+ check=True,
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+ return int(result.stdout.strip())
101
+
102
+
103
+ def _windows_system_memory_bytes() -> int:
104
+ class MemoryStatusEx(ctypes.Structure):
105
+ _fields_ = [
106
+ ("dwLength", ctypes.c_ulong),
107
+ ("dwMemoryLoad", ctypes.c_ulong),
108
+ ("ullTotalPhys", ctypes.c_ulonglong),
109
+ ("ullAvailPhys", ctypes.c_ulonglong),
110
+ ("ullTotalPageFile", ctypes.c_ulonglong),
111
+ ("ullAvailPageFile", ctypes.c_ulonglong),
112
+ ("ullTotalVirtual", ctypes.c_ulonglong),
113
+ ("ullAvailVirtual", ctypes.c_ulonglong),
114
+ ("ullAvailExtendedVirtual", ctypes.c_ulonglong),
115
+ ]
116
+
117
+ status = MemoryStatusEx()
118
+ status.dwLength = ctypes.sizeof(status)
119
+ windll = ctypes.windll # type: ignore[attr-defined]
120
+ if not windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):
121
+ return 0
122
+ return int(status.ullTotalPhys)
123
+
124
+
125
+ def default_memory_limit_bytes() -> int:
126
+ """Return 90% of physical memory, capped at 4 GiB, with a 4 GiB fallback."""
127
+ total = system_memory_bytes()
128
+ return min(DEFAULT_MEMORY_LIMIT_BYTES, total * 90 // 100) if total > 0 else DEFAULT_MEMORY_LIMIT_BYTES
129
+
130
+
131
+ def parse_optional_memory_limit(value: str | None) -> int | None:
132
+ """Parse a user override without allowing an invalid optional value to crash setup."""
133
+ if value is None or not value.strip():
134
+ return None
135
+ try:
136
+ return parse_memory_size(value)
137
+ except (TypeError, ValueError):
138
+ return None
139
+
140
+
141
+ def resolve_memory_limit_bytes(
142
+ explicit_limit_bytes: int | None,
143
+ *,
144
+ environ: Mapping[str, str] | None = None,
145
+ ) -> int:
146
+ """Resolve an explicit or environment override, otherwise use the safe default."""
147
+ if explicit_limit_bytes is not None:
148
+ if explicit_limit_bytes <= 0:
149
+ raise ValueError("wiki max memory must be greater than zero")
150
+ return explicit_limit_bytes
151
+ selected_environment = os.environ if environ is None else environ
152
+ raw_value = selected_environment.get(WIKI_MAX_MEMORY_ENV)
153
+ if raw_value is None or not raw_value.strip():
154
+ return default_memory_limit_bytes()
155
+ try:
156
+ value = parse_memory_size(raw_value)
157
+ except (TypeError, ValueError) as error:
158
+ raise ValueError(
159
+ f"{WIKI_MAX_MEMORY_ENV} must be a positive memory size"
160
+ ) from error
161
+ if value <= 0:
162
+ raise ValueError(f"{WIKI_MAX_MEMORY_ENV} must be greater than zero")
163
+ return value
164
+
165
+
166
+ def process_tree_rss_bytes(
167
+ root_pid: int | None = None,
168
+ *,
169
+ snapshot: Mapping[int, ProcessMemorySnapshot] | None = None,
170
+ ) -> int:
171
+ """Return resident memory for a process and all descendants, or zero on failure."""
172
+ if root_pid is None:
173
+ root_pid = os.getpid()
174
+ try:
175
+ if snapshot is not None:
176
+ processes = snapshot
177
+ else:
178
+ try:
179
+ processes = _process_memory_snapshot()
180
+ except Exception: # noqa: BLE001 - fall back to the root process below.
181
+ processes = {}
182
+ if not processes and root_pid == os.getpid():
183
+ current = current_process_rss_bytes()
184
+ if current > 0:
185
+ return current
186
+ import resource
187
+
188
+ maximum = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
189
+ return maximum if sys.platform == "darwin" else maximum * 1024
190
+ return _sum_descendant_rss(root_pid, processes)
191
+ except Exception: # noqa: BLE001 - process inspection is best effort.
192
+ return 0
193
+
194
+
195
+ def _sum_descendant_rss(
196
+ root_pid: int, snapshot: Mapping[int, ProcessMemorySnapshot]
197
+ ) -> int:
198
+ children: dict[int, list[int]] = defaultdict(list)
199
+ for pid, item in snapshot.items():
200
+ children[item.parent_pid].append(pid)
201
+ pending = deque([root_pid])
202
+ seen: set[int] = set()
203
+ total = 0
204
+ while pending:
205
+ pid = pending.popleft()
206
+ if pid in seen:
207
+ continue
208
+ seen.add(pid)
209
+ current = snapshot.get(pid)
210
+ if current is not None:
211
+ total += max(0, current.rss_bytes)
212
+ pending.extend(children.get(pid, ()))
213
+ return total
214
+
215
+
216
+ def _process_memory_snapshot() -> dict[int, ProcessMemorySnapshot]:
217
+ if os.name == "nt":
218
+ return _windows_process_snapshot()
219
+ if sys.platform.startswith("linux"):
220
+ return _linux_process_snapshot()
221
+ if sys.platform == "darwin":
222
+ return _darwin_process_snapshot()
223
+ return {}
224
+
225
+
226
+ def _linux_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
227
+ result: dict[int, ProcessMemorySnapshot] = {}
228
+ for entry in Path("/proc").iterdir():
229
+ if not entry.name.isdecimal():
230
+ continue
231
+ try:
232
+ fields = entry.joinpath("status").read_text(encoding="ascii").splitlines()
233
+ values = {line.split(":", 1)[0]: line.split(":", 1)[1].split()[0] for line in fields if ":" in line}
234
+ result[int(entry.name)] = ProcessMemorySnapshot(
235
+ parent_pid=int(values["PPid"]), rss_bytes=int(values.get("VmRSS", 0)) * 1024
236
+ )
237
+ except (OSError, KeyError, ValueError, IndexError):
238
+ continue
239
+ return result
240
+
241
+
242
+ def _darwin_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
243
+ try:
244
+ return _darwin_libproc_process_snapshot()
245
+ except Exception: # noqa: BLE001 - retain the portable command fallback.
246
+ return _darwin_ps_process_snapshot()
247
+
248
+
249
+ @lru_cache(maxsize=1)
250
+ def _load_darwin_libproc() -> ctypes.CDLL:
251
+ libproc = ctypes.CDLL("/usr/lib/libproc.dylib", use_errno=True)
252
+ libproc.proc_listallpids.argtypes = [ctypes.c_void_p, ctypes.c_int]
253
+ libproc.proc_listallpids.restype = ctypes.c_int
254
+ libproc.proc_pidinfo.argtypes = [
255
+ ctypes.c_int,
256
+ ctypes.c_int,
257
+ ctypes.c_uint64,
258
+ ctypes.c_void_p,
259
+ ctypes.c_int,
260
+ ]
261
+ libproc.proc_pidinfo.restype = ctypes.c_int
262
+ return libproc
263
+
264
+
265
+ def _darwin_libproc_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
266
+ """Read the macOS process table without spawning ``ps`` at every sample."""
267
+ libproc = _load_darwin_libproc()
268
+ process_count = int(libproc.proc_listallpids(None, 0))
269
+ if process_count <= 0:
270
+ raise OSError("libproc returned no process capacity")
271
+ capacity = max(64, process_count + 64)
272
+ while True:
273
+ pids = (ctypes.c_int * capacity)()
274
+ returned = int(libproc.proc_listallpids(pids, ctypes.sizeof(pids)))
275
+ if returned < 0:
276
+ raise OSError(ctypes.get_errno(), "proc_listallpids failed")
277
+ if returned < capacity:
278
+ break
279
+ capacity *= 2
280
+
281
+ result: dict[int, ProcessMemorySnapshot] = {}
282
+ bsd_info = (ctypes.c_ubyte * 512)()
283
+ task_info = (ctypes.c_ubyte * 256)()
284
+ for raw_pid in pids[:returned]:
285
+ pid = int(raw_pid)
286
+ if pid <= 0:
287
+ continue
288
+ bsd_size = int(
289
+ libproc.proc_pidinfo(
290
+ pid,
291
+ _PROC_PIDTBSDINFO,
292
+ 0,
293
+ bsd_info,
294
+ ctypes.sizeof(bsd_info),
295
+ )
296
+ )
297
+ if bsd_size < 20:
298
+ continue
299
+ task_size = int(
300
+ libproc.proc_pidinfo(
301
+ pid,
302
+ _PROC_PIDTASKINFO,
303
+ 0,
304
+ task_info,
305
+ ctypes.sizeof(task_info),
306
+ )
307
+ )
308
+ if task_size < 16:
309
+ continue
310
+ parent_pid = int(ctypes.c_uint32.from_buffer(bsd_info, 16).value)
311
+ rss_bytes = int(ctypes.c_uint64.from_buffer(task_info, 8).value)
312
+ result[pid] = ProcessMemorySnapshot(parent_pid, rss_bytes)
313
+ if not result:
314
+ raise OSError("libproc returned no readable processes")
315
+ return result
316
+
317
+
318
+ def _darwin_ps_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
319
+ output = subprocess.run(
320
+ ["/bin/ps", "-axo", "pid=,ppid=,rss="],
321
+ check=True,
322
+ capture_output=True,
323
+ text=True,
324
+ ).stdout
325
+ result: dict[int, ProcessMemorySnapshot] = {}
326
+ for line in output.splitlines():
327
+ try:
328
+ pid, parent_pid, rss_kib = line.split()
329
+ result[int(pid)] = ProcessMemorySnapshot(int(parent_pid), int(rss_kib) * 1024)
330
+ except ValueError:
331
+ continue
332
+ return result
333
+
334
+
335
+ def _windows_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
336
+ """Collect parent links and working sets with ToolHelp/PSAPI, no third-party API."""
337
+ from ctypes import wintypes
338
+
339
+ class ProcessEntry32(ctypes.Structure):
340
+ _fields_ = [
341
+ ("dwSize", wintypes.DWORD),
342
+ ("cntUsage", wintypes.DWORD),
343
+ ("th32ProcessID", wintypes.DWORD),
344
+ ("th32DefaultHeapID", ctypes.c_size_t),
345
+ ("th32ModuleID", wintypes.DWORD),
346
+ ("cntThreads", wintypes.DWORD),
347
+ ("th32ParentProcessID", wintypes.DWORD),
348
+ ("pcPriClassBase", ctypes.c_long),
349
+ ("dwFlags", wintypes.DWORD),
350
+ ("szExeFile", ctypes.c_wchar * 260),
351
+ ]
352
+
353
+ class ProcessMemoryCounters(ctypes.Structure):
354
+ _fields_ = [
355
+ ("cb", wintypes.DWORD),
356
+ ("PageFaultCount", wintypes.DWORD),
357
+ ("PeakWorkingSetSize", ctypes.c_size_t),
358
+ ("WorkingSetSize", ctypes.c_size_t),
359
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
360
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
361
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
362
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
363
+ ("PagefileUsage", ctypes.c_size_t),
364
+ ("PeakPagefileUsage", ctypes.c_size_t),
365
+ ]
366
+
367
+ windll = ctypes.windll # type: ignore[attr-defined]
368
+ kernel32 = windll.kernel32
369
+ psapi = windll.psapi
370
+ kernel32.CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD]
371
+ kernel32.CreateToolhelp32Snapshot.restype = wintypes.HANDLE
372
+ kernel32.Process32FirstW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry32)]
373
+ kernel32.Process32FirstW.restype = wintypes.BOOL
374
+ kernel32.Process32NextW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry32)]
375
+ kernel32.Process32NextW.restype = wintypes.BOOL
376
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
377
+ kernel32.OpenProcess.restype = wintypes.HANDLE
378
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
379
+ kernel32.CloseHandle.restype = wintypes.BOOL
380
+ psapi.GetProcessMemoryInfo.argtypes = [
381
+ wintypes.HANDLE,
382
+ ctypes.POINTER(ProcessMemoryCounters),
383
+ wintypes.DWORD,
384
+ ]
385
+ psapi.GetProcessMemoryInfo.restype = wintypes.BOOL
386
+ snapshot = kernel32.CreateToolhelp32Snapshot(0x00000002, 0)
387
+ if not snapshot or snapshot == wintypes.HANDLE(-1).value:
388
+ return {}
389
+ result: dict[int, ProcessMemorySnapshot] = {}
390
+ try:
391
+ entry = ProcessEntry32()
392
+ entry.dwSize = ctypes.sizeof(entry)
393
+ has_process = kernel32.Process32FirstW(snapshot, ctypes.byref(entry))
394
+ while has_process:
395
+ process = kernel32.OpenProcess(0x0400 | 0x0010, False, entry.th32ProcessID)
396
+ rss_bytes = 0
397
+ if process:
398
+ try:
399
+ counters = ProcessMemoryCounters()
400
+ counters.cb = ctypes.sizeof(counters)
401
+ if psapi.GetProcessMemoryInfo(process, ctypes.byref(counters), counters.cb):
402
+ rss_bytes = int(counters.WorkingSetSize)
403
+ finally:
404
+ kernel32.CloseHandle(process)
405
+ result[int(entry.th32ProcessID)] = ProcessMemorySnapshot(
406
+ int(entry.th32ParentProcessID), rss_bytes
407
+ )
408
+ entry.dwSize = ctypes.sizeof(entry)
409
+ has_process = kernel32.Process32NextW(snapshot, ctypes.byref(entry))
410
+ finally:
411
+ kernel32.CloseHandle(snapshot)
412
+ return result
413
+
414
+
415
+ class ProcessTreeMemorySampler:
416
+ """Measure root RSS cheaply while periodically refreshing child RSS."""
417
+
418
+ def __init__(
419
+ self,
420
+ *,
421
+ interval_seconds: float = 5.0,
422
+ root_measure: Callable[[], int] = current_process_rss_bytes,
423
+ tree_measure: Callable[[], int] = process_tree_rss_bytes,
424
+ clock: Callable[[], float] = monotonic,
425
+ ) -> None:
426
+ if interval_seconds <= 0:
427
+ raise ValueError("interval_seconds must be greater than zero")
428
+ self.interval_seconds = interval_seconds
429
+ self._root_measure = root_measure
430
+ self._tree_measure = tree_measure
431
+ self._clock = clock
432
+ self._last_tree_sample = float("-inf")
433
+ self._cached_child_bytes = 0
434
+
435
+ def __call__(self) -> int:
436
+ root_bytes = self._root_measure()
437
+ now = self._clock()
438
+ if now - self._last_tree_sample >= self.interval_seconds:
439
+ return self._sample_tree(root_bytes, now)
440
+ return max(0, root_bytes) + self._cached_child_bytes
441
+
442
+ def force(self) -> int:
443
+ """Refresh the full process tree regardless of the sampling interval."""
444
+ root_bytes = self._root_measure()
445
+ return self._sample_tree(root_bytes, self._clock())
446
+
447
+ def _sample_tree(self, root_bytes: int, now: float) -> int:
448
+ tree_bytes = self._tree_measure()
449
+ self._last_tree_sample = now
450
+ if tree_bytes <= 0:
451
+ self._cached_child_bytes = 0
452
+ return max(0, root_bytes)
453
+ self._cached_child_bytes = max(0, tree_bytes - max(0, root_bytes))
454
+ return max(0, root_bytes) + self._cached_child_bytes
455
+
456
+
457
+ class WikiMemoryGuard:
458
+ """Escalate wiki-export execution strategy without touching emitted data."""
459
+
460
+ def __init__(
461
+ self,
462
+ *,
463
+ limit_bytes: int | None = None,
464
+ measure: Callable[[], int] | None = None,
465
+ compact: Callable[[], None] | None = None,
466
+ reduce_workers: Callable[[], None] | None = None,
467
+ sequentialize: Callable[[], None] | None = None,
468
+ on_transition: Callable[[MemoryTransition], None] | None = None,
469
+ ) -> None:
470
+ self.limit_bytes = limit_bytes if limit_bytes is not None else default_memory_limit_bytes()
471
+ if self.limit_bytes <= 0:
472
+ raise ValueError("limit_bytes must be greater than zero")
473
+ self._measure = measure or ProcessTreeMemorySampler()
474
+ self._actions = {
475
+ MemoryStage.COMPACT: compact or (lambda: None),
476
+ MemoryStage.REDUCE_WORKERS: reduce_workers or (lambda: None),
477
+ MemoryStage.SEQUENTIAL: sequentialize or (lambda: None),
478
+ }
479
+ self._on_transition = on_transition
480
+ self._completed: set[MemoryStage] = set()
481
+ self.peak_bytes = 0
482
+ self.phase_peaks: dict[str, int] = {}
483
+
484
+ def check(
485
+ self,
486
+ used_bytes: int | None = None,
487
+ *,
488
+ phase: str,
489
+ force_process_tree: bool = False,
490
+ ) -> MemoryTransition | None:
491
+ """Apply at most one new stage and return it for progress reporting."""
492
+ if used_bytes is not None:
493
+ used = used_bytes
494
+ elif force_process_tree and isinstance(self._measure, ProcessTreeMemorySampler):
495
+ used = self._measure.force()
496
+ else:
497
+ used = self._measure()
498
+ self.peak_bytes = max(self.peak_bytes, used)
499
+ self.phase_peaks[phase] = max(self.phase_peaks.get(phase, 0), used)
500
+ if used <= 0:
501
+ return None
502
+ required_stage = self._required_stage(used)
503
+ if required_stage is None:
504
+ return None
505
+ ordered_stages = (
506
+ MemoryStage.COMPACT,
507
+ MemoryStage.REDUCE_WORKERS,
508
+ MemoryStage.SEQUENTIAL,
509
+ )
510
+ required_index = ordered_stages.index(required_stage)
511
+ if required_stage in self._completed:
512
+ if required_stage is MemoryStage.SEQUENTIAL:
513
+ if (
514
+ used_bytes is None
515
+ and not force_process_tree
516
+ and isinstance(self._measure, ProcessTreeMemorySampler)
517
+ ):
518
+ used = self._measure.force()
519
+ self.peak_bytes = max(self.peak_bytes, used)
520
+ self.phase_peaks[phase] = max(
521
+ self.phase_peaks.get(phase, 0),
522
+ used,
523
+ )
524
+ if self._required_stage(used) is not MemoryStage.SEQUENTIAL:
525
+ return None
526
+ raise WikiMemoryLimitExceeded(phase=phase, used_bytes=used, limit_bytes=self.limit_bytes)
527
+ return None
528
+ latest_transition: MemoryTransition | None = None
529
+ for stage in ordered_stages[: required_index + 1]:
530
+ if stage in self._completed:
531
+ continue
532
+ transition = MemoryTransition(stage, phase, used, self.limit_bytes)
533
+ self._actions[stage]()
534
+ self._completed.add(stage)
535
+ if self._on_transition is not None:
536
+ self._on_transition(transition)
537
+ latest_transition = transition
538
+ return latest_transition
539
+
540
+ def _required_stage(self, used_bytes: int) -> MemoryStage | None:
541
+ utilization = used_bytes * 100 / self.limit_bytes
542
+ if utilization >= 90:
543
+ return MemoryStage.SEQUENTIAL
544
+ if utilization >= 80:
545
+ return MemoryStage.REDUCE_WORKERS
546
+ if utilization >= 60:
547
+ return MemoryStage.COMPACT
548
+ return None
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from dataclasses import dataclass, field
5
+
6
+ from .consts import AttributeName
7
+ from .nodes import SyntaxNode
8
+ from .parser import DelphiParser
9
+ from .preprocessor import IncludeLoader, PreprocessedSource
10
+ from .semantic import Scope, ScopeKind, SymbolIndex
11
+ from .semantic_builder import SemanticBuilder, SemanticModel
12
+
13
+
14
+ @dataclass
15
+ class WorkspaceSemanticResult:
16
+ models: dict[str, SemanticModel]
17
+ index: SymbolIndex
18
+ preprocessed: dict[str, PreprocessedSource] = field(default_factory=dict)
19
+
20
+
21
+ def build_workspace_semantics(
22
+ sources: dict[str, str],
23
+ *,
24
+ include_paths: Iterable[str] = (),
25
+ defines: Iterable[str] = (),
26
+ include_loader: IncludeLoader | None = None,
27
+ preprocessor_options=None,
28
+ collect_references: bool = True,
29
+ ) -> WorkspaceSemanticResult:
30
+ parser = DelphiParser(
31
+ include_paths=include_paths,
32
+ defines=defines,
33
+ include_loader=include_loader,
34
+ preprocessor_options=preprocessor_options,
35
+ )
36
+ roots: dict[str, SyntaxNode] = {}
37
+ preprocessed_sources: dict[str, PreprocessedSource] = {}
38
+ for file_name, text in sources.items():
39
+ result = parser.parse(text, file_name, build_semantic=False)
40
+ roots[file_name] = result.root
41
+ preprocessed_sources[file_name] = result.preprocessed
42
+
43
+ return build_workspace_semantics_from_roots(
44
+ roots,
45
+ preprocessed_sources=preprocessed_sources,
46
+ collect_references=collect_references,
47
+ )
48
+
49
+
50
+ def build_workspace_semantics_from_roots(
51
+ roots: dict[str, SyntaxNode],
52
+ *,
53
+ preprocessed_sources: Mapping[str, PreprocessedSource] | None = None,
54
+ collect_references: bool = True,
55
+ ) -> WorkspaceSemanticResult:
56
+ """Build project semantics from syntax roots that have already been parsed."""
57
+
58
+ index = SymbolIndex()
59
+ scopes: dict[str, Scope] = {}
60
+ for file_name, root in roots.items():
61
+ unit_name = root.get_attribute(AttributeName.anName) or file_name
62
+ unit_scope = Scope(kind=ScopeKind.UNIT, name=unit_name)
63
+ index.register_unit(unit_name, unit_scope, index_symbols=False)
64
+ scopes[file_name] = unit_scope
65
+
66
+ source_maps = preprocessed_sources or {}
67
+ builder = SemanticBuilder(
68
+ collect_references=collect_references,
69
+ source_maps=source_maps,
70
+ )
71
+ for file_name, root in roots.items():
72
+ builder.declare(root, index=index, unit_scope=scopes[file_name], reset_state=False)
73
+
74
+ models: dict[str, SemanticModel] = {}
75
+ for file_name, root in roots.items():
76
+ model = builder.resolve(root, scopes[file_name])
77
+ models[file_name] = model
78
+
79
+ return WorkspaceSemanticResult(
80
+ models=models,
81
+ index=index,
82
+ preprocessed=dict(source_maps),
83
+ )
delphi_lsp/writer.py ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from .consts import ATTRIBUTE_NAME_STRINGS, SYNTAX_NODE_NAMES
4
+ from .nodes import CompoundSyntaxNode, SyntaxNode, ValuedSyntaxNode
5
+
6
+
7
+ def _xml_encode(data: str) -> str:
8
+ result = []
9
+ for ch in data:
10
+ if ch == '<':
11
+ result.append('&lt;')
12
+ elif ch == '>':
13
+ result.append('&gt;')
14
+ elif ch == '&':
15
+ result.append('&amp;')
16
+ elif ch == '"':
17
+ result.append('&quot;')
18
+ elif ch == "'":
19
+ result.append('&apos;')
20
+ else:
21
+ result.append(ch)
22
+ return ''.join(result)
23
+
24
+
25
+ class SyntaxTreeWriter:
26
+ @staticmethod
27
+ def to_xml(root: SyntaxNode, formatted: bool = False) -> str:
28
+ builder: list[str] = []
29
+ SyntaxTreeWriter._node_to_xml(builder, root, formatted, '')
30
+ return '<?xml version="1.0"?>\n' + ''.join(builder)
31
+
32
+ @staticmethod
33
+ def _node_to_xml(builder: list[str], node: SyntaxNode, formatted: bool, indent: str) -> None:
34
+ has_children = node.has_children
35
+ new_indent = indent + ' '
36
+ if formatted:
37
+ builder.append(indent)
38
+ builder.append('<' + SYNTAX_NODE_NAMES[node.typ].upper())
39
+
40
+ if isinstance(node, CompoundSyntaxNode):
41
+ builder.append(f' begin_line="{node.line}"')
42
+ builder.append(f' begin_col="{node.col}"')
43
+ builder.append(f' end_line="{node.end_line}"')
44
+ builder.append(f' end_col="{node.end_col}"')
45
+ else:
46
+ builder.append(f' line="{node.line}"')
47
+ builder.append(f' col="{node.col}"')
48
+
49
+ if node.file_name:
50
+ builder.append(' file="' + _xml_encode(node.file_name) + '"')
51
+
52
+ if isinstance(node, ValuedSyntaxNode):
53
+ builder.append(' value="' + _xml_encode(node.value) + '"')
54
+
55
+ for attr_key, attr_value in node.attributes:
56
+ builder.append(
57
+ ' ' + ATTRIBUTE_NAME_STRINGS[attr_key] + '="' + _xml_encode(attr_value) + '"'
58
+ )
59
+
60
+ if has_children:
61
+ builder.append('>')
62
+ else:
63
+ builder.append('/>')
64
+ if formatted:
65
+ builder.append('\n')
66
+ for child in node.child_nodes:
67
+ SyntaxTreeWriter._node_to_xml(builder, child, formatted, new_indent)
68
+ if has_children:
69
+ if formatted:
70
+ builder.append(indent)
71
+ builder.append('</' + SYNTAX_NODE_NAMES[node.typ].upper() + '>')
72
+ if formatted:
73
+ builder.append('\n')