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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- 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
|