onec-interactive-runtime-core 0.1.19__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 (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. onec_runtime_build/parser_target_development.py +116 -0
@@ -0,0 +1,1539 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Sequence
4
+ from dataclasses import dataclass, fields, is_dataclass
5
+ from enum import Enum
6
+ from re import fullmatch
7
+ from typing import Any
8
+
9
+ from onec_runtime.bsl.lexer import BslLexError, tokenize
10
+ from onec_runtime.bsl.parser_target import PythonParserTarget, parse_raw_module
11
+ from onec_runtime.bsl.source_maps import (
12
+ MappedSource,
13
+ SourceArtifactKind,
14
+ SourceMap,
15
+ SourceSpan,
16
+ SourceTransformBuilder,
17
+ SourceUnitKind,
18
+ SourceUnitRef,
19
+ mapped_visible_source,
20
+ source_sha256,
21
+ )
22
+
23
+
24
+ class LoweringMode(str, Enum):
25
+ MAIN = "main"
26
+ CAPTURE = "capture"
27
+
28
+
29
+ class SemanticLoweringError(ValueError):
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ span: SourceSpan = SourceSpan(0, 0),
35
+ code: str = "semantic_lowering_error",
36
+ ) -> None:
37
+ super().__init__(message)
38
+ self.span = span
39
+ self.code = code
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class WorkerExport:
44
+ public_path: str
45
+ method: str
46
+ receiver_module: str | None = None
47
+
48
+ def __post_init__(self) -> None:
49
+ if (
50
+ not isinstance(self.public_path, str)
51
+ or not isinstance(self.method, str)
52
+ or not self.public_path
53
+ or not self.method
54
+ ):
55
+ raise ValueError("worker export names must not be empty")
56
+ if self.receiver_module is not None:
57
+ if not isinstance(self.receiver_module, str) or not self.receiver_module:
58
+ raise ValueError("worker export receiver must not be empty")
59
+ qualified = f"{self.receiver_module}.{self.method}"
60
+ if (
61
+ "." in self.public_path
62
+ and self.receiver_module.casefold() != "worker"
63
+ and self.public_path.casefold() != qualified.casefold()
64
+ ):
65
+ raise ValueError("worker export receiver does not match public path")
66
+
67
+
68
+ def _worker_export_identifier(value: str) -> bool:
69
+ return (
70
+ fullmatch(r"[A-Za-zА-Яа-яЁё_][0-9A-Za-zА-Яа-яЁё_]*", value)
71
+ is not None
72
+ )
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class SourceEdit:
77
+ start: int
78
+ end: int
79
+ replacement: str
80
+ reason: str
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class _CopyFragment:
85
+ text: str
86
+ span: SourceSpan
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class _DerivedFragment:
91
+ text: str
92
+ span: SourceSpan
93
+ region: str
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class _SyntheticFragment:
98
+ text: str
99
+ anchor: SourceSpan
100
+ region: str
101
+
102
+
103
+ type _SourceFragment = _CopyFragment | _DerivedFragment | _SyntheticFragment
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class _MappedEdit:
108
+ observed: SourceEdit
109
+ fragments: tuple[_SourceFragment, ...]
110
+
111
+
112
+ @dataclass(frozen=True, slots=True)
113
+ class MethodScope:
114
+ name: str
115
+ parameters: tuple[str, ...]
116
+ local_names: tuple[str, ...]
117
+ references: tuple[NameBinding, ...]
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class NameBinding:
122
+ name: str
123
+ kind: str
124
+ span: SourceSpan
125
+
126
+
127
+ @dataclass(slots=True)
128
+ class _MethodScopeState:
129
+ name: str
130
+ parameters: dict[str, str]
131
+ local_names: dict[str, str]
132
+ references: list[NameBinding]
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class ModuleBinding:
137
+ source: str
138
+ module_names: tuple[str, ...]
139
+ method_names: tuple[str, ...]
140
+ exported_method_names: tuple[str, ...]
141
+ method_scopes: tuple[MethodScope, ...]
142
+
143
+
144
+ @dataclass(frozen=True, slots=True)
145
+ class SemanticLoweringResult:
146
+ mapped_source: MappedSource
147
+ context_names: tuple[str, ...]
148
+ dirty_roots: tuple[str, ...]
149
+ persistent_write_roots: tuple[str, ...]
150
+ worker_dependencies: tuple[str, ...]
151
+ messages_intercepted: int
152
+ edits: tuple[SourceEdit, ...]
153
+
154
+ @property
155
+ def source(self) -> str:
156
+ return self.mapped_source.text
157
+
158
+ @property
159
+ def source_map(self) -> SourceMap:
160
+ return self.mapped_source.source_map
161
+
162
+
163
+ class SemanticNotebookLowerer:
164
+ _RESERVED_WORKER_CONTEXT_MEMBERS = frozenset(
165
+ {
166
+ "runtimeworkeractivegeneration",
167
+ "runtimeworkerpinnedoperationgeneration",
168
+ }
169
+ )
170
+ _RESERVED_WORKER_LOCAL_PREFIX = "__onecpinnedworkergeneration"
171
+ _CONTEXT_MUTATION_METHODS = frozenset(
172
+ {"вставить", "insert", "удалить", "delete"}
173
+ )
174
+ _CONTEXT_READ_METHODS = frozenset(
175
+ {"количество", "count", "получить", "get", "свойство", "property"}
176
+ )
177
+ _CONTEXT_ESCAPE_RESULT_NAMES = frozenset(
178
+ {"результат", "результатинструкции"}
179
+ )
180
+ _CONTEXT_ABRUPT_STATEMENTS = frozenset(
181
+ {
182
+ "BreakStatement",
183
+ "ContinueStatement",
184
+ "GotoStatement",
185
+ "RaiseStatement",
186
+ "ReturnStatement",
187
+ }
188
+ )
189
+ _CONTEXT_UNSTRUCTURED_TRANSFERS = frozenset(
190
+ {"BreakStatement", "ContinueStatement", "GotoStatement"}
191
+ )
192
+ _KNOWN_PLATFORM_GLOBALS = frozenset(
193
+ {
194
+ "статуссообщения",
195
+ "режимзаписидокумента",
196
+ "символы",
197
+ "кодировкатекста",
198
+ "справочники",
199
+ "документы",
200
+ "журналыдокументов",
201
+ "регистрысведений",
202
+ "регистрынакопления",
203
+ "регистрыбухгалтерии",
204
+ "регистрырасчета",
205
+ "планывидовхарактеристик",
206
+ "планысчетов",
207
+ "планывидоврасчета",
208
+ "планыобмена",
209
+ "бизнеспроцессы",
210
+ "задачи",
211
+ "критерииотбора",
212
+ "последовательности",
213
+ "константы",
214
+ "перечисления",
215
+ "внешниеобработки",
216
+ "внешниеотчеты",
217
+ "обработки",
218
+ "отчеты",
219
+ "метаданные",
220
+ "параметрысеанса",
221
+ }
222
+ )
223
+
224
+ def __init__(
225
+ self,
226
+ parser_target: PythonParserTarget,
227
+ *,
228
+ context_names: Iterable[str] = (),
229
+ platform_globals: Iterable[str] = (),
230
+ worker_exports: Iterable[WorkerExport] = (),
231
+ ) -> None:
232
+ self.parser_target = parser_target
233
+ self._initial_context = self._ordered_names(context_names)
234
+ self._module_names: dict[str, str] = {}
235
+ self._platform_globals = self._KNOWN_PLATFORM_GLOBALS | {
236
+ name.casefold() for name in platform_globals
237
+ }
238
+ self._module_context_aliases: set[str] = set()
239
+ self._exports: dict[str, WorkerExport] = {}
240
+ for export in worker_exports:
241
+ key = export.public_path.casefold()
242
+ if key in self._exports:
243
+ raise ValueError(f"duplicate worker export {export.public_path!r}")
244
+ self._exports[key] = export
245
+
246
+ def set_worker_exports(self, exports: Iterable[WorkerExport]) -> None:
247
+ """Replace the active worker generation's public-call catalog."""
248
+ self.commit_worker_exports(self.prepare_worker_exports(exports))
249
+
250
+ @staticmethod
251
+ def prepare_worker_exports(exports: Iterable[WorkerExport]) -> tuple[WorkerExport, ...]:
252
+ """Validate a catalog before an external worker activation can begin."""
253
+ catalog: dict[str, WorkerExport] = {}
254
+ for export in exports:
255
+ if not isinstance(export, WorkerExport):
256
+ raise ValueError("worker export catalog contains an invalid entry")
257
+ if (
258
+ not _worker_export_identifier(export.method)
259
+ or any(
260
+ not _worker_export_identifier(part)
261
+ for part in export.public_path.split(".")
262
+ )
263
+ or (
264
+ export.receiver_module is not None
265
+ and not _worker_export_identifier(export.receiver_module)
266
+ )
267
+ ):
268
+ raise ValueError("worker export components must be BSL identifiers")
269
+ key = export.public_path.casefold()
270
+ if key in catalog:
271
+ raise ValueError(f"duplicate worker export {export.public_path!r}")
272
+ catalog[key] = export
273
+ return tuple(catalog.values())
274
+
275
+ def commit_worker_exports(self, exports: tuple[WorkerExport, ...]) -> None:
276
+ """Commit an already validated immutable catalog without revalidation."""
277
+ self._exports = {export.public_path.casefold(): export for export in exports}
278
+
279
+ def force_commit_worker_exports(self, exports: tuple[WorkerExport, ...]) -> None:
280
+ """Infallible recovery commit used after the server worker has swapped."""
281
+ self._exports = {export.public_path.casefold(): export for export in exports}
282
+
283
+ @property
284
+ def worker_export_identity(
285
+ self,
286
+ ) -> tuple[tuple[str, str, str | None], ...]:
287
+ """Immutable identity of the active lowering catalog.
288
+
289
+ Runtime preparation uses this identity only as a stale-catalog fence;
290
+ it is never an execution artifact or a serialized Worker payload.
291
+ """
292
+ return tuple(
293
+ sorted(
294
+ (
295
+ export.public_path.casefold(),
296
+ export.method.casefold(),
297
+ (
298
+ None
299
+ if export.receiver_module is None
300
+ else export.receiver_module.casefold()
301
+ ),
302
+ )
303
+ for export in self._exports.values()
304
+ )
305
+ )
306
+
307
+ @property
308
+ def context_names(self) -> frozenset[str]:
309
+ """Read-only normalized persistent-name catalog."""
310
+ return frozenset(self._initial_context)
311
+
312
+ @property
313
+ def persistent_names(self) -> tuple[str, ...]:
314
+ """Persistent names with their first committed BSL spelling."""
315
+ return tuple(self._initial_context.values())
316
+
317
+ def restore_persistent_names(self, names: tuple[str, ...]) -> None:
318
+ """Restore a pre-execution catalog after a notebook cell fails."""
319
+ self._initial_context = self._ordered_names(names)
320
+
321
+ def bind_module(self, source: str) -> ModuleBinding:
322
+ """Register worker-module symbols without lowering its executable source."""
323
+ root, _tokens = parse_raw_module(source, self.parser_target)
324
+ module_names, method_names, exported_method_names = self._module_symbols(root)
325
+ self._module_names.update(module_names)
326
+ self._module_names.update(method_names)
327
+ method_scopes: list[_MethodScopeState] = []
328
+
329
+ stack: list[tuple[Any, _MethodScopeState | None]] = [(root, None)]
330
+ while stack:
331
+ node, scope = stack.pop()
332
+ if not is_dataclass(node):
333
+ continue
334
+ kind = type(node).__name__
335
+ if kind == "ModuleVariableDeclaration":
336
+ continue
337
+ if kind in {"ProcedureDeclaration", "FunctionDeclaration"}:
338
+ local_names = self._ordered_names(
339
+ name
340
+ for declaration in node.Body.LocalDeclarations
341
+ for name in declaration.Names
342
+ )
343
+ local_names.update(
344
+ (key, name)
345
+ for key, name in self._method_implicit_locals(
346
+ node.Body.Code
347
+ ).items()
348
+ if key not in self._module_names
349
+ )
350
+ method_scope = _MethodScopeState(
351
+ node.Name,
352
+ self._ordered_names(
353
+ item.Name for item in node.Parameters.Items
354
+ )
355
+ if node.Parameters is not None
356
+ else {},
357
+ local_names,
358
+ [],
359
+ )
360
+ method_scopes.append(method_scope)
361
+ stack.append((node.Body.Code, method_scope))
362
+ continue
363
+ if kind == "AccessChain" and scope is not None:
364
+ root_span = SourceSpan(
365
+ node.span.start, node.span.start + len(node.Root)
366
+ )
367
+ if source[root_span.start:root_span.end] != node.Root:
368
+ raise ValueError("method root span does not match its identifier")
369
+ scope.references.append(
370
+ NameBinding(
371
+ node.Root, self._resolve_method_name(node, scope), root_span
372
+ )
373
+ )
374
+ self._push_bound_children(stack, node, scope)
375
+
376
+ return ModuleBinding(
377
+ source=source,
378
+ module_names=tuple(module_names.values()),
379
+ method_names=tuple(method_names.values()),
380
+ exported_method_names=tuple(exported_method_names.values()),
381
+ method_scopes=tuple(
382
+ MethodScope(
383
+ scope.name,
384
+ tuple(scope.parameters.values()),
385
+ tuple(scope.local_names.values()),
386
+ tuple(scope.references),
387
+ )
388
+ for scope in method_scopes
389
+ ),
390
+ )
391
+
392
+ @classmethod
393
+ def _method_implicit_locals(cls, body: Any) -> dict[str, str]:
394
+ """BSL bare assignments and loop counters are local throughout a method."""
395
+ names: dict[str, str] = {}
396
+ stack = [body]
397
+ while stack:
398
+ node = stack.pop()
399
+ if not is_dataclass(node):
400
+ continue
401
+ kind = type(node).__name__
402
+ if kind == "SimpleStatement" and node.Value is not None:
403
+ target = node.Target
404
+ if (
405
+ type(target).__name__ == "AccessChain"
406
+ and target.Arguments is None
407
+ and not target.Postfix
408
+ ):
409
+ names.setdefault(target.Root.casefold(), target.Root)
410
+ elif kind in {"ForEachStatement", "ForRangeStatement"}:
411
+ names.setdefault(node.Variable.casefold(), node.Variable)
412
+ children: list[Any] = []
413
+ for field in fields(node):
414
+ if field.name == "span":
415
+ continue
416
+ value = getattr(node, field.name)
417
+ if is_dataclass(value):
418
+ children.append(value)
419
+ elif isinstance(value, tuple):
420
+ children.extend(
421
+ item for item in value if is_dataclass(item)
422
+ )
423
+ stack.extend(reversed(children))
424
+ return names
425
+
426
+ def _module_symbols(
427
+ self,
428
+ root: Any,
429
+ ) -> tuple[dict[str, str], dict[str, str], dict[str, str]]:
430
+ module_names = self._ordered_names(
431
+ variable.Name
432
+ for declaration in root.Declarations
433
+ for variable in declaration.Variables
434
+ )
435
+ method_names: dict[str, str] = {}
436
+ exported_method_names: dict[str, str] = {}
437
+ elements = root.Elements
438
+ while elements.Item is not None:
439
+ item = elements.Item
440
+ if type(item).__name__ == "Method":
441
+ declaration = item.Declaration
442
+ method_names.setdefault(
443
+ declaration.Name.casefold(),
444
+ declaration.Name,
445
+ )
446
+ if declaration.Export is not None:
447
+ exported_method_names.setdefault(
448
+ declaration.Name.casefold(),
449
+ declaration.Name,
450
+ )
451
+ elements = elements.Rest
452
+ return module_names, method_names, exported_method_names
453
+
454
+ def _resolve_method_name(
455
+ self,
456
+ node: Any,
457
+ scope: _MethodScopeState,
458
+ ) -> str:
459
+ normalized = node.Root.casefold()
460
+ if normalized in scope.parameters:
461
+ return "parameter"
462
+ if normalized in scope.local_names:
463
+ return "local"
464
+ if normalized in self._module_names:
465
+ return "module"
466
+ if normalized == "сообщить" or normalized in self._platform_globals:
467
+ return "platform"
468
+ call = self._called_path(node)
469
+ if call is not None and call[0].casefold() in self._exports:
470
+ return "worker-export"
471
+ if normalized in self._initial_context:
472
+ return "persistent"
473
+ return "unknown"
474
+
475
+ def lower(
476
+ self,
477
+ source: str,
478
+ *,
479
+ mode: LoweringMode,
480
+ message_collector_key: str = "__onec_cell_messages",
481
+ worker_exports: tuple[WorkerExport, ...] | None = None,
482
+ ) -> SemanticLoweringResult:
483
+ digest = source_sha256(source)
484
+ visible = mapped_visible_source(
485
+ source,
486
+ SourceUnitRef(
487
+ SourceUnitKind.NOTEBOOK_CELL,
488
+ f"anonymous-semantic-lowering:{digest}",
489
+ 0,
490
+ digest,
491
+ ),
492
+ )
493
+ return self.lower_mapped(
494
+ visible,
495
+ mode=mode,
496
+ message_collector_key=message_collector_key,
497
+ worker_exports=worker_exports,
498
+ )
499
+
500
+ def lower_mapped(
501
+ self,
502
+ source: MappedSource,
503
+ *,
504
+ mode: LoweringMode,
505
+ message_collector_key: str = "__onec_cell_messages",
506
+ worker_exports: tuple[WorkerExport, ...] | None = None,
507
+ ) -> SemanticLoweringResult:
508
+ if worker_exports is not None and type(worker_exports) is not tuple:
509
+ raise TypeError("worker exports must be an immutable tuple")
510
+ catalog = (
511
+ tuple(self._exports.values())
512
+ if worker_exports is None
513
+ else self.prepare_worker_exports(worker_exports)
514
+ )
515
+ self._call_exports = {
516
+ export.public_path.casefold(): export for export in catalog
517
+ }
518
+ try:
519
+ return self._lower_mapped(
520
+ source,
521
+ mode=mode,
522
+ message_collector_key=message_collector_key,
523
+ )
524
+ finally:
525
+ del self._call_exports
526
+
527
+ def _lower_mapped(
528
+ self,
529
+ source: MappedSource,
530
+ *,
531
+ mode: LoweringMode,
532
+ message_collector_key: str,
533
+ ) -> SemanticLoweringResult:
534
+ if not isinstance(source, MappedSource):
535
+ raise ValueError("mapped lowering source must be a MappedSource")
536
+ try:
537
+ collector_tokens = tokenize(message_collector_key)
538
+ except BslLexError as error:
539
+ raise ValueError("message collector key must be a BSL identifier") from error
540
+ if (
541
+ len(collector_tokens) != 1
542
+ or collector_tokens[0].type != "ID"
543
+ or collector_tokens[0].text != message_collector_key
544
+ ):
545
+ raise ValueError("message collector key must be a BSL identifier")
546
+ root = self.parser_target.parse_ast(source.text, "БлокНоутбука")
547
+ self._source = source.text
548
+ self._tokens = tokenize(source.text)
549
+ self._mode = mode
550
+ self._context = dict(self._initial_context)
551
+ self._cell_local_names = self._loop_variables(root)
552
+ result_channel = (
553
+ "Результат" if mode is LoweringMode.MAIN else "РезультатИнструкции"
554
+ )
555
+ self._cell_local_names.setdefault(result_channel.casefold(), result_channel)
556
+ module_context_aliases = self._validate_worker_context_flow(root)
557
+ for normalized, name in self._assignment_roots(root).items():
558
+ self._context.setdefault(normalized, name)
559
+ self._dirty: dict[str, str] = {}
560
+ self._persistent_writes: dict[str, str] = {}
561
+ self._dependencies: dict[str, str] = {}
562
+ self._messages = 0
563
+ self._message_collector_key = message_collector_key
564
+ self._edits: list[_MappedEdit] = []
565
+
566
+ stack: list[Any] = [root]
567
+ while stack:
568
+ node = stack.pop()
569
+ if not is_dataclass(node):
570
+ continue
571
+ kind = type(node).__name__
572
+ if kind == "SimpleStatement":
573
+ self._bind_statement(node, stack)
574
+ continue
575
+ if kind == "RaiseStatement":
576
+ self._validate_raise_statement(node)
577
+ if kind == "AccessChain":
578
+ self._bind_access(node)
579
+ self._push_children(stack, node)
580
+
581
+ mapped_edits = self._validated_mapped_edits(
582
+ self._edits,
583
+ source_length=len(source.text),
584
+ )
585
+ lowered = self._build_mapped_source(source, mapped_edits, mode=mode)
586
+ self._initial_context = dict(self._context)
587
+ self._module_context_aliases = module_context_aliases
588
+ return SemanticLoweringResult(
589
+ mapped_source=lowered,
590
+ context_names=tuple(self._context.values()),
591
+ dirty_roots=tuple(self._dirty.values()),
592
+ persistent_write_roots=tuple(self._persistent_writes.values()),
593
+ worker_dependencies=tuple(self._dependencies.values()),
594
+ messages_intercepted=self._messages,
595
+ edits=tuple(item.observed for item in mapped_edits),
596
+ )
597
+
598
+ def _bind_statement(self, node: Any, stack: list[Any]) -> None:
599
+ target = node.Target
600
+ value = node.Value
601
+ normalized = target.Root.casefold()
602
+ self._reject_reserved_worker_local(target)
603
+ if value is None:
604
+ call = self._called_path(target)
605
+ intercept_message = (
606
+ normalized == "сообщить"
607
+ and target.Arguments is not None
608
+ and normalized not in self._module_names
609
+ and (call is None or call[0].casefold() not in self._call_exports)
610
+ )
611
+ if intercept_message:
612
+ first_argument = self._bind_message_call(target)
613
+ if first_argument is not None:
614
+ stack.append(first_argument)
615
+ return
616
+ if normalized == "контекстотладки":
617
+ self._bind_access(target)
618
+ elif not self._has_call(target):
619
+ raise SemanticLoweringError(
620
+ f"bare access-chain statement is forbidden at {target.span.start}",
621
+ span=SourceSpan(target.span.start, target.span.end),
622
+ code="bare_access_chain_statement",
623
+ )
624
+ else:
625
+ self._bind_access(target)
626
+ elif self._is_direct_name(target) and normalized in {
627
+ "контекст",
628
+ "контекстотладки",
629
+ }:
630
+ raise SemanticLoweringError(
631
+ f"runtime namespace {target.Root!r} is reserved at {target.span.start}",
632
+ span=SourceSpan(target.span.start, target.span.end),
633
+ code="reserved_runtime_namespace",
634
+ )
635
+ elif normalized == "контекстотладки":
636
+ self._bind_capture_target(target)
637
+ elif normalized in self._cell_local_names:
638
+ self._bind_access(target)
639
+ elif normalized in self._module_names:
640
+ self._bind_access(target)
641
+ elif self._is_direct_name(target):
642
+ self._bind_persistent_assignment(node, target, value)
643
+ else:
644
+ self._bind_access(target)
645
+ self._push_children(stack, target, skip_root=True)
646
+ if value is not None:
647
+ stack.append(value)
648
+
649
+ def _validate_worker_context_flow(self, root: Any) -> set[str]:
650
+ has_goto = self._context_contains_node_kind(
651
+ root,
652
+ frozenset({"GotoStatement"}),
653
+ )
654
+ clear_taint = not self._context_contains_node_kind(
655
+ root,
656
+ self._CONTEXT_UNSTRUCTURED_TRANSFERS,
657
+ )
658
+ entry = set(self._module_context_aliases)
659
+ aliases = self._validate_context_block(
660
+ root,
661
+ entry,
662
+ clear_taint=clear_taint,
663
+ )
664
+ if has_goto:
665
+ for _attempt in range(self._context_fixed_point_limit()):
666
+ next_entry = entry | aliases
667
+ if next_entry == entry:
668
+ break
669
+ entry = next_entry
670
+ aliases = self._validate_context_block(
671
+ root,
672
+ entry,
673
+ clear_taint=False,
674
+ )
675
+ else:
676
+ self._raise_context_flow_not_converged(root)
677
+ return aliases & set(self._module_names)
678
+
679
+ def _context_fixed_point_limit(self) -> int:
680
+ return len(set(self._module_names) | set(self._cell_local_names)) + 2
681
+
682
+ @staticmethod
683
+ def _context_contains_node_kind(root: Any, kinds: frozenset[str]) -> bool:
684
+ stack = [root]
685
+ while stack:
686
+ node = stack.pop()
687
+ if not is_dataclass(node):
688
+ continue
689
+ if type(node).__name__ in kinds:
690
+ return True
691
+ for field in fields(node):
692
+ if field.name == "span":
693
+ continue
694
+ value = getattr(node, field.name)
695
+ if is_dataclass(value):
696
+ stack.append(value)
697
+ elif isinstance(value, tuple):
698
+ stack.extend(item for item in value if is_dataclass(item))
699
+ return False
700
+
701
+ def _validate_context_block(
702
+ self,
703
+ block: Any,
704
+ aliases: set[str],
705
+ *,
706
+ clear_taint: bool = True,
707
+ ) -> set[str]:
708
+ current = set(aliases)
709
+ abrupt = set[str]()
710
+ cursor = block
711
+ while type(cursor).__name__ == "CodeBlock" and cursor.First is not None:
712
+ statement = cursor.First
713
+ if type(statement).__name__ == "LabeledStatement":
714
+ statement = statement.Statement
715
+ current.update(abrupt)
716
+ current = self._validate_context_statement(
717
+ statement,
718
+ current,
719
+ clear_taint=clear_taint,
720
+ )
721
+ if type(statement).__name__ in self._CONTEXT_ABRUPT_STATEMENTS:
722
+ abrupt.update(current)
723
+ cursor = cursor.Rest
724
+ return current | abrupt
725
+
726
+ def _validate_context_statement(
727
+ self,
728
+ statement: Any,
729
+ aliases: set[str],
730
+ *,
731
+ clear_taint: bool,
732
+ ) -> set[str]:
733
+ kind = type(statement).__name__
734
+ current = set(aliases)
735
+ if kind == "SimpleStatement":
736
+ target = statement.Target
737
+ value = statement.Value
738
+ self._validate_context_access_chain(
739
+ target,
740
+ current,
741
+ is_assignment=value is not None,
742
+ )
743
+ if value is None:
744
+ return current
745
+ self._validate_context_expression(value, current)
746
+ tainted = self._expression_may_be_context(value, current)
747
+ normalized = target.Root.casefold()
748
+ if self._is_direct_name(target):
749
+ if normalized in self._CONTEXT_ESCAPE_RESULT_NAMES:
750
+ if tainted:
751
+ self._raise_context_alias_escape(value)
752
+ current.discard(normalized)
753
+ elif (
754
+ normalized in self._cell_local_names
755
+ or normalized in self._module_names
756
+ ):
757
+ if tainted:
758
+ current.add(normalized)
759
+ elif clear_taint:
760
+ current.discard(normalized)
761
+ elif tainted:
762
+ self._raise_context_alias_escape(value)
763
+ elif tainted:
764
+ self._raise_context_alias_escape(value)
765
+ return current
766
+ if kind == "ReturnStatement":
767
+ value = statement.Value
768
+ if value is not None:
769
+ self._validate_context_expression(value, current)
770
+ if self._expression_may_be_context(value, current):
771
+ self._raise_context_alias_escape(value)
772
+ return current
773
+ if kind == "ExecuteStatement":
774
+ raise SemanticLoweringError(
775
+ "Dynamic notebook execution is forbidden by the Context protocol at "
776
+ f"{statement.span.start}",
777
+ span=SourceSpan(statement.span.start, statement.span.end),
778
+ code="context_protocol_dynamic_execution",
779
+ )
780
+ if kind == "IfStatement":
781
+ self._validate_context_expression(statement.Condition, current)
782
+ outcomes = [
783
+ self._validate_context_block(
784
+ statement.Then,
785
+ set(current),
786
+ clear_taint=clear_taint,
787
+ )
788
+ ]
789
+ for clause in statement.ElseIf:
790
+ self._validate_context_expression(clause.Condition, current)
791
+ outcomes.append(
792
+ self._validate_context_block(
793
+ clause.Body,
794
+ set(current),
795
+ clear_taint=clear_taint,
796
+ )
797
+ )
798
+ if statement.Else is None:
799
+ outcomes.append(current)
800
+ else:
801
+ outcomes.append(
802
+ self._validate_context_block(
803
+ statement.Else.Body,
804
+ set(current),
805
+ clear_taint=clear_taint,
806
+ )
807
+ )
808
+ return set().union(*outcomes)
809
+ if kind == "WhileStatement":
810
+ self._validate_context_expression(statement.Condition, current)
811
+ body = self._validate_context_loop_body(
812
+ statement.Body,
813
+ set(current),
814
+ clear_taint=clear_taint,
815
+ )
816
+ return current | body
817
+ if kind == "ForEachStatement":
818
+ self._validate_context_expression(statement.Iterable, current)
819
+ body_input = set(current)
820
+ reset = frozenset({statement.Variable.casefold()})
821
+ body_input.difference_update(reset)
822
+ body = self._validate_context_loop_body(
823
+ statement.Body,
824
+ body_input,
825
+ clear_taint=clear_taint,
826
+ reset_each_iteration=reset,
827
+ )
828
+ return current | body
829
+ if kind == "ForRangeStatement":
830
+ self._validate_context_expression(statement.Start, current)
831
+ self._validate_context_expression(statement.End, current)
832
+ body_input = set(current)
833
+ reset = frozenset({statement.Variable.casefold()})
834
+ body_input.difference_update(reset)
835
+ body = self._validate_context_loop_body(
836
+ statement.Body,
837
+ body_input,
838
+ clear_taint=clear_taint,
839
+ reset_each_iteration=reset,
840
+ )
841
+ return current | body
842
+ if kind == "TryStatement":
843
+ try_state = self._validate_context_block(
844
+ statement.TryBody,
845
+ set(current),
846
+ clear_taint=clear_taint,
847
+ )
848
+ if clear_taint:
849
+ exception_state = self._validate_context_block(
850
+ statement.TryBody,
851
+ set(current),
852
+ clear_taint=False,
853
+ )
854
+ else:
855
+ exception_state = try_state
856
+ except_state = self._validate_context_block(
857
+ statement.ExceptBody,
858
+ set(current) | exception_state,
859
+ clear_taint=clear_taint,
860
+ )
861
+ return try_state | except_state
862
+ self._validate_context_expression(statement, current)
863
+ return current
864
+
865
+ def _validate_context_loop_body(
866
+ self,
867
+ body: Any,
868
+ aliases: set[str],
869
+ *,
870
+ clear_taint: bool,
871
+ reset_each_iteration: frozenset[str] = frozenset(),
872
+ ) -> set[str]:
873
+ entry = set(aliases)
874
+ output = set(aliases)
875
+ for _attempt in range(self._context_fixed_point_limit()):
876
+ output = self._validate_context_block(
877
+ body,
878
+ entry,
879
+ clear_taint=clear_taint,
880
+ )
881
+ next_entry = set(aliases)
882
+ next_entry.update(output - reset_each_iteration)
883
+ if next_entry <= entry:
884
+ return entry | output
885
+ entry.update(next_entry)
886
+ self._raise_context_flow_not_converged(body)
887
+
888
+ @staticmethod
889
+ def _raise_context_flow_not_converged(node: Any) -> None:
890
+ raise SemanticLoweringError(
891
+ f"Context flow analysis did not converge at {node.span.start}",
892
+ span=SourceSpan(node.span.start, node.span.end),
893
+ code="context_flow_not_converged",
894
+ )
895
+
896
+ def _validate_context_expression(
897
+ self,
898
+ node: Any,
899
+ aliases: set[str],
900
+ ) -> None:
901
+ if not is_dataclass(node):
902
+ return
903
+ if type(node).__name__ == "AccessChain":
904
+ self._validate_context_access_chain(node, aliases, is_assignment=False)
905
+ if type(node).__name__ == "NewExpression":
906
+ for item in self._context_call_arguments(node.Arguments):
907
+ if self._expression_may_be_context(item, aliases):
908
+ self._raise_context_alias_escape(item)
909
+ for field in fields(node):
910
+ if field.name == "span":
911
+ continue
912
+ value = getattr(node, field.name)
913
+ if is_dataclass(value):
914
+ self._validate_context_expression(value, aliases)
915
+ elif isinstance(value, tuple):
916
+ for item in value:
917
+ if is_dataclass(item):
918
+ self._validate_context_expression(item, aliases)
919
+
920
+ def _validate_context_access_chain(
921
+ self,
922
+ node: Any,
923
+ aliases: set[str],
924
+ *,
925
+ is_assignment: bool,
926
+ ) -> None:
927
+ argument_lists = []
928
+ if node.Arguments is not None:
929
+ argument_lists.append(node.Arguments)
930
+ argument_lists.extend(
931
+ postfix.Arguments
932
+ for postfix in node.Postfix
933
+ if type(postfix).__name__ == "MemberAccess"
934
+ and postfix.Arguments is not None
935
+ )
936
+ for arguments in argument_lists:
937
+ for item in self._context_call_arguments(arguments):
938
+ if self._expression_may_be_context(item, aliases):
939
+ self._raise_context_alias_escape(item)
940
+
941
+ normalized = node.Root.casefold()
942
+ tainted_receiver = normalized == "контекст" or normalized in aliases
943
+ if not tainted_receiver or not node.Postfix:
944
+ return
945
+ first = node.Postfix[0]
946
+ kind = type(first).__name__
947
+ if is_assignment:
948
+ if kind == "MemberAccess":
949
+ if first.Name.casefold() in self._RESERVED_WORKER_CONTEXT_MEMBERS:
950
+ self._raise_reserved_worker_protocol(first)
951
+ elif kind == "IndexAccess" and self._worker_context_key_may_be_reserved(
952
+ first.Index
953
+ ):
954
+ self._raise_reserved_worker_protocol(first)
955
+ return
956
+ if kind != "MemberAccess" or first.Arguments is None:
957
+ return
958
+ method = first.Name.casefold()
959
+ if method in self._CONTEXT_MUTATION_METHODS:
960
+ items = self._context_call_arguments(first.Arguments)
961
+ if items and self._worker_context_key_may_be_reserved(items[0]):
962
+ self._raise_reserved_worker_protocol(first)
963
+ return
964
+ if method not in self._CONTEXT_READ_METHODS:
965
+ self._raise_context_alias_escape(first)
966
+
967
+ def _expression_may_be_context(
968
+ self,
969
+ node: Any,
970
+ aliases: set[str],
971
+ ) -> bool:
972
+ if not is_dataclass(node):
973
+ return False
974
+ kind = type(node).__name__
975
+ if kind == "AccessChain":
976
+ return (
977
+ node.Arguments is None
978
+ and not node.Postfix
979
+ and (
980
+ node.Root.casefold() == "контекст"
981
+ or node.Root.casefold() in aliases
982
+ )
983
+ )
984
+ if kind in {"ParenthesizedExpression", "PostfixedPrimary"} and node.Postfix:
985
+ return False
986
+ for field in fields(node):
987
+ if field.name == "span":
988
+ continue
989
+ value = getattr(node, field.name)
990
+ if is_dataclass(value) and self._expression_may_be_context(
991
+ value,
992
+ aliases,
993
+ ):
994
+ return True
995
+ if isinstance(value, tuple) and any(
996
+ is_dataclass(item)
997
+ and self._expression_may_be_context(item, aliases)
998
+ for item in value
999
+ ):
1000
+ return True
1001
+ return False
1002
+
1003
+ @staticmethod
1004
+ def _context_call_arguments(arguments: Any) -> tuple[Any, ...]:
1005
+ if arguments is None or arguments.Items is None:
1006
+ return ()
1007
+ items = arguments.Items.Items
1008
+ return () if items is None else tuple(items)
1009
+
1010
+ @staticmethod
1011
+ def _raise_context_alias_escape(node: Any) -> None:
1012
+ raise SemanticLoweringError(
1013
+ f"Контекст cannot escape into opaque state at {node.span.start}",
1014
+ span=SourceSpan(node.span.start, node.span.end),
1015
+ code="context_alias_escape",
1016
+ )
1017
+
1018
+ @staticmethod
1019
+ def _raise_reserved_worker_protocol(node: Any) -> None:
1020
+ raise SemanticLoweringError(
1021
+ "Worker generation Context protocol member is reserved at "
1022
+ f"{node.span.start}",
1023
+ span=SourceSpan(node.span.start, node.span.end),
1024
+ code="reserved_worker_protocol_member",
1025
+ )
1026
+
1027
+ def _worker_context_key_may_be_reserved(self, expression: Any) -> bool:
1028
+ tokens = tokenize(self._source[expression.span.start : expression.span.end])
1029
+ if len(tokens) != 1 or tokens[0].type != "STRING":
1030
+ return True
1031
+ literal = tokens[0].text[1:-1].replace('""', '"')
1032
+ return literal.casefold() in self._RESERVED_WORKER_CONTEXT_MEMBERS
1033
+
1034
+ def _bind_persistent_assignment(
1035
+ self,
1036
+ statement: Any,
1037
+ target: Any,
1038
+ value: Any,
1039
+ ) -> None:
1040
+ name = target.Root
1041
+ normalized = name.casefold()
1042
+ if normalized in self._platform_globals or normalized == "сообщить":
1043
+ raise SemanticLoweringError(
1044
+ f"cannot assign platform global {name!r} at {target.span.start}",
1045
+ span=SourceSpan(target.span.start, target.span.end),
1046
+ code="platform_global_assignment",
1047
+ )
1048
+ self._context.setdefault(normalized, name)
1049
+ self._persistent_writes.setdefault(normalized, name)
1050
+ equals = next(
1051
+ (
1052
+ token
1053
+ for token in self._tokens
1054
+ if token.type == "="
1055
+ and target.span.end <= token.start < value.span.start
1056
+ ),
1057
+ None,
1058
+ )
1059
+ if equals is None:
1060
+ raise SemanticLoweringError(
1061
+ f"assignment operator is missing at {statement.span.start}",
1062
+ span=SourceSpan(statement.span.start, statement.span.end),
1063
+ code="missing_assignment_operator",
1064
+ )
1065
+ gap = self._source[target.span.end : equals.start]
1066
+ if gap.isspace():
1067
+ self._edit(
1068
+ target.span.start,
1069
+ equals.end,
1070
+ "persistent-assignment-target",
1071
+ _SyntheticFragment(
1072
+ 'Контекст.Вставить("',
1073
+ SourceSpan(target.span.start, target.span.end),
1074
+ "persistent_assignment_open",
1075
+ ),
1076
+ _DerivedFragment(
1077
+ name,
1078
+ SourceSpan(target.span.start, target.span.end),
1079
+ "persistent_name",
1080
+ ),
1081
+ _SyntheticFragment(
1082
+ '",',
1083
+ SourceSpan(equals.start, equals.end),
1084
+ "persistent_assignment_operator",
1085
+ ),
1086
+ )
1087
+ else:
1088
+ self._edit(
1089
+ target.span.start,
1090
+ target.span.end,
1091
+ "persistent-assignment-target",
1092
+ _SyntheticFragment(
1093
+ 'Контекст.Вставить("',
1094
+ SourceSpan(target.span.start, target.span.end),
1095
+ "persistent_assignment_open",
1096
+ ),
1097
+ _DerivedFragment(
1098
+ name,
1099
+ SourceSpan(target.span.start, target.span.end),
1100
+ "persistent_name",
1101
+ ),
1102
+ _SyntheticFragment(
1103
+ '"',
1104
+ SourceSpan(target.span.end, target.span.end),
1105
+ "persistent_assignment_name_close",
1106
+ ),
1107
+ )
1108
+ self._edit(
1109
+ equals.start,
1110
+ equals.end,
1111
+ "persistent-assignment-operator",
1112
+ _SyntheticFragment(
1113
+ ",",
1114
+ SourceSpan(equals.start, equals.end),
1115
+ "persistent_assignment_operator",
1116
+ ),
1117
+ )
1118
+ self._edit(
1119
+ statement.span.end,
1120
+ statement.span.end,
1121
+ "persistent-assignment-close",
1122
+ _SyntheticFragment(
1123
+ ")",
1124
+ SourceSpan(statement.span.end, statement.span.end),
1125
+ "persistent_assignment_close",
1126
+ ),
1127
+ )
1128
+
1129
+ def _bind_capture_target(self, target: Any) -> None:
1130
+ self._require_capture_namespace(target)
1131
+ if self._is_direct_capture_root(target):
1132
+ root = target.Postfix[0].Name
1133
+ self._dirty.setdefault(root.casefold(), root)
1134
+
1135
+ def _bind_access(self, node: Any) -> None:
1136
+ root = node.Root
1137
+ normalized = root.casefold()
1138
+ self._reject_reserved_worker_local(node)
1139
+ if normalized == "контекст":
1140
+ if self._is_context_capture_alias(node):
1141
+ raise SemanticLoweringError(
1142
+ "Контекст.КонтекстОтладки is forbidden at "
1143
+ f"{node.Postfix[0].span.start}",
1144
+ span=SourceSpan(
1145
+ node.Postfix[0].span.start,
1146
+ node.Postfix[0].span.end,
1147
+ ),
1148
+ code="capture_namespace_alias",
1149
+ )
1150
+ return
1151
+ if normalized == "контекстотладки":
1152
+ self._require_capture_namespace(node)
1153
+ return
1154
+ if normalized in self._cell_local_names:
1155
+ return
1156
+ if normalized in self._module_names:
1157
+ return
1158
+
1159
+ call = self._called_path(node)
1160
+ if call is not None:
1161
+ path, arguments_start = call
1162
+ export = self._call_exports.get(path.casefold())
1163
+ if export is not None:
1164
+ self._bind_worker_export(node, arguments_start, export)
1165
+ return
1166
+ if normalized in self._context:
1167
+ self._edit(
1168
+ node.span.start,
1169
+ node.span.start,
1170
+ "persistent-reference",
1171
+ _SyntheticFragment(
1172
+ "Контекст.",
1173
+ SourceSpan(node.span.start, node.span.start),
1174
+ "persistent_reference_prefix",
1175
+ ),
1176
+ )
1177
+ return
1178
+ if normalized in self._platform_globals:
1179
+ return
1180
+ # Unknown reads belong to the native BSL name resolver. Only names
1181
+ # established by notebook writes or imported context are persistent.
1182
+ return
1183
+
1184
+ def _bind_message_call(self, node: Any) -> Any | None:
1185
+ arguments = node.Arguments.Items
1186
+ items = () if arguments is None else arguments.Items
1187
+ prefix = "Контекст." + self._message_collector_key + ".Добавить(Строка("
1188
+ if not items:
1189
+ self._edit(
1190
+ node.span.start,
1191
+ node.span.end,
1192
+ "message-interception",
1193
+ _SyntheticFragment(
1194
+ prefix,
1195
+ SourceSpan(node.span.start, node.span.end),
1196
+ "message_interception_open",
1197
+ ),
1198
+ _SyntheticFragment(
1199
+ "Неопределено",
1200
+ SourceSpan(node.span.start, node.span.end),
1201
+ "message_interception_default",
1202
+ ),
1203
+ _SyntheticFragment(
1204
+ "))",
1205
+ SourceSpan(node.span.start, node.span.end),
1206
+ "message_interception_close",
1207
+ ),
1208
+ )
1209
+ self._messages += 1
1210
+ return None
1211
+ first = items[0]
1212
+ self._edit(
1213
+ node.span.start,
1214
+ first.span.start,
1215
+ "message-interception-open",
1216
+ _SyntheticFragment(
1217
+ prefix,
1218
+ SourceSpan(node.span.start, first.span.start),
1219
+ "message_interception_open",
1220
+ ),
1221
+ )
1222
+ self._edit(
1223
+ first.span.end,
1224
+ node.span.end,
1225
+ "message-interception-close",
1226
+ _SyntheticFragment(
1227
+ "))",
1228
+ SourceSpan(first.span.end, node.span.end),
1229
+ "message_interception_close",
1230
+ ),
1231
+ )
1232
+ self._messages += 1
1233
+ return first
1234
+
1235
+ @staticmethod
1236
+ def _is_context_capture_alias(node: Any) -> bool:
1237
+ return (
1238
+ bool(node.Postfix)
1239
+ and type(node.Postfix[0]).__name__ == "MemberAccess"
1240
+ and node.Postfix[0].Name.casefold() == "контекстотладки"
1241
+ )
1242
+
1243
+ def _loop_variables(self, root: Any) -> dict[str, str]:
1244
+ names: dict[str, str] = {}
1245
+ stack: list[Any] = [root]
1246
+ while stack:
1247
+ node = stack.pop()
1248
+ if not is_dataclass(node):
1249
+ continue
1250
+ if type(node).__name__ in {"ForEachStatement", "ForRangeStatement"}:
1251
+ names.setdefault(node.Variable.casefold(), node.Variable)
1252
+ self._push_children(stack, node)
1253
+ return names
1254
+
1255
+ def _assignment_roots(self, root: Any) -> dict[str, str]:
1256
+ names: dict[str, str] = {}
1257
+ stack: list[Any] = [root]
1258
+ while stack:
1259
+ node = stack.pop()
1260
+ if not is_dataclass(node):
1261
+ continue
1262
+ if type(node).__name__ == "SimpleStatement" and node.Value is not None:
1263
+ target = node.Target
1264
+ normalized = target.Root.casefold()
1265
+ if (
1266
+ self._is_direct_name(target)
1267
+ and normalized not in self._cell_local_names
1268
+ and normalized not in self._module_names
1269
+ and normalized not in self._platform_globals
1270
+ and normalized not in {"сообщить", "контекст", "контекстотладки"}
1271
+ ):
1272
+ names.setdefault(normalized, target.Root)
1273
+ self._push_children(stack, node)
1274
+ return names
1275
+
1276
+ @staticmethod
1277
+ def _validate_raise_statement(node: Any) -> None:
1278
+ parameters = node.Parameters
1279
+ if type(parameters).__name__ != "RaiseCallParameters":
1280
+ return
1281
+ arguments = parameters.Arguments.Items
1282
+ items = () if arguments is None else arguments.Items
1283
+ if len(items) > 1 and (
1284
+ parameters.Postfix
1285
+ or parameters.Tail.span.start != parameters.Tail.span.end
1286
+ ):
1287
+ raise SemanticLoweringError(
1288
+ "multi-argument ВызватьИсключение cannot have a continuation at "
1289
+ f"{parameters.Tail.span.start}",
1290
+ span=SourceSpan(parameters.Tail.span.start, parameters.Tail.span.end),
1291
+ code="raise_continuation",
1292
+ )
1293
+
1294
+ def _bind_worker_export(
1295
+ self,
1296
+ node: Any,
1297
+ arguments_start: int,
1298
+ export: WorkerExport,
1299
+ ) -> None:
1300
+ method_span = self._called_name_span(node.span.start, arguments_start)
1301
+ visible_method = self._source[method_span.start : method_span.end]
1302
+ method_fragment: _SourceFragment = (
1303
+ _CopyFragment(export.method, method_span)
1304
+ if export.method == visible_method
1305
+ else _DerivedFragment(export.method, method_span, "worker_method")
1306
+ )
1307
+ receiver = (
1308
+ "Контекст.RuntimeWorker."
1309
+ if export.receiver_module is None
1310
+ else (
1311
+ "__OnecPinnedWorkerGeneration.Modules.Получить("
1312
+ f'"{export.receiver_module.replace(chr(34), chr(34) * 2)}").'
1313
+ )
1314
+ )
1315
+ self._edit(
1316
+ node.span.start,
1317
+ arguments_start,
1318
+ "worker-export",
1319
+ _SyntheticFragment(
1320
+ receiver,
1321
+ SourceSpan(node.span.start, arguments_start),
1322
+ "worker_receiver",
1323
+ ),
1324
+ method_fragment,
1325
+ )
1326
+ self._dependencies.setdefault(
1327
+ export.public_path.casefold(),
1328
+ export.public_path,
1329
+ )
1330
+
1331
+ def _reject_reserved_worker_local(self, node: Any) -> None:
1332
+ if node.Root.casefold().startswith(self._RESERVED_WORKER_LOCAL_PREFIX):
1333
+ raise SemanticLoweringError(
1334
+ "Worker generation kernel handoff local is reserved at "
1335
+ f"{node.span.start}",
1336
+ span=SourceSpan(node.span.start, node.span.end),
1337
+ code="reserved_worker_protocol_local",
1338
+ )
1339
+
1340
+ def _called_name_span(self, start: int, arguments_start: int) -> SourceSpan:
1341
+ names = tuple(
1342
+ token
1343
+ for token in self._tokens
1344
+ if token.type == "ID" and start <= token.start < arguments_start
1345
+ )
1346
+ if not names:
1347
+ raise SemanticLoweringError(
1348
+ f"worker call name is missing at {start}",
1349
+ span=SourceSpan(start, start),
1350
+ code="missing_worker_call_name",
1351
+ )
1352
+ name = names[-1]
1353
+ return SourceSpan(name.start, name.end)
1354
+
1355
+ def _require_capture_namespace(self, node: Any) -> None:
1356
+ if self._mode is not LoweringMode.CAPTURE:
1357
+ raise SemanticLoweringError(
1358
+ f"КонтекстОтладки is only available in CAPTURE at {node.span.start}",
1359
+ span=SourceSpan(node.span.start, node.span.end),
1360
+ code="capture_namespace_mode",
1361
+ )
1362
+ if node.Arguments is not None:
1363
+ raise SemanticLoweringError(
1364
+ f"capture root must start with a member access at {node.span.start}",
1365
+ span=SourceSpan(node.span.start, node.span.end),
1366
+ code="capture_root_shape",
1367
+ )
1368
+ if not node.Postfix:
1369
+ raise SemanticLoweringError(
1370
+ f"bare capture namespace is forbidden at {node.span.start}",
1371
+ span=SourceSpan(node.span.start, node.span.end),
1372
+ code="bare_capture_namespace",
1373
+ )
1374
+ first = node.Postfix[0]
1375
+ if (
1376
+ type(first).__name__ != "MemberAccess"
1377
+ or first.Arguments is not None
1378
+ ):
1379
+ raise SemanticLoweringError(
1380
+ f"capture root must start with a member access at {node.span.start}",
1381
+ span=SourceSpan(node.span.start, node.span.end),
1382
+ code="capture_root_shape",
1383
+ )
1384
+
1385
+ @staticmethod
1386
+ def _is_direct_name(node: Any) -> bool:
1387
+ return node.Arguments is None and not node.Postfix
1388
+
1389
+ @staticmethod
1390
+ def _has_call(node: Any) -> bool:
1391
+ return node.Arguments is not None or any(
1392
+ type(postfix).__name__ == "MemberAccess"
1393
+ and postfix.Arguments is not None
1394
+ for postfix in node.Postfix
1395
+ )
1396
+
1397
+ @staticmethod
1398
+ def _is_direct_capture_root(node: Any) -> bool:
1399
+ return (
1400
+ node.Arguments is None
1401
+ and len(node.Postfix) == 1
1402
+ and type(node.Postfix[0]).__name__ == "MemberAccess"
1403
+ and node.Postfix[0].Arguments is None
1404
+ )
1405
+
1406
+ @staticmethod
1407
+ def _called_path(node: Any) -> tuple[str, int] | None:
1408
+ parts = [node.Root]
1409
+ if node.Arguments is not None:
1410
+ return ".".join(parts), node.Arguments.span.start
1411
+ for postfix in node.Postfix:
1412
+ if type(postfix).__name__ != "MemberAccess":
1413
+ return None
1414
+ parts.append(postfix.Name)
1415
+ if postfix.Arguments is not None:
1416
+ return ".".join(parts), postfix.Arguments.span.start
1417
+ return None
1418
+
1419
+ def _push_children(
1420
+ self,
1421
+ stack: list[Any],
1422
+ node: Any,
1423
+ *,
1424
+ skip_root: bool = False,
1425
+ ) -> None:
1426
+ children: list[Any] = []
1427
+ for field in fields(node):
1428
+ if field.name == "span" or (skip_root and field.name == "Root"):
1429
+ continue
1430
+ value = getattr(node, field.name)
1431
+ if is_dataclass(value):
1432
+ children.append(value)
1433
+ elif isinstance(value, tuple):
1434
+ children.extend(item for item in value if is_dataclass(item))
1435
+ stack.extend(reversed(children))
1436
+
1437
+ def _push_bound_children(
1438
+ self,
1439
+ stack: list[tuple[Any, _MethodScopeState | None]],
1440
+ node: Any,
1441
+ scope: _MethodScopeState | None,
1442
+ ) -> None:
1443
+ children: list[Any] = []
1444
+ for field in fields(node):
1445
+ if field.name == "span":
1446
+ continue
1447
+ value = getattr(node, field.name)
1448
+ if is_dataclass(value):
1449
+ children.append(value)
1450
+ elif isinstance(value, tuple):
1451
+ children.extend(item for item in value if is_dataclass(item))
1452
+ stack.extend((child, scope) for child in reversed(children))
1453
+
1454
+ def _edit(
1455
+ self,
1456
+ start: int,
1457
+ end: int,
1458
+ reason: str,
1459
+ *fragments: _SourceFragment,
1460
+ ) -> None:
1461
+ replacement = "".join(fragment.text for fragment in fragments)
1462
+ self._edits.append(
1463
+ _MappedEdit(SourceEdit(start, end, replacement, reason), tuple(fragments))
1464
+ )
1465
+
1466
+ def _build_mapped_source(
1467
+ self,
1468
+ source: MappedSource,
1469
+ edits: tuple[_MappedEdit, ...],
1470
+ *,
1471
+ mode: LoweringMode,
1472
+ ) -> MappedSource:
1473
+ builder = SourceTransformBuilder(source)
1474
+ cursor = 0
1475
+ for mapped_edit in edits:
1476
+ edit = mapped_edit.observed
1477
+ if cursor < edit.start:
1478
+ builder.copy(SourceSpan(cursor, edit.start))
1479
+ for fragment in mapped_edit.fragments:
1480
+ if isinstance(fragment, _CopyFragment):
1481
+ builder.copy(fragment.span)
1482
+ elif isinstance(fragment, _DerivedFragment):
1483
+ builder.derived(fragment.text, fragment.span, fragment.region)
1484
+ else:
1485
+ builder.synthetic(fragment.text, fragment.anchor, fragment.region)
1486
+ cursor = edit.end
1487
+ if cursor < len(source.text) or not edits:
1488
+ builder.copy(SourceSpan(cursor, len(source.text)))
1489
+ return builder.build(SourceArtifactKind.SEMANTIC_LOWERING, mode=mode.value)
1490
+
1491
+ @classmethod
1492
+ def _validated_mapped_edits(
1493
+ cls,
1494
+ edits: Sequence[_MappedEdit],
1495
+ *,
1496
+ source_length: int,
1497
+ ) -> tuple[_MappedEdit, ...]:
1498
+ ordered = tuple(
1499
+ sorted(edits, key=lambda item: (item.observed.start, item.observed.end))
1500
+ )
1501
+ cls._validated_edits(
1502
+ tuple(item.observed for item in ordered),
1503
+ source_length=source_length,
1504
+ )
1505
+ return ordered
1506
+
1507
+ @staticmethod
1508
+ def _validated_edits(
1509
+ edits: Sequence[SourceEdit],
1510
+ *,
1511
+ source_length: int,
1512
+ ) -> tuple[SourceEdit, ...]:
1513
+ ordered = tuple(sorted(edits, key=lambda item: (item.start, item.end)))
1514
+ previous_end = 0
1515
+ for edit in ordered:
1516
+ if edit.start < 0 or edit.end < edit.start or edit.end > source_length:
1517
+ offset = max(0, min(edit.start, source_length))
1518
+ raise SemanticLoweringError(
1519
+ "invalid lowering source edit",
1520
+ span=SourceSpan(offset, offset),
1521
+ code="invalid_source_edit",
1522
+ )
1523
+ if edit.start < previous_end:
1524
+ raise SemanticLoweringError(
1525
+ "overlapping lowering source edits",
1526
+ span=SourceSpan(edit.start, edit.end),
1527
+ code="overlapping_source_edits",
1528
+ )
1529
+ previous_end = max(previous_end, edit.end)
1530
+ return ordered
1531
+
1532
+ @staticmethod
1533
+ def _ordered_names(names: Iterable[str]) -> dict[str, str]:
1534
+ result: dict[str, str] = {}
1535
+ for name in names:
1536
+ if not name:
1537
+ raise ValueError("context name must not be empty")
1538
+ result.setdefault(name.casefold(), name)
1539
+ return result