@pulse-compute/wasm-compiler 1.0.0-beta.4 → 1.0.0-beta.6
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 +14 -3
- package/bin/provider-proof-composition.js +29 -31
- package/bin/pulsewasm-extract.js +1 -1
- package/package.json +9 -11
- package/src/canonical-api-compiler.js +1 -9
- package/src/canonical-native-compiler.js +40 -62
- package/src/canonical-native-plan.js +513 -81
- package/src/canonical-project-compiler.js +52 -12
- package/src/execution-plan.js +1 -0
- package/src/extractor.js +36 -0
- package/src/javascript-application-plan.js +4 -1
- package/src/native-result-facts.js +50 -0
- package/src/project/router-module-linker.js +64 -0
- package/src/project-target-support.js +10 -2
- package/src/provider-toolchain.js +29 -5
- package/src/pure-helper-values.js +138 -0
- package/src/shared-stage-plan.js +239 -0
- package/src/source-helper-plan.js +482 -0
- package/src/spine/async-surface-normalizer.js +17 -1
- package/src/spine/bounded-pure-loop.js +210 -0
- package/src/spine/canonical-handler-ir.js +3 -1
- package/src/spine/guest-unit-stage.js +11 -2
- package/src/spine/handler-ir-emitter.js +20 -0
- package/src/spine/handler-ir.js +129 -26
- package/src/spine/handler-surface-authority.js +13 -0
- package/src/spine/incoming-body-source.js +48 -0
- package/src/spine/plain-handler-frontend.js +17 -4
- package/src/spine/pure-helper-source.js +123 -0
- package/src/spine/router-handler-frontend.js +39 -2
- package/src/spine/router-handler-ir.js +71 -3
- package/src/spine/router-topology-frontend.js +3 -1
- package/src/spine/terminal-package-native.js +37 -0
|
@@ -1,56 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
|
+
const { terminalPackageNativeForCompiled } = require('./spine/terminal-package-native.js');
|
|
4
5
|
const fs = require('node:fs');
|
|
5
6
|
const path = require('node:path');
|
|
6
7
|
const ts = require('typescript');
|
|
7
8
|
const { executeCanonicalNativePlanSpine } = require('./spine/canonical-native-plan.js');
|
|
9
|
+
const { inspectBoundedPureLoop, inspectBoundedReadLoop } = require('./spine/bounded-pure-loop.js');
|
|
10
|
+
const resultFacts = require('./native-result-facts.js');
|
|
8
11
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
13
|
-
return require('../../contracts/src/handler/canonical-native-plan.js');
|
|
14
|
-
}
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function loadLoggingContract() {
|
|
20
|
-
try { return require('@pulse-compute/wasm-contracts/logging'); }
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
23
|
-
return require('../../contracts/src/logging.js');
|
|
24
|
-
}
|
|
25
|
-
throw error;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function loadCryptoContract() {
|
|
30
|
-
try { return require('@pulse-compute/wasm-contracts/crypto/contracts'); }
|
|
31
|
-
catch (error) {
|
|
32
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
33
|
-
return require('../../contracts/src/crypto/contracts.js');
|
|
34
|
-
}
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function loadEventContract() {
|
|
40
|
-
try { return require('@pulse-compute/wasm-contracts/events'); }
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
43
|
-
return require('../../contracts/src/events/contracts.js');
|
|
44
|
-
}
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
12
|
+
function containsYield(node) {
|
|
13
|
+
if (ts.isYieldExpression(node)) return true;
|
|
14
|
+
return Boolean(ts.forEachChild(node, child => containsYield(child) || undefined));
|
|
47
15
|
}
|
|
48
16
|
|
|
49
|
-
const contract =
|
|
50
|
-
const loggingContract =
|
|
51
|
-
const cryptoContract =
|
|
52
|
-
const eventContract =
|
|
53
|
-
const
|
|
17
|
+
const contract = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
|
|
18
|
+
const loggingContract = require('@pulse-compute/wasm-contracts/logging');
|
|
19
|
+
const cryptoContract = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
20
|
+
const eventContract = require('@pulse-compute/wasm-contracts/events');
|
|
21
|
+
const pureValues = require('./pure-helper-values');
|
|
22
|
+
const CANONICAL_NATIVE_PLAN_COMPILER_VERSION = 'pulse.canonical-native-plan-compiler.v10';
|
|
23
|
+
const { lowerSharedStages, validateSharedStages } = require('./shared-stage-plan');
|
|
54
24
|
const GENERATED_HANDLER_NAME = '__pulse_handler';
|
|
55
25
|
const PULSE_RUNTIME_PARAMETER = '__pulse';
|
|
56
26
|
|
|
@@ -200,9 +170,12 @@ function intrinsicForContextCall(parts) {
|
|
|
200
170
|
const entries = {
|
|
201
171
|
'req.header': ['request.header', 'string-or-undefined'],
|
|
202
172
|
'req.text': ['request.text', 'string'],
|
|
173
|
+
'req.body': ['request.body.forward-marker', 'incoming-body-marker'],
|
|
174
|
+
'output.close': ['response.output.close', 'pulse-result'],
|
|
203
175
|
'req.json': ['request.json', 'json'],
|
|
204
176
|
json: ['response.json', 'pulse-result'],
|
|
205
177
|
encodeJson: ['schema.encode.text', 'string'],
|
|
178
|
+
decodeJson: ['schema.decode.text', 'json'],
|
|
206
179
|
text: ['response.text', 'pulse-result'],
|
|
207
180
|
response: ['response.custom', 'pulse-result'],
|
|
208
181
|
kv: ['kv.namespace', 'kv-namespace'],
|
|
@@ -219,8 +192,11 @@ function resultKindForEffect(site, decoder, continuation, resultMode) {
|
|
|
219
192
|
if (site.result === 'opaque-response') return 'opaque-response';
|
|
220
193
|
if (site.result === 'structured-response') return 'structured-response';
|
|
221
194
|
if (site.result === 'ack') return 'ack';
|
|
195
|
+
if (site.result === 'string') return 'string';
|
|
222
196
|
if (site.kind === 'fetch') return 'fetch-response';
|
|
223
197
|
if (site.kind === 'config.get' || site.kind === 'secret.get') return 'string-or-undefined';
|
|
198
|
+
if (site.kind === 'output.readTextChunk') return 'json';
|
|
199
|
+
if (site.kind === 'time.now') return 'json';
|
|
224
200
|
if (site.kind === 'kv.get') return 'json-or-undefined';
|
|
225
201
|
if (site.kind === 'kv.put') return 'ack';
|
|
226
202
|
if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(site.kind)) return 'json';
|
|
@@ -228,21 +204,6 @@ function resultKindForEffect(site, decoder, continuation, resultMode) {
|
|
|
228
204
|
return 'unknown';
|
|
229
205
|
}
|
|
230
206
|
|
|
231
|
-
function inferBinaryValueKind(operator, left, right) {
|
|
232
|
-
if (['===', '!==', '==', '!=', '<', '<=', '>', '>=', 'in'].includes(operator)) return 'boolean';
|
|
233
|
-
if (['&&', '||', '??'].includes(operator)) {
|
|
234
|
-
if (left.valueKind === right.valueKind) return left.valueKind;
|
|
235
|
-
if (['||', '??'].includes(operator)) {
|
|
236
|
-
const kinds = new Set([left.valueKind, right.valueKind]);
|
|
237
|
-
if (kinds.has('string') && kinds.has('string-or-undefined')) return 'string';
|
|
238
|
-
}
|
|
239
|
-
return 'unknown';
|
|
240
|
-
}
|
|
241
|
-
if (operator === '+' && (left.valueKind === 'string' || right.valueKind === 'string')) return 'string';
|
|
242
|
-
if (['+', '-', '*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'].includes(operator)) return 'number';
|
|
243
|
-
return 'unknown';
|
|
244
|
-
}
|
|
245
|
-
|
|
246
207
|
class NativePlanBuilder {
|
|
247
208
|
constructor(compiled, options = {}) {
|
|
248
209
|
this.compiled = compiled;
|
|
@@ -250,6 +211,9 @@ class NativePlanBuilder {
|
|
|
250
211
|
this.diagnostics = [];
|
|
251
212
|
this.effects = [];
|
|
252
213
|
this.locals = [];
|
|
214
|
+
this.handlers = [];
|
|
215
|
+
this.helpers = [];
|
|
216
|
+
this.handlerLocalIndex = 0;
|
|
253
217
|
this.effectOccurrences = new Map();
|
|
254
218
|
this.continuationOccurrences = new Map();
|
|
255
219
|
this.localIndex = 0;
|
|
@@ -326,6 +290,20 @@ class NativePlanBuilder {
|
|
|
326
290
|
}
|
|
327
291
|
|
|
328
292
|
this.metadata = compiled.metadata;
|
|
293
|
+
this.pureTypes = new Map();
|
|
294
|
+
this.pureTypes.schemas = compiled.metadata.schemaRegistry;
|
|
295
|
+
this.helperBodies = new Map((this.metadata.router?.helpers || []).map(helper => [helper.name, helper]));
|
|
296
|
+
if (this.metadata.capabilities?.some(id => ['request.body.forward', 'request.body.transform'].includes(id))
|
|
297
|
+
&& this.metadata.capabilities.some(id => ['request.text', 'request.json'].includes(id))) {
|
|
298
|
+
throw new CanonicalNativePlanError('Native incoming forwarding/transforms cannot be combined with structured request reads.', [
|
|
299
|
+
diagnostic(undefined, undefined, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED,
|
|
300
|
+
'Native forwarding/transform applications cannot also project structured request bodies.', { automaticFallback: false })
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
this.packageApplication = terminalPackageNativeForCompiled(compiled);
|
|
304
|
+
this.routerBodies = new Map((this.metadata.router?.entries || [])
|
|
305
|
+
.filter(entry => entry.nativeBody).map(entry => [entry.nativeBody.name, entry]));
|
|
306
|
+
this.bodyNodes = new Map();
|
|
329
307
|
this.generatedFile = `${this.metadata.file || 'app.ts'}.canonical.generated.js`;
|
|
330
308
|
this.sourceFile = ts.createSourceFile(
|
|
331
309
|
this.generatedFile,
|
|
@@ -345,6 +323,14 @@ class NativePlanBuilder {
|
|
|
345
323
|
this.ctxName = this.handler && this.handler.parameters[0] && ts.isIdentifier(this.handler.parameters[0].name)
|
|
346
324
|
? this.handler.parameters[0].name.text
|
|
347
325
|
: String(this.metadata.ctxParameter || 'ctx');
|
|
326
|
+
const collectBodies = node => {
|
|
327
|
+
if (ts.isFunctionDeclaration(node) && (this.routerBodies.has(node.name?.text) || this.helperBodies.has(node.name?.text))) {
|
|
328
|
+
if (this.bodyNodes.has(node.name.text)) this.fail(node, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, 'Duplicate private Router body.');
|
|
329
|
+
this.bodyNodes.set(node.name.text, node);
|
|
330
|
+
}
|
|
331
|
+
ts.forEachChild(node, collectBodies);
|
|
332
|
+
};
|
|
333
|
+
if (this.handler) collectBodies(this.handler);
|
|
348
334
|
this.effectSites = new Map((this.metadata.effectSites || []).map((site) => [String(site.id), site]));
|
|
349
335
|
this.continuationSites = new Map((this.metadata.continuationSites || []).map((site) => [String(site.id), site]));
|
|
350
336
|
this.compilerOwnedCalls = new Set((this.metadata.compilerOwnedCalls || []).map(String));
|
|
@@ -443,8 +429,13 @@ class NativePlanBuilder {
|
|
|
443
429
|
if (ts.isBinaryExpression(current)) {
|
|
444
430
|
const operator = current.operatorToken.getText(this.sourceFile);
|
|
445
431
|
if (ASSIGNMENT_OPERATORS.has(operator)) {
|
|
446
|
-
const target = this.
|
|
432
|
+
const target = this.assignmentTarget(current.left, scope);
|
|
447
433
|
const value = this.expression(current.right, scope);
|
|
434
|
+
if (operator === '=' && target.kind === 'local') {
|
|
435
|
+
const type = pureValues.readType(value, this.pureTypes, this.activeHelper?.pure === true);
|
|
436
|
+
if (type) this.pureTypes.set(target.id, type);
|
|
437
|
+
else this.pureTypes.delete(target.id);
|
|
438
|
+
}
|
|
448
439
|
return Object.freeze({ kind: 'assignment', operator, target, value, valueKind: value.valueKind || 'unknown' });
|
|
449
440
|
}
|
|
450
441
|
if (!PURE_BINARY_OPERATORS.has(operator)) {
|
|
@@ -452,25 +443,25 @@ class NativePlanBuilder {
|
|
|
452
443
|
}
|
|
453
444
|
const left = this.expression(current.left, scope);
|
|
454
445
|
const right = this.expression(current.right, scope);
|
|
455
|
-
return Object.freeze({ kind: 'binary', operator, left, right, valueKind:
|
|
446
|
+
return Object.freeze({ kind: 'binary', operator, left, right, valueKind: pureValues.binaryKind(operator, left.valueKind, right.valueKind) });
|
|
456
447
|
}
|
|
457
448
|
|
|
458
449
|
if (ts.isPrefixUnaryExpression(current)) {
|
|
459
450
|
const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(0, 1);
|
|
460
451
|
if (UPDATE_OPERATORS.has(operator)) {
|
|
461
|
-
return Object.freeze({ kind: 'update', operator, prefix: true, target: this.
|
|
452
|
+
return Object.freeze({ kind: 'update', operator, prefix: true, target: this.assignmentTarget(current.operand, scope), valueKind: 'number' });
|
|
462
453
|
}
|
|
463
454
|
if (!PREFIX_OPERATORS.has(operator)) {
|
|
464
455
|
this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Prefix operator ${operator} is outside the canonical native value model.`, { operator });
|
|
465
456
|
}
|
|
466
457
|
const value = this.expression(current.operand, scope);
|
|
467
|
-
return Object.freeze({ kind: 'unary', operator, value, valueKind:
|
|
458
|
+
return Object.freeze({ kind: 'unary', operator, value, valueKind: pureValues.unaryKind(operator) });
|
|
468
459
|
}
|
|
469
460
|
|
|
470
461
|
if (ts.isPostfixUnaryExpression(current)) {
|
|
471
462
|
const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(-2);
|
|
472
463
|
if (!UPDATE_OPERATORS.has(operator)) this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Postfix operator ${operator} is outside the canonical native value model.`, { operator });
|
|
473
|
-
return Object.freeze({ kind: 'update', operator, prefix: false, target: this.
|
|
464
|
+
return Object.freeze({ kind: 'update', operator, prefix: false, target: this.assignmentTarget(current.operand, scope), valueKind: 'number' });
|
|
474
465
|
}
|
|
475
466
|
|
|
476
467
|
if (ts.isConditionalExpression(current)) {
|
|
@@ -509,17 +500,19 @@ class NativePlanBuilder {
|
|
|
509
500
|
else if (current.name.text === 'headers') valueKind = 'headers';
|
|
510
501
|
else this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Fetch response property .${current.name.text} is outside the canonical native response contract.`, { property: current.name.text });
|
|
511
502
|
}
|
|
512
|
-
|
|
503
|
+
const property = { kind: 'property', object, property: current.name.text, valueKind };
|
|
504
|
+
const type = pureValues.readType(property, this.pureTypes);
|
|
505
|
+
if (type) property.valueKind = pureValues.kind(type);
|
|
506
|
+
property.valueKind = resultFacts.propertyKind(object, current.name.text, scope) || property.valueKind;
|
|
507
|
+
return Object.freeze(property);
|
|
513
508
|
}
|
|
514
509
|
|
|
515
510
|
if (ts.isElementAccessExpression(current)) {
|
|
516
511
|
if (current.questionDotToken) this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Optional element access is outside the canonical native value model.');
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
valueKind: 'unknown'
|
|
522
|
-
});
|
|
512
|
+
const element = { kind: 'element', object: this.expression(current.expression, scope), index: this.expression(current.argumentExpression, scope), valueKind: 'unknown' };
|
|
513
|
+
const type = pureValues.readType(element, this.pureTypes, this.activeHelper?.pure === true);
|
|
514
|
+
if (type) element.valueKind = pureValues.kind(type);
|
|
515
|
+
return Object.freeze(element);
|
|
523
516
|
}
|
|
524
517
|
|
|
525
518
|
if (ts.isTypeOfExpression(current)) {
|
|
@@ -546,6 +539,21 @@ class NativePlanBuilder {
|
|
|
546
539
|
callExpression(call, scope) {
|
|
547
540
|
if (call.questionDotToken) this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Optional calls are outside the canonical native value model.');
|
|
548
541
|
const target = unwrap(call.expression);
|
|
542
|
+
const helper = ts.isIdentifier(target) && this.helperBodies.get(target.text);
|
|
543
|
+
if (helper?.pure) {
|
|
544
|
+
if (this.activeHelper || this.pureCallDepth) {
|
|
545
|
+
this.fail(call, 'PULSE_NATIVE_PURE_HELPER_NESTING_UNSUPPORTED', 'Pure helper calls cannot nest in helpers or call arguments.');
|
|
546
|
+
}
|
|
547
|
+
this.pureCallDepth = (this.pureCallDepth || 0) + 1;
|
|
548
|
+
const args = call.arguments.map(arg => this.expression(arg, scope));
|
|
549
|
+
this.pureCallDepth--;
|
|
550
|
+
if (args.length !== helper.parameters.length || args.some((arg, i) => arg.valueKind !== helper.parameters[i]?.valueKind && !(helper.parameters[i]?.borrow && arg.valueKind === 'json'))) {
|
|
551
|
+
this.fail(call, 'PULSE_NATIVE_PURE_HELPER_ARGUMENT_UNSUPPORTED', 'Pure helper arguments must match their declared scalar or borrowed structural kinds.', { arguments: args.map((arg,i) => ({ index:i, actual:arg.valueKind, expected:helper.parameters[i]?.valueKind })) });
|
|
552
|
+
}
|
|
553
|
+
return Object.freeze({ kind: 'pure-helper-call', helperId: helper.id,
|
|
554
|
+
...((this.pureLoopDepth || this.readLoopDepth) ? { loopContract: contract.CANONICAL_NATIVE_PURE_LOOP_CALL_VERSION } : {}),
|
|
555
|
+
arguments: Object.freeze(args), valueKind: helper.resultKind });
|
|
556
|
+
}
|
|
549
557
|
const ctxPath = contextPath(target, this.ctxName);
|
|
550
558
|
if (ctxPath) {
|
|
551
559
|
const intrinsic = intrinsicForContextCall(ctxPath);
|
|
@@ -576,6 +584,9 @@ class NativePlanBuilder {
|
|
|
576
584
|
|
|
577
585
|
if (ts.isIdentifier(target) && this.compilerOwnedCalls.has(target.text) && this.compilerOwnedIntrinsics.has(target.text)) {
|
|
578
586
|
const intrinsic = this.compilerOwnedIntrinsics.get(target.text);
|
|
587
|
+
if (this.packageApplication?.intrinsic === intrinsic[0]) return Object.freeze({
|
|
588
|
+
kind: 'intrinsic', name: 'package.application', arguments: Object.freeze([]), valueKind: 'response'
|
|
589
|
+
});
|
|
579
590
|
return Object.freeze({
|
|
580
591
|
kind: 'intrinsic',
|
|
581
592
|
name: intrinsic[0],
|
|
@@ -587,6 +598,9 @@ class NativePlanBuilder {
|
|
|
587
598
|
if (ts.isPropertyAccessExpression(target)) {
|
|
588
599
|
const receiver = this.expression(target.expression, scope);
|
|
589
600
|
const method = target.name.text;
|
|
601
|
+
if (method === 'trim' && !target.questionDotToken && !call.questionDotToken && call.arguments.length === 0) {
|
|
602
|
+
return Object.freeze({ kind: 'method-call', receiver, method: 'string.trim', arguments: Object.freeze([]), valueKind: 'string' });
|
|
603
|
+
}
|
|
590
604
|
if (!FETCH_RESPONSE_METHODS.has(method) || receiver.valueKind !== 'fetch-response') {
|
|
591
605
|
this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Method call .${method}() is outside the canonical native fetch-response contract.`, { method, receiverKind: receiver.valueKind });
|
|
592
606
|
}
|
|
@@ -607,16 +621,28 @@ class NativePlanBuilder {
|
|
|
607
621
|
return Object.freeze({ kind: 'intrinsic', name: 'unsupported:call', arguments: Object.freeze([]), valueKind: 'unknown' });
|
|
608
622
|
}
|
|
609
623
|
|
|
624
|
+
assignmentTarget(node, scope) {
|
|
625
|
+
const target = this.expression(node, scope);
|
|
626
|
+
if (target.kind === 'property' || target.kind === 'element') resultFacts.invalidate(scope);
|
|
627
|
+
if (target.kind === 'local' && scope.get(target.name)?.declaration === 'const') {
|
|
628
|
+
this.fail(node, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'A const local cannot be reassigned.', { name: target.name });
|
|
629
|
+
}
|
|
630
|
+
return target;
|
|
631
|
+
}
|
|
632
|
+
|
|
610
633
|
allocateLocal(name, valueKind, statementPath, declaration) {
|
|
611
634
|
this.localIndex += 1;
|
|
612
635
|
const local = Object.freeze({
|
|
613
|
-
id: `local-${this.localIndex}`,
|
|
636
|
+
id: this.activeHandler ? `local:${this.activeHandler.stableId}:${++this.handlerLocalIndex}` : `local-${this.localIndex}`,
|
|
637
|
+
scopeId: this.activeHelper?.id || (this.activeHandler ? this.activeHandler.stableId : 'entry'),
|
|
638
|
+
...(this.activeHandler ? { routerEntryStableId: this.activeHandler.stableId } : {}),
|
|
614
639
|
name: String(name),
|
|
615
640
|
valueKind: contract.CANONICAL_NATIVE_VALUE_KINDS.includes(valueKind) ? valueKind : 'unknown',
|
|
616
641
|
declaration,
|
|
617
642
|
statementPath
|
|
618
643
|
});
|
|
619
644
|
this.locals.push(local);
|
|
645
|
+
if (pureValues.scalar(valueKind)) this.pureTypes.set(local.id, valueKind);
|
|
620
646
|
this.summary.localCount += 1;
|
|
621
647
|
return local;
|
|
622
648
|
}
|
|
@@ -689,6 +715,7 @@ class NativePlanBuilder {
|
|
|
689
715
|
const record = Object.freeze({
|
|
690
716
|
order: this.effectOrder,
|
|
691
717
|
id: effectId || `invalid-effect-${this.effectOrder}`,
|
|
718
|
+
...(this.activeHelper ? { helperId: this.activeHelper.id } : {}),
|
|
692
719
|
kind: kind || 'invalid',
|
|
693
720
|
providerKind: site && site.providerKind ? String(site.providerKind) : undefined,
|
|
694
721
|
operation: site && site.operation ? String(site.operation) : undefined,
|
|
@@ -700,6 +727,7 @@ class NativePlanBuilder {
|
|
|
700
727
|
contractId: site && site.contractId ? String(site.contractId) : undefined,
|
|
701
728
|
declaredResult: site && site.result ? String(site.result) : undefined,
|
|
702
729
|
decoder: site && site.decoder ? String(site.decoder) : null,
|
|
730
|
+
...(site?.borrowedValue && result?.mode === 'bind' ? { borrowedValue: cloneJson(site.borrowedValue) } : {}),
|
|
703
731
|
continuationId,
|
|
704
732
|
statementPath,
|
|
705
733
|
source: Object.freeze({ file: this.metadata.file, ...(cloneJson(site && site.position) || {}) }),
|
|
@@ -743,6 +771,23 @@ class NativePlanBuilder {
|
|
|
743
771
|
for (let index = 0; index < statement.declarationList.declarations.length; index += 1) {
|
|
744
772
|
const item = statement.declarationList.declarations[index];
|
|
745
773
|
const itemPath = formatStatementPath(statement.declarationList.declarations.length === 1 ? pathParts : [...pathParts, 'declaration', index]);
|
|
774
|
+
const helperYield = item.initializer && unwrap(item.initializer);
|
|
775
|
+
const helperCall = helperYield && ts.isYieldExpression(helperYield) && helperYield.asteriskToken && unwrap(helperYield.expression);
|
|
776
|
+
const helper = helperCall && ts.isCallExpression(helperCall) && ts.isIdentifier(helperCall.expression) && this.helperBodies.get(helperCall.expression.text);
|
|
777
|
+
if (helper) {
|
|
778
|
+
if (this.activeHelper || !ts.isIdentifier(item.name) || this.pureLoopDepth) {
|
|
779
|
+
this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, 'Helper calls require a direct binding outside pure loops and cannot nest.'); continue;
|
|
780
|
+
}
|
|
781
|
+
const args = helperCall.arguments.map(arg => this.expression(arg, scope));
|
|
782
|
+
if (args.length !== helper.parameters.length || args.some((arg, i) => arg.valueKind !== helper.parameters[i]?.valueKind)) {
|
|
783
|
+
this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, 'Helper inputs must match their explicit scalar types.'); continue;
|
|
784
|
+
}
|
|
785
|
+
const local = this.allocateLocal(item.name.text, this.helpers.find(h => h.id === helper.id)?.resultKind || 'unknown', itemPath, declaration);
|
|
786
|
+
scope.set(local.name, local);
|
|
787
|
+
out.push(Object.freeze({ kind: 'helper-call', helperId: helper.id, callerEntryId: this.activeHandler?.stableId || this.activeEntry?.stableId, arguments: Object.freeze(args), localId: local.id, statementPath: itemPath,
|
|
788
|
+
...(this.readLoopDepth ? { loopContract: contract.CANONICAL_READ_LOOP_CONTRACT.helperCallVersion } : {}) }));
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
746
791
|
const yielded = item.initializer ? this.pulseYield(item.initializer) : undefined;
|
|
747
792
|
|
|
748
793
|
if (yielded && yielded.mode === 'group') {
|
|
@@ -807,7 +852,9 @@ class NativePlanBuilder {
|
|
|
807
852
|
const local = this.allocateLocal(item.name.text, resultKindForEffect(site || {}, decoder), itemPath, declaration);
|
|
808
853
|
const result = Object.freeze({ mode: 'bind', localId: local.id, localName: local.name, valueKind: local.valueKind, decoder });
|
|
809
854
|
const effect = this.prepareEffect(marker, continuationId, scope, itemPath, result);
|
|
810
|
-
|
|
855
|
+
const resultType = pureValues.effectType(effect, this.pureTypes.schemas);
|
|
856
|
+
if (resultType) this.pureTypes.set(local.id, resultType);
|
|
857
|
+
scope.set(local.name, resultFacts.bind(local, effect));
|
|
811
858
|
out.push(Object.freeze({ kind: 'effect', effectId: effect.id, continuationId, result, statementPath: itemPath }));
|
|
812
859
|
continue;
|
|
813
860
|
}
|
|
@@ -818,6 +865,8 @@ class NativePlanBuilder {
|
|
|
818
865
|
}
|
|
819
866
|
const value = item.initializer ? this.expression(item.initializer, scope) : Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
|
|
820
867
|
const local = this.allocateLocal(item.name.text, value.valueKind || 'unknown', itemPath, declaration);
|
|
868
|
+
const type = pureValues.readType(value, this.pureTypes, this.activeHelper?.pure === true);
|
|
869
|
+
if (type) this.pureTypes.set(local.id, type);
|
|
821
870
|
scope.set(local.name, local);
|
|
822
871
|
out.push(Object.freeze({ kind: 'local', localId: local.id, name: local.name, declaration, valueKind: local.valueKind, value, statementPath: itemPath }));
|
|
823
872
|
}
|
|
@@ -893,6 +942,36 @@ class NativePlanBuilder {
|
|
|
893
942
|
}
|
|
894
943
|
|
|
895
944
|
statement(statement, scope, pathParts, depth) {
|
|
945
|
+
if (ts.isFunctionDeclaration(statement) && this.helperBodies.has(statement.name?.text)) {
|
|
946
|
+
const helper = this.helperBodies.get(statement.name.text);
|
|
947
|
+
if (this.activeHelper || this.activeHandler) { this.fail(statement, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, 'Helper declarations require the entry owner.'); return []; }
|
|
948
|
+
this.activeHelper = helper;
|
|
949
|
+
const start = this.locals.length, helperScope = new Map();
|
|
950
|
+
const parameters = helper.parameters.map(parameter => {
|
|
951
|
+
const local = this.allocateLocal(parameter.name, parameter.valueKind, `helpers.${helper.id}.parameters`, 'const');
|
|
952
|
+
this.pureTypes ||= new Map();
|
|
953
|
+
this.pureTypes.set(local.id, parameter.borrow?.type || parameter.valueKind);
|
|
954
|
+
helperScope.set(parameter.name, local);
|
|
955
|
+
return { ...parameter, localId: local.id };
|
|
956
|
+
});
|
|
957
|
+
const body = this.statementList(statement.body.statements, helperScope, ['helpers', helper.id, 'body'], 0);
|
|
958
|
+
const resultKinds = new Set();
|
|
959
|
+
const visitor = s => { if (s.kind === 'return') resultKinds.add(s.value.valueKind); }; visitor.expression = () => {};
|
|
960
|
+
walkStatements(body, visitor);
|
|
961
|
+
const resultKind = resultKinds.size === 1 ? [...resultKinds][0] : 'unknown';
|
|
962
|
+
if (helper.pure && resultKind !== helper.resultKind) this.fail(statement, 'PULSE_NATIVE_PURE_HELPER_RESULT_UNSUPPORTED', 'Pure helper return paths must match the declared scalar result.');
|
|
963
|
+
this.helpers.push({ resultKind, version: helper.pure ? contract.CANONICAL_NATIVE_PURE_HELPER_VERSION : 'pulse.canonical-native-helper.v1', id: helper.id, source: helper.source,
|
|
964
|
+
parameters, localIds: this.locals.slice(start).map(local => local.id), body,
|
|
965
|
+
frame: { lifetime: 'invocation', reset: 'call', suspension: helper.pure ? 'none' : 'retain', nesting: false },
|
|
966
|
+
outputs: helper.pure ? ['value', 'failure'] : ['value', 'suspend', 'failure'] });
|
|
967
|
+
this.activeHelper = undefined;
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
if (ts.isFunctionDeclaration(statement) && this.routerBodies.has(statement.name?.text)) return [];
|
|
971
|
+
if (ts.isReturnStatement(statement)) {
|
|
972
|
+
const call = this.lowerHandlerCall(statement);
|
|
973
|
+
if (call) return call;
|
|
974
|
+
}
|
|
896
975
|
const statementPath = formatStatementPath(pathParts);
|
|
897
976
|
this.summary.maxStatementDepth = Math.max(this.summary.maxStatementDepth, depth);
|
|
898
977
|
|
|
@@ -900,21 +979,91 @@ class NativePlanBuilder {
|
|
|
900
979
|
|
|
901
980
|
if (ts.isVariableStatement(statement)) return this.lowerVariableStatement(statement, scope, pathParts, depth);
|
|
902
981
|
|
|
982
|
+
if (ts.isForStatement(statement)) {
|
|
983
|
+
// A later iteration can observe a write through an alias in an earlier
|
|
984
|
+
// iteration. Retire outer facts before lowering any loop-body read.
|
|
985
|
+
const writesProperty = node => {
|
|
986
|
+
const target = ts.isBinaryExpression(node) && ASSIGNMENT_OPERATORS.has(node.operatorToken.getText(this.sourceFile)) ? unwrap(node.left)
|
|
987
|
+
: (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && UPDATE_OPERATORS.has(ts.tokenToString(node.operator)) ? unwrap(node.operand) : undefined;
|
|
988
|
+
return target && (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target))
|
|
989
|
+
|| Boolean(ts.forEachChild(node, child => writesProperty(child) || undefined));
|
|
990
|
+
};
|
|
991
|
+
if (writesProperty(statement)) resultFacts.invalidate(scope);
|
|
992
|
+
const readLoop = containsYield(statement);
|
|
993
|
+
const loop = readLoop
|
|
994
|
+
? inspectBoundedReadLoop(statement, { ctxName: this.ctxName, headerOnly: true })
|
|
995
|
+
: inspectBoundedPureLoop(statement, { ctxName: this.ctxName, pureHelper: this.activeHelper?.pure === true,
|
|
996
|
+
pureHelperForCall: call => ts.isIdentifier(call.expression) && this.helperBodies.get(call.expression.text)?.pure });
|
|
997
|
+
for (const error of loop.errors) this.fail(error.node, readLoop ? 'PULSE_CANONICAL_READ_LOOP_UNSUPPORTED' : 'PULSE_CANONICAL_PURE_LOOP_UNSUPPORTED', error.message);
|
|
998
|
+
if (loop.errors.length) return [];
|
|
999
|
+
if (readLoop && (this.readLoopDepth || this.pureLoopDepth)) {
|
|
1000
|
+
this.fail(statement, 'PULSE_CANONICAL_READ_LOOP_UNSUPPORTED', 'Effect loops cannot nest inside another loop.'); return [];
|
|
1001
|
+
}
|
|
1002
|
+
const loopScope = new Map(scope);
|
|
1003
|
+
const local = this.allocateLocal(loop.name, 'number', statementPath, 'let');
|
|
1004
|
+
loopScope.set(loop.name, local);
|
|
1005
|
+
const depthField = readLoop ? 'readLoopDepth' : 'pureLoopDepth';
|
|
1006
|
+
const previousDepth = this[depthField] || 0;
|
|
1007
|
+
this[depthField] = previousDepth + 1;
|
|
1008
|
+
const body = this.statement(statement.statement, loopScope, [...pathParts, 'body'], depth + 1);
|
|
1009
|
+
this[depthField] = previousDepth;
|
|
1010
|
+
return [Object.freeze({ kind: readLoop ? 'read-loop' : 'pure-loop', localId: local.id, maxIterations: loop.maxIterations,
|
|
1011
|
+
...(readLoop ? { version: contract.CANONICAL_READ_LOOP_CONTRACT.version,
|
|
1012
|
+
initial: this.expression(statement.initializer.declarations[0].initializer, loopScope),
|
|
1013
|
+
increment: this.expression(statement.incrementor, loopScope) } : {}),
|
|
1014
|
+
test: this.expression(statement.condition, loopScope), body: Object.freeze(body), statementPath })];
|
|
1015
|
+
}
|
|
1016
|
+
if ((ts.isBreakStatement(statement) || ts.isContinueStatement(statement)) && (this.pureLoopDepth || this.readLoopDepth) && !statement.label) {
|
|
1017
|
+
return [Object.freeze({ kind: ts.isBreakStatement(statement) ? 'break' : 'continue', statementPath })];
|
|
1018
|
+
}
|
|
1019
|
+
|
|
903
1020
|
if (ts.isIfStatement(statement)) {
|
|
904
1021
|
this.summary.branchCount += 1;
|
|
905
1022
|
const thenScope = new Map(scope);
|
|
906
1023
|
const elseScope = new Map(scope);
|
|
1024
|
+
const test = this.expression(statement.expression, scope);
|
|
1025
|
+
resultFacts.refine(test, true, thenScope);
|
|
1026
|
+
resultFacts.refine(test, false, elseScope);
|
|
1027
|
+
// A const primitive cannot change between this guard and a branch read.
|
|
1028
|
+
// A returning branch also lets its surviving sibling dominate later reads.
|
|
1029
|
+
if (test.kind === 'binary' && ['===', '!=='].includes(test.operator)) {
|
|
1030
|
+
const read = test.left.kind === 'undefined' ? test.right : test.right.kind === 'undefined' ? test.left : undefined;
|
|
1031
|
+
const local = read?.kind === 'local' ? scope.get(read.name) : undefined;
|
|
1032
|
+
if (local?.declaration === 'const' && local.valueKind === 'string-or-undefined') {
|
|
1033
|
+
(test.operator === '!==' ? thenScope : elseScope).set(local.name, Object.freeze({ ...local, valueKind: 'string' }));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const previousEntry = this.activeEntry;
|
|
1037
|
+
const entryGuard = test.kind === 'binary' && test.operator === '===' && test.left?.kind === 'local'
|
|
1038
|
+
&& test.left.name === '__pulse_router_cursor' && test.right?.kind === 'literal'
|
|
1039
|
+
&& this.metadata.router?.entries?.find(entry => entry.index === test.right.value);
|
|
1040
|
+
if (entryGuard) this.activeEntry = entryGuard;
|
|
907
1041
|
const thenBody = ts.isBlock(statement.thenStatement)
|
|
908
1042
|
? this.statementList(statement.thenStatement.statements, thenScope, [...pathParts, 'then'], depth + 1)
|
|
909
1043
|
: this.statement(statement.thenStatement, thenScope, [...pathParts, 'then', 0], depth + 1);
|
|
1044
|
+
this.activeEntry = previousEntry;
|
|
910
1045
|
const elseBody = !statement.elseStatement
|
|
911
1046
|
? []
|
|
912
1047
|
: ts.isBlock(statement.elseStatement)
|
|
913
1048
|
? this.statementList(statement.elseStatement.statements, elseScope, [...pathParts, 'else'], depth + 1)
|
|
914
1049
|
: this.statement(statement.elseStatement, elseScope, [...pathParts, 'else', 0], depth + 1);
|
|
1050
|
+
const returns = body => {
|
|
1051
|
+
const last = body.at(-1);
|
|
1052
|
+
return last?.kind === 'return' || last?.kind === 'if' && returns(last.then) && returns(last.else);
|
|
1053
|
+
};
|
|
1054
|
+
const thenReturns = returns(thenBody), elseReturns = returns(elseBody);
|
|
1055
|
+
resultFacts.join(scope, thenScope, elseScope, thenReturns, elseReturns);
|
|
1056
|
+
for (const [name, original] of scope) {
|
|
1057
|
+
if (original.declaration !== 'const' || original.valueKind !== 'string-or-undefined') continue;
|
|
1058
|
+
const yes = thenScope.get(name)?.valueKind === 'string';
|
|
1059
|
+
const no = elseScope.get(name)?.valueKind === 'string';
|
|
1060
|
+
if (thenReturns && no || elseReturns && yes || yes && no) {
|
|
1061
|
+
scope.set(name, Object.freeze({ ...original, valueKind: 'string' }));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
915
1064
|
return [Object.freeze({
|
|
916
1065
|
kind: 'if',
|
|
917
|
-
test
|
|
1066
|
+
test,
|
|
918
1067
|
then: Object.freeze(thenBody),
|
|
919
1068
|
else: Object.freeze(elseBody),
|
|
920
1069
|
statementPath
|
|
@@ -955,6 +1104,35 @@ class NativePlanBuilder {
|
|
|
955
1104
|
return out;
|
|
956
1105
|
}
|
|
957
1106
|
|
|
1107
|
+
lowerHandlerCall(statement) {
|
|
1108
|
+
const yielded = unwrap(statement.expression);
|
|
1109
|
+
if (!yielded || !ts.isYieldExpression(yielded) || !yielded.asteriskToken) return undefined;
|
|
1110
|
+
const call = unwrap(yielded.expression);
|
|
1111
|
+
const entry = call && ts.isCallExpression(call) && ts.isIdentifier(call.expression)
|
|
1112
|
+
&& this.routerBodies.get(call.expression.text);
|
|
1113
|
+
if (!entry) return undefined;
|
|
1114
|
+
const node = this.bodyNodes.get(entry.nativeBody.name);
|
|
1115
|
+
if (this.activeHandler || !node?.body || call.arguments.length || this.handlers.some(handler => handler.id === entry.stableId)) {
|
|
1116
|
+
this.fail(statement, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, 'Private Router bodies require one non-recursive terminal call.');
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
const localStart = this.locals.length;
|
|
1120
|
+
this.activeHandler = entry;
|
|
1121
|
+
this.handlerLocalIndex = 0;
|
|
1122
|
+
const body = this.statementList(node.body.statements, new Map(), ['handlers', entry.stableId, 'body'], 0);
|
|
1123
|
+
this.activeHandler = undefined;
|
|
1124
|
+
this.handlers.push(Object.freeze({
|
|
1125
|
+
version: contract.CANONICAL_NATIVE_HANDLER_BODY_VERSION,
|
|
1126
|
+
id: entry.stableId,
|
|
1127
|
+
handlerId: entry.handlerId,
|
|
1128
|
+
family: 'terminal-route',
|
|
1129
|
+
source: deepFreeze(cloneJson(entry.nativeBody.source)),
|
|
1130
|
+
localIds: Object.freeze(this.locals.slice(localStart).map(local => local.id)),
|
|
1131
|
+
body: Object.freeze(body)
|
|
1132
|
+
}));
|
|
1133
|
+
return [Object.freeze({ kind: 'handler-call', handlerId: entry.stableId })];
|
|
1134
|
+
}
|
|
1135
|
+
|
|
958
1136
|
reconcile() {
|
|
959
1137
|
const expectedEffectIds = (this.metadata.effectSites || []).map((site) => String(site.id));
|
|
960
1138
|
const actualEffectIds = this.effects.map((site) => site.id);
|
|
@@ -977,8 +1155,9 @@ class NativePlanBuilder {
|
|
|
977
1155
|
this.reconcile();
|
|
978
1156
|
if (this.diagnostics.length > 0) throw new CanonicalNativePlanError(`Native-plan lowering failed for ${this.metadata.file}.`, this.diagnostics);
|
|
979
1157
|
|
|
980
|
-
const continuations = (this.metadata.continuationSites || []).map((site, index) => Object.freeze({
|
|
1158
|
+
const continuations = this.packageApplication ? this.packageApplication.continuations : (this.metadata.continuationSites || []).map((site, index) => Object.freeze({
|
|
981
1159
|
id: String(site.id),
|
|
1160
|
+
...(this.effects.find(effect => effect.continuationId === site.id)?.helperId ? { helperId: this.effects.find(effect => effect.continuationId === site.id).helperId } : {}),
|
|
982
1161
|
kind: String(site.kind),
|
|
983
1162
|
effectIds: Object.freeze(site.effectIds.map(String)),
|
|
984
1163
|
stateIndex: index + 1,
|
|
@@ -1001,12 +1180,13 @@ class NativePlanBuilder {
|
|
|
1001
1180
|
eventType: site.eventType ? String(site.eventType) : undefined,
|
|
1002
1181
|
eventSchemaId: site.eventSchemaId !== undefined ? site.eventSchemaId : undefined
|
|
1003
1182
|
}));
|
|
1183
|
+
if (this.packageApplication) this.effects = [...this.packageApplication.effects];
|
|
1004
1184
|
const states = [Object.freeze({ id: 'entry', kind: 'entry', stateIndex: 0 })]
|
|
1005
1185
|
.concat(continuations.map((site) => Object.freeze({ id: site.id, kind: 'continuation', continuationKind: site.kind, effectIds: site.effectIds, stateIndex: site.stateIndex })));
|
|
1006
|
-
this.summary = summarizeNativePlan(body, this.locals, this.effects, continuations);
|
|
1186
|
+
this.summary = summarizeNativePlan(body, this.locals, this.effects, continuations, this.handlers);
|
|
1007
1187
|
const hasInboundEvents = Number(this.metadata.events && this.metadata.events.count || 0) > 0;
|
|
1008
1188
|
|
|
1009
|
-
|
|
1189
|
+
let unsigned = {
|
|
1010
1190
|
version: contract.CANONICAL_NATIVE_PLAN_VERSION,
|
|
1011
1191
|
compilerVersion: CANONICAL_NATIVE_PLAN_COMPILER_VERSION,
|
|
1012
1192
|
hashAlgorithm: contract.CANONICAL_NATIVE_PLAN_HASH_ALGORITHM,
|
|
@@ -1065,10 +1245,12 @@ class NativePlanBuilder {
|
|
|
1065
1245
|
body: Object.freeze(body)
|
|
1066
1246
|
}),
|
|
1067
1247
|
locals: Object.freeze(this.locals),
|
|
1248
|
+
handlers: Object.freeze(this.handlers),
|
|
1249
|
+
...(this.helpers.length ? { helpers: Object.freeze(this.helpers) } : {}),
|
|
1068
1250
|
effects: Object.freeze(this.effects),
|
|
1069
1251
|
continuations: Object.freeze(continuations),
|
|
1070
1252
|
states: Object.freeze(states),
|
|
1071
|
-
capabilities: Object.freeze([...(this.metadata.capabilities || [])].map(String).sort()),
|
|
1253
|
+
capabilities: Object.freeze([...new Set([...(this.metadata.capabilities || []), ...(this.packageApplication?.capabilities || [])])].map(String).sort()),
|
|
1072
1254
|
schemas: Object.freeze({
|
|
1073
1255
|
sourceHash: this.metadata.schemaSourceHash,
|
|
1074
1256
|
ids: Object.freeze([...(this.metadata.schemaIds || [])].map(String)),
|
|
@@ -1082,9 +1264,12 @@ class NativePlanBuilder {
|
|
|
1082
1264
|
...(this.compiled.cryptoRealizationPlan && this.compiled.cryptoRealizationPlan.target === 'native' ? {
|
|
1083
1265
|
crypto: deepFreeze(cloneJson(this.compiled.cryptoRealizationPlan))
|
|
1084
1266
|
} : {}),
|
|
1085
|
-
packages: Object.freeze({ effects: deepFreeze(cloneJson(this.metadata.packageEffects || []))
|
|
1267
|
+
packages: Object.freeze({ effects: deepFreeze(cloneJson(this.metadata.packageEffects || [])),
|
|
1268
|
+
...(this.packageApplication ? { application: this.packageApplication } : {}) }),
|
|
1086
1269
|
summary: Object.freeze({ ...this.summary })
|
|
1087
1270
|
};
|
|
1271
|
+
if (this.options.sharedStages !== false) unsigned = lowerSharedStages(unsigned);
|
|
1272
|
+
unsigned.summary = summarizeNativePlan(unsigned.entry.body, unsigned.locals, unsigned.effects, continuations, [...unsigned.handlers, ...(unsigned.stages || []), ...(unsigned.helpers || [])]);
|
|
1088
1273
|
const planHash = stableHash(stableStringify(unsigned));
|
|
1089
1274
|
const plan = deepFreeze({ ...unsigned, planHash });
|
|
1090
1275
|
assertCanonicalNativePlan(plan);
|
|
@@ -1130,6 +1315,7 @@ function walkExpression(expression, visit) {
|
|
|
1130
1315
|
walkExpression(expression.object, visit);
|
|
1131
1316
|
walkExpression(expression.index, visit);
|
|
1132
1317
|
break;
|
|
1318
|
+
case 'pure-helper-call':
|
|
1133
1319
|
case 'intrinsic':
|
|
1134
1320
|
for (const argument of expression.arguments || []) walkExpression(argument, visit);
|
|
1135
1321
|
break;
|
|
@@ -1155,11 +1341,19 @@ function walkExpression(expression, visit) {
|
|
|
1155
1341
|
function walkStatements(statements, visitor) {
|
|
1156
1342
|
for (const statement of statements || []) {
|
|
1157
1343
|
visitor(statement);
|
|
1158
|
-
if (statement.kind === '
|
|
1344
|
+
if (statement.kind === 'helper-call') for (const arg of statement.arguments || []) walkExpression(arg, visitor.expression);
|
|
1345
|
+
else if (statement.kind === 'local') walkExpression(statement.value, visitor.expression);
|
|
1159
1346
|
else if (statement.kind === 'if') {
|
|
1160
1347
|
walkExpression(statement.test, visitor.expression);
|
|
1161
1348
|
walkStatements(statement.then, visitor);
|
|
1162
1349
|
walkStatements(statement.else, visitor);
|
|
1350
|
+
} else if (statement.kind === 'pure-loop' || statement.kind === 'read-loop') {
|
|
1351
|
+
if (statement.kind === 'read-loop') {
|
|
1352
|
+
walkExpression(statement.initial, visitor.expression);
|
|
1353
|
+
walkExpression(statement.increment, visitor.expression);
|
|
1354
|
+
}
|
|
1355
|
+
walkExpression(statement.test, visitor.expression);
|
|
1356
|
+
walkStatements(statement.body, visitor);
|
|
1163
1357
|
} else if (statement.kind === 'return') walkExpression(statement.value, visitor.expression);
|
|
1164
1358
|
else if (statement.kind === 'expression') walkExpression(statement.expression, visitor.expression);
|
|
1165
1359
|
}
|
|
@@ -1171,7 +1365,7 @@ function countExpression(expression) {
|
|
|
1171
1365
|
return count;
|
|
1172
1366
|
}
|
|
1173
1367
|
|
|
1174
|
-
function summarizeNativePlan(body, locals, effects, continuations) {
|
|
1368
|
+
function summarizeNativePlan(body, locals, effects, continuations, handlers = []) {
|
|
1175
1369
|
const summary = {
|
|
1176
1370
|
statementCount: 0,
|
|
1177
1371
|
expressionCount: 0,
|
|
@@ -1194,9 +1388,15 @@ function summarizeNativePlan(body, locals, effects, continuations) {
|
|
|
1194
1388
|
summary.expressionCount += countExpression(statement.test);
|
|
1195
1389
|
visitStatements(statement.then, depth + 1);
|
|
1196
1390
|
visitStatements(statement.else, depth + 1);
|
|
1391
|
+
} else if (statement.kind === 'pure-loop' || statement.kind === 'read-loop') {
|
|
1392
|
+
if (statement.kind === 'read-loop') summary.expressionCount += countExpression(statement.initial) + countExpression(statement.increment);
|
|
1393
|
+
summary.expressionCount += countExpression(statement.test);
|
|
1394
|
+
visitStatements(statement.body, depth + 1);
|
|
1197
1395
|
} else if (statement.kind === 'return') {
|
|
1198
1396
|
summary.returnCount += 1;
|
|
1199
1397
|
summary.expressionCount += countExpression(statement.value);
|
|
1398
|
+
} else if (statement.kind === 'helper-call') {
|
|
1399
|
+
for (const arg of statement.arguments || []) summary.expressionCount += countExpression(arg);
|
|
1200
1400
|
} else if (statement.kind === 'expression') {
|
|
1201
1401
|
summary.expressionCount += countExpression(statement.expression);
|
|
1202
1402
|
} else if (statement.kind === 'effect') {
|
|
@@ -1208,6 +1408,7 @@ function summarizeNativePlan(body, locals, effects, continuations) {
|
|
|
1208
1408
|
}
|
|
1209
1409
|
|
|
1210
1410
|
visitStatements(body, 0);
|
|
1411
|
+
for (const handler of handlers) visitStatements(handler.body, 0);
|
|
1211
1412
|
for (const effect of effects) {
|
|
1212
1413
|
for (const input of effect.inputs || []) summary.expressionCount += countExpression(input.value);
|
|
1213
1414
|
const decoder = effect.result && effect.result.decoder;
|
|
@@ -1238,6 +1439,140 @@ function validateResult(result, fail, localIds, detail = {}) {
|
|
|
1238
1439
|
}
|
|
1239
1440
|
|
|
1240
1441
|
function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
|
|
1442
|
+
const effects = new Map((plan.effects || []).map(effect => [effect.id, effect]));
|
|
1443
|
+
const helpers = new Map((plan.helpers || []).map(helper => [helper.id, helper]));
|
|
1444
|
+
function validateHelperLoopProduct(helper, callerProduct, label) {
|
|
1445
|
+
// A synchronous call does not reset the enclosing loop's work bound.
|
|
1446
|
+
const check = (body, product) => {
|
|
1447
|
+
for (const statement of body || []) {
|
|
1448
|
+
if (statement.kind === 'pure-loop') {
|
|
1449
|
+
const nested = product * Math.max(1, statement.maxIterations);
|
|
1450
|
+
if (!Number.isSafeInteger(nested) || nested > contract.CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations) fail(`${label} exceeds the combined iteration bound`);
|
|
1451
|
+
check(statement.body, nested);
|
|
1452
|
+
} else if (statement.kind === 'if') {
|
|
1453
|
+
check(statement.then, product); check(statement.else, product);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
check(helper.body, callerProduct);
|
|
1458
|
+
}
|
|
1459
|
+
function validateReadHelper(id, callerProduct) {
|
|
1460
|
+
const helper = helpers.get(id);
|
|
1461
|
+
if (!helper) { fail('read loop helper is unknown'); return; }
|
|
1462
|
+
validateHelperLoopProduct(helper, callerProduct, 'read loop helper');
|
|
1463
|
+
let sites = 0;
|
|
1464
|
+
const visit = statement => {
|
|
1465
|
+
if (['read-loop', 'helper-call', 'handler-call', 'stage-call', 'effect-group'].includes(statement.kind)) fail('read loop helper cannot nest effect loops, calls or groups');
|
|
1466
|
+
if (statement.kind === 'effect') {
|
|
1467
|
+
const effect = effects.get(statement.effectId); sites++;
|
|
1468
|
+
if (!effect || !contract.CANONICAL_READ_LOOP_CONTRACT.effectKinds.includes(effect.kind)) fail('read loop helper effect kind is not admitted');
|
|
1469
|
+
for (const input of effect?.inputs || []) checkReadExpression(input.value, new Set(), true);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
visit.expression = expression => checkReadExpression(expression, new Set());
|
|
1473
|
+
walkStatements(helper.body, visit);
|
|
1474
|
+
if (!sites) fail('read loop helper requires a sequential read effect site');
|
|
1475
|
+
}
|
|
1476
|
+
function validatePureLoopCalls(expression, counters, product, header = false) {
|
|
1477
|
+
walkExpression(expression, node => {
|
|
1478
|
+
if (node.kind !== 'pure-helper-call') return;
|
|
1479
|
+
if (!counters.size) {
|
|
1480
|
+
if (node.loopContract !== undefined) fail('pure loop call contract requires an enclosing loop');
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (header || node.loopContract !== contract.CANONICAL_NATIVE_PURE_LOOP_CALL_VERSION) fail('pure loop call contract or placement is invalid');
|
|
1484
|
+
const helper = helpers.get(node.helperId);
|
|
1485
|
+
if (helper?.version !== contract.CANONICAL_NATIVE_PURE_HELPER_VERSION) { fail('pure loop call requires a pure helper'); return; }
|
|
1486
|
+
for (const argument of node.arguments || []) walkExpression(argument, child => {
|
|
1487
|
+
if (['assignment', 'update'].includes(child.kind)) fail('pure loop call inputs must be read-only');
|
|
1488
|
+
});
|
|
1489
|
+
validateHelperLoopProduct(helper, product, 'pure loop helper');
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
function validateLoops(statements, counters = new Set(), product = 1, mode = 'outside', pureHelper = false) {
|
|
1493
|
+
for (const statement of statements || []) {
|
|
1494
|
+
if (statement.kind === 'pure-loop' || statement.kind === 'read-loop') {
|
|
1495
|
+
const readLoop = statement.kind === 'read-loop';
|
|
1496
|
+
const label = readLoop ? 'read loop' : 'pure loop';
|
|
1497
|
+
const limit = statement.maxIterations;
|
|
1498
|
+
if (!localIds.has(statement.localId) || counters.has(statement.localId)) fail(`${label} requires its own local counter`);
|
|
1499
|
+
if (!Number.isInteger(limit) || limit < 0 || limit > (readLoop ? contract.CANONICAL_READ_LOOP_CONTRACT.maxIterations : contract.CANONICAL_PURE_LOOP_LIMITS.maxIterations)
|
|
1500
|
+
|| product * Math.max(1, limit) > contract.CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations) fail(`${label} iteration bound is invalid`);
|
|
1501
|
+
if (readLoop) {
|
|
1502
|
+
if (counters.size) fail('read loops cannot nest inside another loop');
|
|
1503
|
+
if (statement.version !== contract.CANONICAL_READ_LOOP_CONTRACT.version) fail('read loop contract version is invalid');
|
|
1504
|
+
if (statement.initial?.kind !== 'literal' || statement.initial.value !== 0) fail('read loop must initialize its counter to zero');
|
|
1505
|
+
const step = statement.increment;
|
|
1506
|
+
if (step?.target?.kind !== 'local' || step.target.id !== statement.localId
|
|
1507
|
+
|| !(step.kind === 'update' && step.operator === '++'
|
|
1508
|
+
|| step.kind === 'assignment' && step.operator === '+=' && step.value?.kind === 'literal' && step.value.value === 1)) fail('read loop increment must advance its own counter by one');
|
|
1509
|
+
}
|
|
1510
|
+
let bound = statement.test;
|
|
1511
|
+
while (bound && bound.kind === 'binary' && bound.operator === '&&') bound = bound.left;
|
|
1512
|
+
if (!bound || bound.kind !== 'binary' || bound.operator !== '<' || bound.left?.kind !== 'local' || bound.left.id !== statement.localId
|
|
1513
|
+
|| bound.right?.kind !== 'literal' || bound.right.value !== limit) fail(`${label} test must start with its declared literal cap`);
|
|
1514
|
+
const active = new Set([...counters, statement.localId]);
|
|
1515
|
+
checkPureExpression(statement.test, active, true);
|
|
1516
|
+
validatePureLoopCalls(statement.test, active, product, true);
|
|
1517
|
+
validatePureLoopCalls(statement.initial, active, product, true);
|
|
1518
|
+
validatePureLoopCalls(statement.increment, active, product, true);
|
|
1519
|
+
if (!Array.isArray(statement.body)) { fail(`${label} body must be an array`); continue; }
|
|
1520
|
+
if (readLoop) {
|
|
1521
|
+
let sites = 0;
|
|
1522
|
+
const countSites = item => { if (item.kind === 'effect' || item.kind === 'helper-call') sites += 1; };
|
|
1523
|
+
countSites.expression = () => {};
|
|
1524
|
+
walkStatements(statement.body, countSites);
|
|
1525
|
+
if (!sites) fail('read loop requires a sequential effect site');
|
|
1526
|
+
}
|
|
1527
|
+
validateLoops(statement.body, active, product * Math.max(1, limit), readLoop ? 'read' : 'pure', pureHelper);
|
|
1528
|
+
} else {
|
|
1529
|
+
validatePureLoopCalls(statement.value || statement.expression || statement.test, counters, product);
|
|
1530
|
+
if (mode === 'pure' && !(pureHelper && statement.kind === 'return') && !['local', 'expression', 'if', 'break', 'continue'].includes(statement.kind)) fail('pure loop contains a non-value statement', { kind: statement.kind });
|
|
1531
|
+
if (mode === 'read' && !['local', 'expression', 'if', 'break', 'continue', 'return', 'effect', 'helper-call'].includes(statement.kind)) fail('read loop contains an unsupported statement', { kind: statement.kind });
|
|
1532
|
+
if (statement.kind === 'helper-call') {
|
|
1533
|
+
if (mode === 'read') {
|
|
1534
|
+
if (statement.loopContract !== contract.CANONICAL_READ_LOOP_CONTRACT.helperCallVersion) fail('read loop helper contract version is invalid');
|
|
1535
|
+
if (counters.has(statement.localId)) fail('read loop helper result cannot replace the counter');
|
|
1536
|
+
for (const argument of statement.arguments || []) checkReadExpression(argument, counters, true);
|
|
1537
|
+
validateReadHelper(statement.helperId, product);
|
|
1538
|
+
} else if (statement.loopContract !== undefined) fail('loop helper contract requires an enclosing read loop');
|
|
1539
|
+
}
|
|
1540
|
+
if (!counters.size && ['break', 'continue'].includes(statement.kind)) fail('loop transfer outside a loop');
|
|
1541
|
+
if (counters.size) {
|
|
1542
|
+
if (statement.kind === 'local' && counters.has(statement.localId)) fail('pure loop counter cannot be rebound');
|
|
1543
|
+
if (mode === 'pure') checkPureExpression(statement.value || statement.expression || statement.test, counters);
|
|
1544
|
+
else checkReadExpression(statement.value || statement.expression || statement.test, counters);
|
|
1545
|
+
}
|
|
1546
|
+
if (mode === 'read' && statement.kind === 'effect') {
|
|
1547
|
+
const effect = effects.get(statement.effectId);
|
|
1548
|
+
if (!effect || !contract.CANONICAL_READ_LOOP_CONTRACT.effectKinds.includes(effect.kind)) fail('read loop effect kind is not admitted');
|
|
1549
|
+
for (const result of [statement.result, effect?.result]) {
|
|
1550
|
+
if (!['bind', 'discard'].includes(result?.mode) || counters.has(result?.localId)) fail('read loop effect result cannot replace the counter or return an effect');
|
|
1551
|
+
}
|
|
1552
|
+
for (const input of effect?.inputs || []) checkReadExpression(input.value, counters, true);
|
|
1553
|
+
}
|
|
1554
|
+
if (statement.kind === 'if') {
|
|
1555
|
+
validateLoops(statement.then, counters, product, mode, pureHelper);
|
|
1556
|
+
validateLoops(statement.else, counters, product, mode, pureHelper);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function checkPureExpression(expression, counters, test = false) {
|
|
1562
|
+
walkExpression(expression, node => {
|
|
1563
|
+
if (node.kind === 'intrinsic' || node.kind === 'context-read' || (node.kind === 'method-call' && (node.method !== 'string.trim' || node.arguments?.length))) fail('pure loop contains a non-value call or context read');
|
|
1564
|
+
if (['assignment', 'update'].includes(node.kind) && (test || (node.target?.kind === 'local' && counters.has(node.target.id)))) fail('pure loop test or counter mutation is invalid');
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
function checkReadExpression(expression, counters, readonly = false) {
|
|
1568
|
+
walkExpression(expression, node => {
|
|
1569
|
+
if (node.kind === 'context-read' || node.kind === 'intrinsic' && !contract.CANONICAL_READ_LOOP_CONTRACT.valueIntrinsics.includes(node.name)
|
|
1570
|
+
|| node.kind === 'method-call' && (node.method !== 'string.trim' || node.arguments?.length)) fail('read loop contains an unsupported value operation');
|
|
1571
|
+
if (['assignment', 'update'].includes(node.kind) && (readonly || node.target?.kind === 'local' && counters.has(node.target.id))) fail('read loop counter or effect input mutation is invalid');
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
for (const body of [plan.entry && plan.entry.body, ...[...(plan.handlers || []), ...(plan.stages || [])].map(handler => handler.body)]) validateLoops(body);
|
|
1575
|
+
for (const helper of plan.helpers || []) validateLoops(helper.body, new Set(), 1, 'outside', helper.version === contract.CANONICAL_NATIVE_PURE_HELPER_VERSION);
|
|
1241
1576
|
const visitor = (statement) => {
|
|
1242
1577
|
if (!contract.CANONICAL_NATIVE_STATEMENT_KINDS.includes(statement.kind)) fail('statement kind is unknown', { kind: statement.kind });
|
|
1243
1578
|
if (statement.kind === 'local' && !localIds.has(statement.localId)) fail('local statement references unknown local', { localId: statement.localId });
|
|
@@ -1254,6 +1589,73 @@ function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
|
|
|
1254
1589
|
};
|
|
1255
1590
|
visitor.expression = (expression) => validateExpression(expression, fail, localIds);
|
|
1256
1591
|
walkStatements(plan.entry && plan.entry.body, visitor);
|
|
1592
|
+
for (const handler of [...(plan.handlers || []), ...(plan.stages || []), ...(plan.helpers || [])]) walkStatements(handler.body, visitor);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function validateHandlerBodies(plan, fail) {
|
|
1596
|
+
const handlers = new Map();
|
|
1597
|
+
const stages = new Set([...(plan.stages || []), ...(plan.helpers || [])].map(stage => stage.id));
|
|
1598
|
+
const entries = new Map((plan.routing?.entries || []).map(entry => [entry.stableId, entry]));
|
|
1599
|
+
const locals = Array.isArray(plan.locals) ? plan.locals : [];
|
|
1600
|
+
const localsByOwner = new Map();
|
|
1601
|
+
for (const local of locals) {
|
|
1602
|
+
if (!localsByOwner.has(local.scopeId)) localsByOwner.set(local.scopeId, []);
|
|
1603
|
+
localsByOwner.get(local.scopeId).push(local);
|
|
1604
|
+
}
|
|
1605
|
+
const effects = new Map((plan.effects || []).map(effect => [effect.id, effect]));
|
|
1606
|
+
const calls = new Map();
|
|
1607
|
+
for (const handler of plan.handlers || []) {
|
|
1608
|
+
const entry = handler && entries.get(handler.id);
|
|
1609
|
+
if (!handler || handlers.has(handler.id) || handler.version !== contract.CANONICAL_NATIVE_HANDLER_BODY_VERSION
|
|
1610
|
+
|| handler.family !== 'terminal-route' || entry?.kind !== 'route' || entry.nativeBody?.version !== 'pulse.router-native-body.v1'
|
|
1611
|
+
|| handler.handlerId !== entry.handlerId || !Array.isArray(handler.body)
|
|
1612
|
+
|| stableStringify(handler.source) !== stableStringify(entry.nativeBody.source)) {
|
|
1613
|
+
fail('private handler body must have a unique terminal route identity and original source');
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
handlers.set(handler.id, handler);
|
|
1617
|
+
const owned = localsByOwner.get(handler.id) || [];
|
|
1618
|
+
if (stableStringify(handler.localIds) !== stableStringify(owned.map(local => local.id))) fail('private handler local namespace mismatch', { handlerId: handler.id });
|
|
1619
|
+
if (owned.some(local => local.routerEntryStableId !== handler.id)) fail('private handler local ownership mismatch', { handlerId: handler.id });
|
|
1620
|
+
}
|
|
1621
|
+
for (const local of locals) {
|
|
1622
|
+
if (local.scopeId !== 'entry' && !handlers.has(local.scopeId) && !stages.has(local.scopeId)) fail('local requires an existing lexical owner', { localId: local.id });
|
|
1623
|
+
if (local.scopeId === 'entry' && local.routerEntryStableId !== undefined) fail('dispatcher local cannot claim private handler ownership', { localId: local.id });
|
|
1624
|
+
}
|
|
1625
|
+
function validateBody(body, owner) {
|
|
1626
|
+
const owned = new Set((localsByOwner.get(owner) || []).map(local => local.id));
|
|
1627
|
+
const checkEffect = id => {
|
|
1628
|
+
const effect = effects.get(id);
|
|
1629
|
+
if (owner !== 'entry' && effect?.routerEntryStableId !== owner) fail('private handler effect ownership mismatch', { effectId: id, owner });
|
|
1630
|
+
for (const input of effect?.inputs || []) walkExpression(input.value, visitor.expression);
|
|
1631
|
+
for (const arg of effect?.result?.decoder?.arguments || []) walkExpression(arg, visitor.expression);
|
|
1632
|
+
if (effect?.result?.localId && !owned.has(effect.result.localId)) fail('effect result crosses a lexical boundary', { effectId: id });
|
|
1633
|
+
};
|
|
1634
|
+
const visitor = statement => {
|
|
1635
|
+
if (statement.localId && !owned.has(statement.localId)) fail('statement crosses a lexical boundary', { localId: statement.localId });
|
|
1636
|
+
if (statement.kind === 'stage-call' && owner !== 'entry') fail('stage calls cannot nest inside private handlers');
|
|
1637
|
+
if (statement.kind === 'handler-call') {
|
|
1638
|
+
if (owner !== 'entry' || !handlers.has(statement.handlerId)) fail('private handler call must originate in the dispatcher and select an existing body');
|
|
1639
|
+
calls.set(statement.handlerId, (calls.get(statement.handlerId) || 0) + 1);
|
|
1640
|
+
}
|
|
1641
|
+
if (statement.kind === 'effect') checkEffect(statement.effectId);
|
|
1642
|
+
if (statement.kind === 'effect-group') for (const id of statement.effectIds || []) checkEffect(id);
|
|
1643
|
+
};
|
|
1644
|
+
visitor.expression = expression => {
|
|
1645
|
+
if (expression.kind === 'local' && !owned.has(expression.id)) fail('expression crosses a lexical boundary', { localId: expression.id, owner });
|
|
1646
|
+
};
|
|
1647
|
+
walkStatements(body, visitor);
|
|
1648
|
+
}
|
|
1649
|
+
validateBody(plan.entry?.body, 'entry');
|
|
1650
|
+
for (const handler of handlers.values()) {
|
|
1651
|
+
validateBody(handler.body, handler.id);
|
|
1652
|
+
if (calls.get(handler.id) !== 1) fail('private handler requires exactly one static call', { handlerId: handler.id });
|
|
1653
|
+
}
|
|
1654
|
+
for (const continuation of plan.continuations || []) {
|
|
1655
|
+
for (const effectId of continuation.effectIds || []) {
|
|
1656
|
+
if (effects.get(effectId)?.routerEntryStableId !== continuation.routerEntryStableId) fail('continuation must resume in its effect owner', { effectId });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1257
1659
|
}
|
|
1258
1660
|
|
|
1259
1661
|
function assertCanonicalNativePlan(plan) {
|
|
@@ -1296,6 +1698,7 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1296
1698
|
}
|
|
1297
1699
|
}
|
|
1298
1700
|
if (!Array.isArray(plan.locals)) fail('plan locals must be an array');
|
|
1701
|
+
if (!Array.isArray(plan.handlers)) fail('plan handlers must be an array');
|
|
1299
1702
|
if (!Array.isArray(plan.effects)) fail('plan effects must be an array');
|
|
1300
1703
|
if (!Array.isArray(plan.continuations)) fail('plan continuations must be an array');
|
|
1301
1704
|
if (!Array.isArray(plan.states)) fail('plan states must be an array');
|
|
@@ -1355,11 +1758,36 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1355
1758
|
fail('plan logging evidence is invalid', { logging: plan.logging });
|
|
1356
1759
|
}
|
|
1357
1760
|
|
|
1761
|
+
if (plan.helpers !== undefined && !Array.isArray(plan.helpers)) {
|
|
1762
|
+
throw new CanonicalNativePlanError('Invalid helper table.', [diagnostic(undefined, undefined, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, 'helpers must be an array')]);
|
|
1763
|
+
}
|
|
1358
1764
|
const unsigned = { ...plan };
|
|
1359
1765
|
delete unsigned.planHash;
|
|
1360
1766
|
const expectedHash = stableHash(stableStringify(unsigned));
|
|
1361
1767
|
if (plan.planHash !== expectedHash) fail('plan hash mismatch', { expectedHash, actual: plan.planHash });
|
|
1362
1768
|
|
|
1769
|
+
const application = plan.packages && plan.packages.application;
|
|
1770
|
+
let packageCalls = 0;
|
|
1771
|
+
const packageVisitor = () => {};
|
|
1772
|
+
packageVisitor.expression = expression => { if (expression.kind === 'intrinsic' && expression.name === 'package.application') packageCalls++; };
|
|
1773
|
+
walkStatements(plan.entry && plan.entry.body, packageVisitor);
|
|
1774
|
+
for (const handler of plan.handlers || []) walkStatements(handler.body, packageVisitor);
|
|
1775
|
+
if (application) {
|
|
1776
|
+
const terminal = plan.entry?.body?.[0];
|
|
1777
|
+
if (application.version !== 'pulse.package-native-application.v1' || application.automaticFallback !== false
|
|
1778
|
+
|| typeof application.contractId !== 'string' || !application.contractId
|
|
1779
|
+
|| typeof application.package !== 'string' || !application.package
|
|
1780
|
+
|| typeof application.intrinsic !== 'string' || !application.intrinsic
|
|
1781
|
+
|| typeof application.source !== 'string' || stableHash(application.source) !== application.sourceHash
|
|
1782
|
+
|| application.effectFailure !== 'package-completion'
|
|
1783
|
+
|| stableStringify(application.effects) !== stableStringify(plan.effects)
|
|
1784
|
+
|| stableStringify(application.continuations) !== stableStringify(plan.continuations)
|
|
1785
|
+
|| packageCalls !== 1 || plan.entry.kind !== 'handler' || plan.entry.body.length !== 1
|
|
1786
|
+
|| terminal.kind !== 'return' || terminal.value?.name !== 'package.application'
|
|
1787
|
+
|| terminal.value.arguments?.length !== 0 || plan.handlers?.length !== 0) {
|
|
1788
|
+
fail('terminal package Native application does not match its source, entry, or effect plan');
|
|
1789
|
+
}
|
|
1790
|
+
} else if (packageCalls) fail('package application intrinsic requires a trusted Native source contribution');
|
|
1363
1791
|
const locals = Array.isArray(plan.locals) ? plan.locals : [];
|
|
1364
1792
|
const effects = Array.isArray(plan.effects) ? plan.effects : [];
|
|
1365
1793
|
const continuations = Array.isArray(plan.continuations) ? plan.continuations : [];
|
|
@@ -1397,6 +1825,9 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1397
1825
|
}
|
|
1398
1826
|
|
|
1399
1827
|
if (plan.entry && Array.isArray(plan.entry.body)) validatePlanTree(plan, fail, localIds, effectIds, continuationIds);
|
|
1828
|
+
if (Array.isArray(plan.handlers)) validateHandlerBodies(plan, fail);
|
|
1829
|
+
validateSharedStages(plan, fail, walkStatements, walkExpression, stableStringify);
|
|
1830
|
+
require("./source-helper-plan").validateHelpers(plan, fail, walkStatements, walkExpression);
|
|
1400
1831
|
|
|
1401
1832
|
const states = Array.isArray(plan.states) ? plan.states : [];
|
|
1402
1833
|
if (states.length !== continuations.length + 1) fail('state table must contain entry plus one state per continuation', { expected: continuations.length + 1, actual: states.length });
|
|
@@ -1425,10 +1856,11 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1425
1856
|
};
|
|
1426
1857
|
statementVisitor.expression = () => {};
|
|
1427
1858
|
if (plan.entry && Array.isArray(plan.entry.body)) walkStatements(plan.entry.body, statementVisitor);
|
|
1859
|
+
for (const handler of [...(plan.handlers || []), ...(plan.stages || []), ...(plan.helpers || [])]) walkStatements(handler.body, statementVisitor);
|
|
1428
1860
|
|
|
1429
1861
|
if (!plan.summary || typeof plan.summary !== 'object') fail('plan summary is required');
|
|
1430
1862
|
else {
|
|
1431
|
-
const expectedSummary = summarizeNativePlan(plan.entry && plan.entry.body, locals, effects, continuations);
|
|
1863
|
+
const expectedSummary = summarizeNativePlan(plan.entry && plan.entry.body, locals, effects, continuations, [...(plan.handlers || []), ...(plan.stages || []), ...(plan.helpers || [])]);
|
|
1432
1864
|
for (const [key, expected] of Object.entries(expectedSummary)) {
|
|
1433
1865
|
if (plan.summary[key] !== expected) fail(`summary ${key} mismatch`, { expected, actual: plan.summary[key] });
|
|
1434
1866
|
}
|