@pulse-compute/wasm-compiler 1.0.0-beta.5 → 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 +37 -64
- package/src/canonical-native-plan.js +450 -101
- package/src/canonical-project-compiler.js +52 -12
- package/src/execution-plan.js +1 -0
- package/src/extractor.js +36 -0
- package/src/native-result-facts.js +50 -0
- package/src/project/router-module-linker.js +64 -0
- 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 +78 -13
- 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 +95 -8
- package/src/spine/handler-surface-authority.js +9 -0
- package/src/spine/incoming-body-source.js +48 -0
- package/src/spine/plain-handler-frontend.js +5 -0
- package/src/spine/pure-helper-source.js +123 -0
- package/src/spine/router-handler-frontend.js +38 -1
- 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,57 +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');
|
|
8
|
-
const { inspectBoundedPureLoop } = require('./spine/bounded-pure-loop.js');
|
|
9
|
+
const { inspectBoundedPureLoop, inspectBoundedReadLoop } = require('./spine/bounded-pure-loop.js');
|
|
10
|
+
const resultFacts = require('./native-result-facts.js');
|
|
9
11
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
14
|
-
return require('../../contracts/src/handler/canonical-native-plan.js');
|
|
15
|
-
}
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function loadLoggingContract() {
|
|
21
|
-
try { return require('@pulse-compute/wasm-contracts/logging'); }
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
24
|
-
return require('../../contracts/src/logging.js');
|
|
25
|
-
}
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function loadCryptoContract() {
|
|
31
|
-
try { return require('@pulse-compute/wasm-contracts/crypto/contracts'); }
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
34
|
-
return require('../../contracts/src/crypto/contracts.js');
|
|
35
|
-
}
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function loadEventContract() {
|
|
41
|
-
try { return require('@pulse-compute/wasm-contracts/events'); }
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
|
|
44
|
-
return require('../../contracts/src/events/contracts.js');
|
|
45
|
-
}
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
12
|
+
function containsYield(node) {
|
|
13
|
+
if (ts.isYieldExpression(node)) return true;
|
|
14
|
+
return Boolean(ts.forEachChild(node, child => containsYield(child) || undefined));
|
|
48
15
|
}
|
|
49
16
|
|
|
50
|
-
const contract =
|
|
51
|
-
const loggingContract =
|
|
52
|
-
const cryptoContract =
|
|
53
|
-
const eventContract =
|
|
54
|
-
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');
|
|
55
24
|
const GENERATED_HANDLER_NAME = '__pulse_handler';
|
|
56
25
|
const PULSE_RUNTIME_PARAMETER = '__pulse';
|
|
57
26
|
|
|
@@ -201,6 +170,8 @@ function intrinsicForContextCall(parts) {
|
|
|
201
170
|
const entries = {
|
|
202
171
|
'req.header': ['request.header', 'string-or-undefined'],
|
|
203
172
|
'req.text': ['request.text', 'string'],
|
|
173
|
+
'req.body': ['request.body.forward-marker', 'incoming-body-marker'],
|
|
174
|
+
'output.close': ['response.output.close', 'pulse-result'],
|
|
204
175
|
'req.json': ['request.json', 'json'],
|
|
205
176
|
json: ['response.json', 'pulse-result'],
|
|
206
177
|
encodeJson: ['schema.encode.text', 'string'],
|
|
@@ -221,8 +192,10 @@ function resultKindForEffect(site, decoder, continuation, resultMode) {
|
|
|
221
192
|
if (site.result === 'opaque-response') return 'opaque-response';
|
|
222
193
|
if (site.result === 'structured-response') return 'structured-response';
|
|
223
194
|
if (site.result === 'ack') return 'ack';
|
|
195
|
+
if (site.result === 'string') return 'string';
|
|
224
196
|
if (site.kind === 'fetch') return 'fetch-response';
|
|
225
197
|
if (site.kind === 'config.get' || site.kind === 'secret.get') return 'string-or-undefined';
|
|
198
|
+
if (site.kind === 'output.readTextChunk') return 'json';
|
|
226
199
|
if (site.kind === 'time.now') return 'json';
|
|
227
200
|
if (site.kind === 'kv.get') return 'json-or-undefined';
|
|
228
201
|
if (site.kind === 'kv.put') return 'ack';
|
|
@@ -231,21 +204,6 @@ function resultKindForEffect(site, decoder, continuation, resultMode) {
|
|
|
231
204
|
return 'unknown';
|
|
232
205
|
}
|
|
233
206
|
|
|
234
|
-
function inferBinaryValueKind(operator, left, right) {
|
|
235
|
-
if (['===', '!==', '==', '!=', '<', '<=', '>', '>=', 'in'].includes(operator)) return 'boolean';
|
|
236
|
-
if (['&&', '||', '??'].includes(operator)) {
|
|
237
|
-
if (left.valueKind === right.valueKind) return left.valueKind;
|
|
238
|
-
if (['||', '??'].includes(operator)) {
|
|
239
|
-
const kinds = new Set([left.valueKind, right.valueKind]);
|
|
240
|
-
if (kinds.has('string') && kinds.has('string-or-undefined')) return 'string';
|
|
241
|
-
}
|
|
242
|
-
return 'unknown';
|
|
243
|
-
}
|
|
244
|
-
if (operator === '+' && (left.valueKind === 'string' || right.valueKind === 'string')) return 'string';
|
|
245
|
-
if (['+', '-', '*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'].includes(operator)) return 'number';
|
|
246
|
-
return 'unknown';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
207
|
class NativePlanBuilder {
|
|
250
208
|
constructor(compiled, options = {}) {
|
|
251
209
|
this.compiled = compiled;
|
|
@@ -253,6 +211,9 @@ class NativePlanBuilder {
|
|
|
253
211
|
this.diagnostics = [];
|
|
254
212
|
this.effects = [];
|
|
255
213
|
this.locals = [];
|
|
214
|
+
this.handlers = [];
|
|
215
|
+
this.helpers = [];
|
|
216
|
+
this.handlerLocalIndex = 0;
|
|
256
217
|
this.effectOccurrences = new Map();
|
|
257
218
|
this.continuationOccurrences = new Map();
|
|
258
219
|
this.localIndex = 0;
|
|
@@ -329,6 +290,20 @@ class NativePlanBuilder {
|
|
|
329
290
|
}
|
|
330
291
|
|
|
331
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();
|
|
332
307
|
this.generatedFile = `${this.metadata.file || 'app.ts'}.canonical.generated.js`;
|
|
333
308
|
this.sourceFile = ts.createSourceFile(
|
|
334
309
|
this.generatedFile,
|
|
@@ -348,6 +323,14 @@ class NativePlanBuilder {
|
|
|
348
323
|
this.ctxName = this.handler && this.handler.parameters[0] && ts.isIdentifier(this.handler.parameters[0].name)
|
|
349
324
|
? this.handler.parameters[0].name.text
|
|
350
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);
|
|
351
334
|
this.effectSites = new Map((this.metadata.effectSites || []).map((site) => [String(site.id), site]));
|
|
352
335
|
this.continuationSites = new Map((this.metadata.continuationSites || []).map((site) => [String(site.id), site]));
|
|
353
336
|
this.compilerOwnedCalls = new Set((this.metadata.compilerOwnedCalls || []).map(String));
|
|
@@ -448,6 +431,11 @@ class NativePlanBuilder {
|
|
|
448
431
|
if (ASSIGNMENT_OPERATORS.has(operator)) {
|
|
449
432
|
const target = this.assignmentTarget(current.left, scope);
|
|
450
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
|
+
}
|
|
451
439
|
return Object.freeze({ kind: 'assignment', operator, target, value, valueKind: value.valueKind || 'unknown' });
|
|
452
440
|
}
|
|
453
441
|
if (!PURE_BINARY_OPERATORS.has(operator)) {
|
|
@@ -455,7 +443,7 @@ class NativePlanBuilder {
|
|
|
455
443
|
}
|
|
456
444
|
const left = this.expression(current.left, scope);
|
|
457
445
|
const right = this.expression(current.right, scope);
|
|
458
|
-
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) });
|
|
459
447
|
}
|
|
460
448
|
|
|
461
449
|
if (ts.isPrefixUnaryExpression(current)) {
|
|
@@ -467,7 +455,7 @@ class NativePlanBuilder {
|
|
|
467
455
|
this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Prefix operator ${operator} is outside the canonical native value model.`, { operator });
|
|
468
456
|
}
|
|
469
457
|
const value = this.expression(current.operand, scope);
|
|
470
|
-
return Object.freeze({ kind: 'unary', operator, value, valueKind:
|
|
458
|
+
return Object.freeze({ kind: 'unary', operator, value, valueKind: pureValues.unaryKind(operator) });
|
|
471
459
|
}
|
|
472
460
|
|
|
473
461
|
if (ts.isPostfixUnaryExpression(current)) {
|
|
@@ -512,17 +500,19 @@ class NativePlanBuilder {
|
|
|
512
500
|
else if (current.name.text === 'headers') valueKind = 'headers';
|
|
513
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 });
|
|
514
502
|
}
|
|
515
|
-
|
|
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);
|
|
516
508
|
}
|
|
517
509
|
|
|
518
510
|
if (ts.isElementAccessExpression(current)) {
|
|
519
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.');
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
valueKind: 'unknown'
|
|
525
|
-
});
|
|
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);
|
|
526
516
|
}
|
|
527
517
|
|
|
528
518
|
if (ts.isTypeOfExpression(current)) {
|
|
@@ -549,6 +539,21 @@ class NativePlanBuilder {
|
|
|
549
539
|
callExpression(call, scope) {
|
|
550
540
|
if (call.questionDotToken) this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Optional calls are outside the canonical native value model.');
|
|
551
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
|
+
}
|
|
552
557
|
const ctxPath = contextPath(target, this.ctxName);
|
|
553
558
|
if (ctxPath) {
|
|
554
559
|
const intrinsic = intrinsicForContextCall(ctxPath);
|
|
@@ -579,6 +584,9 @@ class NativePlanBuilder {
|
|
|
579
584
|
|
|
580
585
|
if (ts.isIdentifier(target) && this.compilerOwnedCalls.has(target.text) && this.compilerOwnedIntrinsics.has(target.text)) {
|
|
581
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
|
+
});
|
|
582
590
|
return Object.freeze({
|
|
583
591
|
kind: 'intrinsic',
|
|
584
592
|
name: intrinsic[0],
|
|
@@ -615,6 +623,7 @@ class NativePlanBuilder {
|
|
|
615
623
|
|
|
616
624
|
assignmentTarget(node, scope) {
|
|
617
625
|
const target = this.expression(node, scope);
|
|
626
|
+
if (target.kind === 'property' || target.kind === 'element') resultFacts.invalidate(scope);
|
|
618
627
|
if (target.kind === 'local' && scope.get(target.name)?.declaration === 'const') {
|
|
619
628
|
this.fail(node, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'A const local cannot be reassigned.', { name: target.name });
|
|
620
629
|
}
|
|
@@ -624,13 +633,16 @@ class NativePlanBuilder {
|
|
|
624
633
|
allocateLocal(name, valueKind, statementPath, declaration) {
|
|
625
634
|
this.localIndex += 1;
|
|
626
635
|
const local = Object.freeze({
|
|
627
|
-
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 } : {}),
|
|
628
639
|
name: String(name),
|
|
629
640
|
valueKind: contract.CANONICAL_NATIVE_VALUE_KINDS.includes(valueKind) ? valueKind : 'unknown',
|
|
630
641
|
declaration,
|
|
631
642
|
statementPath
|
|
632
643
|
});
|
|
633
644
|
this.locals.push(local);
|
|
645
|
+
if (pureValues.scalar(valueKind)) this.pureTypes.set(local.id, valueKind);
|
|
634
646
|
this.summary.localCount += 1;
|
|
635
647
|
return local;
|
|
636
648
|
}
|
|
@@ -703,6 +715,7 @@ class NativePlanBuilder {
|
|
|
703
715
|
const record = Object.freeze({
|
|
704
716
|
order: this.effectOrder,
|
|
705
717
|
id: effectId || `invalid-effect-${this.effectOrder}`,
|
|
718
|
+
...(this.activeHelper ? { helperId: this.activeHelper.id } : {}),
|
|
706
719
|
kind: kind || 'invalid',
|
|
707
720
|
providerKind: site && site.providerKind ? String(site.providerKind) : undefined,
|
|
708
721
|
operation: site && site.operation ? String(site.operation) : undefined,
|
|
@@ -714,6 +727,7 @@ class NativePlanBuilder {
|
|
|
714
727
|
contractId: site && site.contractId ? String(site.contractId) : undefined,
|
|
715
728
|
declaredResult: site && site.result ? String(site.result) : undefined,
|
|
716
729
|
decoder: site && site.decoder ? String(site.decoder) : null,
|
|
730
|
+
...(site?.borrowedValue && result?.mode === 'bind' ? { borrowedValue: cloneJson(site.borrowedValue) } : {}),
|
|
717
731
|
continuationId,
|
|
718
732
|
statementPath,
|
|
719
733
|
source: Object.freeze({ file: this.metadata.file, ...(cloneJson(site && site.position) || {}) }),
|
|
@@ -757,6 +771,23 @@ class NativePlanBuilder {
|
|
|
757
771
|
for (let index = 0; index < statement.declarationList.declarations.length; index += 1) {
|
|
758
772
|
const item = statement.declarationList.declarations[index];
|
|
759
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
|
+
}
|
|
760
791
|
const yielded = item.initializer ? this.pulseYield(item.initializer) : undefined;
|
|
761
792
|
|
|
762
793
|
if (yielded && yielded.mode === 'group') {
|
|
@@ -821,7 +852,9 @@ class NativePlanBuilder {
|
|
|
821
852
|
const local = this.allocateLocal(item.name.text, resultKindForEffect(site || {}, decoder), itemPath, declaration);
|
|
822
853
|
const result = Object.freeze({ mode: 'bind', localId: local.id, localName: local.name, valueKind: local.valueKind, decoder });
|
|
823
854
|
const effect = this.prepareEffect(marker, continuationId, scope, itemPath, result);
|
|
824
|
-
|
|
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));
|
|
825
858
|
out.push(Object.freeze({ kind: 'effect', effectId: effect.id, continuationId, result, statementPath: itemPath }));
|
|
826
859
|
continue;
|
|
827
860
|
}
|
|
@@ -832,6 +865,8 @@ class NativePlanBuilder {
|
|
|
832
865
|
}
|
|
833
866
|
const value = item.initializer ? this.expression(item.initializer, scope) : Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
|
|
834
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);
|
|
835
870
|
scope.set(local.name, local);
|
|
836
871
|
out.push(Object.freeze({ kind: 'local', localId: local.id, name: local.name, declaration, valueKind: local.valueKind, value, statementPath: itemPath }));
|
|
837
872
|
}
|
|
@@ -907,6 +942,36 @@ class NativePlanBuilder {
|
|
|
907
942
|
}
|
|
908
943
|
|
|
909
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
|
+
}
|
|
910
975
|
const statementPath = formatStatementPath(pathParts);
|
|
911
976
|
this.summary.maxStatementDepth = Math.max(this.summary.maxStatementDepth, depth);
|
|
912
977
|
|
|
@@ -915,20 +980,40 @@ class NativePlanBuilder {
|
|
|
915
980
|
if (ts.isVariableStatement(statement)) return this.lowerVariableStatement(statement, scope, pathParts, depth);
|
|
916
981
|
|
|
917
982
|
if (ts.isForStatement(statement)) {
|
|
918
|
-
|
|
919
|
-
|
|
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);
|
|
920
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
|
+
}
|
|
921
1002
|
const loopScope = new Map(scope);
|
|
922
1003
|
const local = this.allocateLocal(loop.name, 'number', statementPath, 'let');
|
|
923
1004
|
loopScope.set(loop.name, local);
|
|
924
|
-
const
|
|
925
|
-
|
|
1005
|
+
const depthField = readLoop ? 'readLoopDepth' : 'pureLoopDepth';
|
|
1006
|
+
const previousDepth = this[depthField] || 0;
|
|
1007
|
+
this[depthField] = previousDepth + 1;
|
|
926
1008
|
const body = this.statement(statement.statement, loopScope, [...pathParts, 'body'], depth + 1);
|
|
927
|
-
this
|
|
928
|
-
return [Object.freeze({ kind: 'pure-loop', localId: local.id, maxIterations: loop.maxIterations,
|
|
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) } : {}),
|
|
929
1014
|
test: this.expression(statement.condition, loopScope), body: Object.freeze(body), statementPath })];
|
|
930
1015
|
}
|
|
931
|
-
if ((ts.isBreakStatement(statement) || ts.isContinueStatement(statement)) && this.pureLoopDepth && !statement.label) {
|
|
1016
|
+
if ((ts.isBreakStatement(statement) || ts.isContinueStatement(statement)) && (this.pureLoopDepth || this.readLoopDepth) && !statement.label) {
|
|
932
1017
|
return [Object.freeze({ kind: ts.isBreakStatement(statement) ? 'break' : 'continue', statementPath })];
|
|
933
1018
|
}
|
|
934
1019
|
|
|
@@ -937,8 +1022,10 @@ class NativePlanBuilder {
|
|
|
937
1022
|
const thenScope = new Map(scope);
|
|
938
1023
|
const elseScope = new Map(scope);
|
|
939
1024
|
const test = this.expression(statement.expression, scope);
|
|
1025
|
+
resultFacts.refine(test, true, thenScope);
|
|
1026
|
+
resultFacts.refine(test, false, elseScope);
|
|
940
1027
|
// A const primitive cannot change between this guard and a branch read.
|
|
941
|
-
//
|
|
1028
|
+
// A returning branch also lets its surviving sibling dominate later reads.
|
|
942
1029
|
if (test.kind === 'binary' && ['===', '!=='].includes(test.operator)) {
|
|
943
1030
|
const read = test.left.kind === 'undefined' ? test.right : test.right.kind === 'undefined' ? test.left : undefined;
|
|
944
1031
|
const local = read?.kind === 'local' ? scope.get(read.name) : undefined;
|
|
@@ -946,14 +1033,34 @@ class NativePlanBuilder {
|
|
|
946
1033
|
(test.operator === '!==' ? thenScope : elseScope).set(local.name, Object.freeze({ ...local, valueKind: 'string' }));
|
|
947
1034
|
}
|
|
948
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;
|
|
949
1041
|
const thenBody = ts.isBlock(statement.thenStatement)
|
|
950
1042
|
? this.statementList(statement.thenStatement.statements, thenScope, [...pathParts, 'then'], depth + 1)
|
|
951
1043
|
: this.statement(statement.thenStatement, thenScope, [...pathParts, 'then', 0], depth + 1);
|
|
1044
|
+
this.activeEntry = previousEntry;
|
|
952
1045
|
const elseBody = !statement.elseStatement
|
|
953
1046
|
? []
|
|
954
1047
|
: ts.isBlock(statement.elseStatement)
|
|
955
1048
|
? this.statementList(statement.elseStatement.statements, elseScope, [...pathParts, 'else'], depth + 1)
|
|
956
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
|
+
}
|
|
957
1064
|
return [Object.freeze({
|
|
958
1065
|
kind: 'if',
|
|
959
1066
|
test,
|
|
@@ -997,6 +1104,35 @@ class NativePlanBuilder {
|
|
|
997
1104
|
return out;
|
|
998
1105
|
}
|
|
999
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
|
+
|
|
1000
1136
|
reconcile() {
|
|
1001
1137
|
const expectedEffectIds = (this.metadata.effectSites || []).map((site) => String(site.id));
|
|
1002
1138
|
const actualEffectIds = this.effects.map((site) => site.id);
|
|
@@ -1019,8 +1155,9 @@ class NativePlanBuilder {
|
|
|
1019
1155
|
this.reconcile();
|
|
1020
1156
|
if (this.diagnostics.length > 0) throw new CanonicalNativePlanError(`Native-plan lowering failed for ${this.metadata.file}.`, this.diagnostics);
|
|
1021
1157
|
|
|
1022
|
-
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({
|
|
1023
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 } : {}),
|
|
1024
1161
|
kind: String(site.kind),
|
|
1025
1162
|
effectIds: Object.freeze(site.effectIds.map(String)),
|
|
1026
1163
|
stateIndex: index + 1,
|
|
@@ -1043,12 +1180,13 @@ class NativePlanBuilder {
|
|
|
1043
1180
|
eventType: site.eventType ? String(site.eventType) : undefined,
|
|
1044
1181
|
eventSchemaId: site.eventSchemaId !== undefined ? site.eventSchemaId : undefined
|
|
1045
1182
|
}));
|
|
1183
|
+
if (this.packageApplication) this.effects = [...this.packageApplication.effects];
|
|
1046
1184
|
const states = [Object.freeze({ id: 'entry', kind: 'entry', stateIndex: 0 })]
|
|
1047
1185
|
.concat(continuations.map((site) => Object.freeze({ id: site.id, kind: 'continuation', continuationKind: site.kind, effectIds: site.effectIds, stateIndex: site.stateIndex })));
|
|
1048
|
-
this.summary = summarizeNativePlan(body, this.locals, this.effects, continuations);
|
|
1186
|
+
this.summary = summarizeNativePlan(body, this.locals, this.effects, continuations, this.handlers);
|
|
1049
1187
|
const hasInboundEvents = Number(this.metadata.events && this.metadata.events.count || 0) > 0;
|
|
1050
1188
|
|
|
1051
|
-
|
|
1189
|
+
let unsigned = {
|
|
1052
1190
|
version: contract.CANONICAL_NATIVE_PLAN_VERSION,
|
|
1053
1191
|
compilerVersion: CANONICAL_NATIVE_PLAN_COMPILER_VERSION,
|
|
1054
1192
|
hashAlgorithm: contract.CANONICAL_NATIVE_PLAN_HASH_ALGORITHM,
|
|
@@ -1107,10 +1245,12 @@ class NativePlanBuilder {
|
|
|
1107
1245
|
body: Object.freeze(body)
|
|
1108
1246
|
}),
|
|
1109
1247
|
locals: Object.freeze(this.locals),
|
|
1248
|
+
handlers: Object.freeze(this.handlers),
|
|
1249
|
+
...(this.helpers.length ? { helpers: Object.freeze(this.helpers) } : {}),
|
|
1110
1250
|
effects: Object.freeze(this.effects),
|
|
1111
1251
|
continuations: Object.freeze(continuations),
|
|
1112
1252
|
states: Object.freeze(states),
|
|
1113
|
-
capabilities: Object.freeze([...(this.metadata.capabilities || [])].map(String).sort()),
|
|
1253
|
+
capabilities: Object.freeze([...new Set([...(this.metadata.capabilities || []), ...(this.packageApplication?.capabilities || [])])].map(String).sort()),
|
|
1114
1254
|
schemas: Object.freeze({
|
|
1115
1255
|
sourceHash: this.metadata.schemaSourceHash,
|
|
1116
1256
|
ids: Object.freeze([...(this.metadata.schemaIds || [])].map(String)),
|
|
@@ -1124,9 +1264,12 @@ class NativePlanBuilder {
|
|
|
1124
1264
|
...(this.compiled.cryptoRealizationPlan && this.compiled.cryptoRealizationPlan.target === 'native' ? {
|
|
1125
1265
|
crypto: deepFreeze(cloneJson(this.compiled.cryptoRealizationPlan))
|
|
1126
1266
|
} : {}),
|
|
1127
|
-
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 } : {}) }),
|
|
1128
1269
|
summary: Object.freeze({ ...this.summary })
|
|
1129
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 || [])]);
|
|
1130
1273
|
const planHash = stableHash(stableStringify(unsigned));
|
|
1131
1274
|
const plan = deepFreeze({ ...unsigned, planHash });
|
|
1132
1275
|
assertCanonicalNativePlan(plan);
|
|
@@ -1172,6 +1315,7 @@ function walkExpression(expression, visit) {
|
|
|
1172
1315
|
walkExpression(expression.object, visit);
|
|
1173
1316
|
walkExpression(expression.index, visit);
|
|
1174
1317
|
break;
|
|
1318
|
+
case 'pure-helper-call':
|
|
1175
1319
|
case 'intrinsic':
|
|
1176
1320
|
for (const argument of expression.arguments || []) walkExpression(argument, visit);
|
|
1177
1321
|
break;
|
|
@@ -1197,12 +1341,17 @@ function walkExpression(expression, visit) {
|
|
|
1197
1341
|
function walkStatements(statements, visitor) {
|
|
1198
1342
|
for (const statement of statements || []) {
|
|
1199
1343
|
visitor(statement);
|
|
1200
|
-
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);
|
|
1201
1346
|
else if (statement.kind === 'if') {
|
|
1202
1347
|
walkExpression(statement.test, visitor.expression);
|
|
1203
1348
|
walkStatements(statement.then, visitor);
|
|
1204
1349
|
walkStatements(statement.else, visitor);
|
|
1205
|
-
} else if (statement.kind === 'pure-loop') {
|
|
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
|
+
}
|
|
1206
1355
|
walkExpression(statement.test, visitor.expression);
|
|
1207
1356
|
walkStatements(statement.body, visitor);
|
|
1208
1357
|
} else if (statement.kind === 'return') walkExpression(statement.value, visitor.expression);
|
|
@@ -1216,7 +1365,7 @@ function countExpression(expression) {
|
|
|
1216
1365
|
return count;
|
|
1217
1366
|
}
|
|
1218
1367
|
|
|
1219
|
-
function summarizeNativePlan(body, locals, effects, continuations) {
|
|
1368
|
+
function summarizeNativePlan(body, locals, effects, continuations, handlers = []) {
|
|
1220
1369
|
const summary = {
|
|
1221
1370
|
statementCount: 0,
|
|
1222
1371
|
expressionCount: 0,
|
|
@@ -1239,12 +1388,15 @@ function summarizeNativePlan(body, locals, effects, continuations) {
|
|
|
1239
1388
|
summary.expressionCount += countExpression(statement.test);
|
|
1240
1389
|
visitStatements(statement.then, depth + 1);
|
|
1241
1390
|
visitStatements(statement.else, depth + 1);
|
|
1242
|
-
} else if (statement.kind === 'pure-loop') {
|
|
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);
|
|
1243
1393
|
summary.expressionCount += countExpression(statement.test);
|
|
1244
1394
|
visitStatements(statement.body, depth + 1);
|
|
1245
1395
|
} else if (statement.kind === 'return') {
|
|
1246
1396
|
summary.returnCount += 1;
|
|
1247
1397
|
summary.expressionCount += countExpression(statement.value);
|
|
1398
|
+
} else if (statement.kind === 'helper-call') {
|
|
1399
|
+
for (const arg of statement.arguments || []) summary.expressionCount += countExpression(arg);
|
|
1248
1400
|
} else if (statement.kind === 'expression') {
|
|
1249
1401
|
summary.expressionCount += countExpression(statement.expression);
|
|
1250
1402
|
} else if (statement.kind === 'effect') {
|
|
@@ -1256,6 +1408,7 @@ function summarizeNativePlan(body, locals, effects, continuations) {
|
|
|
1256
1408
|
}
|
|
1257
1409
|
|
|
1258
1410
|
visitStatements(body, 0);
|
|
1411
|
+
for (const handler of handlers) visitStatements(handler.body, 0);
|
|
1259
1412
|
for (const effect of effects) {
|
|
1260
1413
|
for (const input of effect.inputs || []) summary.expressionCount += countExpression(input.value);
|
|
1261
1414
|
const decoder = effect.result && effect.result.decoder;
|
|
@@ -1286,30 +1439,121 @@ function validateResult(result, fail, localIds, detail = {}) {
|
|
|
1286
1439
|
}
|
|
1287
1440
|
|
|
1288
1441
|
function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
|
|
1289
|
-
|
|
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) {
|
|
1290
1493
|
for (const statement of statements || []) {
|
|
1291
|
-
if (statement.kind === 'pure-loop') {
|
|
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';
|
|
1292
1497
|
const limit = statement.maxIterations;
|
|
1293
|
-
if (!localIds.has(statement.localId) || counters.has(statement.localId)) fail(
|
|
1294
|
-
if (!Number.isInteger(limit) || limit < 0 || limit > contract.CANONICAL_PURE_LOOP_LIMITS.maxIterations
|
|
1295
|
-
|| product * Math.max(1, limit) > contract.CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations) fail(
|
|
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
|
+
}
|
|
1296
1510
|
let bound = statement.test;
|
|
1297
1511
|
while (bound && bound.kind === 'binary' && bound.operator === '&&') bound = bound.left;
|
|
1298
1512
|
if (!bound || bound.kind !== 'binary' || bound.operator !== '<' || bound.left?.kind !== 'local' || bound.left.id !== statement.localId
|
|
1299
|
-
|| bound.right?.kind !== 'literal' || bound.right.value !== limit) fail(
|
|
1513
|
+
|| bound.right?.kind !== 'literal' || bound.right.value !== limit) fail(`${label} test must start with its declared literal cap`);
|
|
1300
1514
|
const active = new Set([...counters, statement.localId]);
|
|
1301
1515
|
checkPureExpression(statement.test, active, true);
|
|
1302
|
-
|
|
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);
|
|
1303
1528
|
} else {
|
|
1304
|
-
|
|
1305
|
-
if (!
|
|
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');
|
|
1306
1541
|
if (counters.size) {
|
|
1307
1542
|
if (statement.kind === 'local' && counters.has(statement.localId)) fail('pure loop counter cannot be rebound');
|
|
1308
|
-
checkPureExpression(statement.value || statement.expression || statement.test, counters);
|
|
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);
|
|
1309
1553
|
}
|
|
1310
1554
|
if (statement.kind === 'if') {
|
|
1311
|
-
validateLoops(statement.then, counters, product);
|
|
1312
|
-
validateLoops(statement.else, counters, product);
|
|
1555
|
+
validateLoops(statement.then, counters, product, mode, pureHelper);
|
|
1556
|
+
validateLoops(statement.else, counters, product, mode, pureHelper);
|
|
1313
1557
|
}
|
|
1314
1558
|
}
|
|
1315
1559
|
}
|
|
@@ -1320,7 +1564,15 @@ function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
|
|
|
1320
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');
|
|
1321
1565
|
});
|
|
1322
1566
|
}
|
|
1323
|
-
|
|
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);
|
|
1324
1576
|
const visitor = (statement) => {
|
|
1325
1577
|
if (!contract.CANONICAL_NATIVE_STATEMENT_KINDS.includes(statement.kind)) fail('statement kind is unknown', { kind: statement.kind });
|
|
1326
1578
|
if (statement.kind === 'local' && !localIds.has(statement.localId)) fail('local statement references unknown local', { localId: statement.localId });
|
|
@@ -1337,6 +1589,73 @@ function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
|
|
|
1337
1589
|
};
|
|
1338
1590
|
visitor.expression = (expression) => validateExpression(expression, fail, localIds);
|
|
1339
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
|
+
}
|
|
1340
1659
|
}
|
|
1341
1660
|
|
|
1342
1661
|
function assertCanonicalNativePlan(plan) {
|
|
@@ -1379,6 +1698,7 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1379
1698
|
}
|
|
1380
1699
|
}
|
|
1381
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');
|
|
1382
1702
|
if (!Array.isArray(plan.effects)) fail('plan effects must be an array');
|
|
1383
1703
|
if (!Array.isArray(plan.continuations)) fail('plan continuations must be an array');
|
|
1384
1704
|
if (!Array.isArray(plan.states)) fail('plan states must be an array');
|
|
@@ -1438,11 +1758,36 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1438
1758
|
fail('plan logging evidence is invalid', { logging: plan.logging });
|
|
1439
1759
|
}
|
|
1440
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
|
+
}
|
|
1441
1764
|
const unsigned = { ...plan };
|
|
1442
1765
|
delete unsigned.planHash;
|
|
1443
1766
|
const expectedHash = stableHash(stableStringify(unsigned));
|
|
1444
1767
|
if (plan.planHash !== expectedHash) fail('plan hash mismatch', { expectedHash, actual: plan.planHash });
|
|
1445
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');
|
|
1446
1791
|
const locals = Array.isArray(plan.locals) ? plan.locals : [];
|
|
1447
1792
|
const effects = Array.isArray(plan.effects) ? plan.effects : [];
|
|
1448
1793
|
const continuations = Array.isArray(plan.continuations) ? plan.continuations : [];
|
|
@@ -1480,6 +1825,9 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1480
1825
|
}
|
|
1481
1826
|
|
|
1482
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);
|
|
1483
1831
|
|
|
1484
1832
|
const states = Array.isArray(plan.states) ? plan.states : [];
|
|
1485
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 });
|
|
@@ -1508,10 +1856,11 @@ function assertCanonicalNativePlan(plan) {
|
|
|
1508
1856
|
};
|
|
1509
1857
|
statementVisitor.expression = () => {};
|
|
1510
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);
|
|
1511
1860
|
|
|
1512
1861
|
if (!plan.summary || typeof plan.summary !== 'object') fail('plan summary is required');
|
|
1513
1862
|
else {
|
|
1514
|
-
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 || [])]);
|
|
1515
1864
|
for (const [key, expected] of Object.entries(expectedSummary)) {
|
|
1516
1865
|
if (plan.summary[key] !== expected) fail(`summary ${key} mismatch`, { expected, actual: plan.summary[key] });
|
|
1517
1866
|
}
|