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,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from bisect import bisect_left
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from onec_runtime.bsl.lexer import tokenize
|
|
8
|
+
from onec_runtime.bsl.parser_target import PythonParserTarget
|
|
9
|
+
from onec_runtime.bsl.semantic_lowering import WorkerExport
|
|
10
|
+
from onec_runtime.bsl.source_maps import (
|
|
11
|
+
MappedSource,
|
|
12
|
+
SourceArtifactKind,
|
|
13
|
+
SourceSpan,
|
|
14
|
+
SourceTransformBuilder,
|
|
15
|
+
SourceUnitKind,
|
|
16
|
+
SourceUnitRef,
|
|
17
|
+
mapped_visible_source,
|
|
18
|
+
source_sha256,
|
|
19
|
+
)
|
|
20
|
+
from onec_runtime.errors import ProtocolError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class NotebookMethodProjection:
|
|
25
|
+
"""One complete AST method, including decorations and generated export."""
|
|
26
|
+
|
|
27
|
+
export: WorkerExport
|
|
28
|
+
mapped_source: MappedSource = field(repr=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class NotebookCellProjection:
|
|
33
|
+
"""Generated-AST projections of one immutable visible notebook cell."""
|
|
34
|
+
|
|
35
|
+
visible: MappedSource = field(repr=False)
|
|
36
|
+
worker: MappedSource | None = field(repr=False)
|
|
37
|
+
statements: MappedSource | None = field(repr=False)
|
|
38
|
+
exports: tuple[WorkerExport, ...]
|
|
39
|
+
methods: tuple[NotebookMethodProjection, ...] = field(default=(), repr=False)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def worker_source(self) -> str:
|
|
43
|
+
return "" if self.worker is None else self.worker.text
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def statement_source(self) -> str:
|
|
47
|
+
return "" if self.statements is None else self.statements.text
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def has_methods(self) -> bool:
|
|
51
|
+
return self.worker is not None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def has_statements(self) -> bool:
|
|
55
|
+
return self.statements is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Preserve imports of the former plain-string result type while making mapped
|
|
59
|
+
# projections authoritative.
|
|
60
|
+
NotebookCell = NotebookCellProjection
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class _ProjectionItem:
|
|
65
|
+
ordinal: int
|
|
66
|
+
node: Any
|
|
67
|
+
end: int
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def split_notebook_cell(
|
|
71
|
+
parser_target: PythonParserTarget,
|
|
72
|
+
source: str,
|
|
73
|
+
*,
|
|
74
|
+
source_unit: SourceUnitRef | None = None,
|
|
75
|
+
) -> NotebookCellProjection:
|
|
76
|
+
"""Split a generated cell AST into independent mapped execution branches."""
|
|
77
|
+
unit = source_unit or SourceUnitRef(
|
|
78
|
+
SourceUnitKind.NOTEBOOK_CELL,
|
|
79
|
+
"anonymous-notebook-cell",
|
|
80
|
+
0,
|
|
81
|
+
source_sha256(source),
|
|
82
|
+
)
|
|
83
|
+
visible = mapped_visible_source(source, unit)
|
|
84
|
+
root = parser_target.parse_ast(source, "ЯчейкаНоутбука")
|
|
85
|
+
tokens = tokenize(source)
|
|
86
|
+
token_starts = tuple(token.start for token in tokens)
|
|
87
|
+
elements = root.Elements
|
|
88
|
+
ordered: list[Any] = []
|
|
89
|
+
while elements is not None and elements.Item is not None:
|
|
90
|
+
ordered.append(elements.Item)
|
|
91
|
+
elements = elements.Rest
|
|
92
|
+
|
|
93
|
+
method_items: list[_ProjectionItem] = []
|
|
94
|
+
statement_items: list[_ProjectionItem] = []
|
|
95
|
+
exports: dict[str, WorkerExport] = {}
|
|
96
|
+
for ordinal, item in enumerate(ordered):
|
|
97
|
+
following_start = (
|
|
98
|
+
ordered[ordinal + 1].span.start
|
|
99
|
+
if ordinal + 1 < len(ordered)
|
|
100
|
+
else len(source)
|
|
101
|
+
)
|
|
102
|
+
projected = _ProjectionItem(
|
|
103
|
+
ordinal,
|
|
104
|
+
item,
|
|
105
|
+
_projection_end(item.span.end, following_start, tokens, token_starts),
|
|
106
|
+
)
|
|
107
|
+
if type(item).__name__ == "Method":
|
|
108
|
+
declaration = item.Declaration
|
|
109
|
+
export = WorkerExport(declaration.Name, declaration.Name)
|
|
110
|
+
key = export.public_path.casefold()
|
|
111
|
+
if key in exports:
|
|
112
|
+
raise ProtocolError(
|
|
113
|
+
f"Notebook cell declares duplicate method {declaration.Name!r}"
|
|
114
|
+
)
|
|
115
|
+
exports[key] = export
|
|
116
|
+
method_items.append(projected)
|
|
117
|
+
else:
|
|
118
|
+
statement_items.append(projected)
|
|
119
|
+
|
|
120
|
+
return NotebookCellProjection(
|
|
121
|
+
visible,
|
|
122
|
+
_build_projection(
|
|
123
|
+
visible,
|
|
124
|
+
method_items,
|
|
125
|
+
tokens,
|
|
126
|
+
SourceArtifactKind.WORKER_PROJECTION,
|
|
127
|
+
),
|
|
128
|
+
_build_projection(
|
|
129
|
+
visible,
|
|
130
|
+
statement_items,
|
|
131
|
+
tokens,
|
|
132
|
+
SourceArtifactKind.STATEMENT_PROJECTION,
|
|
133
|
+
),
|
|
134
|
+
tuple(exports.values()),
|
|
135
|
+
tuple(
|
|
136
|
+
NotebookMethodProjection(
|
|
137
|
+
export,
|
|
138
|
+
_build_method_projection(visible, item, tokens),
|
|
139
|
+
)
|
|
140
|
+
for export, item in zip(exports.values(), method_items, strict=True)
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _build_method_projection(
|
|
146
|
+
visible: MappedSource,
|
|
147
|
+
item: _ProjectionItem,
|
|
148
|
+
tokens: tuple[Any, ...],
|
|
149
|
+
) -> MappedSource:
|
|
150
|
+
builder = SourceTransformBuilder(visible)
|
|
151
|
+
# The optional semicolon belongs to the notebook element separator, not
|
|
152
|
+
# the method AST. In a composed Worker it starts the module body and makes
|
|
153
|
+
# every subsequently appended declaration illegal on the platform.
|
|
154
|
+
method = _ProjectionItem(item.ordinal, item.node, item.node.span.end)
|
|
155
|
+
_copy_method(builder, method, tokens)
|
|
156
|
+
return builder.build(SourceArtifactKind.WORKER_PROJECTION)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _projection_end(
|
|
160
|
+
item_end: int,
|
|
161
|
+
following_start: int,
|
|
162
|
+
tokens: tuple[Any, ...],
|
|
163
|
+
token_starts: tuple[int, ...],
|
|
164
|
+
) -> int:
|
|
165
|
+
"""Include one grammar separator using its generated token span."""
|
|
166
|
+
token_index = bisect_left(token_starts, item_end)
|
|
167
|
+
following = (
|
|
168
|
+
tokens[token_index]
|
|
169
|
+
if token_index < len(tokens) and tokens[token_index].start < following_start
|
|
170
|
+
else None
|
|
171
|
+
)
|
|
172
|
+
return following.end if following is not None and following.type == ";" else item_end
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_projection(
|
|
176
|
+
visible: MappedSource,
|
|
177
|
+
items: list[_ProjectionItem],
|
|
178
|
+
tokens: tuple[Any, ...],
|
|
179
|
+
kind: SourceArtifactKind,
|
|
180
|
+
) -> MappedSource | None:
|
|
181
|
+
if not items:
|
|
182
|
+
return None
|
|
183
|
+
builder = SourceTransformBuilder(visible)
|
|
184
|
+
previous: _ProjectionItem | None = None
|
|
185
|
+
for item in items:
|
|
186
|
+
if previous is not None:
|
|
187
|
+
if item.ordinal == previous.ordinal + 1:
|
|
188
|
+
separator = SourceSpan(previous.end, item.node.span.start)
|
|
189
|
+
if separator.start < separator.end:
|
|
190
|
+
builder.copy(separator)
|
|
191
|
+
else:
|
|
192
|
+
builder.synthetic(
|
|
193
|
+
"\n",
|
|
194
|
+
SourceSpan(item.node.span.start, item.node.span.start),
|
|
195
|
+
"notebook_projection_join",
|
|
196
|
+
)
|
|
197
|
+
if kind is SourceArtifactKind.WORKER_PROJECTION:
|
|
198
|
+
_copy_method(builder, item, tokens)
|
|
199
|
+
else:
|
|
200
|
+
builder.copy(SourceSpan(item.node.span.start, item.end))
|
|
201
|
+
previous = item
|
|
202
|
+
return builder.build(kind)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _copy_method(
|
|
206
|
+
builder: SourceTransformBuilder,
|
|
207
|
+
item: _ProjectionItem,
|
|
208
|
+
tokens: tuple[Any, ...],
|
|
209
|
+
) -> None:
|
|
210
|
+
method = item.node
|
|
211
|
+
declaration = method.Declaration
|
|
212
|
+
if declaration.Export is not None:
|
|
213
|
+
builder.copy(SourceSpan(method.span.start, item.end))
|
|
214
|
+
return
|
|
215
|
+
body_start = declaration.Body.span.start
|
|
216
|
+
closing = next(
|
|
217
|
+
(
|
|
218
|
+
token
|
|
219
|
+
for token in reversed(tokens)
|
|
220
|
+
if method.span.start <= token.start < body_start and token.type == ")"
|
|
221
|
+
),
|
|
222
|
+
None,
|
|
223
|
+
)
|
|
224
|
+
if closing is None:
|
|
225
|
+
raise ProtocolError("Notebook method declaration has no parameter-list terminator")
|
|
226
|
+
builder.copy(SourceSpan(method.span.start, closing.end))
|
|
227
|
+
builder.synthetic(
|
|
228
|
+
" Экспорт",
|
|
229
|
+
SourceSpan(method.span.start, closing.end),
|
|
230
|
+
"notebook_method_export",
|
|
231
|
+
)
|
|
232
|
+
builder.copy(SourceSpan(closing.end, item.end))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Bind free notebook-method reads to the live request-scoped BSL context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from onec_runtime.bsl.lexer import tokenize
|
|
6
|
+
from onec_runtime.bsl.parser_target import PythonParserTarget
|
|
7
|
+
from onec_runtime.bsl.semantic_lowering import SemanticNotebookLowerer, WorkerExport
|
|
8
|
+
from onec_runtime.bsl.source_maps import (
|
|
9
|
+
MappedSource,
|
|
10
|
+
SourceArtifactKind,
|
|
11
|
+
SourceSpan,
|
|
12
|
+
SourceTransformBuilder,
|
|
13
|
+
)
|
|
14
|
+
from onec_runtime.errors import ProtocolError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
WORKER_GLOBALS = "__OnecNotebookGlobals"
|
|
18
|
+
_CONTEXT_PREFIX = f"{WORKER_GLOBALS}."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def bind_notebook_method_globals(
|
|
22
|
+
source: MappedSource,
|
|
23
|
+
*,
|
|
24
|
+
context_names: tuple[str, ...],
|
|
25
|
+
exports: tuple[WorkerExport, ...],
|
|
26
|
+
) -> tuple[MappedSource, tuple[str, ...]]:
|
|
27
|
+
"""Qualify persistent roots without changing method-local BSL semantics."""
|
|
28
|
+
if not context_names:
|
|
29
|
+
return source, ()
|
|
30
|
+
if any(
|
|
31
|
+
token.type == "ID" and token.text.casefold() == WORKER_GLOBALS.casefold()
|
|
32
|
+
for token in tokenize(source.text)
|
|
33
|
+
):
|
|
34
|
+
raise ProtocolError("Notebook method uses a reserved Worker globals name")
|
|
35
|
+
binding = SemanticNotebookLowerer(
|
|
36
|
+
PythonParserTarget.from_generated(),
|
|
37
|
+
context_names=context_names,
|
|
38
|
+
worker_exports=exports,
|
|
39
|
+
platform_globals=("RuntimeContextStoreServer",),
|
|
40
|
+
).bind_module(source.text)
|
|
41
|
+
references = sorted(
|
|
42
|
+
(
|
|
43
|
+
reference
|
|
44
|
+
for method in binding.method_scopes
|
|
45
|
+
for reference in method.references
|
|
46
|
+
if reference.kind == "persistent"
|
|
47
|
+
),
|
|
48
|
+
key=lambda reference: reference.span.start,
|
|
49
|
+
)
|
|
50
|
+
if not references:
|
|
51
|
+
return source, ()
|
|
52
|
+
|
|
53
|
+
builder = SourceTransformBuilder(source)
|
|
54
|
+
builder.synthetic(
|
|
55
|
+
f"Перем {WORKER_GLOBALS} Экспорт;\n",
|
|
56
|
+
SourceSpan(0, 0),
|
|
57
|
+
"notebook_method_globals_declaration",
|
|
58
|
+
)
|
|
59
|
+
cursor = 0
|
|
60
|
+
names: dict[str, str] = {}
|
|
61
|
+
for reference in references:
|
|
62
|
+
root = reference.span
|
|
63
|
+
names.setdefault(reference.name.casefold(), reference.name)
|
|
64
|
+
if root.start < cursor:
|
|
65
|
+
raise ValueError("overlapping notebook method bindings")
|
|
66
|
+
builder.copy(SourceSpan(cursor, root.start))
|
|
67
|
+
builder.synthetic(
|
|
68
|
+
_CONTEXT_PREFIX,
|
|
69
|
+
SourceSpan(root.start, root.start),
|
|
70
|
+
"notebook_method_persistent_read",
|
|
71
|
+
)
|
|
72
|
+
cursor = root.start
|
|
73
|
+
builder.copy(SourceSpan(cursor, len(source.text)))
|
|
74
|
+
return builder.build(SourceArtifactKind.WORKER_PROJECTION), tuple(names.values())
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Immutable notebook method upserts with original visible source ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from onec_runtime.bsl.diagnostics import VisibleSourceContext
|
|
8
|
+
from onec_runtime.bsl.lexer import Token, tokenize
|
|
9
|
+
from onec_runtime.bsl.notebook_cells import NotebookCellProjection, NotebookMethodProjection
|
|
10
|
+
from onec_runtime.bsl.semantic_lowering import WorkerExport
|
|
11
|
+
from onec_runtime.bsl.source_maps import (
|
|
12
|
+
MappedSource,
|
|
13
|
+
SourceArtifactKind,
|
|
14
|
+
SourceSpan,
|
|
15
|
+
SourceTransformBuilder,
|
|
16
|
+
SourceUnitRef,
|
|
17
|
+
compose_mapped_sources,
|
|
18
|
+
)
|
|
19
|
+
from onec_runtime.errors import ProtocolError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class _RetainedMethod:
|
|
24
|
+
projection: NotebookMethodProjection = field(repr=False)
|
|
25
|
+
visible: MappedSource = field(repr=False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class NotebookMethodSet:
|
|
30
|
+
"""A candidate value; publication and commitment belong to the runtime."""
|
|
31
|
+
|
|
32
|
+
mapped_source: MappedSource = field(repr=False)
|
|
33
|
+
exports: tuple[WorkerExport, ...]
|
|
34
|
+
intercepts_messages: bool
|
|
35
|
+
_methods: tuple[_RetainedMethod, ...] = field(repr=False)
|
|
36
|
+
_visible_sources: tuple[MappedSource, ...] = field(repr=False)
|
|
37
|
+
bound_globals: tuple[str, ...] = ()
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def visible_source_context(self) -> VisibleSourceContext:
|
|
41
|
+
# VisibleSourceContext is mutable. Hand out a detached index rather than
|
|
42
|
+
# allowing consumers to alter the retained candidate's diagnostic state.
|
|
43
|
+
return VisibleSourceContext(
|
|
44
|
+
{_visible_unit(source): source.text for source in self._visible_sources}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _visible_unit(source: MappedSource) -> SourceUnitRef:
|
|
49
|
+
unit = source.source_map.map_offset(0).unit
|
|
50
|
+
if source.artifact.kind is not SourceArtifactKind.VISIBLE or unit is None:
|
|
51
|
+
raise ProtocolError("Notebook method origin must be a visible source unit")
|
|
52
|
+
return unit
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _unique_visible_sources(sources: tuple[MappedSource, ...]) -> tuple[MappedSource, ...]:
|
|
56
|
+
retained: dict[tuple[object, str, int], MappedSource] = {}
|
|
57
|
+
for source in sources:
|
|
58
|
+
unit = _visible_unit(source)
|
|
59
|
+
key = (unit.kind, unit.unit_id, unit.revision)
|
|
60
|
+
existing = retained.get(key)
|
|
61
|
+
if existing is not None and _visible_unit(existing).source_sha256 != unit.source_sha256:
|
|
62
|
+
raise ProtocolError("Notebook methods have conflicting source identities")
|
|
63
|
+
retained[key] = source
|
|
64
|
+
return tuple(retained.values())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_MESSAGE_SINK = "__OnecWorkerMessageSink"
|
|
68
|
+
_MESSAGE_HELPER = "__OnecWorkerMessage"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _message_call_tokens(source: str) -> tuple[Token, ...]:
|
|
72
|
+
tokens = tokenize(source)
|
|
73
|
+
return tuple(
|
|
74
|
+
token
|
|
75
|
+
for index, token in enumerate(tokens)
|
|
76
|
+
if token.type == "ID"
|
|
77
|
+
and token.text.casefold() in {"сообщить", "message"}
|
|
78
|
+
and index + 1 < len(tokens)
|
|
79
|
+
and tokens[index + 1].type == "("
|
|
80
|
+
and (
|
|
81
|
+
index == 0
|
|
82
|
+
or tokens[index - 1].type not in {".", "ПРОЦЕДУРА", "ФУНКЦИЯ"}
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def instrument_notebook_worker_messages(source: MappedSource) -> MappedSource:
|
|
88
|
+
"""Route notebook method messages to the active cell without losing origins."""
|
|
89
|
+
calls = _message_call_tokens(source.text)
|
|
90
|
+
if not calls:
|
|
91
|
+
return source
|
|
92
|
+
reserved = {_MESSAGE_SINK.casefold(), _MESSAGE_HELPER.casefold()}
|
|
93
|
+
if any(
|
|
94
|
+
token.type == "ID" and token.text.casefold() in reserved
|
|
95
|
+
for token in tokenize(source.text)
|
|
96
|
+
):
|
|
97
|
+
raise ProtocolError("Notebook method uses a reserved Worker message name")
|
|
98
|
+
builder = SourceTransformBuilder(source)
|
|
99
|
+
builder.synthetic(
|
|
100
|
+
f"Перем {_MESSAGE_SINK} Экспорт;\n",
|
|
101
|
+
SourceSpan(0, 0),
|
|
102
|
+
"worker_message_sink_declaration",
|
|
103
|
+
)
|
|
104
|
+
position = 0
|
|
105
|
+
for call in calls:
|
|
106
|
+
builder.copy(SourceSpan(position, call.start))
|
|
107
|
+
builder.derived(
|
|
108
|
+
_MESSAGE_HELPER,
|
|
109
|
+
SourceSpan(call.start, call.end),
|
|
110
|
+
"worker_message_call",
|
|
111
|
+
)
|
|
112
|
+
position = call.end
|
|
113
|
+
builder.copy(SourceSpan(position, len(source.text)))
|
|
114
|
+
builder.synthetic(
|
|
115
|
+
"\n\n"
|
|
116
|
+
f"Процедура {_MESSAGE_HELPER}(Текст = Неопределено, Статус = Неопределено)\n"
|
|
117
|
+
f" Если {_MESSAGE_SINK} = Неопределено Тогда\n"
|
|
118
|
+
" Если Статус = Неопределено Тогда\n"
|
|
119
|
+
" Сообщить(Текст);\n"
|
|
120
|
+
" Иначе\n"
|
|
121
|
+
" Сообщить(Текст, Статус);\n"
|
|
122
|
+
" КонецЕсли;\n"
|
|
123
|
+
" Иначе\n"
|
|
124
|
+
f" RuntimeKernelServer.ДобавитьСообщение({_MESSAGE_SINK}, Текст, Статус);\n"
|
|
125
|
+
" КонецЕсли;\n"
|
|
126
|
+
"КонецПроцедуры",
|
|
127
|
+
SourceSpan(len(source.text), len(source.text)),
|
|
128
|
+
"worker_message_helper",
|
|
129
|
+
)
|
|
130
|
+
return builder.build(SourceArtifactKind.WORKER_PROJECTION)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def merge_notebook_methods(
|
|
134
|
+
previous: NotebookMethodSet | None,
|
|
135
|
+
cell: NotebookCellProjection,
|
|
136
|
+
) -> NotebookMethodSet:
|
|
137
|
+
"""Replace declared names in place, append new names, retain omitted names."""
|
|
138
|
+
if previous is not None and not isinstance(previous, NotebookMethodSet):
|
|
139
|
+
raise ProtocolError("Previous notebook methods must be a NotebookMethodSet")
|
|
140
|
+
if not isinstance(cell, NotebookCellProjection):
|
|
141
|
+
raise ProtocolError("Notebook methods require a parsed cell projection")
|
|
142
|
+
if tuple(method.export for method in cell.methods) != cell.exports:
|
|
143
|
+
raise ProtocolError("Notebook method projections do not match their exports")
|
|
144
|
+
# Check before replacement: even replacing the last old method cannot reuse
|
|
145
|
+
# its explicit identity with a different hash.
|
|
146
|
+
_unique_visible_sources(
|
|
147
|
+
(() if previous is None else previous._visible_sources) + (cell.visible,)
|
|
148
|
+
)
|
|
149
|
+
methods = {
|
|
150
|
+
method.projection.export.method.casefold(): method
|
|
151
|
+
for method in (() if previous is None else previous._methods)
|
|
152
|
+
}
|
|
153
|
+
declared: set[str] = set()
|
|
154
|
+
for projection in cell.methods:
|
|
155
|
+
key = projection.export.method.casefold()
|
|
156
|
+
if key in declared:
|
|
157
|
+
raise ProtocolError("Notebook cell declares duplicate methods")
|
|
158
|
+
declared.add(key)
|
|
159
|
+
methods[key] = _RetainedMethod(projection, cell.visible)
|
|
160
|
+
retained = tuple(methods.values())
|
|
161
|
+
visible_sources = _unique_visible_sources(
|
|
162
|
+
tuple(method.visible for method in retained) + (cell.visible,)
|
|
163
|
+
)
|
|
164
|
+
mapped = compose_mapped_sources(
|
|
165
|
+
tuple(method.projection.mapped_source for method in retained),
|
|
166
|
+
visible_sources=visible_sources,
|
|
167
|
+
kind=SourceArtifactKind.WORKER_PROJECTION,
|
|
168
|
+
)
|
|
169
|
+
return NotebookMethodSet(
|
|
170
|
+
mapped,
|
|
171
|
+
tuple(method.projection.export for method in retained),
|
|
172
|
+
bool(_message_call_tokens(mapped.text)),
|
|
173
|
+
retained,
|
|
174
|
+
visible_sources,
|
|
175
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Canonical provenance for consumer-owned generated parser artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TypeAlias
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ManifestOptionValue: TypeAlias = bool | int | str
|
|
13
|
+
ManifestOptions: TypeAlias = tuple[tuple[str, ManifestOptionValue], ...]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parsergen_package_sha256(parsergen_source: Path) -> str:
|
|
17
|
+
"""Hash the normalized Python inputs for one parsergen package checkout."""
|
|
18
|
+
package = parsergen_source / "parsergen"
|
|
19
|
+
digest = sha256()
|
|
20
|
+
for source in sorted(package.rglob("*.py"), key=lambda path: path.as_posix()):
|
|
21
|
+
relative = source.relative_to(parsergen_source).as_posix().encode("utf-8")
|
|
22
|
+
digest.update(relative)
|
|
23
|
+
digest.update(b"\0")
|
|
24
|
+
normalized = source.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
25
|
+
digest.update(normalized)
|
|
26
|
+
digest.update(b"\0")
|
|
27
|
+
return digest.hexdigest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _canonical_json(value: object) -> str:
|
|
31
|
+
return json.dumps(
|
|
32
|
+
value,
|
|
33
|
+
ensure_ascii=False,
|
|
34
|
+
sort_keys=True,
|
|
35
|
+
separators=(",", ":"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class ParserArtifactManifest:
|
|
41
|
+
"""Every consumer input that distinguishes one parser artifact from another."""
|
|
42
|
+
|
|
43
|
+
schema_version: int
|
|
44
|
+
backend_id: str
|
|
45
|
+
artifact_role: str
|
|
46
|
+
grammar_source_sha256: str
|
|
47
|
+
parsergen_package_sha256: str | None
|
|
48
|
+
entrypoints: tuple[tuple[str, str], ...]
|
|
49
|
+
lookahead: int
|
|
50
|
+
production_names: tuple[str, ...]
|
|
51
|
+
optimizer_options: ManifestOptions
|
|
52
|
+
codegen_options: ManifestOptions
|
|
53
|
+
identity_sha256: str
|
|
54
|
+
|
|
55
|
+
def to_json(self) -> str:
|
|
56
|
+
return _canonical_json(
|
|
57
|
+
{
|
|
58
|
+
"schema_version": self.schema_version,
|
|
59
|
+
"backend_id": self.backend_id,
|
|
60
|
+
"artifact_role": self.artifact_role,
|
|
61
|
+
"grammar_source_sha256": self.grammar_source_sha256,
|
|
62
|
+
"parsergen_package_sha256": self.parsergen_package_sha256,
|
|
63
|
+
"entrypoints": self.entrypoints,
|
|
64
|
+
"lookahead": self.lookahead,
|
|
65
|
+
"production_names": self.production_names,
|
|
66
|
+
"optimizer_options": self.optimizer_options,
|
|
67
|
+
"codegen_options": self.codegen_options,
|
|
68
|
+
"identity_sha256": self.identity_sha256,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_parser_artifact_manifest(
|
|
74
|
+
*,
|
|
75
|
+
backend_id: str,
|
|
76
|
+
artifact_role: str,
|
|
77
|
+
grammar_bytes: bytes,
|
|
78
|
+
parsergen_package_sha256: str | None,
|
|
79
|
+
entrypoints: tuple[tuple[str, str], ...],
|
|
80
|
+
lookahead: int,
|
|
81
|
+
production_names: tuple[str, ...],
|
|
82
|
+
optimizer_options: ManifestOptions,
|
|
83
|
+
codegen_options: ManifestOptions,
|
|
84
|
+
) -> ParserArtifactManifest:
|
|
85
|
+
"""Build an identity from raw inputs before any parsing normalization."""
|
|
86
|
+
payload = {
|
|
87
|
+
"schema_version": 2,
|
|
88
|
+
"backend_id": backend_id,
|
|
89
|
+
"artifact_role": artifact_role,
|
|
90
|
+
"grammar_source_sha256": sha256(grammar_bytes).hexdigest(),
|
|
91
|
+
"parsergen_package_sha256": parsergen_package_sha256,
|
|
92
|
+
"entrypoints": entrypoints,
|
|
93
|
+
"lookahead": lookahead,
|
|
94
|
+
"production_names": production_names,
|
|
95
|
+
"optimizer_options": optimizer_options,
|
|
96
|
+
"codegen_options": codegen_options,
|
|
97
|
+
}
|
|
98
|
+
return ParserArtifactManifest(
|
|
99
|
+
**payload,
|
|
100
|
+
identity_sha256=sha256(_canonical_json(payload).encode("utf-8")).hexdigest(),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def verify_parser_artifact_manifest(raw: str) -> ParserArtifactManifest:
|
|
105
|
+
"""Decode a manifest only when its recorded consumer identity is authentic."""
|
|
106
|
+
decoded = json.loads(raw)
|
|
107
|
+
if decoded.get("schema_version") != 2:
|
|
108
|
+
raise ValueError("unsupported parser artifact manifest schema")
|
|
109
|
+
recorded_identity = decoded.pop("identity_sha256")
|
|
110
|
+
expected_identity = sha256(_canonical_json(decoded).encode("utf-8")).hexdigest()
|
|
111
|
+
if recorded_identity != expected_identity:
|
|
112
|
+
raise ValueError("generated parser artifact identity is invalid")
|
|
113
|
+
decoded["entrypoints"] = tuple(tuple(item) for item in decoded["entrypoints"])
|
|
114
|
+
decoded["production_names"] = tuple(decoded["production_names"])
|
|
115
|
+
decoded["optimizer_options"] = tuple(
|
|
116
|
+
tuple(item) for item in decoded["optimizer_options"]
|
|
117
|
+
)
|
|
118
|
+
decoded["codegen_options"] = tuple(tuple(item) for item in decoded["codegen_options"])
|
|
119
|
+
return ParserArtifactManifest(**decoded, identity_sha256=recorded_identity)
|