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,1877 @@
1
+ """Immutable admission records for server-side common-module lowering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from bisect import bisect_left
6
+ from collections.abc import Callable, Iterable
7
+ from dataclasses import dataclass, fields, is_dataclass, replace
8
+ from hashlib import sha256
9
+ from hmac import compare_digest, new as hmac_new
10
+ import json
11
+ import re
12
+ from secrets import token_bytes
13
+ from threading import RLock
14
+ from typing import Any, Literal
15
+ from weakref import ReferenceType, WeakKeyDictionary, ref
16
+
17
+ from onec_runtime.bsl.lexer import BslLexError, tokenize
18
+ from onec_runtime.bsl.parser_target import (
19
+ BslParseError,
20
+ PythonParserTarget,
21
+ parse_raw_module,
22
+ )
23
+ from onec_runtime.bsl.module_catalog import (
24
+ CommonModuleCatalogSnapshot,
25
+ CommonModuleDescriptor,
26
+ CommonModuleScope,
27
+ )
28
+ from onec_runtime.bsl.source_maps import (
29
+ MappedSource,
30
+ SourceArtifactKind,
31
+ SourceSpan,
32
+ SourceTransformBuilder,
33
+ SourceUnitKind,
34
+ SourceUnitRef,
35
+ _observe_text_work,
36
+ _trusted_mapped_semantic_summary,
37
+ _validated_mapped_semantic_summary,
38
+ )
39
+ from onec_runtime.bsl.worker_dependency_resolver import (
40
+ ResolvedMethodPlan,
41
+ ResolvedModulePlan,
42
+ )
43
+ from onec_runtime.bsl.worker_projection_model import BareNameKind, ParsedModuleModel
44
+ from onec_runtime.bsl.worker_reload_source_map import (
45
+ ReloadInsertion,
46
+ ReloadMethodBoundary,
47
+ materialize_worker_reload_source,
48
+ )
49
+ from onec_runtime.errors import ModuleUniverseAdmissionError
50
+ from onec_runtime.performance_profile import PhaseRecorder
51
+
52
+
53
+ _BSL_IDENTIFIER_RE = re.compile(r"[A-Za-zА-Яа-яЁё_][0-9A-Za-zА-Яа-яЁё_]*\Z")
54
+
55
+ MODULE_SCOPE_DEPENDENCY = "module_scope_dependency"
56
+ DYNAMIC_EXECUTE = "dynamic_execute"
57
+ AMBIGUOUS_BINDING = "ambiguous_module_dependency"
58
+
59
+ DEPENDENCY_FIELD_REGION = "worker_dependency_field"
60
+ DEPENDENCY_ALIAS_DECLARATION_REGION = "worker_dependency_alias_declaration"
61
+ DEPENDENCY_ALIAS_INITIALIZER_REGION = "worker_dependency_alias_initializer"
62
+ IMPLICIT_LOCAL_DECLARATION_REGION = "worker_implicit_local_declaration"
63
+
64
+ _PLATFORM_GLOBALS = frozenset(
65
+ {
66
+ "статуссообщения",
67
+ "символы",
68
+ "кодировкатекста",
69
+ "справочники",
70
+ "документы",
71
+ "журналыдокументов",
72
+ "регистрысведений",
73
+ "регистрынакопления",
74
+ "регистрыбухгалтерии",
75
+ "регистрырасчета",
76
+ "планывидовхарактеристик",
77
+ "планысчетов",
78
+ "планывидоврасчета",
79
+ "планыобмена",
80
+ "бизнеспроцессы",
81
+ "задачи",
82
+ "критерииотбора",
83
+ "последовательности",
84
+ "константы",
85
+ "перечисления",
86
+ "внешниеобработки",
87
+ "внешниеотчеты",
88
+ "обработки",
89
+ "отчеты",
90
+ "метаданные",
91
+ "параметрысеанса",
92
+ "частидаты",
93
+ "обходрезультатазапроса",
94
+ "видсравнениякомпоновкиданных",
95
+ "типгруппыэлементовотборакомпоновкиданных",
96
+ "цветастиля",
97
+ }
98
+ )
99
+
100
+
101
+ @dataclass(frozen=True, slots=True, repr=False)
102
+ class WorkerModuleUnit:
103
+ logical_name: str
104
+ kind: Literal["module", "test-module"]
105
+ revision: int
106
+ mapped_source: MappedSource
107
+
108
+ def __post_init__(self) -> None:
109
+ if not _is_bsl_identifier(self.logical_name):
110
+ raise ModuleUniverseAdmissionError("worker module source identity is invalid")
111
+ if self.kind not in ("module", "test-module"):
112
+ raise ModuleUniverseAdmissionError("worker module source identity is invalid")
113
+ if type(self.revision) is not int or self.revision < 0:
114
+ raise ModuleUniverseAdmissionError("worker module source identity is invalid")
115
+ if not isinstance(self.mapped_source, MappedSource):
116
+ raise ModuleUniverseAdmissionError("worker module source identity is invalid")
117
+ unit = _single_visible_unit(self.mapped_source)
118
+ expected_kind = (
119
+ SourceUnitKind.MODULE if self.kind == "module" else SourceUnitKind.TEST_MODULE
120
+ )
121
+ if (
122
+ unit is None
123
+ or unit.kind is not expected_kind
124
+ or unit.unit_id != self.logical_name
125
+ or unit.revision != self.revision
126
+ or unit.source_sha256 != self.mapped_source.artifact.source_sha256
127
+ ):
128
+ raise ModuleUniverseAdmissionError("worker module source identity is invalid")
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class DependencyUse:
133
+ method_name: str
134
+ category: Literal["call", "access", "value"]
135
+ span: SourceSpan
136
+ method_declaration: SourceSpan
137
+
138
+
139
+ @dataclass(frozen=True, slots=True)
140
+ class ModuleDependencyBinding:
141
+ target_module: str
142
+ export_variable: str
143
+ uses: tuple[DependencyUse, ...]
144
+ sha256: str
145
+
146
+
147
+ @dataclass(frozen=True, slots=True)
148
+ class MethodInsertionPoints:
149
+ method_name: str
150
+ method_declaration: SourceSpan
151
+ declarations_end: int
152
+ first_statement_start: int
153
+
154
+
155
+ @dataclass(frozen=True, slots=True)
156
+ class WorkerModuleAnalysisContext:
157
+ module_variable_names: frozenset[str]
158
+ method_names: frozenset[str]
159
+ source_identifier_names: frozenset[str]
160
+
161
+
162
+ @dataclass(frozen=True, slots=True)
163
+ class WorkerMethodAnalysis:
164
+ method_name: str
165
+ exported: bool
166
+ signature_sha256: str
167
+ method_declaration: SourceSpan
168
+ executable_body: SourceSpan
169
+ declarations_end: int
170
+ first_statement_start: int
171
+ dependency_uses: tuple[tuple[str, DependencyUse], ...]
172
+
173
+
174
+ @dataclass(frozen=True, slots=True)
175
+ class _IsolatedWorkerMethodAnalysis:
176
+ method: WorkerMethodAnalysis
177
+ insertion_point: MethodInsertionPoints
178
+
179
+
180
+ @dataclass(frozen=True, slots=True)
181
+ class _PreparedIsolatedWorkerMethodAnalysis:
182
+ analyze: Callable[[], _IsolatedWorkerMethodAnalysis | None]
183
+
184
+
185
+ @dataclass(frozen=True, slots=True, repr=False)
186
+ class AnalyzedWorkerModule:
187
+ unit: WorkerModuleUnit
188
+ dependencies: tuple[ModuleDependencyBinding, ...]
189
+ exported_methods: tuple[str, ...]
190
+ method_insertion_points: tuple[MethodInsertionPoints, ...]
191
+ context: WorkerModuleAnalysisContext
192
+ methods: tuple[WorkerMethodAnalysis, ...]
193
+ catalog_identity: tuple[str, str, int, str]
194
+ parser_identity: tuple[str, str]
195
+
196
+
197
+ @dataclass(frozen=True, slots=True, repr=False, weakref_slot=True, eq=False)
198
+ class LoweredWorkerModule:
199
+ analysis: AnalyzedWorkerModule
200
+ mapped_source: MappedSource
201
+ dependency_bindings_sha256: str
202
+ transform_version: str = "module-universe-v1"
203
+
204
+
205
+ class WorkerSemanticAdmission:
206
+ """Opaque process-local proof for one exact lowered semantic result."""
207
+
208
+ __slots__ = ("__lowered", "__identity", "__proof")
209
+
210
+ def __init__(
211
+ self,
212
+ lowered: ReferenceType[LoweredWorkerModule],
213
+ identity: _WorkerSemanticIdentity,
214
+ proof: bytes,
215
+ *,
216
+ _authority: object,
217
+ ) -> None:
218
+ if _authority is not _SEMANTIC_ADMISSION_AUTHORITY:
219
+ raise TypeError("Worker semantic admissions are process-private")
220
+ object.__setattr__(self, "_WorkerSemanticAdmission__lowered", lowered)
221
+ object.__setattr__(self, "_WorkerSemanticAdmission__identity", identity)
222
+ object.__setattr__(self, "_WorkerSemanticAdmission__proof", proof)
223
+
224
+ def __setattr__(self, name: str, value: object) -> None:
225
+ raise AttributeError("Worker semantic admissions are immutable")
226
+
227
+ def __repr__(self) -> str:
228
+ return "<redacted worker semantic admission>"
229
+
230
+ def _contents(
231
+ self,
232
+ ) -> tuple[
233
+ ReferenceType[LoweredWorkerModule],
234
+ _WorkerSemanticIdentity,
235
+ bytes,
236
+ ]:
237
+ return self.__lowered, self.__identity, self.__proof
238
+
239
+
240
+ @dataclass(frozen=True, slots=True)
241
+ class _WorkerSemanticIdentity:
242
+ logical_name: str
243
+ raw_source_object_id: int
244
+ raw_text_object_id: int
245
+ raw_source_sha256: str
246
+ raw_text_sha256: str
247
+ lowered_source_object_id: int
248
+ lowered_text_object_id: int
249
+ lowered_source_sha256: str
250
+ lowered_text_sha256: str
251
+ source_map_object_id: int
252
+ lowered_source_map_sha256: str
253
+ lineage_object_id: int
254
+ lineage_sha256: str
255
+ dependency_bindings_sha256: str
256
+ canonical_exported_methods: tuple[str, ...]
257
+ catalog_identity: tuple[str, str, int, str]
258
+ parser_identity: tuple[str, str]
259
+ transform_version: str
260
+ packer_identity: str | None
261
+
262
+
263
+ _SEMANTIC_ADMISSION_AUTHORITY = object()
264
+ _SEMANTIC_ADMISSION_PROOF_KEY = token_bytes(32)
265
+ _SEMANTIC_ADMISSIONS: WeakKeyDictionary[
266
+ LoweredWorkerModule,
267
+ dict[str | None, WorkerSemanticAdmission],
268
+ ] = WeakKeyDictionary()
269
+ _SEMANTIC_ADMISSIONS_LOCK = RLock()
270
+
271
+
272
+ def dependency_export_name(canonical_name: str) -> str:
273
+ if not _is_bsl_identifier(canonical_name):
274
+ raise ValueError("dependency canonical name is invalid")
275
+ digest = sha256(canonical_name.casefold().encode("utf-8")).hexdigest()[:12]
276
+ return f"__OnecDependency_{digest}"
277
+
278
+
279
+ def analyze_worker_module(
280
+ unit: WorkerModuleUnit,
281
+ catalog: CommonModuleCatalogSnapshot,
282
+ parser_target: PythonParserTarget,
283
+ *,
284
+ profiler: PhaseRecorder | None = None,
285
+ ) -> AnalyzedWorkerModule:
286
+ if not isinstance(unit, WorkerModuleUnit):
287
+ raise _analysis_error(
288
+ "worker module analysis input is invalid",
289
+ SourceSpan(0, 0),
290
+ AMBIGUOUS_BINDING,
291
+ )
292
+ if not isinstance(catalog, CommonModuleCatalogSnapshot):
293
+ raise _analysis_error(
294
+ "worker module analysis input is invalid",
295
+ SourceSpan(0, 0),
296
+ AMBIGUOUS_BINDING,
297
+ )
298
+ if not isinstance(parser_target, PythonParserTarget):
299
+ raise _analysis_error(
300
+ "worker module analysis input is invalid",
301
+ SourceSpan(0, 0),
302
+ AMBIGUOUS_BINDING,
303
+ )
304
+ parse = lambda: parse_raw_module(unit.mapped_source.text, parser_target)
305
+ root, tokens = (
306
+ parse()
307
+ if profiler is None
308
+ else profiler.measure_parser("full_module_parses", parse)
309
+ )
310
+ analyze = lambda: _analyze_parsed_worker_module(
311
+ unit,
312
+ catalog,
313
+ root,
314
+ tokens,
315
+ parser_identity=(
316
+ parser_target.metadata.parser_identity_sha256,
317
+ parser_target.metadata.parsergen_package_sha256 or "",
318
+ ),
319
+ )
320
+ return (
321
+ analyze()
322
+ if profiler is None
323
+ else profiler.measure("dependency_analysis", analyze)
324
+ )
325
+
326
+
327
+ def analyze_resolved_worker_module(
328
+ unit: WorkerModuleUnit,
329
+ plan: ResolvedModulePlan,
330
+ ) -> AnalyzedWorkerModule:
331
+ """Convert a resolved minimal projection to the legacy admission shape."""
332
+
333
+ if not isinstance(unit, WorkerModuleUnit) or not isinstance(
334
+ plan, ResolvedModulePlan
335
+ ):
336
+ raise _analysis_error(
337
+ "worker module analysis input is invalid",
338
+ SourceSpan(0, 0),
339
+ AMBIGUOUS_BINDING,
340
+ )
341
+ if not _resolved_plan_structure_is_valid(plan):
342
+ raise _analysis_error(
343
+ "worker module analysis input is invalid",
344
+ SourceSpan(0, 0),
345
+ AMBIGUOUS_BINDING,
346
+ )
347
+ if unit.mapped_source.artifact.source_sha256 != plan.source.source_sha256:
348
+ raise _analysis_error(
349
+ "worker module projection does not match its source",
350
+ SourceSpan(0, 0),
351
+ AMBIGUOUS_BINDING,
352
+ )
353
+ if (
354
+ type(plan.catalog_identity) is not tuple
355
+ or len(plan.catalog_identity) != 4
356
+ or any(
357
+ type(value) is not str or not value
358
+ for value in plan.catalog_identity[:2]
359
+ )
360
+ or type(plan.catalog_generation) is not int
361
+ or plan.catalog_generation < 0
362
+ or type(plan.catalog_identity[2]) is not int
363
+ or plan.catalog_identity[2] != plan.catalog_generation
364
+ or not re.fullmatch(r"[0-9a-f]{64}", plan.catalog_identity[3])
365
+ ):
366
+ raise _analysis_error(
367
+ "worker module analysis input is invalid",
368
+ SourceSpan(0, 0),
369
+ AMBIGUOUS_BINDING,
370
+ )
371
+
372
+ source = unit.mapped_source.text
373
+ identifier_names = set(plan.source.module_variables)
374
+ method_names = frozenset(method.normalized_name for method in plan.source.methods)
375
+ identifier_names.update(method_names)
376
+ for method in plan.source.methods:
377
+ identifier_names.update(method.declared_names)
378
+ identifier_names.update(item.normalized_name for item in method.bare_names)
379
+ identifier_names.update(
380
+ item.normalized_name for item in plan.source.module_bare_names
381
+ )
382
+
383
+ uses_by_dependency: dict[str, list[DependencyUse]] = {
384
+ dependency.casefold(): [] for dependency in plan.dependencies
385
+ }
386
+ insertion_points: list[MethodInsertionPoints] = []
387
+ method_analyses: list[WorkerMethodAnalysis] = []
388
+ exported_methods: list[str] = []
389
+ for method in plan.methods:
390
+ declaration = method.source.declaration_span
391
+ if method.source.exported:
392
+ exported_methods.append(method.source.name)
393
+ insertion_points.append(
394
+ MethodInsertionPoints(
395
+ method.source.name,
396
+ declaration,
397
+ method.source.alias_declaration_offset,
398
+ method.source.alias_initializer_offset,
399
+ )
400
+ )
401
+ dependency_uses: list[tuple[str, DependencyUse]] = []
402
+ for dependency in method.dependencies:
403
+ normalized = dependency.casefold()
404
+ use = DependencyUse(
405
+ method.source.name,
406
+ "value",
407
+ declaration,
408
+ declaration,
409
+ )
410
+ dependency_uses.append((normalized, use))
411
+ try:
412
+ uses_by_dependency[normalized].append(use)
413
+ except KeyError:
414
+ raise _analysis_error(
415
+ "resolved worker dependency plan is inconsistent",
416
+ declaration,
417
+ AMBIGUOUS_BINDING,
418
+ ) from None
419
+ method_analyses.append(
420
+ WorkerMethodAnalysis(
421
+ method.source.name,
422
+ method.source.exported,
423
+ sha256(
424
+ source[
425
+ declaration.start : method.source.alias_initializer_offset
426
+ ].encode("utf-8")
427
+ ).hexdigest(),
428
+ declaration,
429
+ SourceSpan(method.source.alias_initializer_offset, declaration.end),
430
+ method.source.alias_declaration_offset,
431
+ method.source.alias_initializer_offset,
432
+ tuple(dependency_uses),
433
+ )
434
+ )
435
+
436
+ bindings: list[ModuleDependencyBinding] = []
437
+ generated_names: set[str] = set()
438
+ for dependency in plan.dependencies:
439
+ normalized = dependency.casefold()
440
+ uses = tuple(uses_by_dependency[normalized])
441
+ if not uses:
442
+ raise _analysis_error(
443
+ "resolved worker dependency plan is inconsistent",
444
+ SourceSpan(0, 0),
445
+ AMBIGUOUS_BINDING,
446
+ )
447
+ binding = _dependency_binding(
448
+ CommonModuleDescriptor(dependency, CommonModuleScope.SERVER),
449
+ uses,
450
+ )
451
+ generated = binding.export_variable.casefold()
452
+ if generated in generated_names or generated in identifier_names:
453
+ raise _analysis_error(
454
+ "generated dependency name collides with a source identifier",
455
+ uses[0].span,
456
+ AMBIGUOUS_BINDING,
457
+ )
458
+ generated_names.add(generated)
459
+ bindings.append(binding)
460
+
461
+ parser_identity = plan.source.parser_identity
462
+ if parser_identity is None:
463
+ raise _analysis_error(
464
+ "worker module parser provenance is unavailable",
465
+ SourceSpan(0, 0),
466
+ AMBIGUOUS_BINDING,
467
+ )
468
+ return AnalyzedWorkerModule(
469
+ unit,
470
+ tuple(bindings),
471
+ tuple(exported_methods),
472
+ tuple(insertion_points),
473
+ WorkerModuleAnalysisContext(
474
+ frozenset(plan.source.module_variables),
475
+ method_names,
476
+ frozenset(identifier_names),
477
+ ),
478
+ tuple(method_analyses),
479
+ plan.catalog_identity,
480
+ parser_identity,
481
+ )
482
+
483
+
484
+ def _resolved_plan_structure_is_valid(plan: ResolvedModulePlan) -> bool:
485
+ if (
486
+ not isinstance(plan.source, ParsedModuleModel)
487
+ or type(plan.methods) is not tuple
488
+ or not all(isinstance(method, ResolvedMethodPlan) for method in plan.methods)
489
+ or tuple(method.source for method in plan.methods) != plan.source.methods
490
+ or not _normalized_name_tuple(plan.implicit_local_names)
491
+ or not _casefold_unique_names(plan.forbidden_global_writes)
492
+ or not _casefold_unique_names(plan.dependencies)
493
+ or not _implicit_names_match_writes(
494
+ plan.implicit_local_names,
495
+ plan.source.module_bare_names,
496
+ )
497
+ ):
498
+ return False
499
+
500
+ dependencies = {name.casefold() for name in plan.dependencies}
501
+ module_implicit = set(plan.implicit_local_names)
502
+ method_names = {
503
+ method.normalized_name for method in plan.source.methods
504
+ }
505
+ module_hard_locals = (
506
+ set(plan.source.module_variables) | method_names | set(_PLATFORM_GLOBALS)
507
+ )
508
+ module_forbidden = {
509
+ name.casefold() for name in plan.forbidden_global_writes
510
+ }
511
+ if module_implicit & (
512
+ module_forbidden | dependencies | module_hard_locals
513
+ ):
514
+ return False
515
+ resolved_dependencies: set[str] = set()
516
+ for method in plan.methods:
517
+ expected_locals = tuple(
518
+ sorted(
519
+ {
520
+ *plan.source.module_variables,
521
+ *plan.implicit_local_names,
522
+ *method.source.declared_names,
523
+ *method.implicit_local_names,
524
+ }
525
+ )
526
+ )
527
+ if (
528
+ not _normalized_name_tuple(method.local_names)
529
+ or not _normalized_name_tuple(method.implicit_local_names)
530
+ or method.local_names != expected_locals
531
+ or not _implicit_names_match_writes(
532
+ method.implicit_local_names,
533
+ method.source.bare_names,
534
+ )
535
+ or not _casefold_unique_names(method.forbidden_global_writes)
536
+ or not _casefold_unique_names(method.dependencies)
537
+ ):
538
+ return False
539
+ method_dependencies = {
540
+ dependency.casefold() for dependency in method.dependencies
541
+ }
542
+ method_forbidden = {
543
+ name.casefold() for name in method.forbidden_global_writes
544
+ }
545
+ method_hard_locals = (
546
+ set(plan.source.module_variables)
547
+ | module_implicit
548
+ | set(method.source.declared_names)
549
+ | method_names
550
+ | set(_PLATFORM_GLOBALS)
551
+ )
552
+ if (
553
+ not method_dependencies.issubset(dependencies)
554
+ or method_dependencies & method_forbidden
555
+ or set(method.implicit_local_names)
556
+ & (method_dependencies | method_forbidden | method_hard_locals)
557
+ ):
558
+ return False
559
+ resolved_dependencies.update(method_dependencies)
560
+ return resolved_dependencies == dependencies
561
+
562
+
563
+ def _normalized_name_tuple(names: object) -> bool:
564
+ return (
565
+ type(names) is tuple
566
+ and all(
567
+ type(name) is str and name and name == name.casefold()
568
+ for name in names
569
+ )
570
+ and names == tuple(sorted(set(names)))
571
+ )
572
+
573
+
574
+ def _casefold_unique_names(names: object) -> bool:
575
+ return (
576
+ type(names) is tuple
577
+ and all(type(name) is str and name for name in names)
578
+ and len(names) == len({name.casefold() for name in names})
579
+ )
580
+
581
+
582
+ def _implicit_names_match_writes(
583
+ names: tuple[str, ...],
584
+ bare_names: tuple[Any, ...],
585
+ ) -> bool:
586
+ candidates = {item.normalized_name: item for item in bare_names}
587
+ return all(
588
+ (candidate := candidates.get(name)) is not None
589
+ and candidate.kinds & BareNameKind.BARE_WRITE
590
+ and _is_bsl_identifier(candidate.name)
591
+ for name in names
592
+ )
593
+
594
+
595
+ def _analyze_parsed_worker_module(
596
+ unit: WorkerModuleUnit,
597
+ catalog: CommonModuleCatalogSnapshot,
598
+ root: Any,
599
+ tokens: tuple[Any, ...],
600
+ *,
601
+ parser_identity: tuple[str, str] = ("", ""),
602
+ ) -> AnalyzedWorkerModule:
603
+ module_variables = tuple(
604
+ variable
605
+ for declaration in root.Declarations
606
+ for variable in declaration.Variables
607
+ )
608
+ module_names = {variable.Name.casefold() for variable in module_variables}
609
+ methods = tuple(_module_methods(root))
610
+ method_names: set[str] = set()
611
+ for method in methods:
612
+ normalized = method.Declaration.Name.casefold()
613
+ if normalized in method_names:
614
+ raise _analysis_error(
615
+ "worker module contains an ambiguous method declaration",
616
+ _source_span(method.span),
617
+ AMBIGUOUS_BINDING,
618
+ )
619
+ method_names.add(normalized)
620
+ catalog_by_name = {item.canonical_name.casefold(): item for item in catalog.modules}
621
+ for variable in module_variables:
622
+ if variable.Export is not None and variable.Name.casefold() in catalog_by_name:
623
+ raise _analysis_error(
624
+ "exported module variable conflicts with a common module",
625
+ _source_span(variable.span),
626
+ AMBIGUOUS_BINDING,
627
+ )
628
+ identifier_spans: dict[str, SourceSpan] = {}
629
+ for_token_starts: list[int] = []
630
+ for token in tokens:
631
+ if token.type == "ID":
632
+ identifier_spans.setdefault(
633
+ token.text.casefold(), SourceSpan(token.start, token.end)
634
+ )
635
+ elif token.type == "ДЛЯ":
636
+ for_token_starts.append(token.start)
637
+ loop_token_starts = tuple(for_token_starts)
638
+ context = WorkerModuleAnalysisContext(
639
+ frozenset(module_names),
640
+ frozenset(method_names),
641
+ frozenset(identifier_spans),
642
+ )
643
+ uses_by_module: dict[str, list[DependencyUse]] = {}
644
+ insertion_points: list[MethodInsertionPoints] = []
645
+ exported_methods: list[str] = []
646
+ method_analyses: list[WorkerMethodAnalysis] = []
647
+
648
+ elements = root.Elements
649
+ while elements.Item is not None:
650
+ item = elements.Item
651
+ if type(item).__name__ != "Method":
652
+ for node in _walk_ast(item):
653
+ kind = type(node).__name__
654
+ if kind == "ExecuteStatement":
655
+ raise _analysis_error(
656
+ "dynamic execution is unsupported in worker modules",
657
+ _source_span(node.span),
658
+ DYNAMIC_EXECUTE,
659
+ )
660
+ if kind != "AccessChain":
661
+ continue
662
+ normalized = node.Root.casefold()
663
+ if (
664
+ normalized in module_names
665
+ or normalized in _PLATFORM_GLOBALS
666
+ ):
667
+ continue
668
+ if normalized in method_names and node.Arguments is not None:
669
+ continue
670
+ if normalized in catalog_by_name:
671
+ raise _analysis_error(
672
+ "common module dependency is unsupported in module scope",
673
+ _source_span(node.span),
674
+ MODULE_SCOPE_DEPENDENCY,
675
+ )
676
+ if node.Postfix:
677
+ raise _analysis_error(
678
+ "qualified module dependency cannot be resolved",
679
+ _source_span(node.span),
680
+ AMBIGUOUS_BINDING,
681
+ )
682
+ elements = elements.Rest
683
+
684
+ for method in methods:
685
+ declaration = method.Declaration
686
+ if declaration.Export is not None:
687
+ exported_methods.append(declaration.Name)
688
+ method_span = _source_span(method.span)
689
+ first_statement_start = declaration.Body.Code.span.start
690
+ declarations_end = (
691
+ _declaration_end(declaration.Body.LocalDeclarations[-1], tokens)
692
+ if declaration.Body.LocalDeclarations
693
+ else declaration.Body.Code.span.start
694
+ )
695
+ insertion_points.append(
696
+ MethodInsertionPoints(
697
+ declaration.Name,
698
+ method_span,
699
+ declarations_end,
700
+ first_statement_start,
701
+ )
702
+ )
703
+ executable_body = _executable_body(
704
+ declaration.Body.Code,
705
+ loop_token_starts,
706
+ )
707
+ shadowed = set(module_names)
708
+ if declaration.Parameters is not None:
709
+ shadowed |= {item.Name.casefold() for item in declaration.Parameters.Items}
710
+ shadowed |= {
711
+ name.casefold()
712
+ for local in declaration.Body.LocalDeclarations
713
+ for name in local.Names
714
+ }
715
+
716
+ accesses: list[Any] = []
717
+ for node in _walk_ast(declaration.Body.Code):
718
+ kind = type(node).__name__
719
+ if kind == "ExecuteStatement":
720
+ raise _analysis_error(
721
+ "dynamic execution is unsupported in worker modules",
722
+ _source_span(node.span),
723
+ DYNAMIC_EXECUTE,
724
+ )
725
+ if kind in {"ForEachStatement", "ForRangeStatement"}:
726
+ shadowed.add(node.Variable.casefold())
727
+ if kind == "SimpleStatement" and node.Value is not None:
728
+ target = node.Target
729
+ if (
730
+ type(target).__name__ == "AccessChain"
731
+ and target.Arguments is None
732
+ and not target.Postfix
733
+ ):
734
+ shadowed.add(target.Root.casefold())
735
+ if kind == "AccessChain":
736
+ accesses.append(node)
737
+
738
+ dependency_uses: list[tuple[str, DependencyUse]] = []
739
+ for node in accesses:
740
+ normalized = node.Root.casefold()
741
+ if normalized in shadowed or normalized in _PLATFORM_GLOBALS:
742
+ continue
743
+ if normalized in method_names and node.Arguments is not None:
744
+ continue
745
+ descriptor = catalog_by_name.get(normalized)
746
+ if descriptor is None:
747
+ if node.Arguments is not None and not node.Postfix:
748
+ continue
749
+ if node.Postfix:
750
+ raise _analysis_error(
751
+ "qualified module dependency cannot be resolved",
752
+ _source_span(node.span),
753
+ AMBIGUOUS_BINDING,
754
+ )
755
+ continue
756
+
757
+ category = _dependency_category(node)
758
+ use = DependencyUse(
759
+ declaration.Name,
760
+ category,
761
+ _source_span(node.span),
762
+ method_span,
763
+ )
764
+ dependency_uses.append((normalized, use))
765
+ uses_by_module.setdefault(normalized, []).append(use)
766
+
767
+ method_analyses.append(
768
+ WorkerMethodAnalysis(
769
+ declaration.Name,
770
+ declaration.Export is not None,
771
+ sha256(
772
+ unit.mapped_source.text[
773
+ method_span.start : executable_body.start
774
+ ].encode("utf-8")
775
+ ).hexdigest(),
776
+ method_span,
777
+ executable_body,
778
+ declarations_end,
779
+ first_statement_start,
780
+ tuple(dependency_uses),
781
+ )
782
+ )
783
+
784
+ generated_names: set[str] = set()
785
+ for normalized in sorted(uses_by_module):
786
+ generated_name = dependency_export_name(
787
+ catalog_by_name[normalized].canonical_name
788
+ ).casefold()
789
+ if generated_name in generated_names:
790
+ raise _analysis_error(
791
+ "generated dependency names collide",
792
+ uses_by_module[normalized][0].span,
793
+ AMBIGUOUS_BINDING,
794
+ )
795
+ generated_names.add(generated_name)
796
+ if generated_name in context.source_identifier_names:
797
+ raise _analysis_error(
798
+ "generated dependency name collides with a source identifier",
799
+ identifier_spans[generated_name],
800
+ AMBIGUOUS_BINDING,
801
+ )
802
+
803
+ bindings = tuple(
804
+ _dependency_binding(catalog_by_name[name], tuple(uses_by_module[name]))
805
+ for name in sorted(uses_by_module)
806
+ )
807
+ return AnalyzedWorkerModule(
808
+ unit,
809
+ bindings,
810
+ tuple(exported_methods),
811
+ tuple(insertion_points),
812
+ context,
813
+ tuple(method_analyses),
814
+ (
815
+ catalog.profile,
816
+ catalog.preprocessor_profile,
817
+ catalog.revision,
818
+ catalog.sha256,
819
+ ),
820
+ parser_identity,
821
+ )
822
+
823
+
824
+ def _prepare_isolated_worker_method(
825
+ unit: WorkerModuleUnit,
826
+ catalog: CommonModuleCatalogSnapshot,
827
+ parser_target: PythonParserTarget,
828
+ method_span: SourceSpan,
829
+ context: WorkerModuleAnalysisContext,
830
+ *,
831
+ profiler: PhaseRecorder | None = None,
832
+ ) -> _PreparedIsolatedWorkerMethodAnalysis:
833
+ """Parse one complete method and defer its context-dependent analysis."""
834
+
835
+ method_source = unit.mapped_source.text[method_span.start : method_span.end]
836
+
837
+ def parse() -> tuple[Any, tuple[Any, ...]] | None:
838
+ try:
839
+ return parse_raw_module(method_source, parser_target)
840
+ except BslLexError as error:
841
+ if error.code in {
842
+ "unterminated_string",
843
+ "unterminated_date_literal",
844
+ }:
845
+ try:
846
+ tokenize(unit.mapped_source.text)
847
+ except BslLexError as canonical_error:
848
+ raise canonical_error from error
849
+ return None
850
+ absolute_start = method_span.start + error.span.start
851
+ raise BslLexError(
852
+ _remap_isolated_error_message(
853
+ str(error),
854
+ error.span.start,
855
+ absolute_start,
856
+ ),
857
+ span=SourceSpan(
858
+ absolute_start,
859
+ method_span.start + error.span.end,
860
+ ),
861
+ code=error.code,
862
+ ) from error
863
+ except BslParseError as error:
864
+ absolute_start = method_span.start + error.span.start
865
+ raise BslParseError(
866
+ _remap_isolated_error_message(
867
+ str(error),
868
+ error.span.start,
869
+ absolute_start,
870
+ ),
871
+ span=SourceSpan(
872
+ absolute_start,
873
+ method_span.start + error.span.end,
874
+ ),
875
+ code=error.code,
876
+ ) from error
877
+
878
+ parsed = (
879
+ parse()
880
+ if profiler is None
881
+ else profiler.measure_parser("delta_method_parses", parse)
882
+ )
883
+ if parsed is None:
884
+ return _PreparedIsolatedWorkerMethodAnalysis(lambda: None)
885
+ root, tokens = parsed
886
+
887
+ def analyze() -> _IsolatedWorkerMethodAnalysis | None:
888
+ elements: list[Any] = []
889
+ cursor = root.Elements
890
+ while cursor.Item is not None:
891
+ elements.append(cursor.Item)
892
+ cursor = cursor.Rest
893
+ if (
894
+ root.Declarations
895
+ or len(elements) != 1
896
+ or type(elements[0]).__name__ != "Method"
897
+ ):
898
+ return None
899
+
900
+ method = elements[0]
901
+ declaration = method.Declaration
902
+ local_method_span = _source_span(method.span)
903
+ absolute_method_span = _offset_source_span(local_method_span, method_span.start)
904
+ loop_token_starts = tuple(
905
+ token.start for token in tokens if token.type == "ДЛЯ"
906
+ )
907
+ local_executable_body = _executable_body(
908
+ declaration.Body.Code,
909
+ loop_token_starts,
910
+ )
911
+ executable_body = _offset_source_span(
912
+ local_executable_body,
913
+ method_span.start,
914
+ )
915
+ declarations_end = (
916
+ _declaration_end(declaration.Body.LocalDeclarations[-1], tokens)
917
+ if declaration.Body.LocalDeclarations
918
+ else declaration.Body.Code.span.start
919
+ )
920
+ first_statement_start = declaration.Body.Code.span.start
921
+ shadowed = set(context.module_variable_names)
922
+ if declaration.Parameters is not None:
923
+ shadowed |= {
924
+ item.Name.casefold() for item in declaration.Parameters.Items
925
+ }
926
+ shadowed |= {
927
+ name.casefold()
928
+ for local in declaration.Body.LocalDeclarations
929
+ for name in local.Names
930
+ }
931
+
932
+ accesses: list[Any] = []
933
+ for node in _walk_ast(declaration.Body.Code):
934
+ kind = type(node).__name__
935
+ if kind == "ExecuteStatement":
936
+ raise _analysis_error(
937
+ "dynamic execution is unsupported in worker modules",
938
+ _offset_source_span(_source_span(node.span), method_span.start),
939
+ DYNAMIC_EXECUTE,
940
+ )
941
+ if kind in {"ForEachStatement", "ForRangeStatement"}:
942
+ shadowed.add(node.Variable.casefold())
943
+ if kind == "SimpleStatement" and node.Value is not None:
944
+ target = node.Target
945
+ if (
946
+ type(target).__name__ == "AccessChain"
947
+ and target.Arguments is None
948
+ and not target.Postfix
949
+ ):
950
+ shadowed.add(target.Root.casefold())
951
+ if kind == "AccessChain":
952
+ accesses.append(node)
953
+
954
+ catalog_by_name = {
955
+ item.canonical_name.casefold(): item for item in catalog.modules
956
+ }
957
+ dependency_uses: list[tuple[str, DependencyUse]] = []
958
+ for node in accesses:
959
+ normalized = node.Root.casefold()
960
+ if normalized in shadowed or normalized in _PLATFORM_GLOBALS:
961
+ continue
962
+ if normalized in context.method_names and node.Arguments is not None:
963
+ continue
964
+ descriptor = catalog_by_name.get(normalized)
965
+ if descriptor is None:
966
+ if node.Arguments is not None and not node.Postfix:
967
+ continue
968
+ if node.Postfix:
969
+ raise _analysis_error(
970
+ "qualified module dependency cannot be resolved",
971
+ _offset_source_span(
972
+ _source_span(node.span), method_span.start
973
+ ),
974
+ AMBIGUOUS_BINDING,
975
+ )
976
+ continue
977
+ try:
978
+ category = _dependency_category(node)
979
+ except ModuleUniverseAdmissionError as error:
980
+ raise _analysis_error(
981
+ str(error),
982
+ _offset_source_span(error.span, method_span.start),
983
+ error.code,
984
+ ) from error
985
+ use = DependencyUse(
986
+ declaration.Name,
987
+ category,
988
+ _offset_source_span(_source_span(node.span), method_span.start),
989
+ absolute_method_span,
990
+ )
991
+ dependency_uses.append((normalized, use))
992
+
993
+ analysis = WorkerMethodAnalysis(
994
+ declaration.Name,
995
+ declaration.Export is not None,
996
+ sha256(
997
+ method_source[
998
+ local_method_span.start : local_executable_body.start
999
+ ].encode("utf-8")
1000
+ ).hexdigest(),
1001
+ absolute_method_span,
1002
+ executable_body,
1003
+ method_span.start + declarations_end,
1004
+ method_span.start + first_statement_start,
1005
+ tuple(dependency_uses),
1006
+ )
1007
+ return _IsolatedWorkerMethodAnalysis(
1008
+ analysis,
1009
+ MethodInsertionPoints(
1010
+ declaration.Name,
1011
+ absolute_method_span,
1012
+ method_span.start + declarations_end,
1013
+ method_span.start + first_statement_start,
1014
+ ),
1015
+ )
1016
+
1017
+ return _PreparedIsolatedWorkerMethodAnalysis(analyze)
1018
+
1019
+
1020
+ @dataclass(frozen=True, slots=True)
1021
+ class _AliasTransformPlan:
1022
+ source: MappedSource
1023
+ line_ending: str
1024
+ fields: tuple[tuple[str, SourceSpan, str], ...]
1025
+ declarations: dict[int, tuple[tuple[str, DependencyUse, str], ...]]
1026
+ initializers: dict[int, tuple[tuple[str, DependencyUse, str], ...]]
1027
+ reuse_boundaries: tuple[int, ...]
1028
+
1029
+
1030
+ def lower_worker_module(
1031
+ analysis: AnalyzedWorkerModule,
1032
+ *,
1033
+ profiler: PhaseRecorder | None = None,
1034
+ ) -> LoweredWorkerModule:
1035
+ if not isinstance(analysis, AnalyzedWorkerModule):
1036
+ raise ValueError("worker module lowering input is invalid")
1037
+
1038
+ plan_operation = lambda: _plan_alias_transform(analysis)
1039
+ plan = (
1040
+ plan_operation()
1041
+ if profiler is None
1042
+ else profiler.measure("alias_transform", plan_operation)
1043
+ )
1044
+ compose = lambda: _compose_worker_module_source(plan)
1045
+ mapped_source = (
1046
+ compose()
1047
+ if profiler is None
1048
+ else profiler.measure("source_map_composition", compose)
1049
+ )
1050
+ dependency_bindings_sha256 = sha256(
1051
+ json.dumps(
1052
+ [binding.sha256 for binding in analysis.dependencies],
1053
+ ensure_ascii=False,
1054
+ separators=(",", ":"),
1055
+ ).encode("utf-8")
1056
+ ).hexdigest()
1057
+ lowered = LoweredWorkerModule(
1058
+ analysis,
1059
+ mapped_source,
1060
+ dependency_bindings_sha256,
1061
+ )
1062
+ admit = lambda: _mint_worker_semantic_admission(lowered)
1063
+ if profiler is None:
1064
+ admit()
1065
+ else:
1066
+ profiler.measure("admission", admit)
1067
+ return lowered
1068
+
1069
+
1070
+ def lower_resolved_worker_module(
1071
+ unit: WorkerModuleUnit,
1072
+ plan: ResolvedModulePlan,
1073
+ *,
1074
+ profiler: PhaseRecorder | None = None,
1075
+ ) -> LoweredWorkerModule:
1076
+ """Lower a resolved projection directly, without parsing or map composition."""
1077
+
1078
+ analyze = lambda: analyze_resolved_worker_module(unit, plan)
1079
+ analysis = (
1080
+ analyze()
1081
+ if profiler is None
1082
+ else profiler.measure("resolved_analysis_adapter", analyze)
1083
+ )
1084
+ plan_insertions = lambda: _resolved_reload_insertions(analysis, plan)
1085
+ insertions = (
1086
+ plan_insertions()
1087
+ if profiler is None
1088
+ else profiler.measure("alias_transform", plan_insertions)
1089
+ )
1090
+ materialize = lambda: materialize_worker_reload_source(
1091
+ unit.mapped_source,
1092
+ insertions,
1093
+ method_boundaries=tuple(
1094
+ ReloadMethodBoundary(
1095
+ method.source.name,
1096
+ method.source.declaration_span.start,
1097
+ method.source.alias_initializer_offset,
1098
+ method.source.declaration_span.end,
1099
+ )
1100
+ for method in plan.methods
1101
+ ),
1102
+ )
1103
+ mapped_source = (
1104
+ materialize()
1105
+ if profiler is None
1106
+ else profiler.measure("source_map_composition", materialize)
1107
+ )
1108
+ lowered = LoweredWorkerModule(
1109
+ analysis,
1110
+ mapped_source,
1111
+ _canonical_dependency_bindings_sha256(analysis.dependencies),
1112
+ "module-universe-v2",
1113
+ )
1114
+ admit = lambda: _mint_worker_semantic_admission(lowered)
1115
+ if profiler is None:
1116
+ admit()
1117
+ else:
1118
+ profiler.measure("admission", admit)
1119
+ return lowered
1120
+
1121
+
1122
+ def _resolved_reload_insertions(
1123
+ analysis: AnalyzedWorkerModule,
1124
+ plan: ResolvedModulePlan,
1125
+ ) -> tuple[ReloadInsertion, ...]:
1126
+ source = analysis.unit.mapped_source.text
1127
+ bindings = {
1128
+ binding.target_module.casefold(): binding
1129
+ for binding in analysis.dependencies
1130
+ }
1131
+ insertions: list[ReloadInsertion] = []
1132
+ for binding in analysis.dependencies:
1133
+ insertions.append(
1134
+ ReloadInsertion.synthetic(
1135
+ source_offset=0,
1136
+ text=f"Перем {binding.export_variable} Экспорт;\n",
1137
+ method_name=None,
1138
+ synthetic_region=DEPENDENCY_FIELD_REGION,
1139
+ anchor=SourceSpan(0, 0),
1140
+ )
1141
+ )
1142
+ module_implicit_names = _implicit_local_spellings(
1143
+ plan.implicit_local_names,
1144
+ plan.source.module_bare_names,
1145
+ )
1146
+ if module_implicit_names:
1147
+ module_span = SourceSpan(0, len(source))
1148
+ insertions.append(
1149
+ ReloadInsertion.derived(
1150
+ source_offset=0,
1151
+ text="".join(f"Перем {name};\n" for name in module_implicit_names),
1152
+ method_name=None,
1153
+ synthetic_region=IMPLICIT_LOCAL_DECLARATION_REGION,
1154
+ origin=module_span,
1155
+ anchor=module_span,
1156
+ )
1157
+ )
1158
+
1159
+ dependent_offsets: list[int] = []
1160
+ for method in plan.methods:
1161
+ if method.dependencies or method.implicit_local_names:
1162
+ dependent_offsets.append(method.source.alias_declaration_offset)
1163
+ if method.dependencies:
1164
+ dependent_offsets.append(method.source.alias_initializer_offset)
1165
+ line_contexts = (
1166
+ _reload_line_contexts(source, tuple(dependent_offsets))
1167
+ if dependent_offsets
1168
+ else {}
1169
+ )
1170
+ for method in plan.methods:
1171
+ if not method.dependencies and not method.implicit_local_names:
1172
+ continue
1173
+ method_bindings = tuple(
1174
+ bindings[dependency.casefold()]
1175
+ for dependency in method.dependencies
1176
+ )
1177
+ declaration_offset = method.source.alias_declaration_offset
1178
+ implicit_names = _implicit_local_spellings(
1179
+ method.implicit_local_names,
1180
+ method.source.bare_names,
1181
+ )
1182
+ if implicit_names:
1183
+ insertions.append(
1184
+ ReloadInsertion.derived(
1185
+ source_offset=declaration_offset,
1186
+ text=_render_reload_block(
1187
+ tuple(f"Перем {name};" for name in implicit_names),
1188
+ line_contexts[declaration_offset],
1189
+ ),
1190
+ method_name=method.source.name,
1191
+ synthetic_region=IMPLICIT_LOCAL_DECLARATION_REGION,
1192
+ origin=method.source.declaration_span,
1193
+ anchor=method.source.declaration_span,
1194
+ )
1195
+ )
1196
+ if method_bindings:
1197
+ initializer_offset = method.source.alias_initializer_offset
1198
+ insertions.append(
1199
+ ReloadInsertion.derived(
1200
+ source_offset=declaration_offset,
1201
+ text=_render_reload_block(
1202
+ tuple(
1203
+ f"Перем {binding.target_module};"
1204
+ for binding in method_bindings
1205
+ ),
1206
+ line_contexts[declaration_offset],
1207
+ ),
1208
+ method_name=method.source.name,
1209
+ synthetic_region=DEPENDENCY_ALIAS_DECLARATION_REGION,
1210
+ origin=method.source.declaration_span,
1211
+ anchor=method.source.declaration_span,
1212
+ )
1213
+ )
1214
+ insertions.append(
1215
+ ReloadInsertion.derived(
1216
+ source_offset=initializer_offset,
1217
+ text=_render_reload_block(
1218
+ tuple(
1219
+ f"{binding.target_module} = {binding.export_variable};"
1220
+ for binding in method_bindings
1221
+ ),
1222
+ line_contexts[initializer_offset],
1223
+ ),
1224
+ method_name=method.source.name,
1225
+ synthetic_region=DEPENDENCY_ALIAS_INITIALIZER_REGION,
1226
+ origin=method.source.declaration_span,
1227
+ anchor=method.source.declaration_span,
1228
+ )
1229
+ )
1230
+ return tuple(insertions)
1231
+
1232
+
1233
+ def _implicit_local_spellings(
1234
+ names: tuple[str, ...],
1235
+ bare_names: tuple[Any, ...],
1236
+ ) -> tuple[str, ...]:
1237
+ by_name = {item.normalized_name: item.name for item in bare_names}
1238
+ return tuple(by_name[name] for name in names)
1239
+
1240
+
1241
+ def _render_reload_block(
1242
+ lines: tuple[str, ...],
1243
+ context: tuple[bool, str, bool],
1244
+ ) -> str:
1245
+ prefix_has_code, indent, next_is_newline = context
1246
+ leading = f"\n{indent}" if prefix_has_code else ""
1247
+ trailing = "" if next_is_newline else f"\n{indent}"
1248
+ return leading + f"\n{indent}".join(lines) + trailing
1249
+
1250
+
1251
+ def _reload_line_contexts(
1252
+ source: str,
1253
+ offsets: tuple[int, ...],
1254
+ ) -> dict[int, tuple[bool, str, bool]]:
1255
+ _observe_text_work("reload_plan_source_scan", len(source))
1256
+ result: dict[int, tuple[bool, str, bool]] = {}
1257
+ line_start = 0
1258
+ previous = 0
1259
+ for offset in sorted(set(offsets)):
1260
+ newline = max(
1261
+ source.rfind("\n", previous, offset),
1262
+ source.rfind("\r", previous, offset),
1263
+ )
1264
+ if newline >= 0:
1265
+ line_start = newline + 1
1266
+ prefix = source[line_start:offset]
1267
+ indent_end = 0
1268
+ while indent_end < len(prefix) and prefix[indent_end] in " \t":
1269
+ indent_end += 1
1270
+ result[offset] = (
1271
+ bool(prefix.strip()),
1272
+ prefix[:indent_end],
1273
+ offset < len(source) and source[offset] in "\r\n",
1274
+ )
1275
+ previous = offset
1276
+ return result
1277
+
1278
+
1279
+ def _mint_worker_semantic_admission(
1280
+ lowered: LoweredWorkerModule,
1281
+ *,
1282
+ packer_identity: str | None = None,
1283
+ semantic_identity: _WorkerSemanticIdentity | None = None,
1284
+ ) -> WorkerSemanticAdmission:
1285
+ """Mint the one semantic capability shared by full and future delta lowering."""
1286
+ if not isinstance(lowered, LoweredWorkerModule):
1287
+ raise ValueError("worker semantic admission input is invalid")
1288
+ if packer_identity is not None and not _safe_packer_identity(packer_identity):
1289
+ raise ValueError("worker semantic admission input is invalid")
1290
+ identity = (
1291
+ _worker_semantic_admission_identity(
1292
+ lowered,
1293
+ packer_identity=packer_identity,
1294
+ )
1295
+ if semantic_identity is None
1296
+ else replace(semantic_identity, packer_identity=packer_identity)
1297
+ )
1298
+ proof = _worker_semantic_admission_proof(identity)
1299
+ admission = WorkerSemanticAdmission(
1300
+ ref(lowered),
1301
+ identity,
1302
+ proof,
1303
+ _authority=_SEMANTIC_ADMISSION_AUTHORITY,
1304
+ )
1305
+ with _SEMANTIC_ADMISSIONS_LOCK:
1306
+ admissions = _SEMANTIC_ADMISSIONS.setdefault(lowered, {})
1307
+ admissions[packer_identity] = admission
1308
+ return admission
1309
+
1310
+
1311
+ def worker_semantic_admission(
1312
+ lowered: LoweredWorkerModule,
1313
+ *,
1314
+ packer_identity: str,
1315
+ ) -> WorkerSemanticAdmission:
1316
+ """Return the process-private semantic proof minted during lowering."""
1317
+ if not isinstance(lowered, LoweredWorkerModule):
1318
+ raise ValueError("worker semantic admission is invalid")
1319
+ if not _safe_packer_identity(packer_identity):
1320
+ raise ValueError("worker semantic admission is invalid")
1321
+ with _SEMANTIC_ADMISSIONS_LOCK:
1322
+ admissions = _SEMANTIC_ADMISSIONS.get(lowered)
1323
+ basis = None if admissions is None else admissions.get(None)
1324
+ _validate_worker_semantic_admission(
1325
+ basis,
1326
+ source=lowered.mapped_source,
1327
+ exported_methods=lowered.analysis.exported_methods,
1328
+ expected_packer_identity=None,
1329
+ )
1330
+ with _SEMANTIC_ADMISSIONS_LOCK:
1331
+ admissions = _SEMANTIC_ADMISSIONS.get(lowered)
1332
+ admission = None if admissions is None else admissions.get(packer_identity)
1333
+ if admission is None:
1334
+ assert basis is not None
1335
+ _, basis_identity, _ = basis._contents()
1336
+ admission = _mint_worker_semantic_admission(
1337
+ lowered,
1338
+ packer_identity=packer_identity,
1339
+ semantic_identity=basis_identity,
1340
+ )
1341
+ assert admission is not None
1342
+ return admission
1343
+
1344
+
1345
+ def _validate_worker_semantic_admission(
1346
+ admission: WorkerSemanticAdmission | None,
1347
+ *,
1348
+ source: MappedSource,
1349
+ exported_methods: tuple[str, ...],
1350
+ expected_packer_identity: str | None,
1351
+ ) -> LoweredWorkerModule:
1352
+ """Validate an admission without exposing its semantic manifest."""
1353
+ try:
1354
+ if (
1355
+ not isinstance(admission, WorkerSemanticAdmission)
1356
+ or not isinstance(source, MappedSource)
1357
+ or (
1358
+ expected_packer_identity is not None
1359
+ and not _safe_packer_identity(expected_packer_identity)
1360
+ )
1361
+ or type(exported_methods) is not tuple
1362
+ or any(type(item) is not str for item in exported_methods)
1363
+ ):
1364
+ raise ValueError
1365
+ lowered_reference, identity, proof = admission._contents()
1366
+ lowered = lowered_reference()
1367
+ if (
1368
+ lowered is None
1369
+ or lowered.mapped_source is not source
1370
+ or _canonical_exported_methods(exported_methods)
1371
+ != _canonical_exported_methods(lowered.analysis.exported_methods)
1372
+ or identity.packer_identity != expected_packer_identity
1373
+ or not _worker_semantic_identity_matches(identity, lowered)
1374
+ or not compare_digest(
1375
+ proof,
1376
+ _worker_semantic_admission_proof(identity),
1377
+ )
1378
+ ):
1379
+ raise ValueError
1380
+ with _SEMANTIC_ADMISSIONS_LOCK:
1381
+ admissions = _SEMANTIC_ADMISSIONS.get(lowered)
1382
+ if (
1383
+ admissions is None
1384
+ or admissions.get(expected_packer_identity) is not admission
1385
+ ):
1386
+ raise ValueError
1387
+ return lowered
1388
+ except Exception:
1389
+ raise ValueError("worker semantic admission is invalid") from None
1390
+
1391
+
1392
+ def _worker_semantic_admission_proof(identity: _WorkerSemanticIdentity) -> bytes:
1393
+ mac = hmac_new(_SEMANTIC_ADMISSION_PROOF_KEY, digestmod="sha256")
1394
+
1395
+ def update(value: object) -> int:
1396
+ if value is None:
1397
+ mac.update(b"n")
1398
+ return 1
1399
+ if type(value) is int:
1400
+ mac.update(b"i")
1401
+ if value < 0:
1402
+ raise ValueError("worker semantic admission identity is invalid")
1403
+ payload = value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big")
1404
+ length = len(payload).to_bytes(8, "big", signed=False)
1405
+ mac.update(length)
1406
+ mac.update(payload)
1407
+ return 1 + len(length) + len(payload)
1408
+ if type(value) is str:
1409
+ mac.update(b"s")
1410
+ payload = value.encode("utf-8")
1411
+ length = len(payload).to_bytes(8, "big", signed=False)
1412
+ mac.update(length)
1413
+ mac.update(payload)
1414
+ return 1 + len(length) + len(payload)
1415
+ if type(value) is tuple:
1416
+ mac.update(b"t")
1417
+ length = len(value).to_bytes(8, "big", signed=False)
1418
+ mac.update(length)
1419
+ return 1 + len(length) + sum(update(item) for item in value)
1420
+ raise ValueError("worker semantic admission identity is invalid")
1421
+
1422
+ width = 0
1423
+ for identity_field in fields(identity):
1424
+ width += update(identity_field.name)
1425
+ width += update(getattr(identity, identity_field.name))
1426
+ _observe_text_work("semantic_admission_proof_bytes", width)
1427
+ return mac.digest()
1428
+
1429
+
1430
+ def _worker_semantic_admission_identity(
1431
+ lowered: LoweredWorkerModule,
1432
+ *,
1433
+ packer_identity: str | None,
1434
+ ) -> _WorkerSemanticIdentity:
1435
+ analysis = lowered.analysis
1436
+ raw = analysis.unit.mapped_source
1437
+ mapped = lowered.mapped_source
1438
+ summary = _trusted_mapped_semantic_summary(mapped)
1439
+ return _WorkerSemanticIdentity(
1440
+ analysis.unit.logical_name,
1441
+ id(raw),
1442
+ id(raw.text),
1443
+ raw.artifact.source_sha256,
1444
+ raw.artifact.source_sha256,
1445
+ id(mapped),
1446
+ id(mapped.text),
1447
+ mapped.artifact.source_sha256,
1448
+ mapped.artifact.source_sha256,
1449
+ id(mapped.source_map),
1450
+ summary.source_map_sha256,
1451
+ id(mapped.lineage),
1452
+ summary.lineage_sha256,
1453
+ _canonical_dependency_bindings_sha256(analysis.dependencies),
1454
+ _canonical_exported_methods(analysis.exported_methods),
1455
+ analysis.catalog_identity,
1456
+ analysis.parser_identity,
1457
+ lowered.transform_version,
1458
+ packer_identity,
1459
+ )
1460
+
1461
+
1462
+ def _worker_semantic_identity_matches(
1463
+ identity: _WorkerSemanticIdentity,
1464
+ lowered: LoweredWorkerModule,
1465
+ ) -> bool:
1466
+ analysis = lowered.analysis
1467
+ raw = analysis.unit.mapped_source
1468
+ mapped = lowered.mapped_source
1469
+ summary = _validated_mapped_semantic_summary(mapped)
1470
+ dependency_bindings_sha256 = _canonical_dependency_bindings_sha256(
1471
+ analysis.dependencies
1472
+ )
1473
+ return (
1474
+ identity.logical_name == analysis.unit.logical_name
1475
+ and identity.raw_source_object_id == id(raw)
1476
+ and identity.raw_text_object_id == id(raw.text)
1477
+ and identity.raw_source_sha256 == raw.artifact.source_sha256
1478
+ and identity.raw_text_sha256 == raw.artifact.source_sha256
1479
+ and identity.lowered_source_object_id == id(mapped)
1480
+ and identity.lowered_text_object_id == id(mapped.text)
1481
+ and identity.lowered_source_sha256 == mapped.artifact.source_sha256
1482
+ and identity.lowered_text_sha256 == mapped.artifact.source_sha256
1483
+ and identity.source_map_object_id == id(mapped.source_map)
1484
+ and identity.lowered_source_map_sha256 == summary.source_map_sha256
1485
+ and identity.lineage_object_id == id(mapped.lineage)
1486
+ and identity.lineage_sha256 == summary.lineage_sha256
1487
+ and identity.dependency_bindings_sha256
1488
+ == dependency_bindings_sha256
1489
+ and lowered.dependency_bindings_sha256 == dependency_bindings_sha256
1490
+ and identity.canonical_exported_methods
1491
+ == _canonical_exported_methods(analysis.exported_methods)
1492
+ and identity.catalog_identity == analysis.catalog_identity
1493
+ and identity.parser_identity == analysis.parser_identity
1494
+ and identity.transform_version == lowered.transform_version
1495
+ )
1496
+
1497
+
1498
+ def _canonical_exported_methods(methods: tuple[str, ...]) -> tuple[str, ...]:
1499
+ return tuple(sorted(item.casefold() for item in methods))
1500
+
1501
+
1502
+ def _safe_packer_identity(value: object) -> bool:
1503
+ return (
1504
+ isinstance(value, str)
1505
+ and bool(value)
1506
+ and all(
1507
+ character not in value
1508
+ for character in ("\x00", "\r", "\n", "/", "\\", "|")
1509
+ )
1510
+ )
1511
+
1512
+
1513
+ def _canonical_dependency_bindings_sha256(
1514
+ bindings: tuple[ModuleDependencyBinding, ...],
1515
+ ) -> str:
1516
+ canonical = tuple(_canonical_dependency_binding_sha256(item) for item in bindings)
1517
+ return sha256(
1518
+ json.dumps(
1519
+ canonical,
1520
+ ensure_ascii=False,
1521
+ separators=(",", ":"),
1522
+ ).encode("utf-8")
1523
+ ).hexdigest()
1524
+
1525
+
1526
+ def _canonical_dependency_binding_sha256(
1527
+ binding: ModuleDependencyBinding,
1528
+ ) -> str:
1529
+ payload = json.dumps(
1530
+ {
1531
+ "target_module": binding.target_module,
1532
+ "export_variable": binding.export_variable,
1533
+ "uses": [
1534
+ {
1535
+ "method_name": use.method_name,
1536
+ "category": use.category,
1537
+ "span": [use.span.start, use.span.end],
1538
+ "method_declaration": [
1539
+ use.method_declaration.start,
1540
+ use.method_declaration.end,
1541
+ ],
1542
+ }
1543
+ for use in binding.uses
1544
+ ],
1545
+ },
1546
+ ensure_ascii=False,
1547
+ sort_keys=True,
1548
+ separators=(",", ":"),
1549
+ )
1550
+ return sha256(payload.encode("utf-8")).hexdigest()
1551
+
1552
+
1553
+ def _plan_alias_transform(analysis: AnalyzedWorkerModule) -> _AliasTransformPlan:
1554
+ source = analysis.unit.mapped_source
1555
+ line_ending = _preferred_line_ending(source.text)
1556
+ insertion_points = {
1557
+ item.method_name.casefold(): item
1558
+ for item in analysis.method_insertion_points
1559
+ }
1560
+ conditional_intervals = _conditional_directive_intervals(source.text)
1561
+ fields: list[tuple[str, SourceSpan, str]] = []
1562
+ declarations: dict[int, list[tuple[str, DependencyUse, str]]] = {}
1563
+ initializers: dict[int, list[tuple[str, DependencyUse, str]]] = {}
1564
+
1565
+ for binding in analysis.dependencies:
1566
+ first_use = min(binding.uses, key=lambda item: item.span.start)
1567
+ fields.append(
1568
+ (
1569
+ f"Перем {binding.export_variable} Экспорт;{line_ending}",
1570
+ first_use.span,
1571
+ DEPENDENCY_FIELD_REGION,
1572
+ )
1573
+ )
1574
+ method_names = {
1575
+ use.method_name.casefold()
1576
+ for use in binding.uses
1577
+ }
1578
+ for method_name in sorted(method_names):
1579
+ point = insertion_points[method_name]
1580
+ method_uses = tuple(
1581
+ use
1582
+ for use in binding.uses
1583
+ if use.method_name.casefold() == method_name
1584
+ )
1585
+ method_first_use = min(method_uses, key=lambda item: item.span.start)
1586
+ declaration_offset = _unconditional_method_offset(
1587
+ point.declarations_end,
1588
+ point.method_declaration,
1589
+ conditional_intervals,
1590
+ )
1591
+ initializer_offset = _unconditional_method_offset(
1592
+ point.first_statement_start,
1593
+ point.method_declaration,
1594
+ conditional_intervals,
1595
+ )
1596
+ declaration_text = f"Перем {binding.target_module};{line_ending}"
1597
+ declarations.setdefault(declaration_offset, []).append(
1598
+ (
1599
+ declaration_text,
1600
+ method_first_use,
1601
+ DEPENDENCY_ALIAS_DECLARATION_REGION,
1602
+ )
1603
+ )
1604
+ initializers.setdefault(initializer_offset, []).append(
1605
+ (
1606
+ f"{binding.target_module} = {binding.export_variable};{line_ending}",
1607
+ method_first_use,
1608
+ DEPENDENCY_ALIAS_INITIALIZER_REGION,
1609
+ )
1610
+ )
1611
+
1612
+ return _AliasTransformPlan(
1613
+ source,
1614
+ line_ending,
1615
+ tuple(fields),
1616
+ {offset: tuple(items) for offset, items in declarations.items()},
1617
+ {offset: tuple(items) for offset, items in initializers.items()},
1618
+ tuple(
1619
+ sorted(
1620
+ {
1621
+ boundary
1622
+ for method in analysis.methods
1623
+ for boundary in (
1624
+ method.method_declaration.start,
1625
+ method.method_declaration.end,
1626
+ )
1627
+ }
1628
+ )
1629
+ ),
1630
+ )
1631
+
1632
+
1633
+ def _compose_worker_module_source(plan: _AliasTransformPlan) -> MappedSource:
1634
+ source = plan.source
1635
+ line_ending = plan.line_ending
1636
+ fields = plan.fields
1637
+ declarations = plan.declarations
1638
+ initializers = plan.initializers
1639
+
1640
+ builder = SourceTransformBuilder(source)
1641
+ offsets = sorted(
1642
+ {
1643
+ 0,
1644
+ len(source.text),
1645
+ *declarations,
1646
+ *initializers,
1647
+ *plan.reuse_boundaries,
1648
+ }
1649
+ )
1650
+ cursor = 0
1651
+ for offset in offsets:
1652
+ if cursor < offset:
1653
+ builder.copy(SourceSpan(cursor, offset))
1654
+ if offset == 0:
1655
+ for text, anchor, region in fields:
1656
+ builder.synthetic(text, anchor, region)
1657
+ declaration_items = declarations.get(offset, ())
1658
+ for index, (text, use, region) in enumerate(declaration_items):
1659
+ if (
1660
+ index == 0
1661
+ and offset > 0
1662
+ and source.text[offset - 1] not in "\r\n"
1663
+ ):
1664
+ text = f"{line_ending}{text}"
1665
+ builder.derived(
1666
+ text,
1667
+ use.span,
1668
+ region,
1669
+ anchor=use.method_declaration,
1670
+ )
1671
+ for text, use, region in initializers.get(offset, ()):
1672
+ builder.derived(
1673
+ text,
1674
+ use.span,
1675
+ region,
1676
+ anchor=use.method_declaration,
1677
+ )
1678
+ cursor = offset
1679
+ if cursor < len(source.text):
1680
+ builder.copy(SourceSpan(cursor, len(source.text)))
1681
+
1682
+ transformed = builder.build(SourceArtifactKind.WORKER_PROJECTION)
1683
+ from onec_runtime.worker_epf import prepare_worker_module_source
1684
+
1685
+ return prepare_worker_module_source(transformed)
1686
+
1687
+
1688
+ def _module_methods(root: Any) -> Iterable[Any]:
1689
+ elements = root.Elements
1690
+ while elements.Item is not None:
1691
+ if type(elements.Item).__name__ == "Method":
1692
+ yield elements.Item
1693
+ elements = elements.Rest
1694
+
1695
+
1696
+ def _preferred_line_ending(source: str) -> str:
1697
+ without_crlf = source.replace("\r\n", "")
1698
+ if "\r\n" in source and "\n" not in without_crlf and "\r" not in without_crlf:
1699
+ return "\r\n"
1700
+ if "\r" in source and "\n" not in source:
1701
+ return "\r"
1702
+ return "\n"
1703
+
1704
+
1705
+ def _conditional_directive_intervals(source: str) -> tuple[SourceSpan, ...]:
1706
+ stack: list[int] = []
1707
+ intervals: list[SourceSpan] = []
1708
+ line_start = 0
1709
+ for line in source.splitlines(keepends=True):
1710
+ match = re.match(
1711
+ r"[ \t]*#[ \t]*([A-Za-zА-Яа-яЁё]+)",
1712
+ line,
1713
+ )
1714
+ if match is not None:
1715
+ keyword = match.group(1).casefold()
1716
+ if keyword in {"если", "if"}:
1717
+ stack.append(line_start)
1718
+ elif keyword in {"конецесли", "endif"} and stack:
1719
+ intervals.append(SourceSpan(stack.pop(), line_start + len(line)))
1720
+ line_start += len(line)
1721
+ intervals.extend(SourceSpan(start, len(source)) for start in stack)
1722
+ return tuple(sorted(intervals))
1723
+
1724
+
1725
+ def _unconditional_method_offset(
1726
+ offset: int,
1727
+ method: SourceSpan,
1728
+ conditional_intervals: tuple[SourceSpan, ...],
1729
+ ) -> int:
1730
+ containing = tuple(
1731
+ interval
1732
+ for interval in conditional_intervals
1733
+ if method.start < interval.start <= offset < interval.end <= method.end
1734
+ )
1735
+ return min((interval.start for interval in containing), default=offset)
1736
+
1737
+
1738
+ def _walk_ast(root: Any) -> Iterable[Any]:
1739
+ stack = [root]
1740
+ while stack:
1741
+ node = stack.pop()
1742
+ if not is_dataclass(node):
1743
+ continue
1744
+ yield node
1745
+ children: list[Any] = []
1746
+ for field in fields(node):
1747
+ if field.name == "span":
1748
+ continue
1749
+ value = getattr(node, field.name)
1750
+ if is_dataclass(value):
1751
+ children.append(value)
1752
+ elif isinstance(value, tuple):
1753
+ children.extend(item for item in value if is_dataclass(item))
1754
+ stack.extend(reversed(children))
1755
+
1756
+
1757
+ def _source_span(span: Any) -> SourceSpan:
1758
+ return SourceSpan(span.start, span.end)
1759
+
1760
+
1761
+ def _offset_source_span(span: SourceSpan, offset: int) -> SourceSpan:
1762
+ return SourceSpan(span.start + offset, span.end + offset)
1763
+
1764
+
1765
+ def _remap_isolated_error_message(
1766
+ message: str,
1767
+ local_start: int,
1768
+ absolute_start: int,
1769
+ ) -> str:
1770
+ return re.sub(
1771
+ rf" at {local_start}(?=;|$)",
1772
+ f" at {absolute_start}",
1773
+ message,
1774
+ count=1,
1775
+ )
1776
+
1777
+
1778
+ def _declaration_end(declaration: Any, tokens: tuple[Any, ...]) -> int:
1779
+ delimiter = next(
1780
+ (
1781
+ token
1782
+ for token in tokens
1783
+ if token.start == declaration.span.end and token.type == ";"
1784
+ ),
1785
+ None,
1786
+ )
1787
+ return delimiter.end if delimiter is not None else declaration.span.end
1788
+
1789
+
1790
+ def _executable_body(code: Any, for_token_starts: tuple[int, ...]) -> SourceSpan:
1791
+ if code.First is None:
1792
+ return SourceSpan(code.span.end, code.span.end)
1793
+ start = code.First.span.start
1794
+ if type(code.First).__name__ in {"ForEachStatement", "ForRangeStatement"}:
1795
+ # The generated loop-node spans omit their leading `Для` token.
1796
+ start = for_token_starts[bisect_left(for_token_starts, code.span.start)]
1797
+ return SourceSpan(start, code.span.end)
1798
+
1799
+
1800
+ def _dependency_category(node: Any) -> Literal["call", "access", "value"]:
1801
+ if node.Arguments is not None:
1802
+ raise _analysis_error(
1803
+ "common module object cannot be called directly",
1804
+ _source_span(node.span),
1805
+ AMBIGUOUS_BINDING,
1806
+ )
1807
+ if not node.Postfix:
1808
+ return "value"
1809
+ first = node.Postfix[0]
1810
+ if type(first).__name__ != "MemberAccess":
1811
+ raise _analysis_error(
1812
+ "common module dependency access is ambiguous",
1813
+ _source_span(node.span),
1814
+ AMBIGUOUS_BINDING,
1815
+ )
1816
+ return "call" if first.Arguments is not None else "access"
1817
+
1818
+
1819
+ def _analysis_error(
1820
+ message: str,
1821
+ span: SourceSpan,
1822
+ code: str,
1823
+ ) -> ModuleUniverseAdmissionError:
1824
+ error = ModuleUniverseAdmissionError(message)
1825
+ error.span = span
1826
+ error.code = code
1827
+ return error
1828
+
1829
+
1830
+ def _dependency_binding(
1831
+ descriptor: CommonModuleDescriptor,
1832
+ uses: tuple[DependencyUse, ...],
1833
+ ) -> ModuleDependencyBinding:
1834
+ export_variable = dependency_export_name(descriptor.canonical_name)
1835
+ payload = json.dumps(
1836
+ {
1837
+ "target_module": descriptor.canonical_name,
1838
+ "export_variable": export_variable,
1839
+ "uses": [
1840
+ {
1841
+ "method_name": use.method_name,
1842
+ "category": use.category,
1843
+ "span": [use.span.start, use.span.end],
1844
+ "method_declaration": [
1845
+ use.method_declaration.start,
1846
+ use.method_declaration.end,
1847
+ ],
1848
+ }
1849
+ for use in uses
1850
+ ],
1851
+ },
1852
+ ensure_ascii=False,
1853
+ sort_keys=True,
1854
+ separators=(",", ":"),
1855
+ )
1856
+ return ModuleDependencyBinding(
1857
+ descriptor.canonical_name,
1858
+ export_variable,
1859
+ uses,
1860
+ sha256(payload.encode("utf-8")).hexdigest(),
1861
+ )
1862
+
1863
+
1864
+ def _is_bsl_identifier(value: object) -> bool:
1865
+ return isinstance(value, str) and _BSL_IDENTIFIER_RE.fullmatch(value) is not None
1866
+
1867
+
1868
+ def _single_visible_unit(mapped_source: MappedSource) -> SourceUnitRef | None:
1869
+ units = {
1870
+ reference
1871
+ for segment in mapped_source.source_map.segments
1872
+ for reference in (segment.origin_ref, segment.anchor_ref)
1873
+ if isinstance(reference, SourceUnitRef)
1874
+ }
1875
+ if len(units) != 1:
1876
+ return None
1877
+ return next(iter(units))