@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.2
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.
- package/README.md +53 -1
- package/bin/provider-proof-composition.js +34 -0
- package/bin/pulsewasm-extract.js +20 -0
- package/package.json +60 -5
- package/src/artifacts-dir.js +13 -0
- package/src/ast-json.js +52 -0
- package/src/build-manifest.js +354 -0
- package/src/canonical-api-compiler.js +269 -0
- package/src/canonical-native-compiler.js +411 -0
- package/src/canonical-native-plan.js +1478 -0
- package/src/canonical-project-compiler.js +1224 -0
- package/src/canonical-router-compiler.js +25 -0
- package/src/cli-intents.js +1235 -0
- package/src/cli.js +1927 -0
- package/src/codegen/assemblyscript-compile.js +3 -0
- package/src/codegen/assemblyscript-core.js +3 -0
- package/src/codegen/assemblyscript-shape.js +3 -0
- package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
- package/src/codegen/backend-capabilities.js +3 -0
- package/src/codegen/channel-broadcaster.js +3 -0
- package/src/codegen/compiled-handlers.js +3 -0
- package/src/codegen/compiled-wasm-runtime.js +3 -0
- package/src/codegen/config-references.js +248 -0
- package/src/codegen/dispatch-ts.js +145 -0
- package/src/codegen/effect-composition.js +331 -0
- package/src/codegen/effect-runtime.js +418 -0
- package/src/codegen/execution-harness-ts.js +467 -0
- package/src/codegen/handler-bindings-ts.js +298 -0
- package/src/codegen/handler-library-contracts.js +3 -0
- package/src/codegen/host-capabilities.js +3 -0
- package/src/codegen/host-runtime-kernel.js +3 -0
- package/src/codegen/integrated-compiled-app.js +3 -0
- package/src/codegen/json-body.js +3 -0
- package/src/codegen/library-sidecars.js +3 -0
- package/src/codegen/local-harness-ts.js +453 -0
- package/src/codegen/pulse-wrapper.js +217 -0
- package/src/codegen/request-result-headers.js +3 -0
- package/src/codegen/schema-json-compile.js +3 -0
- package/src/codegen/schema-json-sidecar-v2.js +3 -0
- package/src/codegen/schema-json-sidecar.js +3 -0
- package/src/codegen/streaming-passthrough.js +3 -0
- package/src/codegen/wasm-host-abi.js +3 -0
- package/src/codegen/wasm-host-bridge.js +3 -0
- package/src/compiled-wasm-host-runtime-kv.js +3 -0
- package/src/config-resolver.js +813 -0
- package/src/crypto-requirement-planner.js +89 -0
- package/src/definitions/config-schema.js +14 -0
- package/src/definitions/handler-roles.js +14 -0
- package/src/definitions/path-grammar.js +14 -0
- package/src/definitions/router-api.js +14 -0
- package/src/diagnostics/codes.js +14 -0
- package/src/diagnostics/reporter.js +14 -0
- package/src/diagnostics.js +14 -0
- package/src/dispatch-table.js +400 -0
- package/src/events/event-emit.js +265 -0
- package/src/events/event-topology.js +127 -0
- package/src/execution-plan.js +463 -0
- package/src/extractor.js +2816 -0
- package/src/handler-eval.js +1154 -0
- package/src/handler-table.js +326 -0
- package/src/index.js +19 -0
- package/src/javascript-application-plan.js +181 -0
- package/src/kv-provider.js +3 -0
- package/src/path-table.js +60 -0
- package/src/path.js +14 -0
- package/src/patterns/config-define.js +30 -0
- package/src/patterns/dependency-call.js +18 -0
- package/src/patterns/env-lookup.js +13 -0
- package/src/patterns/handler-reference.js +29 -0
- package/src/patterns/path-literal.js +35 -0
- package/src/patterns/result.js +15 -0
- package/src/patterns/router-chain-call.js +50 -0
- package/src/patterns/router-construction.js +19 -0
- package/src/project/package-reachability.js +782 -0
- package/src/project/reachable-graph-builder.js +992 -0
- package/src/project/reachable-graph-contract.js +54 -0
- package/src/project/reachable-graph-implementation.js +36 -0
- package/src/project/router-module-linker.js +710 -0
- package/src/project-config-compiler.js +222 -0
- package/src/project-target-support.js +500 -0
- package/src/provider-toolchain.js +299 -0
- package/src/spine/async-surface-normalizer.js +328 -0
- package/src/spine/canonical-handler-ir.js +336 -0
- package/src/spine/canonical-native-module.js +87 -0
- package/src/spine/canonical-native-plan.js +89 -0
- package/src/spine/canonical-project.js +76 -0
- package/src/spine/canonical-router.js +155 -0
- package/src/spine/canonical-source.js +165 -0
- package/src/spine/diagnostic-authority.js +290 -0
- package/src/spine/equivalence.js +262 -0
- package/src/spine/guest-unit-stage.js +87 -0
- package/src/spine/handler-ir-emitter.js +455 -0
- package/src/spine/handler-ir-managed.js +1598 -0
- package/src/spine/handler-ir.js +797 -0
- package/src/spine/handler-surface-authority.js +588 -0
- package/src/spine/package-operation-seam.js +1015 -0
- package/src/spine/pipeline.js +202 -0
- package/src/spine/plain-handler-frontend.js +545 -0
- package/src/spine/provider-requirement-authority.js +208 -0
- package/src/spine/router-control-contract.js +17 -0
- package/src/spine/router-handler-frontend.js +514 -0
- package/src/spine/router-handler-ir.js +372 -0
- package/src/spine/router-topology-frontend.js +638 -0
- package/src/stable-id.js +14 -0
|
@@ -0,0 +1,1598 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const ts = require('typescript');
|
|
5
|
+
const {
|
|
6
|
+
buildPlainHandlerIr,
|
|
7
|
+
handlerIrSnapshot
|
|
8
|
+
} = require('./handler-ir.js');
|
|
9
|
+
const {
|
|
10
|
+
normalizeManagedHandler
|
|
11
|
+
} = require('./async-surface-normalizer.js');
|
|
12
|
+
const {
|
|
13
|
+
extractKvNamespaceDeclaration,
|
|
14
|
+
recognizeHandlerSurface
|
|
15
|
+
} = require('./handler-surface-authority.js');
|
|
16
|
+
const {
|
|
17
|
+
PACKAGE_OPERATION_RECOGNITION_VERSION,
|
|
18
|
+
lowerCanonicalPackageOperations
|
|
19
|
+
} = require('./package-operation-seam.js');
|
|
20
|
+
const {
|
|
21
|
+
collectProviderRequirements
|
|
22
|
+
} = require('./provider-requirement-authority.js');
|
|
23
|
+
const {
|
|
24
|
+
projectGraphContext,
|
|
25
|
+
resolutionForImport
|
|
26
|
+
} = require('../project/reachable-graph-builder.js');
|
|
27
|
+
const {
|
|
28
|
+
sha256Hex,
|
|
29
|
+
stableStringify
|
|
30
|
+
} = require('../stable-id.js');
|
|
31
|
+
const {
|
|
32
|
+
PACKAGE_MANAGED_HANDLER_DESCRIPTOR_VERSION
|
|
33
|
+
} = require('@pulse-compute/wasm-contracts/package/package-contract');
|
|
34
|
+
|
|
35
|
+
function loadCanonicalNativePlanContract() {
|
|
36
|
+
try { return require('@pulse-compute/wasm-contracts/handler/canonical-native-plan'); }
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
39
|
+
return require('../../../contracts/src/handler/canonical-native-plan.js');
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const canonicalNativePlanContract = loadCanonicalNativePlanContract();
|
|
46
|
+
|
|
47
|
+
const managedHandlerDescriptorVersion = PACKAGE_MANAGED_HANDLER_DESCRIPTOR_VERSION;
|
|
48
|
+
const managedHandlerIrVersion = 'pulse.managed-handler-ir.v2';
|
|
49
|
+
const managedHandlerIrBundleVersion = 'pulse.managed-handler-ir-bundle.v2';
|
|
50
|
+
const managedHandlerEffectPlanVersion = 'pulse.managed-handler-effect-plan.v1';
|
|
51
|
+
const managedHandlerReachabilityVersion = 'pulse.managed-handler-reachability.v1';
|
|
52
|
+
const managedHandlerNativeFactsVersion = 'pulse.managed-handler-native-facts.v1';
|
|
53
|
+
const managedHandlerNativeBundleVersion = 'pulse.managed-handler-native-bundle.v1';
|
|
54
|
+
const MANAGED_HANDLER_ROLE = 'schema-operation';
|
|
55
|
+
const managedHandlerInputKinds = Object.freeze(['schema-value', 'empty-value']);
|
|
56
|
+
const managedHandlerResultKinds = Object.freeze(['schema-value', 'completion']);
|
|
57
|
+
const managedHandlerOperationKinds = Object.freeze([
|
|
58
|
+
'block',
|
|
59
|
+
'local',
|
|
60
|
+
'if',
|
|
61
|
+
'effect',
|
|
62
|
+
'parallel',
|
|
63
|
+
'logging',
|
|
64
|
+
'schema-result',
|
|
65
|
+
'completion',
|
|
66
|
+
'rejected'
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const ASSIGNMENT_OPERATORS = new Set([
|
|
70
|
+
'=', '+=', '-=', '*=', '/=', '%=', '**=', '&&=', '||=', '??=', '&=', '|=', '^=', '<<=', '>>=', '>>>='
|
|
71
|
+
]);
|
|
72
|
+
const PURE_BINARY_OPERATORS = new Set([
|
|
73
|
+
'===', '!==', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%', '**',
|
|
74
|
+
'&&', '||', '??', '&', '|', '^', '<<', '>>', '>>>', 'in'
|
|
75
|
+
]);
|
|
76
|
+
const PURE_PREFIX_OPERATORS = new Set(['!', '+', '-', '~', 'typeof', 'void']);
|
|
77
|
+
const SOURCE_EXTENSIONS = Object.freeze(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
78
|
+
|
|
79
|
+
const canonicalHandlerIr = new WeakMap();
|
|
80
|
+
const managedHandlerNativeFacts = new WeakMap();
|
|
81
|
+
|
|
82
|
+
class ManagedHandlerCompileError extends Error {
|
|
83
|
+
constructor(message, diagnostics = []) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = 'ManagedHandlerCompileError';
|
|
86
|
+
this.code = 'PULSE_MANAGED_HANDLER_COMPILE_FAILED';
|
|
87
|
+
this.diagnostics = Object.freeze([...diagnostics]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function deepFreeze(value) {
|
|
92
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
93
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
94
|
+
return Object.freeze(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isPlainObject(value) {
|
|
98
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
99
|
+
const prototype = Object.getPrototypeOf(value);
|
|
100
|
+
return prototype === Object.prototype || prototype === null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function requiredString(value, field) {
|
|
104
|
+
if (typeof value !== 'string' || value.trim() === '') throw new TypeError(`${field} must be a non-empty string.`);
|
|
105
|
+
return value.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function portableFile(value, field) {
|
|
109
|
+
const input = requiredString(value, field).replace(/\\/g, '/');
|
|
110
|
+
const normalized = path.posix.normalize(input).replace(/^\.\//, '');
|
|
111
|
+
if (normalized === '..' || normalized.startsWith('../') || normalized.startsWith('/')) {
|
|
112
|
+
throw new TypeError(`${field} must be a contained portable file path.`);
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sourcePoint(input, field) {
|
|
118
|
+
if (input == null) return undefined;
|
|
119
|
+
if (!isPlainObject(input)) throw new TypeError(`${field} must be an object.`);
|
|
120
|
+
const line = Number(input.line || 1);
|
|
121
|
+
const column = Number(input.column || 1);
|
|
122
|
+
if (!Number.isSafeInteger(line) || line < 1 || !Number.isSafeInteger(column) || column < 1) {
|
|
123
|
+
throw new TypeError(`${field} line and column must be positive safe integers.`);
|
|
124
|
+
}
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
file: portableFile(input.file, `${field}.file`),
|
|
127
|
+
line,
|
|
128
|
+
column
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeSchemaBinding(input, field, emptyKind, valueKind) {
|
|
133
|
+
if (!isPlainObject(input)) throw new TypeError(`${field} must be an object.`);
|
|
134
|
+
const schemaId = input.schemaId == null ? null : requiredString(input.schemaId, `${field}.schemaId`);
|
|
135
|
+
const kind = input.kind == null ? (schemaId === null ? emptyKind : valueKind) : requiredString(input.kind, `${field}.kind`);
|
|
136
|
+
const allowed = field === 'input' ? managedHandlerInputKinds : managedHandlerResultKinds;
|
|
137
|
+
if (!allowed.includes(kind)) throw new TypeError(`${field}.kind must be one of ${allowed.join(', ')}.`);
|
|
138
|
+
if ((kind === valueKind) !== (schemaId !== null)) {
|
|
139
|
+
throw new TypeError(`${field} must pair ${valueKind} with one schemaId and ${emptyKind} with null.`);
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze({ kind, schemaId });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeDescriptor(input, index) {
|
|
145
|
+
if (!isPlainObject(input)) throw new TypeError(`descriptors[${index}] must be an object.`);
|
|
146
|
+
if (input.version !== managedHandlerDescriptorVersion) {
|
|
147
|
+
throw new TypeError(`descriptors[${index}].version must be ${managedHandlerDescriptorVersion}.`);
|
|
148
|
+
}
|
|
149
|
+
if (input.role !== MANAGED_HANDLER_ROLE) {
|
|
150
|
+
throw new TypeError(`descriptors[${index}].role must be ${MANAGED_HANDLER_ROLE}.`);
|
|
151
|
+
}
|
|
152
|
+
if (!isPlainObject(input.source)) throw new TypeError(`descriptors[${index}].source must be an object.`);
|
|
153
|
+
const origin = sourcePoint(input.origin, `descriptors[${index}].origin`);
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
version: managedHandlerDescriptorVersion,
|
|
156
|
+
id: requiredString(input.id, `descriptors[${index}].id`),
|
|
157
|
+
role: MANAGED_HANDLER_ROLE,
|
|
158
|
+
origin,
|
|
159
|
+
source: Object.freeze({
|
|
160
|
+
file: portableFile(input.source.file, `descriptors[${index}].source.file`),
|
|
161
|
+
exportName: requiredString(input.source.exportName, `descriptors[${index}].source.exportName`),
|
|
162
|
+
localName: requiredString(input.source.localName, `descriptors[${index}].source.localName`)
|
|
163
|
+
}),
|
|
164
|
+
input: normalizeSchemaBinding(input.input, `input`, 'empty-value', 'schema-value'),
|
|
165
|
+
result: normalizeSchemaBinding(input.result, `result`, 'completion', 'schema-value')
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function positionFor(sourceFile, node, fallback) {
|
|
170
|
+
const candidates = [
|
|
171
|
+
node,
|
|
172
|
+
node && node.expression,
|
|
173
|
+
node && node.statements && node.statements[0],
|
|
174
|
+
node && ts.getOriginalNode(node)
|
|
175
|
+
].filter(Boolean);
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
if (!sourceFile || typeof candidate.getStart !== 'function' || candidate.pos < 0) continue;
|
|
178
|
+
try {
|
|
179
|
+
const offset = candidate.getStart(sourceFile);
|
|
180
|
+
const point = sourceFile.getLineAndCharacterOfPosition(offset);
|
|
181
|
+
return Object.freeze({ line: point.line + 1, column: point.character + 1, offset });
|
|
182
|
+
} catch (_) {
|
|
183
|
+
// Synthetic wrapper nodes use the descriptor/source fallback below.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
line: fallback && fallback.line || 1,
|
|
188
|
+
column: fallback && fallback.column || 1,
|
|
189
|
+
offset: 0
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function diagnostic(options) {
|
|
194
|
+
const fallback = options.fallback || options.descriptor && options.descriptor.origin;
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
code: String(options.code),
|
|
197
|
+
kind: 'ManagedHandlerCompileDiagnostic',
|
|
198
|
+
severity: 'error',
|
|
199
|
+
message: String(options.message),
|
|
200
|
+
file: options.sourceFile && options.sourceFile.fileName || fallback && fallback.file || '<managed-handler>',
|
|
201
|
+
position: positionFor(options.sourceFile, options.node, fallback),
|
|
202
|
+
detail: Object.freeze({
|
|
203
|
+
...(options.descriptor ? { descriptorId: options.descriptor.id } : {}),
|
|
204
|
+
...(options.detail || {})
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeDiagnostics(entries) {
|
|
210
|
+
return Object.freeze([...entries].sort((left, right) => (
|
|
211
|
+
String(left.file || '').localeCompare(String(right.file || ''))
|
|
212
|
+
|| Number(left.position && left.position.line || 0) - Number(right.position && right.position.line || 0)
|
|
213
|
+
|| Number(left.position && left.position.column || 0) - Number(right.position && right.position.column || 0)
|
|
214
|
+
|| String(left.code).localeCompare(String(right.code))
|
|
215
|
+
|| String(left.message).localeCompare(String(right.message))
|
|
216
|
+
)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function modulePathCandidates(input) {
|
|
220
|
+
const value = portableFile(input, 'handler source file');
|
|
221
|
+
const extension = path.posix.extname(value);
|
|
222
|
+
const stem = extension ? value.slice(0, -extension.length) : value;
|
|
223
|
+
const candidates = new Set([value]);
|
|
224
|
+
if (extension) for (const next of SOURCE_EXTENSIONS) candidates.add(`${stem}${next}`);
|
|
225
|
+
else for (const next of SOURCE_EXTENSIONS) candidates.add(`${value}${next}`);
|
|
226
|
+
for (const next of SOURCE_EXTENSIONS) candidates.add(path.posix.join(stem, `index${next}`));
|
|
227
|
+
return candidates;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findModulePath(input, available) {
|
|
231
|
+
const matches = [...modulePathCandidates(input)].filter((candidate) => available.has(candidate));
|
|
232
|
+
return Object.freeze({ matches: Object.freeze(matches.sort()), selected: matches.length === 1 ? matches[0] : undefined });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function unwrap(node) {
|
|
236
|
+
let current = node;
|
|
237
|
+
while (current && (
|
|
238
|
+
ts.isParenthesizedExpression(current)
|
|
239
|
+
|| ts.isAsExpression(current)
|
|
240
|
+
|| ts.isNonNullExpression(current)
|
|
241
|
+
|| ts.isTypeAssertionExpression(current)
|
|
242
|
+
|| ts.isSatisfiesExpression?.(current)
|
|
243
|
+
|| ts.isPartiallyEmittedExpression(current)
|
|
244
|
+
)) current = current.expression;
|
|
245
|
+
return current;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function functionDeclarations(sourceFile, localName) {
|
|
249
|
+
const matches = [];
|
|
250
|
+
for (const statement of sourceFile.statements) {
|
|
251
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.name.text === localName) {
|
|
252
|
+
matches.push(Object.freeze({ functionNode: statement, declaration: statement }));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
256
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
257
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== localName || !declaration.initializer) continue;
|
|
258
|
+
const initializer = unwrap(declaration.initializer);
|
|
259
|
+
if (initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
|
|
260
|
+
matches.push(Object.freeze({ functionNode: initializer, declaration }));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return matches;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveDescriptors(graphBuild, descriptors, diagnostics) {
|
|
268
|
+
const context = projectGraphContext(graphBuild);
|
|
269
|
+
if (!context || !graphBuild.graph || !Array.isArray(graphBuild.graph.modules)) {
|
|
270
|
+
throw new TypeError('compileManagedHandlerDescriptors requires one completed reachable project graph build.');
|
|
271
|
+
}
|
|
272
|
+
const graphModules = new Map(graphBuild.graph.modules.filter((entry) => entry.kind === 'project').map((entry) => [entry.path, entry]));
|
|
273
|
+
const available = new Set(graphModules.keys());
|
|
274
|
+
const resolved = [];
|
|
275
|
+
|
|
276
|
+
for (const descriptor of descriptors) {
|
|
277
|
+
const ownerMatch = findModulePath(descriptor.origin.file, available);
|
|
278
|
+
if (!ownerMatch.selected) {
|
|
279
|
+
diagnostics.push(diagnostic({
|
|
280
|
+
descriptor,
|
|
281
|
+
code: ownerMatch.matches.length > 1 ? 'PULSE_MANAGED_HANDLER_OWNER_AMBIGUOUS' : 'PULSE_MANAGED_HANDLER_OWNER_UNREACHABLE',
|
|
282
|
+
message: ownerMatch.matches.length > 1
|
|
283
|
+
? `Managed handler owner ${descriptor.origin.file} is ambiguous in the reachable graph.`
|
|
284
|
+
: `Managed handler owner ${descriptor.origin.file} is not present in the reachable graph.`,
|
|
285
|
+
detail: { candidates: ownerMatch.matches }
|
|
286
|
+
}));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const ownerPath = ownerMatch.selected;
|
|
291
|
+
let targetPath;
|
|
292
|
+
let importedResolution;
|
|
293
|
+
const localTarget = findModulePath(descriptor.source.file, available);
|
|
294
|
+
if (localTarget.selected === ownerPath) {
|
|
295
|
+
targetPath = ownerPath;
|
|
296
|
+
} else {
|
|
297
|
+
importedResolution = resolutionForImport(graphBuild, ownerPath, descriptor.source.localName);
|
|
298
|
+
if (importedResolution && available.has(importedResolution.targetKey)) targetPath = importedResolution.targetKey;
|
|
299
|
+
else if (localTarget.selected) targetPath = localTarget.selected;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!targetPath) {
|
|
303
|
+
diagnostics.push(diagnostic({
|
|
304
|
+
descriptor,
|
|
305
|
+
code: localTarget.matches.length > 1 ? 'PULSE_MANAGED_HANDLER_SOURCE_AMBIGUOUS' : 'PULSE_MANAGED_HANDLER_SOURCE_UNREACHABLE',
|
|
306
|
+
message: localTarget.matches.length > 1
|
|
307
|
+
? `Managed handler source ${descriptor.source.file} is ambiguous in the reachable graph.`
|
|
308
|
+
: `Managed handler source ${descriptor.source.file} is not present in the reachable graph.`,
|
|
309
|
+
detail: { candidates: localTarget.matches }
|
|
310
|
+
}));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const graphModule = graphModules.get(targetPath);
|
|
315
|
+
const sourceModule = context.projectModules.get(targetPath);
|
|
316
|
+
if (!graphModule || !sourceModule || graphModule.runtime !== true) {
|
|
317
|
+
diagnostics.push(diagnostic({
|
|
318
|
+
descriptor,
|
|
319
|
+
code: 'PULSE_MANAGED_HANDLER_SOURCE_UNREACHABLE',
|
|
320
|
+
message: `Managed handler source ${targetPath} is not runtime-reachable.`,
|
|
321
|
+
detail: { modulePath: targetPath, moduleId: graphModule && graphModule.id }
|
|
322
|
+
}));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const imported = ownerPath !== targetPath;
|
|
327
|
+
if (imported) {
|
|
328
|
+
if (!importedResolution
|
|
329
|
+
|| importedResolution.targetKey !== targetPath
|
|
330
|
+
|| importedResolution.binding.importedName !== descriptor.source.exportName) {
|
|
331
|
+
diagnostics.push(diagnostic({
|
|
332
|
+
descriptor,
|
|
333
|
+
sourceFile: context.projectModules.get(ownerPath).sourceFile,
|
|
334
|
+
node: importedResolution && importedResolution.relation.statement,
|
|
335
|
+
code: 'PULSE_MANAGED_HANDLER_IMPORT_UNRESOLVED',
|
|
336
|
+
message: `Managed handler import ${descriptor.source.localName} does not resolve to ${descriptor.source.exportName} in ${targetPath}.`,
|
|
337
|
+
detail: { ownerPath, targetPath, exportName: descriptor.source.exportName, localName: descriptor.source.localName }
|
|
338
|
+
}));
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (!graphModule.exports.includes(descriptor.source.exportName)) {
|
|
342
|
+
diagnostics.push(diagnostic({
|
|
343
|
+
descriptor,
|
|
344
|
+
sourceFile: sourceModule.sourceFile,
|
|
345
|
+
code: 'PULSE_MANAGED_HANDLER_EXPORT_UNRESOLVED',
|
|
346
|
+
message: `Managed handler export ${descriptor.source.exportName} is not declared by ${targetPath}.`,
|
|
347
|
+
detail: { targetPath, exports: graphModule.exports }
|
|
348
|
+
}));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const symbol = imported
|
|
354
|
+
? sourceModule.localExports.get(descriptor.source.exportName)
|
|
355
|
+
: descriptor.source.localName;
|
|
356
|
+
if (!symbol) {
|
|
357
|
+
diagnostics.push(diagnostic({
|
|
358
|
+
descriptor,
|
|
359
|
+
sourceFile: sourceModule.sourceFile,
|
|
360
|
+
code: 'PULSE_MANAGED_HANDLER_EXPORT_UNRESOLVED',
|
|
361
|
+
message: `Managed handler export ${descriptor.source.exportName} does not resolve to a direct project declaration.`,
|
|
362
|
+
detail: { targetPath, exportName: descriptor.source.exportName }
|
|
363
|
+
}));
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const declarations = functionDeclarations(sourceModule.sourceFile, symbol);
|
|
368
|
+
if (declarations.length !== 1) {
|
|
369
|
+
diagnostics.push(diagnostic({
|
|
370
|
+
descriptor,
|
|
371
|
+
sourceFile: sourceModule.sourceFile,
|
|
372
|
+
node: declarations[1] && declarations[1].declaration,
|
|
373
|
+
code: declarations.length === 0 ? 'PULSE_MANAGED_HANDLER_DECLARATION_UNRESOLVED' : 'PULSE_MANAGED_HANDLER_DECLARATION_AMBIGUOUS',
|
|
374
|
+
message: declarations.length === 0
|
|
375
|
+
? `Managed handler ${symbol} is not a module-level function declaration or const function.`
|
|
376
|
+
: `Managed handler ${symbol} resolves to more than one module-level function declaration.`,
|
|
377
|
+
detail: { targetPath, symbol }
|
|
378
|
+
}));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
resolved.push(Object.freeze({
|
|
383
|
+
descriptor,
|
|
384
|
+
ownerPath,
|
|
385
|
+
targetPath,
|
|
386
|
+
graphModule,
|
|
387
|
+
sourceModule,
|
|
388
|
+
symbol,
|
|
389
|
+
declaration: declarations[0].declaration,
|
|
390
|
+
functionNode: declarations[0].functionNode
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
return Object.freeze(resolved);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function hasSimpleParameter(parameter) {
|
|
397
|
+
return Boolean(
|
|
398
|
+
parameter
|
|
399
|
+
&& ts.isIdentifier(parameter.name)
|
|
400
|
+
&& !parameter.dotDotDotToken
|
|
401
|
+
&& !parameter.initializer
|
|
402
|
+
&& !parameter.questionToken
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function containsIdentifier(node, name) {
|
|
407
|
+
let found = false;
|
|
408
|
+
function visit(current) {
|
|
409
|
+
if (found || !current) return;
|
|
410
|
+
if (ts.isIdentifier(current) && current.text === name) {
|
|
411
|
+
found = true;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
ts.forEachChild(current, visit);
|
|
415
|
+
}
|
|
416
|
+
visit(node);
|
|
417
|
+
return found;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function contextPath(node, ctxName) {
|
|
421
|
+
const parts = [];
|
|
422
|
+
let current = unwrap(node);
|
|
423
|
+
while (current && ts.isPropertyAccessExpression(current)) {
|
|
424
|
+
parts.unshift(current.name.text);
|
|
425
|
+
current = unwrap(current.expression);
|
|
426
|
+
}
|
|
427
|
+
return current && ts.isIdentifier(current) && current.text === ctxName ? Object.freeze(parts) : undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function expressionContainsBareContext(plan) {
|
|
431
|
+
if (!plan || typeof plan !== 'object') return false;
|
|
432
|
+
if (plan.kind === 'context' || (plan.kind === 'context-read' && plan.path.length === 0)) return true;
|
|
433
|
+
return Object.values(plan).some((value) => (
|
|
434
|
+
Array.isArray(value)
|
|
435
|
+
? value.some(expressionContainsBareContext)
|
|
436
|
+
: expressionContainsBareContext(value)
|
|
437
|
+
));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function createOperation(kind, fields = {}) {
|
|
441
|
+
if (!managedHandlerOperationKinds.includes(kind)) throw new TypeError(`Unsupported managed Handler IR operation kind: ${kind}`);
|
|
442
|
+
return Object.freeze({ kind, ...fields });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createExpressionCompiler(state) {
|
|
446
|
+
const {
|
|
447
|
+
sourceFile,
|
|
448
|
+
descriptor,
|
|
449
|
+
diagnostics,
|
|
450
|
+
ctxName,
|
|
451
|
+
inputName,
|
|
452
|
+
handlerTargets,
|
|
453
|
+
currentSymbol
|
|
454
|
+
} = state;
|
|
455
|
+
|
|
456
|
+
function fail(node, code, message, detail = {}) {
|
|
457
|
+
diagnostics.push(diagnostic({ descriptor, sourceFile, node, code, message, detail }));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function propertyKey(node) {
|
|
461
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return String(node.text);
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function compile(node, scope) {
|
|
466
|
+
const current = unwrap(node);
|
|
467
|
+
if (!current) return Object.freeze({ kind: 'undefined' });
|
|
468
|
+
|
|
469
|
+
if (ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current)) {
|
|
470
|
+
return Object.freeze({ kind: 'literal', value: current.text });
|
|
471
|
+
}
|
|
472
|
+
if (ts.isNumericLiteral(current)) return Object.freeze({ kind: 'literal', value: Number(current.text) });
|
|
473
|
+
if (current.kind === ts.SyntaxKind.TrueKeyword || current.kind === ts.SyntaxKind.FalseKeyword) {
|
|
474
|
+
return Object.freeze({ kind: 'literal', value: current.kind === ts.SyntaxKind.TrueKeyword });
|
|
475
|
+
}
|
|
476
|
+
if (current.kind === ts.SyntaxKind.NullKeyword) return Object.freeze({ kind: 'literal', value: null });
|
|
477
|
+
|
|
478
|
+
if (ts.isIdentifier(current)) {
|
|
479
|
+
if (current.text === 'undefined') return Object.freeze({ kind: 'undefined' });
|
|
480
|
+
const binding = scope.get(current.text);
|
|
481
|
+
if (binding) return Object.freeze({ kind: binding.kind, id: binding.id, name: binding.name });
|
|
482
|
+
if (handlerTargets.has(current.text)) {
|
|
483
|
+
fail(current, 'PULSE_MANAGED_HANDLER_REFERENCE_ESCAPE', `Managed handler reference ${current.text} cannot be used as a value.`, { reference: current.text });
|
|
484
|
+
return Object.freeze({ kind: 'rejected-reference', name: current.text });
|
|
485
|
+
}
|
|
486
|
+
fail(current, 'PULSE_MANAGED_HANDLER_REFERENCE_UNRESOLVED', `Managed handler expression references unresolved identifier ${current.text}.`, { reference: current.text });
|
|
487
|
+
return Object.freeze({ kind: 'unresolved', name: current.text });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
kind: 'array',
|
|
493
|
+
items: Object.freeze(current.elements.map((entry) => {
|
|
494
|
+
if (ts.isOmittedExpression(entry)) return Object.freeze({ kind: 'undefined' });
|
|
495
|
+
if (ts.isSpreadElement(entry)) return Object.freeze({ kind: 'spread', value: compile(entry.expression, scope) });
|
|
496
|
+
return compile(entry, scope);
|
|
497
|
+
}))
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (ts.isObjectLiteralExpression(current)) {
|
|
502
|
+
const entries = [];
|
|
503
|
+
for (const property of current.properties) {
|
|
504
|
+
if (ts.isSpreadAssignment(property)) {
|
|
505
|
+
entries.push(Object.freeze({ kind: 'spread', value: compile(property.expression, scope) }));
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
509
|
+
entries.push(Object.freeze({ kind: 'property', key: property.name.text, value: compile(property.name, scope) }));
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (ts.isPropertyAssignment(property)) {
|
|
513
|
+
const key = ts.isComputedPropertyName(property.name)
|
|
514
|
+
? Object.freeze({ kind: 'computed', value: compile(property.name.expression, scope) })
|
|
515
|
+
: propertyKey(property.name);
|
|
516
|
+
if (key === undefined || key === '__proto__') {
|
|
517
|
+
fail(property.name, 'PULSE_MANAGED_HANDLER_OBJECT_KEY_UNSUPPORTED', 'Managed handler object results require safe static or computed property keys.');
|
|
518
|
+
}
|
|
519
|
+
entries.push(Object.freeze({ kind: 'property', key, value: compile(property.initializer, scope) }));
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
fail(property, 'PULSE_MANAGED_HANDLER_CALLBACK_UNSUPPORTED', 'Object methods, accessors, and function-valued members are outside pure managed handler lowering.');
|
|
523
|
+
}
|
|
524
|
+
return Object.freeze({ kind: 'object', entries: Object.freeze(entries) });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (ts.isTemplateExpression(current)) {
|
|
528
|
+
const parts = [Object.freeze({ kind: 'text', value: current.head.text })];
|
|
529
|
+
for (const span of current.templateSpans) {
|
|
530
|
+
parts.push(Object.freeze({ kind: 'value', value: compile(span.expression, scope) }));
|
|
531
|
+
parts.push(Object.freeze({ kind: 'text', value: span.literal.text }));
|
|
532
|
+
}
|
|
533
|
+
return Object.freeze({ kind: 'template', parts: Object.freeze(parts) });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (ts.isBinaryExpression(current)) {
|
|
537
|
+
const operator = current.operatorToken.getText(sourceFile);
|
|
538
|
+
if (ASSIGNMENT_OPERATORS.has(operator)) {
|
|
539
|
+
const inputAssignment = containsIdentifier(current.left, inputName) || containsIdentifier(current.right, inputName);
|
|
540
|
+
fail(
|
|
541
|
+
current,
|
|
542
|
+
inputAssignment ? 'PULSE_MANAGED_HANDLER_INPUT_ESCAPE' : 'PULSE_MANAGED_HANDLER_MUTATION_UNSUPPORTED',
|
|
543
|
+
inputAssignment
|
|
544
|
+
? 'The schema-bound input is immutable and cannot escape through assignment.'
|
|
545
|
+
: 'Assignment is outside pure managed handler lowering.',
|
|
546
|
+
{ operator }
|
|
547
|
+
);
|
|
548
|
+
return Object.freeze({ kind: 'rejected-assignment', operator, target: compile(current.left, scope), value: compile(current.right, scope) });
|
|
549
|
+
}
|
|
550
|
+
if (!PURE_BINARY_OPERATORS.has(operator)) {
|
|
551
|
+
fail(current.operatorToken, 'PULSE_MANAGED_HANDLER_EXPRESSION_UNSUPPORTED', `Binary operator ${operator} is outside pure managed handler lowering.`, { operator });
|
|
552
|
+
}
|
|
553
|
+
return Object.freeze({
|
|
554
|
+
kind: 'binary',
|
|
555
|
+
operator,
|
|
556
|
+
left: compile(current.left, scope),
|
|
557
|
+
right: compile(current.right, scope)
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (ts.isPrefixUnaryExpression(current)) {
|
|
562
|
+
const operator = ts.tokenToString(current.operator) || current.getText(sourceFile).slice(0, 1);
|
|
563
|
+
if (!PURE_PREFIX_OPERATORS.has(operator)) {
|
|
564
|
+
fail(current, 'PULSE_MANAGED_HANDLER_MUTATION_UNSUPPORTED', `Prefix operator ${operator} is outside pure managed handler lowering.`, { operator });
|
|
565
|
+
}
|
|
566
|
+
return Object.freeze({ kind: 'unary', operator, value: compile(current.operand, scope) });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (ts.isPostfixUnaryExpression(current)) {
|
|
570
|
+
const operator = ts.tokenToString(current.operator) || current.getText(sourceFile).slice(-2);
|
|
571
|
+
fail(current, 'PULSE_MANAGED_HANDLER_MUTATION_UNSUPPORTED', `Postfix operator ${operator} is outside pure managed handler lowering.`, { operator });
|
|
572
|
+
return Object.freeze({ kind: 'rejected-update', operator, value: compile(current.operand, scope) });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (ts.isConditionalExpression(current)) {
|
|
576
|
+
return Object.freeze({
|
|
577
|
+
kind: 'conditional',
|
|
578
|
+
test: compile(current.condition, scope),
|
|
579
|
+
whenTrue: compile(current.whenTrue, scope),
|
|
580
|
+
whenFalse: compile(current.whenFalse, scope)
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
585
|
+
if (current.questionDotToken) fail(current, 'PULSE_MANAGED_HANDLER_EXPRESSION_UNSUPPORTED', 'Optional property access is outside pure managed handler lowering.');
|
|
586
|
+
const ctxPath = contextPath(current, ctxName);
|
|
587
|
+
if (ctxPath) {
|
|
588
|
+
const contextKey = ctxPath.join('.');
|
|
589
|
+
if (!canonicalNativePlanContract.CANONICAL_NATIVE_CONTEXT_READS.includes(contextKey)) {
|
|
590
|
+
fail(current, 'PULSE_MANAGED_HANDLER_CONTEXT_READ_UNSUPPORTED', `Context read ctx.${contextKey} is outside the canonical native context contract.`, { path: ctxPath });
|
|
591
|
+
}
|
|
592
|
+
return Object.freeze({ kind: 'context-read', path: ctxPath });
|
|
593
|
+
}
|
|
594
|
+
return Object.freeze({ kind: 'property', object: compile(current.expression, scope), property: current.name.text });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (ts.isElementAccessExpression(current)) {
|
|
598
|
+
if (current.questionDotToken) fail(current, 'PULSE_MANAGED_HANDLER_EXPRESSION_UNSUPPORTED', 'Optional element access is outside pure managed handler lowering.');
|
|
599
|
+
return Object.freeze({
|
|
600
|
+
kind: 'element',
|
|
601
|
+
object: compile(current.expression, scope),
|
|
602
|
+
index: compile(current.argumentExpression, scope)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (ts.isCallExpression(current)) {
|
|
607
|
+
const callee = unwrap(current.expression);
|
|
608
|
+
const calleeName = callee && ts.isIdentifier(callee) ? callee.text : undefined;
|
|
609
|
+
const inputEscapes = containsIdentifier(current.expression, inputName)
|
|
610
|
+
|| current.arguments.some((argument) => containsIdentifier(argument, inputName));
|
|
611
|
+
if (inputEscapes) {
|
|
612
|
+
fail(current, 'PULSE_MANAGED_HANDLER_INPUT_ESCAPE', 'The schema-bound input cannot escape through a call or callback.', { callee: callee && callee.getText(sourceFile) });
|
|
613
|
+
}
|
|
614
|
+
if (calleeName === currentSymbol) {
|
|
615
|
+
fail(current, 'PULSE_MANAGED_HANDLER_RECURSION_UNSUPPORTED', `Managed handler ${currentSymbol} cannot call itself.`, { callee: calleeName });
|
|
616
|
+
} else if (calleeName && handlerTargets.has(calleeName)) {
|
|
617
|
+
fail(current, 'PULSE_MANAGED_HANDLER_DIRECT_CALL_UNSUPPORTED', `Managed handler ${currentSymbol} cannot call managed handler ${calleeName}.`, { callee: calleeName });
|
|
618
|
+
} else {
|
|
619
|
+
fail(current, 'PULSE_MANAGED_HANDLER_CALL_UNSUPPORTED', 'Function and method calls enter the effectful managed-handler pass and are not part of pure lowering.', { callee: callee && callee.getText(sourceFile) });
|
|
620
|
+
}
|
|
621
|
+
const target = ts.isPropertyAccessExpression(callee)
|
|
622
|
+
? compile(callee.expression, scope)
|
|
623
|
+
: Object.freeze({ kind: 'callee', name: calleeName || callee && callee.getText(sourceFile) || '<unknown>' });
|
|
624
|
+
return Object.freeze({
|
|
625
|
+
kind: 'rejected-call',
|
|
626
|
+
target,
|
|
627
|
+
arguments: Object.freeze(current.arguments.map((argument) => compile(argument, scope)))
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (ts.isArrowFunction(current) || ts.isFunctionExpression(current) || ts.isClassExpression(current)) {
|
|
632
|
+
const capturesInput = containsIdentifier(current.body || current, inputName);
|
|
633
|
+
fail(current, 'PULSE_MANAGED_HANDLER_CALLBACK_UNSUPPORTED', 'Nested functions, closures, classes, and arbitrary callbacks are outside managed handler lowering.');
|
|
634
|
+
if (capturesInput) fail(current, 'PULSE_MANAGED_HANDLER_INPUT_ESCAPE', 'The schema-bound input cannot be captured by a nested function or callback.');
|
|
635
|
+
return Object.freeze({ kind: 'rejected-callback' });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (ts.isAwaitExpression(current) || ts.isYieldExpression(current) || ts.isNewExpression(current)) {
|
|
639
|
+
fail(current, 'PULSE_MANAGED_HANDLER_EXPRESSION_UNSUPPORTED', `${ts.SyntaxKind[current.kind]} enters effectful or dynamic execution and is outside pure managed handler lowering.`, { syntax: ts.SyntaxKind[current.kind] });
|
|
640
|
+
return Object.freeze({ kind: 'rejected-expression', syntax: ts.SyntaxKind[current.kind] });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
fail(current, 'PULSE_MANAGED_HANDLER_EXPRESSION_UNSUPPORTED', `Expression ${ts.SyntaxKind[current.kind]} is outside pure managed handler lowering.`, { syntax: ts.SyntaxKind[current.kind] });
|
|
644
|
+
return Object.freeze({ kind: 'rejected-expression', syntax: ts.SyntaxKind[current.kind] });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return compile;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function handlerTargetNames(context, resolved, byModule) {
|
|
651
|
+
const names = new Set(byModule.get(resolved.targetPath) || []);
|
|
652
|
+
const module = context.projectModules.get(resolved.targetPath);
|
|
653
|
+
for (const relation of module && module.imports || []) {
|
|
654
|
+
for (const binding of relation.bindings || []) {
|
|
655
|
+
const resolution = resolutionForImport(resolved.graphBuild, resolved.targetPath, binding.localName);
|
|
656
|
+
if (!resolution) continue;
|
|
657
|
+
const targetSymbols = byModule.get(resolution.targetKey);
|
|
658
|
+
if (!targetSymbols) continue;
|
|
659
|
+
const targetModule = context.projectModules.get(resolution.targetKey);
|
|
660
|
+
const exportedLocal = targetModule && targetModule.localExports.get(binding.importedName);
|
|
661
|
+
if (exportedLocal && targetSymbols.has(exportedLocal)) names.add(binding.localName);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return names;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function normalizedSourceIdentity(value) {
|
|
668
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function sourceIdentityMatches(value, resolved) {
|
|
672
|
+
const actual = normalizedSourceIdentity(value);
|
|
673
|
+
if (!actual) return false;
|
|
674
|
+
const expected = [
|
|
675
|
+
resolved.targetPath,
|
|
676
|
+
resolved.sourceModule && resolved.sourceModule.absolutePath,
|
|
677
|
+
resolved.sourceModule && resolved.sourceModule.sourceFile && resolved.sourceModule.sourceFile.fileName
|
|
678
|
+
].map(normalizedSourceIdentity).filter(Boolean);
|
|
679
|
+
return expected.some((candidate) => actual === candidate || actual.endsWith(`/${normalizedSourceIdentity(resolved.targetPath)}`));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function sourceRangeContains(range, start, end) {
|
|
683
|
+
const rangeStart = Number(range && range.start);
|
|
684
|
+
const rangeEnd = Number(range && range.end);
|
|
685
|
+
return Number.isSafeInteger(rangeStart)
|
|
686
|
+
&& Number.isSafeInteger(rangeEnd)
|
|
687
|
+
&& rangeStart >= start
|
|
688
|
+
&& rangeEnd <= end;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function packageInputsForHandler(recognition, resolved) {
|
|
692
|
+
if (!recognition) {
|
|
693
|
+
return Object.freeze({
|
|
694
|
+
effects: Object.freeze([]),
|
|
695
|
+
operations: Object.freeze([]),
|
|
696
|
+
intrinsics: Object.freeze([]),
|
|
697
|
+
resultAdapters: Object.freeze([]),
|
|
698
|
+
lowering: lowerCanonicalPackageOperations(Object.freeze({
|
|
699
|
+
version: PACKAGE_OPERATION_RECOGNITION_VERSION,
|
|
700
|
+
operations: Object.freeze([]),
|
|
701
|
+
packages: Object.freeze([]),
|
|
702
|
+
schemaReferences: Object.freeze([]),
|
|
703
|
+
realizationArtifacts: Object.freeze([]),
|
|
704
|
+
guestUnits: Object.freeze([]),
|
|
705
|
+
cryptoRequirements: Object.freeze([])
|
|
706
|
+
}))
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const sourceFile = resolved.sourceModule.sourceFile;
|
|
710
|
+
const start = resolved.functionNode.getStart(sourceFile);
|
|
711
|
+
const end = resolved.functionNode.getEnd();
|
|
712
|
+
const extensions = recognition.publicExtensions || {};
|
|
713
|
+
const within = (entry) => sourceIdentityMatches(entry && entry.loc && entry.loc.file, resolved)
|
|
714
|
+
&& sourceRangeContains(entry && entry.range, start, end);
|
|
715
|
+
const withinOperation = (entry) => sourceIdentityMatches(entry && entry.source && entry.source.file, resolved)
|
|
716
|
+
&& sourceRangeContains(entry && entry.range, start, end);
|
|
717
|
+
const sortByRange = (left, right) => Number(left.range.start) - Number(right.range.start)
|
|
718
|
+
|| Number(left.range.end) - Number(right.range.end)
|
|
719
|
+
|| String(left.kind || left.intrinsic || '').localeCompare(String(right.kind || right.intrinsic || ''));
|
|
720
|
+
const operations = Object.freeze([...(recognition.operations || [])].filter(withinOperation).sort(sortByRange));
|
|
721
|
+
const contractIds = new Set(operations.map((entry) => String(entry.contractId || '')).filter(Boolean));
|
|
722
|
+
const packages = Object.freeze([...(recognition.packages || [])]
|
|
723
|
+
.filter((entry) => contractIds.has(String(entry.contractId || '')))
|
|
724
|
+
.map((entry) => Object.freeze({
|
|
725
|
+
...entry,
|
|
726
|
+
operationCount: operations.filter((operation) => operation.contractId === entry.contractId).length,
|
|
727
|
+
intrinsicCount: [...(extensions.intrinsics || [])].filter(within)
|
|
728
|
+
.filter((intrinsic) => intrinsic.contractId === entry.contractId).length
|
|
729
|
+
})));
|
|
730
|
+
const packageNames = new Set(packages.flatMap((entry) => [entry.package, entry.lowerableSubpath]).map(String).filter(Boolean));
|
|
731
|
+
const packageOwned = (entry) => contractIds.has(String(entry && entry.contractId || ''))
|
|
732
|
+
|| packageNames.has(String(entry && (entry.package || entry.requestedBy || entry.owner) || ''));
|
|
733
|
+
const filteredRecognition = Object.freeze({
|
|
734
|
+
version: PACKAGE_OPERATION_RECOGNITION_VERSION,
|
|
735
|
+
operations,
|
|
736
|
+
packages,
|
|
737
|
+
schemaReferences: Object.freeze([...(recognition.schemaReferences || [])].filter((entry) => (
|
|
738
|
+
sourceIdentityMatches(entry && (entry.file || entry.loc && entry.loc.file), resolved)
|
|
739
|
+
&& (!entry.range || sourceRangeContains(entry.range, start, end))
|
|
740
|
+
))),
|
|
741
|
+
realizationArtifacts: Object.freeze([...(recognition.realizationArtifacts || [])]
|
|
742
|
+
.filter(packageOwned)),
|
|
743
|
+
guestUnits: Object.freeze([...(recognition.guestUnits || [])]
|
|
744
|
+
.filter(packageOwned)),
|
|
745
|
+
cryptoRequirements: Object.freeze([...(recognition.cryptoRequirements || [])]
|
|
746
|
+
.filter(packageOwned))
|
|
747
|
+
});
|
|
748
|
+
return Object.freeze({
|
|
749
|
+
effects: Object.freeze(operations.map((entry) => entry.canonicalEffect)),
|
|
750
|
+
operations,
|
|
751
|
+
intrinsics: Object.freeze([...(extensions.intrinsics || [])].filter(within).sort(sortByRange)),
|
|
752
|
+
resultAdapters: Object.freeze([...(extensions.resultAdapters || [])].filter(within).sort(sortByRange)),
|
|
753
|
+
lowering: lowerCanonicalPackageOperations(filteredRecognition)
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function lookupByRange(entries) {
|
|
758
|
+
const byStart = new Map();
|
|
759
|
+
for (const entry of entries || []) {
|
|
760
|
+
const start = Number(entry && entry.range && entry.range.start);
|
|
761
|
+
if (Number.isSafeInteger(start) && !byStart.has(start)) byStart.set(start, entry);
|
|
762
|
+
}
|
|
763
|
+
return (call, sourceFile) => {
|
|
764
|
+
if (!call || typeof call.getStart !== 'function') return undefined;
|
|
765
|
+
try { return byStart.get(call.getStart(sourceFile)); }
|
|
766
|
+
catch (_) { return undefined; }
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function sortedUnique(values) {
|
|
771
|
+
return Object.freeze([...new Set((values || []).map(String).filter(Boolean))].sort());
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function continuationForEffect(operationIr, effectId) {
|
|
775
|
+
return operationIr.continuationSites.find((entry) => entry.effectIds.includes(effectId));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function effectPlanForHandler(options) {
|
|
779
|
+
const {
|
|
780
|
+
descriptor,
|
|
781
|
+
source,
|
|
782
|
+
operationIr,
|
|
783
|
+
capabilities,
|
|
784
|
+
packageEffects,
|
|
785
|
+
packageLowering
|
|
786
|
+
} = options;
|
|
787
|
+
const effects = Object.freeze(operationIr.effectSites.map((site, index) => {
|
|
788
|
+
const continuation = continuationForEffect(operationIr, site.id);
|
|
789
|
+
return deepFreeze({
|
|
790
|
+
...site,
|
|
791
|
+
order: index + 1,
|
|
792
|
+
...(continuation ? { continuationId: continuation.id } : {}),
|
|
793
|
+
source: Object.freeze({
|
|
794
|
+
file: source.file,
|
|
795
|
+
line: Number(site.position && site.position.line || source.position.line),
|
|
796
|
+
column: Number(site.position && site.position.column || source.position.column),
|
|
797
|
+
handlerId: descriptor.id
|
|
798
|
+
})
|
|
799
|
+
});
|
|
800
|
+
}));
|
|
801
|
+
const semantic = deepFreeze({
|
|
802
|
+
version: managedHandlerEffectPlanVersion,
|
|
803
|
+
kind: 'managed-handler-effects',
|
|
804
|
+
handlerId: descriptor.id,
|
|
805
|
+
source,
|
|
806
|
+
capabilities,
|
|
807
|
+
effects,
|
|
808
|
+
packages: Object.freeze({ effects: packageLowering.effects }),
|
|
809
|
+
ownership: Object.freeze({ providerNeutral: true })
|
|
810
|
+
});
|
|
811
|
+
const plan = deepFreeze({ ...semantic, planHash: sha256Hex(stableStringify(semantic)) });
|
|
812
|
+
return Object.freeze({
|
|
813
|
+
plan,
|
|
814
|
+
providerRequirements: collectProviderRequirements(plan, packageLowering)
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageRecognition) {
|
|
819
|
+
const descriptor = resolved.descriptor;
|
|
820
|
+
const sourceFile = resolved.sourceModule.sourceFile;
|
|
821
|
+
const original = resolved.functionNode;
|
|
822
|
+
|
|
823
|
+
if (original.asteriskToken) {
|
|
824
|
+
diagnostics.push(diagnostic({
|
|
825
|
+
descriptor,
|
|
826
|
+
sourceFile,
|
|
827
|
+
node: original,
|
|
828
|
+
code: 'PULSE_MANAGED_HANDLER_GENERATOR_UNSUPPORTED',
|
|
829
|
+
message: 'Generator functions are outside managed handler lowering.'
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
if (original.parameters.length !== 2 || !original.parameters.every(hasSimpleParameter)) {
|
|
833
|
+
diagnostics.push(diagnostic({
|
|
834
|
+
descriptor,
|
|
835
|
+
sourceFile,
|
|
836
|
+
node: original,
|
|
837
|
+
code: 'PULSE_MANAGED_HANDLER_SIGNATURE_INVALID',
|
|
838
|
+
message: 'Managed schema operations require exactly two simple identifier parameters: (ctx, input).',
|
|
839
|
+
detail: { parameterCount: original.parameters.length }
|
|
840
|
+
}));
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const ctxName = original.parameters[0].name.text;
|
|
845
|
+
const inputName = original.parameters[1].name.text;
|
|
846
|
+
if (ctxName === inputName) {
|
|
847
|
+
diagnostics.push(diagnostic({
|
|
848
|
+
descriptor,
|
|
849
|
+
sourceFile,
|
|
850
|
+
node: original.parameters[1],
|
|
851
|
+
code: 'PULSE_MANAGED_HANDLER_SIGNATURE_INVALID',
|
|
852
|
+
message: 'Managed handler context and input parameters must have distinct identifiers.'
|
|
853
|
+
}));
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const packageInputs = packageInputsForHandler(packageRecognition, resolved);
|
|
858
|
+
const findPackageEffect = lookupByRange(packageInputs.effects);
|
|
859
|
+
const findPackageIntrinsic = lookupByRange(packageInputs.intrinsics);
|
|
860
|
+
const findPackageResultAdapter = lookupByRange(packageInputs.resultAdapters);
|
|
861
|
+
const packageEffectForCall = (call) => findPackageEffect(call, sourceFile);
|
|
862
|
+
const packageIntrinsicForCall = (call) => findPackageIntrinsic(call, sourceFile);
|
|
863
|
+
const packageResultAdapterForCall = (call) => findPackageResultAdapter(call, sourceFile);
|
|
864
|
+
|
|
865
|
+
const normalizer = normalizeManagedHandler(original, {
|
|
866
|
+
sourceFile,
|
|
867
|
+
ctxName,
|
|
868
|
+
frontend: 'canonical-source',
|
|
869
|
+
role: MANAGED_HANDLER_ROLE,
|
|
870
|
+
strict: true,
|
|
871
|
+
requireEffectAwait: false,
|
|
872
|
+
packageEffectForCall
|
|
873
|
+
});
|
|
874
|
+
diagnostics.push(...normalizer.diagnostics);
|
|
875
|
+
const handler = normalizer.functionNode;
|
|
876
|
+
|
|
877
|
+
const schemaReferences = Object.freeze([
|
|
878
|
+
descriptor.input.schemaId && Object.freeze({ id: descriptor.input.schemaId, usage: 'managed-input', capability: 'schema.decode' }),
|
|
879
|
+
descriptor.result.schemaId && Object.freeze({ id: descriptor.result.schemaId, usage: 'managed-result', capability: 'schema.encode' })
|
|
880
|
+
].filter(Boolean));
|
|
881
|
+
const frontend = {
|
|
882
|
+
frontend: 'canonical-source',
|
|
883
|
+
fileName: resolved.targetPath,
|
|
884
|
+
sourceFile,
|
|
885
|
+
handler,
|
|
886
|
+
diagnostics,
|
|
887
|
+
analysis: Object.freeze({
|
|
888
|
+
capabilities: Object.freeze([]),
|
|
889
|
+
fetchCount: 0,
|
|
890
|
+
schemaReferences,
|
|
891
|
+
providerOperations: Object.freeze([])
|
|
892
|
+
}),
|
|
893
|
+
schemaBundle: Object.freeze({
|
|
894
|
+
declaredSchemaIds: Object.freeze(schemaReferences.map((entry) => entry.id)),
|
|
895
|
+
schemaIds: Object.freeze(schemaReferences.map((entry) => entry.id))
|
|
896
|
+
})
|
|
897
|
+
};
|
|
898
|
+
const rawOperationIr = buildPlainHandlerIr(frontend, {
|
|
899
|
+
packageEffects: packageInputs.effects,
|
|
900
|
+
packageIntrinsics: packageInputs.intrinsics,
|
|
901
|
+
packageResultAdapters: packageInputs.resultAdapters,
|
|
902
|
+
packageEffectForCall,
|
|
903
|
+
packageIntrinsicForCall,
|
|
904
|
+
packageResultAdapterForCall
|
|
905
|
+
});
|
|
906
|
+
if (!rawOperationIr) return undefined;
|
|
907
|
+
|
|
908
|
+
const context = projectGraphContext(graphBuild);
|
|
909
|
+
const symbolsByModule = new Map();
|
|
910
|
+
for (const entry of allResolved) {
|
|
911
|
+
if (!symbolsByModule.has(entry.targetPath)) symbolsByModule.set(entry.targetPath, new Set());
|
|
912
|
+
symbolsByModule.get(entry.targetPath).add(entry.symbol);
|
|
913
|
+
}
|
|
914
|
+
const withBuild = Object.freeze({ ...resolved, graphBuild });
|
|
915
|
+
const handlerTargets = handlerTargetNames(context, withBuild, symbolsByModule);
|
|
916
|
+
const compileExpression = createExpressionCompiler({
|
|
917
|
+
sourceFile,
|
|
918
|
+
descriptor,
|
|
919
|
+
diagnostics,
|
|
920
|
+
ctxName,
|
|
921
|
+
inputName,
|
|
922
|
+
handlerTargets,
|
|
923
|
+
currentSymbol: resolved.symbol
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
const state = {
|
|
927
|
+
localIndex: 0,
|
|
928
|
+
returnIndex: 0,
|
|
929
|
+
loggingIndex: 0,
|
|
930
|
+
locals: [],
|
|
931
|
+
resultSites: [],
|
|
932
|
+
loggingSites: [],
|
|
933
|
+
effectRuntimeInputs: new Map()
|
|
934
|
+
};
|
|
935
|
+
const rootScope = new Map([
|
|
936
|
+
[ctxName, Object.freeze({ kind: 'context', id: 'parameter-0', name: ctxName })],
|
|
937
|
+
[inputName, Object.freeze({ kind: 'input', id: 'parameter-1', name: inputName })]
|
|
938
|
+
]);
|
|
939
|
+
|
|
940
|
+
function fail(node, code, message, detail = {}) {
|
|
941
|
+
diagnostics.push(diagnostic({ descriptor, sourceFile, node, code, message, detail }));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function registerEffectRuntimeInputs(site, candidate, scope) {
|
|
945
|
+
if (!site || state.effectRuntimeInputs.has(site.id)) {
|
|
946
|
+
if (site && state.effectRuntimeInputs.has(site.id)) {
|
|
947
|
+
fail(handler, 'PULSE_MANAGED_HANDLER_EFFECT_INPUT_DUPLICATE', `Managed effect ${site.id} was projected more than once.`, { effectId: site.id });
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const inputs = [];
|
|
952
|
+
const names = new Set();
|
|
953
|
+
function add(name, node) {
|
|
954
|
+
if (!node) return;
|
|
955
|
+
const normalized = String(name);
|
|
956
|
+
if (names.has(normalized)) {
|
|
957
|
+
fail(node, 'PULSE_MANAGED_HANDLER_EFFECT_INPUT_DUPLICATE', `Managed effect ${site.id} repeats runtime input ${normalized}.`, { effectId: site.id, input: normalized });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
names.add(normalized);
|
|
961
|
+
inputs.push(Object.freeze({ name: normalized, value: compileExpression(node, scope) }));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const chain = candidate && candidate.chain;
|
|
965
|
+
const provider = candidate && candidate.operation && candidate.operation.providerKind
|
|
966
|
+
? candidate.operation
|
|
967
|
+
: candidate && candidate.providerKind
|
|
968
|
+
? candidate
|
|
969
|
+
: undefined;
|
|
970
|
+
const packageEffect = candidate && candidate.effect;
|
|
971
|
+
const packageCall = candidate && candidate.call;
|
|
972
|
+
if (chain) {
|
|
973
|
+
add('url', chain.url);
|
|
974
|
+
add('init', chain.init);
|
|
975
|
+
for (const [index, argument] of (chain.decoderArgs || []).entries()) add(`decoderArgument${index}`, argument);
|
|
976
|
+
} else if (provider) {
|
|
977
|
+
if (provider.providerKind === 'config' || provider.providerKind === 'secret') {
|
|
978
|
+
add('name', provider.name || provider.resource);
|
|
979
|
+
} else if (provider.providerKind === 'kv') {
|
|
980
|
+
add('store', provider.store || provider.resource);
|
|
981
|
+
add('key', provider.key);
|
|
982
|
+
add('generation', provider.generation);
|
|
983
|
+
add('value', provider.value);
|
|
984
|
+
} else if (provider.providerKind === 'event') {
|
|
985
|
+
add('type', provider.type);
|
|
986
|
+
add('emission', provider.emission);
|
|
987
|
+
} else {
|
|
988
|
+
for (const [index, argument] of (provider.args || []).entries()) add(`argument${index}`, argument);
|
|
989
|
+
}
|
|
990
|
+
} else if (packageEffect && packageCall) {
|
|
991
|
+
for (const input of packageEffect.runtimeInputs || []) {
|
|
992
|
+
add(input.name, packageCall.arguments && packageCall.arguments[input.argumentIndex]);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
state.effectRuntimeInputs.set(site.id, deepFreeze({ effectId: site.id, inputs: Object.freeze(inputs) }));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function requireConst(statement) {
|
|
999
|
+
if (statement && ts.isVariableStatement(statement) && (statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
1000
|
+
fail(statement, 'PULSE_MANAGED_HANDLER_MUTATION_UNSUPPORTED', 'Managed handler locals and effect results must be declared with const.');
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function bindLocal(name, node, scope, binding = 'value') {
|
|
1005
|
+
if (scope.has(name)) {
|
|
1006
|
+
fail(node, 'PULSE_MANAGED_HANDLER_BINDING_DUPLICATE', `Managed handler local ${name} shadows an existing binding.`, { local: name });
|
|
1007
|
+
}
|
|
1008
|
+
state.localIndex += 1;
|
|
1009
|
+
const local = Object.freeze({ kind: 'local', id: `local-${state.localIndex}`, name });
|
|
1010
|
+
scope.set(name, local);
|
|
1011
|
+
const position = positionFor(sourceFile, node);
|
|
1012
|
+
state.locals.push(Object.freeze({ id: local.id, name, binding, position }));
|
|
1013
|
+
return Object.freeze({ ...local, binding, position });
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function compileReturn(statement, scope) {
|
|
1017
|
+
state.returnIndex += 1;
|
|
1018
|
+
const position = positionFor(sourceFile, statement);
|
|
1019
|
+
if (descriptor.result.kind === 'schema-value') {
|
|
1020
|
+
if (!statement.expression || (ts.isIdentifier(unwrap(statement.expression)) && unwrap(statement.expression).text === 'undefined')) {
|
|
1021
|
+
fail(statement, 'PULSE_MANAGED_HANDLER_RESULT_REQUIRED', `Managed handler ${descriptor.id} must return a value for schema ${descriptor.result.schemaId}.`, { schemaId: descriptor.result.schemaId });
|
|
1022
|
+
return Object.freeze({ operation: createOperation('rejected', { position, reason: 'result-required' }), terminates: true });
|
|
1023
|
+
}
|
|
1024
|
+
const expression = compileExpression(statement.expression, scope);
|
|
1025
|
+
if (expressionContainsBareContext(expression)) {
|
|
1026
|
+
fail(statement.expression, 'PULSE_MANAGED_HANDLER_CONTEXT_ESCAPE', 'The managed request context cannot be adopted as a schema-bound result.');
|
|
1027
|
+
}
|
|
1028
|
+
const operation = createOperation('schema-result', {
|
|
1029
|
+
site: `result-${state.returnIndex}`,
|
|
1030
|
+
schemaId: descriptor.result.schemaId,
|
|
1031
|
+
adoption: 'schema-bound-value',
|
|
1032
|
+
expression,
|
|
1033
|
+
position
|
|
1034
|
+
});
|
|
1035
|
+
state.resultSites.push(operation);
|
|
1036
|
+
return Object.freeze({ operation, terminates: true });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (statement.expression) {
|
|
1040
|
+
const expression = unwrap(statement.expression);
|
|
1041
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'undefined') {
|
|
1042
|
+
fail(statement.expression, 'PULSE_MANAGED_HANDLER_RESULT_UNSUPPORTED', 'A completion-only managed handler must not return a value.');
|
|
1043
|
+
compileExpression(statement.expression, scope);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const operation = createOperation('completion', {
|
|
1047
|
+
site: `result-${state.returnIndex}`,
|
|
1048
|
+
adoption: 'completion-only',
|
|
1049
|
+
position
|
|
1050
|
+
});
|
|
1051
|
+
state.resultSites.push(operation);
|
|
1052
|
+
return Object.freeze({ operation, terminates: true });
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function compileVariable(statement, scope) {
|
|
1056
|
+
requireConst(statement);
|
|
1057
|
+
const operations = [];
|
|
1058
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1059
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
1060
|
+
fail(declaration, 'PULSE_MANAGED_HANDLER_BINDING_UNSUPPORTED', 'Managed handler locals require simple identifier bindings with initializers.');
|
|
1061
|
+
operations.push(createOperation('rejected', { position: positionFor(sourceFile, declaration), reason: 'binding' }));
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
const initializer = compileExpression(declaration.initializer, scope);
|
|
1065
|
+
if (expressionContainsBareContext(initializer)) {
|
|
1066
|
+
fail(declaration.initializer, 'PULSE_MANAGED_HANDLER_CONTEXT_ESCAPE', 'The managed request context cannot be captured in a local value.');
|
|
1067
|
+
}
|
|
1068
|
+
const local = bindLocal(declaration.name.text, declaration.name, scope);
|
|
1069
|
+
operations.push(createOperation('local', {
|
|
1070
|
+
id: local.id,
|
|
1071
|
+
name: local.name,
|
|
1072
|
+
initializer,
|
|
1073
|
+
position: local.position
|
|
1074
|
+
}));
|
|
1075
|
+
}
|
|
1076
|
+
return Object.freeze({ operations: Object.freeze(operations), terminates: false });
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function compileNamespace(statement, scope) {
|
|
1080
|
+
const namespace = extractKvNamespaceDeclaration(statement, ctxName);
|
|
1081
|
+
if (!namespace) return undefined;
|
|
1082
|
+
requireConst(statement);
|
|
1083
|
+
const local = bindLocal(namespace.variableName, namespace.declaration.name, scope, 'provider-namespace');
|
|
1084
|
+
return Object.freeze({
|
|
1085
|
+
operation: createOperation('local', {
|
|
1086
|
+
id: local.id,
|
|
1087
|
+
name: local.name,
|
|
1088
|
+
initializer: Object.freeze({
|
|
1089
|
+
kind: 'provider-namespace',
|
|
1090
|
+
providerKind: 'kv',
|
|
1091
|
+
resource: compileExpression(namespace.store, scope)
|
|
1092
|
+
}),
|
|
1093
|
+
position: local.position
|
|
1094
|
+
}),
|
|
1095
|
+
terminates: false
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function effectOperation(site, continuation, position, fields = {}) {
|
|
1100
|
+
return createOperation('effect', {
|
|
1101
|
+
effect: site,
|
|
1102
|
+
continuation,
|
|
1103
|
+
...fields,
|
|
1104
|
+
position
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function compileBoundEffect(candidate, site, continuation, scope) {
|
|
1109
|
+
registerEffectRuntimeInputs(site, candidate, scope);
|
|
1110
|
+
requireConst(candidate.statement);
|
|
1111
|
+
const local = bindLocal(candidate.variableName, candidate.declaration.name, scope, 'effect-result');
|
|
1112
|
+
return Object.freeze({
|
|
1113
|
+
operation: effectOperation(site, continuation, positionFor(sourceFile, candidate.statement), {
|
|
1114
|
+
binding: Object.freeze({ id: local.id, name: local.name, kind: 'effect-result' })
|
|
1115
|
+
}),
|
|
1116
|
+
terminates: false
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function compileEffectReturn(operation, scope) {
|
|
1121
|
+
registerEffectRuntimeInputs(operation.site, Object.freeze({ effect: operation.effect, call: operation.call }), scope);
|
|
1122
|
+
const position = positionFor(sourceFile, operation.statement);
|
|
1123
|
+
if (descriptor.result.kind !== 'schema-value') {
|
|
1124
|
+
fail(operation.statement, 'PULSE_MANAGED_HANDLER_RESULT_UNSUPPORTED', 'A completion-only managed handler cannot adopt a package effect value as its result.');
|
|
1125
|
+
return Object.freeze({ operation: createOperation('rejected', { position, reason: 'effect-result' }), terminates: true });
|
|
1126
|
+
}
|
|
1127
|
+
state.returnIndex += 1;
|
|
1128
|
+
const resultSite = createOperation('schema-result', {
|
|
1129
|
+
site: `result-${state.returnIndex}`,
|
|
1130
|
+
schemaId: descriptor.result.schemaId,
|
|
1131
|
+
adoption: 'schema-bound-effect-value',
|
|
1132
|
+
expression: Object.freeze({ kind: 'effect-result', effectId: operation.site.id }),
|
|
1133
|
+
position
|
|
1134
|
+
});
|
|
1135
|
+
state.resultSites.push(resultSite);
|
|
1136
|
+
return Object.freeze({
|
|
1137
|
+
operation: effectOperation(operation.site, operation.continuation, position, {
|
|
1138
|
+
result: Object.freeze({ site: resultSite.site, schemaId: descriptor.result.schemaId, adoption: resultSite.adoption })
|
|
1139
|
+
}),
|
|
1140
|
+
terminates: true
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function parallelBindings(candidate, scope) {
|
|
1145
|
+
if (candidate.discard || !candidate.declaration) return Object.freeze([]);
|
|
1146
|
+
requireConst(candidate.statement);
|
|
1147
|
+
const name = candidate.declaration.name;
|
|
1148
|
+
if (ts.isIdentifier(name)) {
|
|
1149
|
+
const local = bindLocal(name.text, name, scope, 'parallel-result');
|
|
1150
|
+
return Object.freeze([Object.freeze({ id: local.id, name: local.name, key: null })]);
|
|
1151
|
+
}
|
|
1152
|
+
const bindings = [];
|
|
1153
|
+
for (const element of name.elements || []) {
|
|
1154
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
1155
|
+
const keyNode = element.propertyName || element.name;
|
|
1156
|
+
const key = ts.isIdentifier(keyNode) || ts.isStringLiteral(keyNode) ? keyNode.text : undefined;
|
|
1157
|
+
const local = bindLocal(element.name.text, element.name, scope, 'parallel-member');
|
|
1158
|
+
bindings.push(Object.freeze({ id: local.id, name: local.name, key }));
|
|
1159
|
+
}
|
|
1160
|
+
return Object.freeze(bindings);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function compileParallel(operation, scope, strategy) {
|
|
1164
|
+
if (operation.kind === 'fetch-group') {
|
|
1165
|
+
const bindings = [];
|
|
1166
|
+
for (const [index, candidate] of operation.candidates.entries()) {
|
|
1167
|
+
registerEffectRuntimeInputs(operation.sites[index], candidate, scope);
|
|
1168
|
+
requireConst(candidate.statement);
|
|
1169
|
+
const local = bindLocal(candidate.variableName, candidate.declaration.name, scope, 'effect-result');
|
|
1170
|
+
bindings.push(Object.freeze({ id: local.id, name: local.name, key: null }));
|
|
1171
|
+
}
|
|
1172
|
+
return Object.freeze({
|
|
1173
|
+
operation: createOperation('parallel', {
|
|
1174
|
+
strategy,
|
|
1175
|
+
effects: operation.sites,
|
|
1176
|
+
continuation: operation.continuation,
|
|
1177
|
+
bindings: Object.freeze(bindings),
|
|
1178
|
+
position: positionFor(sourceFile, operation.candidates[0].statement)
|
|
1179
|
+
}),
|
|
1180
|
+
terminates: false
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
for (const [index, member] of operation.candidate.members.entries()) {
|
|
1184
|
+
registerEffectRuntimeInputs(operation.sites[index], member, scope);
|
|
1185
|
+
}
|
|
1186
|
+
return Object.freeze({
|
|
1187
|
+
operation: createOperation('parallel', {
|
|
1188
|
+
strategy,
|
|
1189
|
+
effects: operation.sites,
|
|
1190
|
+
continuation: operation.continuation,
|
|
1191
|
+
bindings: parallelBindings(operation.candidate, scope),
|
|
1192
|
+
keys: Object.freeze(operation.candidate.members.map((entry) => entry.key)),
|
|
1193
|
+
position: positionFor(sourceFile, operation.candidate.statement)
|
|
1194
|
+
}),
|
|
1195
|
+
terminates: false
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function compileLogging(statement, scope) {
|
|
1200
|
+
if (!ts.isExpressionStatement(statement)) return undefined;
|
|
1201
|
+
const surface = recognizeHandlerSurface(statement.expression, { ctxName, strict: true, unwrap: true });
|
|
1202
|
+
if (!surface || !surface.surfaceId.startsWith('ctx.log.')) return undefined;
|
|
1203
|
+
state.loggingIndex += 1;
|
|
1204
|
+
const position = positionFor(sourceFile, statement.expression);
|
|
1205
|
+
const site = Object.freeze({
|
|
1206
|
+
id: `logging-${state.loggingIndex}`,
|
|
1207
|
+
capability: 'logging',
|
|
1208
|
+
level: surface.detail.level,
|
|
1209
|
+
position
|
|
1210
|
+
});
|
|
1211
|
+
state.loggingSites.push(site);
|
|
1212
|
+
return Object.freeze({
|
|
1213
|
+
operation: createOperation('logging', {
|
|
1214
|
+
site,
|
|
1215
|
+
arguments: Object.freeze(surface.detail.arguments.map((entry) => compileExpression(entry, scope))),
|
|
1216
|
+
position
|
|
1217
|
+
}),
|
|
1218
|
+
terminates: false
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function compileSourceStatement(statement, scope) {
|
|
1223
|
+
if (ts.isVariableStatement(statement)) {
|
|
1224
|
+
const namespace = compileNamespace(statement, scope);
|
|
1225
|
+
return namespace || compileVariable(statement, scope);
|
|
1226
|
+
}
|
|
1227
|
+
if (ts.isReturnStatement(statement)) return compileReturn(statement, scope);
|
|
1228
|
+
if (ts.isEmptyStatement(statement)) {
|
|
1229
|
+
return Object.freeze({ operation: createOperation('block', { statements: Object.freeze([]), position: positionFor(sourceFile, statement) }), terminates: false });
|
|
1230
|
+
}
|
|
1231
|
+
if (ts.isExpressionStatement(statement)) {
|
|
1232
|
+
const logging = compileLogging(statement, scope);
|
|
1233
|
+
if (logging) return logging;
|
|
1234
|
+
compileExpression(statement.expression, scope);
|
|
1235
|
+
fail(statement, 'PULSE_MANAGED_HANDLER_STATEMENT_UNSUPPORTED', 'Managed handlers accept canonical effects, logging, local declarations, if/else branches, and schema-bound returns only.');
|
|
1236
|
+
return Object.freeze({ operation: createOperation('rejected', { position: positionFor(sourceFile, statement), reason: 'expression-statement' }), terminates: false });
|
|
1237
|
+
}
|
|
1238
|
+
if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) {
|
|
1239
|
+
const capturesInput = containsIdentifier(statement, inputName);
|
|
1240
|
+
fail(statement, 'PULSE_MANAGED_HANDLER_CALLBACK_UNSUPPORTED', 'Nested functions, classes, and callbacks are outside managed handler lowering.');
|
|
1241
|
+
if (capturesInput) fail(statement, 'PULSE_MANAGED_HANDLER_INPUT_ESCAPE', 'The schema-bound input cannot be captured by a nested declaration.');
|
|
1242
|
+
return Object.freeze({ operation: createOperation('rejected', { position: positionFor(sourceFile, statement), reason: 'nested-declaration' }), terminates: false });
|
|
1243
|
+
}
|
|
1244
|
+
fail(statement, 'PULSE_MANAGED_HANDLER_CONTROL_FLOW_UNSUPPORTED', `Statement ${ts.SyntaxKind[statement.kind]} is outside managed handler lowering.`, { syntax: ts.SyntaxKind[statement.kind] });
|
|
1245
|
+
return Object.freeze({ operation: createOperation('rejected', { position: positionFor(sourceFile, statement), reason: 'control-flow' }), terminates: false });
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function compileOperation(operation, scope) {
|
|
1249
|
+
if (operation.kind === 'block') return compileOperationList(operation.statements, new Map(scope), operation.statement);
|
|
1250
|
+
if (operation.kind === 'if') {
|
|
1251
|
+
const test = compileExpression(operation.expression, scope);
|
|
1252
|
+
const thenResult = compileOperation(operation.thenOperation, new Map(scope));
|
|
1253
|
+
const elseResult = operation.elseOperation
|
|
1254
|
+
? compileOperation(operation.elseOperation, new Map(scope))
|
|
1255
|
+
: Object.freeze({ operation: undefined, terminates: false });
|
|
1256
|
+
return Object.freeze({
|
|
1257
|
+
operation: createOperation('if', {
|
|
1258
|
+
test,
|
|
1259
|
+
thenOperation: thenResult.operation || createOperation('block', { statements: Object.freeze([]), position: positionFor(sourceFile, operation.statement) }),
|
|
1260
|
+
elseOperation: elseResult.operation,
|
|
1261
|
+
position: positionFor(sourceFile, operation.statement)
|
|
1262
|
+
}),
|
|
1263
|
+
terminates: thenResult.terminates && elseResult.terminates
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (operation.kind === 'source-statement') return compileSourceStatement(operation.statement, scope);
|
|
1267
|
+
if (operation.kind === 'fetch-single' || operation.kind === 'provider-variable' || operation.kind === 'package-variable') {
|
|
1268
|
+
return compileBoundEffect(operation.candidate, operation.site, operation.continuation, scope);
|
|
1269
|
+
}
|
|
1270
|
+
if (operation.kind === 'provider-expression') {
|
|
1271
|
+
registerEffectRuntimeInputs(operation.site, operation.candidate, scope);
|
|
1272
|
+
return Object.freeze({
|
|
1273
|
+
operation: effectOperation(operation.site, operation.continuation, positionFor(sourceFile, operation.candidate.statement)),
|
|
1274
|
+
terminates: false
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
if (operation.kind === 'package-effect') {
|
|
1278
|
+
if (operation.returnResult) return compileEffectReturn(operation, scope);
|
|
1279
|
+
registerEffectRuntimeInputs(operation.site, Object.freeze({ effect: operation.effect, call: operation.call }), scope);
|
|
1280
|
+
return Object.freeze({
|
|
1281
|
+
operation: effectOperation(operation.site, operation.continuation, positionFor(sourceFile, operation.statement)),
|
|
1282
|
+
terminates: false
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (operation.kind === 'fetch-group') return compileParallel(operation, scope, 'implicit-independent-fetch-group');
|
|
1286
|
+
if (operation.kind === 'parallel-group') return compileParallel(operation, scope, 'explicit-keyed');
|
|
1287
|
+
if (operation.kind === 'opaque-fetch-return') {
|
|
1288
|
+
fail(operation.statement, 'PULSE_MANAGED_HANDLER_RESPONSE_RESULT_UNSUPPORTED', 'Managed schema operations cannot return an opaque fetch Response; the adapter owns completion framing.');
|
|
1289
|
+
return Object.freeze({ operation: createOperation('rejected', { position: positionFor(sourceFile, operation.statement), reason: 'opaque-response' }), terminates: true });
|
|
1290
|
+
}
|
|
1291
|
+
if (operation.kind === 'package-result-adapter') {
|
|
1292
|
+
fail(operation.statement, 'PULSE_MANAGED_HANDLER_RESPONSE_RESULT_UNSUPPORTED', 'Managed schema operations cannot invoke a package response adapter; the entity adapter owns completion framing.');
|
|
1293
|
+
return Object.freeze({ operation: createOperation('rejected', { position: positionFor(sourceFile, operation.statement), reason: 'package-result-adapter' }), terminates: true });
|
|
1294
|
+
}
|
|
1295
|
+
fail(handler, 'PULSE_MANAGED_HANDLER_CONTROL_FLOW_UNSUPPORTED', `Canonical Handler IR operation ${operation.kind} is outside managed handler lowering.`, { operationKind: operation.kind });
|
|
1296
|
+
return Object.freeze({ operation: createOperation('rejected', { position: positionFor(sourceFile, handler), reason: 'canonical-operation' }), terminates: false });
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function compileOperationList(operations, scope, blockNode) {
|
|
1300
|
+
const statements = [];
|
|
1301
|
+
let terminates = false;
|
|
1302
|
+
for (const operation of operations) {
|
|
1303
|
+
if (terminates) {
|
|
1304
|
+
fail(operation.statement || blockNode || handler, 'PULSE_MANAGED_HANDLER_UNREACHABLE', 'Statements after a managed handler result are unreachable.');
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
const result = compileOperation(operation, scope);
|
|
1308
|
+
if (result.operations) statements.push(...result.operations);
|
|
1309
|
+
else if (result.operation) statements.push(result.operation);
|
|
1310
|
+
terminates = result.terminates;
|
|
1311
|
+
}
|
|
1312
|
+
return Object.freeze({
|
|
1313
|
+
operation: createOperation('block', {
|
|
1314
|
+
statements: Object.freeze(statements),
|
|
1315
|
+
position: positionFor(sourceFile, blockNode || handler)
|
|
1316
|
+
}),
|
|
1317
|
+
terminates
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const compiledBody = compileOperation(rawOperationIr.body, rootScope);
|
|
1322
|
+
let body = compiledBody.operation;
|
|
1323
|
+
if (!compiledBody.terminates) {
|
|
1324
|
+
if (descriptor.result.kind === 'schema-value') {
|
|
1325
|
+
fail(handler.body, 'PULSE_MANAGED_HANDLER_RESULT_FALLTHROUGH', `Managed handler ${descriptor.id} must return schema ${descriptor.result.schemaId} on every path.`, { schemaId: descriptor.result.schemaId });
|
|
1326
|
+
} else {
|
|
1327
|
+
state.returnIndex += 1;
|
|
1328
|
+
const completion = createOperation('completion', {
|
|
1329
|
+
site: `result-${state.returnIndex}`,
|
|
1330
|
+
adoption: 'completion-only',
|
|
1331
|
+
implicit: true,
|
|
1332
|
+
position: positionFor(sourceFile, handler.body)
|
|
1333
|
+
});
|
|
1334
|
+
state.resultSites.push(completion);
|
|
1335
|
+
body = createOperation('block', {
|
|
1336
|
+
...body,
|
|
1337
|
+
statements: Object.freeze([...body.statements, completion])
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const capabilities = sortedUnique([
|
|
1343
|
+
...rawOperationIr.effectSites.map((entry) => entry.capability),
|
|
1344
|
+
...schemaReferences.map((entry) => entry.capability),
|
|
1345
|
+
...(state.loggingSites.length > 0 ? ['logging'] : [])
|
|
1346
|
+
]);
|
|
1347
|
+
const analysis = deepFreeze({
|
|
1348
|
+
capabilities,
|
|
1349
|
+
fetchCount: rawOperationIr.effectSites.filter((entry) => entry.kind === 'fetch').length,
|
|
1350
|
+
genericJsonCount: 0,
|
|
1351
|
+
schemaBoundJsonCount: 0,
|
|
1352
|
+
schemaReferences,
|
|
1353
|
+
providerOperations: Object.freeze(rawOperationIr.effectSites
|
|
1354
|
+
.filter((entry) => ['config', 'secret', 'kv'].includes(entry.providerKind))
|
|
1355
|
+
.map((entry) => Object.freeze({
|
|
1356
|
+
id: entry.id,
|
|
1357
|
+
kind: entry.providerKind,
|
|
1358
|
+
operation: entry.operation,
|
|
1359
|
+
capability: entry.capability,
|
|
1360
|
+
resource: entry.resource,
|
|
1361
|
+
position: entry.position
|
|
1362
|
+
})))
|
|
1363
|
+
});
|
|
1364
|
+
const operationIr = Object.freeze({ ...rawOperationIr, analysis });
|
|
1365
|
+
const source = Object.freeze({
|
|
1366
|
+
moduleId: resolved.graphModule.id,
|
|
1367
|
+
file: resolved.targetPath,
|
|
1368
|
+
exportName: descriptor.source.exportName,
|
|
1369
|
+
localName: resolved.symbol,
|
|
1370
|
+
position: positionFor(sourceFile, resolved.declaration)
|
|
1371
|
+
});
|
|
1372
|
+
const effectPlan = effectPlanForHandler({
|
|
1373
|
+
descriptor,
|
|
1374
|
+
source,
|
|
1375
|
+
operationIr,
|
|
1376
|
+
capabilities,
|
|
1377
|
+
packageEffects: packageInputs.effects,
|
|
1378
|
+
packageLowering: packageInputs.lowering
|
|
1379
|
+
});
|
|
1380
|
+
const semantic = {
|
|
1381
|
+
version: managedHandlerIrVersion,
|
|
1382
|
+
kind: 'managed-handler',
|
|
1383
|
+
id: descriptor.id,
|
|
1384
|
+
role: MANAGED_HANDLER_ROLE,
|
|
1385
|
+
source,
|
|
1386
|
+
parameters: Object.freeze({
|
|
1387
|
+
context: Object.freeze({ kind: 'managed-context', index: 0, name: ctxName }),
|
|
1388
|
+
input: Object.freeze({ ...descriptor.input, index: 1, name: inputName, binding: 'pre-bound' })
|
|
1389
|
+
}),
|
|
1390
|
+
result: Object.freeze({ ...descriptor.result, adoption: descriptor.result.kind === 'schema-value' ? 'schema-bound-value' : 'completion-only' }),
|
|
1391
|
+
normalization: normalizer.summary,
|
|
1392
|
+
body,
|
|
1393
|
+
locals: Object.freeze(state.locals),
|
|
1394
|
+
resultSites: Object.freeze(state.resultSites),
|
|
1395
|
+
canonical: handlerIrSnapshot(operationIr),
|
|
1396
|
+
effects: Object.freeze({
|
|
1397
|
+
count: operationIr.effectSites.length,
|
|
1398
|
+
sites: operationIr.effectSites,
|
|
1399
|
+
continuations: operationIr.continuationSites,
|
|
1400
|
+
logging: Object.freeze(state.loggingSites),
|
|
1401
|
+
packageOperations: Object.freeze(packageInputs.effects)
|
|
1402
|
+
}),
|
|
1403
|
+
effectPlan: effectPlan.plan,
|
|
1404
|
+
providerRequirements: effectPlan.providerRequirements,
|
|
1405
|
+
eligibility: Object.freeze({
|
|
1406
|
+
scope: 'handler-syntax',
|
|
1407
|
+
inspectionScope: 'declared-handler-effects',
|
|
1408
|
+
inspected: true,
|
|
1409
|
+
packageTargetPromotion: false,
|
|
1410
|
+
javascript: Object.freeze({ eligible: true, blockers: Object.freeze([]) }),
|
|
1411
|
+
native: Object.freeze({ eligible: true, blockers: Object.freeze([]) })
|
|
1412
|
+
})
|
|
1413
|
+
};
|
|
1414
|
+
const record = deepFreeze({ ...semantic, handlerHash: sha256Hex(stableStringify(semantic)) });
|
|
1415
|
+
const runtimeInputs = Object.freeze(operationIr.effectSites.map((site) => (
|
|
1416
|
+
state.effectRuntimeInputs.get(site.id)
|
|
1417
|
+
|| deepFreeze({ effectId: site.id, inputs: Object.freeze([]) })
|
|
1418
|
+
)));
|
|
1419
|
+
const nativeSemantic = {
|
|
1420
|
+
version: managedHandlerNativeFactsVersion,
|
|
1421
|
+
handlerIrVersion: managedHandlerIrVersion,
|
|
1422
|
+
id: record.id,
|
|
1423
|
+
source: record.source,
|
|
1424
|
+
parameters: record.parameters,
|
|
1425
|
+
result: record.result,
|
|
1426
|
+
body: record.body,
|
|
1427
|
+
locals: record.locals,
|
|
1428
|
+
resultSites: record.resultSites,
|
|
1429
|
+
effects: Object.freeze({
|
|
1430
|
+
count: record.effects.count,
|
|
1431
|
+
sites: record.effects.sites,
|
|
1432
|
+
continuations: record.effects.continuations,
|
|
1433
|
+
logging: record.effects.logging,
|
|
1434
|
+
runtimeInputs
|
|
1435
|
+
}),
|
|
1436
|
+
providerRequirements: record.providerRequirements,
|
|
1437
|
+
handlerHash: record.handlerHash
|
|
1438
|
+
};
|
|
1439
|
+
managedHandlerNativeFacts.set(record, deepFreeze({
|
|
1440
|
+
...nativeSemantic,
|
|
1441
|
+
nativeFactsHash: sha256Hex(stableStringify(nativeSemantic))
|
|
1442
|
+
}));
|
|
1443
|
+
canonicalHandlerIr.set(record, operationIr);
|
|
1444
|
+
return record;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function compileManagedHandlerDescriptors(inputs = {}) {
|
|
1448
|
+
if (!inputs || typeof inputs !== 'object') throw new TypeError('compileManagedHandlerDescriptors requires an input object.');
|
|
1449
|
+
const rawDescriptors = inputs.descriptors;
|
|
1450
|
+
if (!Array.isArray(rawDescriptors) || rawDescriptors.length === 0) {
|
|
1451
|
+
throw new TypeError('compileManagedHandlerDescriptors requires at least one descriptor.');
|
|
1452
|
+
}
|
|
1453
|
+
const descriptors = Object.freeze(rawDescriptors.map(normalizeDescriptor));
|
|
1454
|
+
const ids = new Set();
|
|
1455
|
+
for (const descriptor of descriptors) {
|
|
1456
|
+
if (ids.has(descriptor.id)) throw new TypeError(`Managed handler descriptor ID ${descriptor.id} is duplicated.`);
|
|
1457
|
+
ids.add(descriptor.id);
|
|
1458
|
+
}
|
|
1459
|
+
const packageRecognition = inputs.packageOperationRecognition;
|
|
1460
|
+
if (packageRecognition !== undefined && (!packageRecognition || packageRecognition.version !== PACKAGE_OPERATION_RECOGNITION_VERSION)) {
|
|
1461
|
+
throw new TypeError(`packageOperationRecognition must be ${PACKAGE_OPERATION_RECOGNITION_VERSION}.`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const diagnostics = [];
|
|
1465
|
+
const resolved = resolveDescriptors(inputs.graphBuild, descriptors, diagnostics);
|
|
1466
|
+
const handlers = [];
|
|
1467
|
+
for (const entry of resolved) {
|
|
1468
|
+
const handler = compileHandler(entry, inputs.graphBuild, resolved, diagnostics, packageRecognition);
|
|
1469
|
+
if (handler) handlers.push(handler);
|
|
1470
|
+
}
|
|
1471
|
+
if (diagnostics.length > 0) {
|
|
1472
|
+
const normalized = normalizeDiagnostics(diagnostics);
|
|
1473
|
+
throw new ManagedHandlerCompileError(`Managed handler compilation failed with ${normalized.length} diagnostic(s).`, normalized);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
handlers.sort((left, right) => left.id.localeCompare(right.id) || left.handlerHash.localeCompare(right.handlerHash));
|
|
1477
|
+
const reachableCapabilities = sortedUnique(handlers.flatMap((entry) => entry.providerRequirements.capabilities));
|
|
1478
|
+
const reachableProviderKinds = sortedUnique(handlers.flatMap((entry) => entry.providerRequirements.providerKinds));
|
|
1479
|
+
const reachablePackages = Object.freeze(handlers.flatMap((entry) => entry.providerRequirements.packages
|
|
1480
|
+
.map((identity) => Object.freeze({ handlerId: entry.id, ...identity })))
|
|
1481
|
+
.sort((left, right) => left.handlerId.localeCompare(right.handlerId)
|
|
1482
|
+
|| left.contractId.localeCompare(right.contractId)
|
|
1483
|
+
|| left.package.localeCompare(right.package)));
|
|
1484
|
+
const reachability = Object.freeze({
|
|
1485
|
+
version: managedHandlerReachabilityVersion,
|
|
1486
|
+
scope: 'declared-reachable-managed-handlers',
|
|
1487
|
+
graphHash: inputs.graphBuild.graph.graphHash,
|
|
1488
|
+
handlers: Object.freeze(handlers.map((entry) => Object.freeze({
|
|
1489
|
+
id: entry.id,
|
|
1490
|
+
source: entry.source,
|
|
1491
|
+
handlerHash: entry.handlerHash,
|
|
1492
|
+
effectPlanHash: entry.effectPlan.planHash
|
|
1493
|
+
}))),
|
|
1494
|
+
capabilities: reachableCapabilities,
|
|
1495
|
+
providerKinds: reachableProviderKinds,
|
|
1496
|
+
packages: reachablePackages,
|
|
1497
|
+
policy: Object.freeze({
|
|
1498
|
+
staticHandlerTable: true,
|
|
1499
|
+
selectedHandlerExecutesAtRuntime: true,
|
|
1500
|
+
undeclaredHandlersExcluded: true,
|
|
1501
|
+
recursiveDispatch: false,
|
|
1502
|
+
dynamicDispatch: false
|
|
1503
|
+
})
|
|
1504
|
+
});
|
|
1505
|
+
const semantic = {
|
|
1506
|
+
version: managedHandlerIrBundleVersion,
|
|
1507
|
+
descriptorVersion: managedHandlerDescriptorVersion,
|
|
1508
|
+
handlerIrVersion: managedHandlerIrVersion,
|
|
1509
|
+
role: MANAGED_HANDLER_ROLE,
|
|
1510
|
+
graphHash: inputs.graphBuild.graph.graphHash,
|
|
1511
|
+
handlers: Object.freeze(handlers),
|
|
1512
|
+
reachability,
|
|
1513
|
+
summary: Object.freeze({
|
|
1514
|
+
handlers: handlers.length,
|
|
1515
|
+
schemaInputs: handlers.filter((entry) => entry.parameters.input.kind === 'schema-value').length,
|
|
1516
|
+
schemaResults: handlers.filter((entry) => entry.result.kind === 'schema-value').length,
|
|
1517
|
+
completions: handlers.filter((entry) => entry.result.kind === 'completion').length,
|
|
1518
|
+
effects: handlers.reduce((sum, entry) => sum + entry.effects.count, 0),
|
|
1519
|
+
continuations: handlers.reduce((sum, entry) => sum + entry.effects.continuations.length, 0),
|
|
1520
|
+
capabilities: reachableCapabilities.length,
|
|
1521
|
+
nativeEligible: handlers.filter((entry) => entry.eligibility.native.eligible).length
|
|
1522
|
+
})
|
|
1523
|
+
};
|
|
1524
|
+
return deepFreeze({ ...semantic, bundleHash: sha256Hex(stableStringify(semantic)) });
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function canonicalHandlerIrForManagedHandler(handler) {
|
|
1528
|
+
return handler && typeof handler === 'object' ? canonicalHandlerIr.get(handler) : undefined;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function compileManagedHandlerNativeBundle(bundle) {
|
|
1532
|
+
if (!bundle || typeof bundle !== 'object' || bundle.version !== managedHandlerIrBundleVersion) {
|
|
1533
|
+
throw new TypeError(`compileManagedHandlerNativeBundle requires ${managedHandlerIrBundleVersion}.`);
|
|
1534
|
+
}
|
|
1535
|
+
if (!Array.isArray(bundle.handlers)) {
|
|
1536
|
+
throw new TypeError('compileManagedHandlerNativeBundle requires managed handlers.');
|
|
1537
|
+
}
|
|
1538
|
+
const handlers = bundle.handlers.map((handler) => {
|
|
1539
|
+
const facts = managedHandlerNativeFacts.get(handler);
|
|
1540
|
+
if (!facts) {
|
|
1541
|
+
const error = new TypeError(`Managed handler ${String(handler && handler.id || '<unknown>')} has no private Native projection facts.`);
|
|
1542
|
+
error.code = 'PULSE_MANAGED_HANDLER_NATIVE_FACTS_UNAVAILABLE';
|
|
1543
|
+
throw error;
|
|
1544
|
+
}
|
|
1545
|
+
if (!handler.eligibility || !handler.eligibility.native || handler.eligibility.native.eligible !== true) {
|
|
1546
|
+
const error = new TypeError(`Managed handler ${handler.id} is not eligible for Native lowering.`);
|
|
1547
|
+
error.code = 'PULSE_MANAGED_HANDLER_NATIVE_INELIGIBLE';
|
|
1548
|
+
throw error;
|
|
1549
|
+
}
|
|
1550
|
+
return facts;
|
|
1551
|
+
}).sort((left, right) => left.id.localeCompare(right.id) || left.nativeFactsHash.localeCompare(right.nativeFactsHash));
|
|
1552
|
+
const capabilities = sortedUnique(handlers.flatMap((handler) => handler.providerRequirements.capabilities));
|
|
1553
|
+
const providerKinds = sortedUnique(handlers.flatMap((handler) => handler.providerRequirements.providerKinds));
|
|
1554
|
+
const semantic = {
|
|
1555
|
+
version: managedHandlerNativeBundleVersion,
|
|
1556
|
+
handlerIrBundleVersion: managedHandlerIrBundleVersion,
|
|
1557
|
+
handlerIrVersion: managedHandlerIrVersion,
|
|
1558
|
+
nativeFactsVersion: managedHandlerNativeFactsVersion,
|
|
1559
|
+
graphHash: bundle.graphHash,
|
|
1560
|
+
handlers: Object.freeze(handlers),
|
|
1561
|
+
reachability: bundle.reachability,
|
|
1562
|
+
capabilities,
|
|
1563
|
+
providerKinds,
|
|
1564
|
+
summary: Object.freeze({
|
|
1565
|
+
handlers: handlers.length,
|
|
1566
|
+
effects: handlers.reduce((sum, handler) => sum + handler.effects.count, 0),
|
|
1567
|
+
continuations: handlers.reduce((sum, handler) => sum + handler.effects.continuations.length, 0),
|
|
1568
|
+
runtimeInputs: handlers.reduce((sum, handler) => sum + handler.effects.runtimeInputs.reduce((count, effect) => count + effect.inputs.length, 0), 0),
|
|
1569
|
+
capabilities: capabilities.length
|
|
1570
|
+
}),
|
|
1571
|
+
policy: Object.freeze({
|
|
1572
|
+
genericDataOnly: true,
|
|
1573
|
+
sourceAstExcluded: true,
|
|
1574
|
+
packageSemanticsExcluded: true,
|
|
1575
|
+
publicHandlerIrVersionFrozen: true,
|
|
1576
|
+
automaticFallback: false
|
|
1577
|
+
})
|
|
1578
|
+
};
|
|
1579
|
+
return deepFreeze({ ...semantic, bundleHash: sha256Hex(stableStringify(semantic)) });
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
module.exports = Object.freeze({
|
|
1583
|
+
MANAGED_HANDLER_DESCRIPTOR_VERSION: managedHandlerDescriptorVersion,
|
|
1584
|
+
MANAGED_HANDLER_IR_VERSION: managedHandlerIrVersion,
|
|
1585
|
+
MANAGED_HANDLER_IR_BUNDLE_VERSION: managedHandlerIrBundleVersion,
|
|
1586
|
+
MANAGED_HANDLER_EFFECT_PLAN_VERSION: managedHandlerEffectPlanVersion,
|
|
1587
|
+
MANAGED_HANDLER_REACHABILITY_VERSION: managedHandlerReachabilityVersion,
|
|
1588
|
+
MANAGED_HANDLER_NATIVE_FACTS_VERSION: managedHandlerNativeFactsVersion,
|
|
1589
|
+
MANAGED_HANDLER_NATIVE_BUNDLE_VERSION: managedHandlerNativeBundleVersion,
|
|
1590
|
+
MANAGED_HANDLER_ROLE,
|
|
1591
|
+
MANAGED_HANDLER_INPUT_KINDS: managedHandlerInputKinds,
|
|
1592
|
+
MANAGED_HANDLER_RESULT_KINDS: managedHandlerResultKinds,
|
|
1593
|
+
MANAGED_HANDLER_OPERATION_KINDS: managedHandlerOperationKinds,
|
|
1594
|
+
ManagedHandlerCompileError,
|
|
1595
|
+
compileManagedHandlerDescriptors,
|
|
1596
|
+
compileManagedHandlerNativeBundle,
|
|
1597
|
+
canonicalHandlerIrForManagedHandler
|
|
1598
|
+
});
|