@pulse-compute/wasm-compiler 1.0.0-beta.4 → 1.0.0-beta.5
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 +3 -3
- package/package.json +11 -11
- package/src/canonical-native-compiler.js +5 -0
- package/src/canonical-native-plan.js +87 -4
- package/src/javascript-application-plan.js +4 -1
- package/src/project-target-support.js +10 -2
- package/src/spine/bounded-pure-loop.js +145 -0
- package/src/spine/handler-ir.js +37 -21
- package/src/spine/handler-surface-authority.js +4 -0
- package/src/spine/plain-handler-frontend.js +12 -4
- package/src/spine/router-handler-frontend.js +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
> **Stability:** Internal compiler interface; no application-author compatibility guarantee.<br>
|
|
9
9
|
> **npm:** [`@pulse-compute/wasm-compiler`](https://www.npmjs.com/package/@pulse-compute/wasm-compiler)<br>
|
|
10
10
|
> **Canonical replacement:** `@pulse-compute/cli`<br>
|
|
11
|
-
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
11
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/)
|
|
12
12
|
>
|
|
13
|
-
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.
|
|
13
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.5` package policy.
|
|
14
14
|
<!-- pulse-package-status:end -->
|
|
15
15
|
|
|
16
16
|
Whole-project compiler implementation used by the `pulse` workflow. Application projects should use `@pulse-compute/cli`, not import this package directly.
|
|
@@ -52,4 +52,4 @@ The package exports its root implementation plus focused compiler, CLI, extracto
|
|
|
52
52
|
|
|
53
53
|
Compiler changes should be covered by focused static/lowering tests, canonical project inspection, provider-plan parity, packed-package discovery where relevant, and the release documentation gates.
|
|
54
54
|
|
|
55
|
-
See the installed [implementation package guide](https://pulsecompute.io/v1.0.0-beta.
|
|
55
|
+
See the installed [implementation package guide](https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/) and [compilation concepts](https://pulsecompute.io/v1.0.0-beta.5/concepts/compilation-and-lowering/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/wasm-compiler",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "PulseWasm compiler package inside the Pulse monorepo.",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"pulsewasm": "./bin/pulsewasm-extract.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@pulse-compute/provider-fastly": "1.0.0-beta.
|
|
13
|
-
"@pulse-compute/provider-node": "1.0.0-beta.
|
|
14
|
-
"@pulse-compute/wasm-build-support": "1.0.0-beta.
|
|
15
|
-
"@pulse-compute/wasm-contracts": "1.0.0-beta.
|
|
16
|
-
"@pulse-compute/wasm-guest-link": "1.0.0-beta.
|
|
17
|
-
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.
|
|
18
|
-
"@pulse-compute/wasm-library-kit": "1.0.0-beta.
|
|
19
|
-
"@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.
|
|
20
|
-
"@pulse-compute/wasm-schema-json": "1.0.0-beta.
|
|
12
|
+
"@pulse-compute/provider-fastly": "1.0.0-beta.5",
|
|
13
|
+
"@pulse-compute/provider-node": "1.0.0-beta.5",
|
|
14
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.5",
|
|
15
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.5",
|
|
16
|
+
"@pulse-compute/wasm-guest-link": "1.0.0-beta.5",
|
|
17
|
+
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.5",
|
|
18
|
+
"@pulse-compute/wasm-library-kit": "1.0.0-beta.5",
|
|
19
|
+
"@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.5",
|
|
20
|
+
"@pulse-compute/wasm-schema-json": "1.0.0-beta.5",
|
|
21
21
|
"assemblyscript": "0.28.18",
|
|
22
22
|
"json-as": "1.5.0",
|
|
23
23
|
"typescript": "5.9.3"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
59
59
|
"directory": "wasm/packages/compiler"
|
|
60
60
|
},
|
|
61
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
61
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/",
|
|
62
62
|
"bugs": {
|
|
63
63
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
64
64
|
},
|
|
@@ -230,6 +230,11 @@ function realizeCanonicalNativePlan(plan, options = {}, providerRequirements) {
|
|
|
230
230
|
'--optimize'
|
|
231
231
|
];
|
|
232
232
|
const optimization = appendAssemblyScriptOptimizationArgs(args, options.nativeOptimization);
|
|
233
|
+
// The text capacity profile bounds each Native module to 256 MiB.
|
|
234
|
+
// Linked guests retain their separately owned fixed-memory ABI.
|
|
235
|
+
if (guestUnits.length === 0 && plan.effects.some(effect => effect.kind === 'crypto.digestText' || effect.kind.startsWith('s3.'))) {
|
|
236
|
+
args.push('--maximumMemory', '4096');
|
|
237
|
+
}
|
|
233
238
|
if (guestUnits.length > 0) {
|
|
234
239
|
args.push(
|
|
235
240
|
'--importMemory',
|
|
@@ -5,6 +5,7 @@ const fs = require('node:fs');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const ts = require('typescript');
|
|
7
7
|
const { executeCanonicalNativePlanSpine } = require('./spine/canonical-native-plan.js');
|
|
8
|
+
const { inspectBoundedPureLoop } = require('./spine/bounded-pure-loop.js');
|
|
8
9
|
|
|
9
10
|
function loadNativePlanContract() {
|
|
10
11
|
try { return require('@pulse-compute/wasm-contracts/handler/canonical-native-plan'); }
|
|
@@ -203,6 +204,7 @@ function intrinsicForContextCall(parts) {
|
|
|
203
204
|
'req.json': ['request.json', 'json'],
|
|
204
205
|
json: ['response.json', 'pulse-result'],
|
|
205
206
|
encodeJson: ['schema.encode.text', 'string'],
|
|
207
|
+
decodeJson: ['schema.decode.text', 'json'],
|
|
206
208
|
text: ['response.text', 'pulse-result'],
|
|
207
209
|
response: ['response.custom', 'pulse-result'],
|
|
208
210
|
kv: ['kv.namespace', 'kv-namespace'],
|
|
@@ -221,6 +223,7 @@ function resultKindForEffect(site, decoder, continuation, resultMode) {
|
|
|
221
223
|
if (site.result === 'ack') return 'ack';
|
|
222
224
|
if (site.kind === 'fetch') return 'fetch-response';
|
|
223
225
|
if (site.kind === 'config.get' || site.kind === 'secret.get') return 'string-or-undefined';
|
|
226
|
+
if (site.kind === 'time.now') return 'json';
|
|
224
227
|
if (site.kind === 'kv.get') return 'json-or-undefined';
|
|
225
228
|
if (site.kind === 'kv.put') return 'ack';
|
|
226
229
|
if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(site.kind)) return 'json';
|
|
@@ -443,7 +446,7 @@ class NativePlanBuilder {
|
|
|
443
446
|
if (ts.isBinaryExpression(current)) {
|
|
444
447
|
const operator = current.operatorToken.getText(this.sourceFile);
|
|
445
448
|
if (ASSIGNMENT_OPERATORS.has(operator)) {
|
|
446
|
-
const target = this.
|
|
449
|
+
const target = this.assignmentTarget(current.left, scope);
|
|
447
450
|
const value = this.expression(current.right, scope);
|
|
448
451
|
return Object.freeze({ kind: 'assignment', operator, target, value, valueKind: value.valueKind || 'unknown' });
|
|
449
452
|
}
|
|
@@ -458,7 +461,7 @@ class NativePlanBuilder {
|
|
|
458
461
|
if (ts.isPrefixUnaryExpression(current)) {
|
|
459
462
|
const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(0, 1);
|
|
460
463
|
if (UPDATE_OPERATORS.has(operator)) {
|
|
461
|
-
return Object.freeze({ kind: 'update', operator, prefix: true, target: this.
|
|
464
|
+
return Object.freeze({ kind: 'update', operator, prefix: true, target: this.assignmentTarget(current.operand, scope), valueKind: 'number' });
|
|
462
465
|
}
|
|
463
466
|
if (!PREFIX_OPERATORS.has(operator)) {
|
|
464
467
|
this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Prefix operator ${operator} is outside the canonical native value model.`, { operator });
|
|
@@ -470,7 +473,7 @@ class NativePlanBuilder {
|
|
|
470
473
|
if (ts.isPostfixUnaryExpression(current)) {
|
|
471
474
|
const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(-2);
|
|
472
475
|
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.
|
|
476
|
+
return Object.freeze({ kind: 'update', operator, prefix: false, target: this.assignmentTarget(current.operand, scope), valueKind: 'number' });
|
|
474
477
|
}
|
|
475
478
|
|
|
476
479
|
if (ts.isConditionalExpression(current)) {
|
|
@@ -587,6 +590,9 @@ class NativePlanBuilder {
|
|
|
587
590
|
if (ts.isPropertyAccessExpression(target)) {
|
|
588
591
|
const receiver = this.expression(target.expression, scope);
|
|
589
592
|
const method = target.name.text;
|
|
593
|
+
if (method === 'trim' && !target.questionDotToken && !call.questionDotToken && call.arguments.length === 0) {
|
|
594
|
+
return Object.freeze({ kind: 'method-call', receiver, method: 'string.trim', arguments: Object.freeze([]), valueKind: 'string' });
|
|
595
|
+
}
|
|
590
596
|
if (!FETCH_RESPONSE_METHODS.has(method) || receiver.valueKind !== 'fetch-response') {
|
|
591
597
|
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
598
|
}
|
|
@@ -607,6 +613,14 @@ class NativePlanBuilder {
|
|
|
607
613
|
return Object.freeze({ kind: 'intrinsic', name: 'unsupported:call', arguments: Object.freeze([]), valueKind: 'unknown' });
|
|
608
614
|
}
|
|
609
615
|
|
|
616
|
+
assignmentTarget(node, scope) {
|
|
617
|
+
const target = this.expression(node, scope);
|
|
618
|
+
if (target.kind === 'local' && scope.get(target.name)?.declaration === 'const') {
|
|
619
|
+
this.fail(node, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'A const local cannot be reassigned.', { name: target.name });
|
|
620
|
+
}
|
|
621
|
+
return target;
|
|
622
|
+
}
|
|
623
|
+
|
|
610
624
|
allocateLocal(name, valueKind, statementPath, declaration) {
|
|
611
625
|
this.localIndex += 1;
|
|
612
626
|
const local = Object.freeze({
|
|
@@ -900,10 +914,38 @@ class NativePlanBuilder {
|
|
|
900
914
|
|
|
901
915
|
if (ts.isVariableStatement(statement)) return this.lowerVariableStatement(statement, scope, pathParts, depth);
|
|
902
916
|
|
|
917
|
+
if (ts.isForStatement(statement)) {
|
|
918
|
+
const loop = inspectBoundedPureLoop(statement, { ctxName: this.ctxName });
|
|
919
|
+
for (const error of loop.errors) this.fail(error.node, 'PULSE_CANONICAL_PURE_LOOP_UNSUPPORTED', error.message);
|
|
920
|
+
if (loop.errors.length) return [];
|
|
921
|
+
const loopScope = new Map(scope);
|
|
922
|
+
const local = this.allocateLocal(loop.name, 'number', statementPath, 'let');
|
|
923
|
+
loopScope.set(loop.name, local);
|
|
924
|
+
const previousDepth = this.pureLoopDepth || 0;
|
|
925
|
+
this.pureLoopDepth = previousDepth + 1;
|
|
926
|
+
const body = this.statement(statement.statement, loopScope, [...pathParts, 'body'], depth + 1);
|
|
927
|
+
this.pureLoopDepth = previousDepth;
|
|
928
|
+
return [Object.freeze({ kind: 'pure-loop', localId: local.id, maxIterations: loop.maxIterations,
|
|
929
|
+
test: this.expression(statement.condition, loopScope), body: Object.freeze(body), statementPath })];
|
|
930
|
+
}
|
|
931
|
+
if ((ts.isBreakStatement(statement) || ts.isContinueStatement(statement)) && this.pureLoopDepth && !statement.label) {
|
|
932
|
+
return [Object.freeze({ kind: ts.isBreakStatement(statement) ? 'break' : 'continue', statementPath })];
|
|
933
|
+
}
|
|
934
|
+
|
|
903
935
|
if (ts.isIfStatement(statement)) {
|
|
904
936
|
this.summary.branchCount += 1;
|
|
905
937
|
const thenScope = new Map(scope);
|
|
906
938
|
const elseScope = new Map(scope);
|
|
939
|
+
const test = this.expression(statement.expression, scope);
|
|
940
|
+
// A const primitive cannot change between this guard and a branch read.
|
|
941
|
+
// Keep mutable locals and the scope after the branch unrefined.
|
|
942
|
+
if (test.kind === 'binary' && ['===', '!=='].includes(test.operator)) {
|
|
943
|
+
const read = test.left.kind === 'undefined' ? test.right : test.right.kind === 'undefined' ? test.left : undefined;
|
|
944
|
+
const local = read?.kind === 'local' ? scope.get(read.name) : undefined;
|
|
945
|
+
if (local?.declaration === 'const' && local.valueKind === 'string-or-undefined') {
|
|
946
|
+
(test.operator === '!==' ? thenScope : elseScope).set(local.name, Object.freeze({ ...local, valueKind: 'string' }));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
907
949
|
const thenBody = ts.isBlock(statement.thenStatement)
|
|
908
950
|
? this.statementList(statement.thenStatement.statements, thenScope, [...pathParts, 'then'], depth + 1)
|
|
909
951
|
: this.statement(statement.thenStatement, thenScope, [...pathParts, 'then', 0], depth + 1);
|
|
@@ -914,7 +956,7 @@ class NativePlanBuilder {
|
|
|
914
956
|
: this.statement(statement.elseStatement, elseScope, [...pathParts, 'else', 0], depth + 1);
|
|
915
957
|
return [Object.freeze({
|
|
916
958
|
kind: 'if',
|
|
917
|
-
test
|
|
959
|
+
test,
|
|
918
960
|
then: Object.freeze(thenBody),
|
|
919
961
|
else: Object.freeze(elseBody),
|
|
920
962
|
statementPath
|
|
@@ -1160,6 +1202,9 @@ function walkStatements(statements, visitor) {
|
|
|
1160
1202
|
walkExpression(statement.test, visitor.expression);
|
|
1161
1203
|
walkStatements(statement.then, visitor);
|
|
1162
1204
|
walkStatements(statement.else, visitor);
|
|
1205
|
+
} else if (statement.kind === 'pure-loop') {
|
|
1206
|
+
walkExpression(statement.test, visitor.expression);
|
|
1207
|
+
walkStatements(statement.body, visitor);
|
|
1163
1208
|
} else if (statement.kind === 'return') walkExpression(statement.value, visitor.expression);
|
|
1164
1209
|
else if (statement.kind === 'expression') walkExpression(statement.expression, visitor.expression);
|
|
1165
1210
|
}
|
|
@@ -1194,6 +1239,9 @@ function summarizeNativePlan(body, locals, effects, continuations) {
|
|
|
1194
1239
|
summary.expressionCount += countExpression(statement.test);
|
|
1195
1240
|
visitStatements(statement.then, depth + 1);
|
|
1196
1241
|
visitStatements(statement.else, depth + 1);
|
|
1242
|
+
} else if (statement.kind === 'pure-loop') {
|
|
1243
|
+
summary.expressionCount += countExpression(statement.test);
|
|
1244
|
+
visitStatements(statement.body, depth + 1);
|
|
1197
1245
|
} else if (statement.kind === 'return') {
|
|
1198
1246
|
summary.returnCount += 1;
|
|
1199
1247
|
summary.expressionCount += countExpression(statement.value);
|
|
@@ -1238,6 +1286,41 @@ function validateResult(result, fail, localIds, detail = {}) {
|
|
|
1238
1286
|
}
|
|
1239
1287
|
|
|
1240
1288
|
function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
|
|
1289
|
+
function validateLoops(statements, counters = new Set(), product = 1) {
|
|
1290
|
+
for (const statement of statements || []) {
|
|
1291
|
+
if (statement.kind === 'pure-loop') {
|
|
1292
|
+
const limit = statement.maxIterations;
|
|
1293
|
+
if (!localIds.has(statement.localId) || counters.has(statement.localId)) fail('pure loop requires its own local counter');
|
|
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('pure loop iteration bound is invalid');
|
|
1296
|
+
let bound = statement.test;
|
|
1297
|
+
while (bound && bound.kind === 'binary' && bound.operator === '&&') bound = bound.left;
|
|
1298
|
+
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('pure loop test must start with its declared literal cap');
|
|
1300
|
+
const active = new Set([...counters, statement.localId]);
|
|
1301
|
+
checkPureExpression(statement.test, active, true);
|
|
1302
|
+
validateLoops(statement.body, active, product * Math.max(1, limit));
|
|
1303
|
+
} else {
|
|
1304
|
+
if (counters.size && !['local', 'expression', 'if', 'break', 'continue'].includes(statement.kind)) fail('pure loop contains a non-value statement', { kind: statement.kind });
|
|
1305
|
+
if (!counters.size && ['break', 'continue'].includes(statement.kind)) fail('loop transfer outside pure loop');
|
|
1306
|
+
if (counters.size) {
|
|
1307
|
+
if (statement.kind === 'local' && counters.has(statement.localId)) fail('pure loop counter cannot be rebound');
|
|
1308
|
+
checkPureExpression(statement.value || statement.expression || statement.test, counters);
|
|
1309
|
+
}
|
|
1310
|
+
if (statement.kind === 'if') {
|
|
1311
|
+
validateLoops(statement.then, counters, product);
|
|
1312
|
+
validateLoops(statement.else, counters, product);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function checkPureExpression(expression, counters, test = false) {
|
|
1318
|
+
walkExpression(expression, node => {
|
|
1319
|
+
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');
|
|
1320
|
+
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
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
validateLoops(plan.entry && plan.entry.body);
|
|
1241
1324
|
const visitor = (statement) => {
|
|
1242
1325
|
if (!contract.CANONICAL_NATIVE_STATEMENT_KINDS.includes(statement.kind)) fail('statement kind is unknown', { kind: statement.kind });
|
|
1243
1326
|
if (statement.kind === 'local' && !localIds.has(statement.localId)) fail('local statement references unknown local', { localId: statement.localId });
|
|
@@ -38,7 +38,10 @@ function packageStatus(module, graphBuild) {
|
|
|
38
38
|
.find((entry) => entry.packageName === module.packageName);
|
|
39
39
|
if (product) {
|
|
40
40
|
const target = product.javascriptTarget;
|
|
41
|
-
|
|
41
|
+
// Source loadability is separate from selected-provider eligibility. The
|
|
42
|
+
// target-support owner checks provider-dependent requirements before build.
|
|
43
|
+
if (target && ['supported', 'provider-dependent'].includes(target.status)
|
|
44
|
+
&& target.realization === 'javascript-package-runtime' && target.entry) {
|
|
42
45
|
return Object.freeze({ status: 'package-runtime', runtimeEntry: target.entry, reasonCode: null });
|
|
43
46
|
}
|
|
44
47
|
return Object.freeze({
|
|
@@ -92,7 +92,8 @@ function classifyPackage(reachable, planned, product, context, options = {}) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const targetStatus = product && product.javascriptTarget && product.javascriptTarget.status;
|
|
95
|
-
if (targetStatus === 'not-realized'
|
|
95
|
+
if (targetStatus === 'not-realized'
|
|
96
|
+
|| (targetStatus === 'provider-dependent' && (!planned || planned.status !== 'package-runtime'))) {
|
|
96
97
|
return Object.freeze({ ...common, status: 'pending', reasonId: 'package-javascript-realization-pending', owner: pendingPackageOwner(product) });
|
|
97
98
|
}
|
|
98
99
|
if (targetStatus === 'unsupported') {
|
|
@@ -335,7 +336,14 @@ function buildJavascriptTargetSupportEvidence(project, options = {}, policy = {}
|
|
|
335
336
|
const capabilities = Object.freeze(compilerCapabilities
|
|
336
337
|
.map((id) => policy.classifyCapability(id, context))
|
|
337
338
|
.sort((a, b) => a.id.localeCompare(b.id)));
|
|
338
|
-
const
|
|
339
|
+
const requiredContracts = new Set(packages.filter((entry) => entry.required).map((entry) => entry.contractId));
|
|
340
|
+
const packageRequirements = (compiled.packageProduct && compiled.packageProduct.contracts || [])
|
|
341
|
+
.filter((entry) => requiredContracts.has(entry.contractId) && entry.javascriptTarget
|
|
342
|
+
&& entry.javascriptTarget.status === 'provider-dependent')
|
|
343
|
+
.flatMap((entry) => entry.javascriptTarget.providerRequirements || []);
|
|
344
|
+
const providerRequirements = Object.freeze([...new Set([
|
|
345
|
+
...requirementsFromMetadata(compiled.metadata), ...packageRequirements
|
|
346
|
+
])]
|
|
339
347
|
.map((id) => policy.classifyProviderRequirement(id, compilerCapabilities, context))
|
|
340
348
|
.sort((a, b) => a.id.localeCompare(b.id)));
|
|
341
349
|
const status = expectedProjectStatus(application, capabilities, providerRequirements, packages);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
function loadContract() {
|
|
5
|
+
try { return require('@pulse-compute/wasm-contracts/handler/canonical-native-plan'); }
|
|
6
|
+
catch (error) {
|
|
7
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../contracts/src/handler/canonical-native-plan.js');
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const { CANONICAL_PURE_LOOP_LIMITS } = loadContract();
|
|
12
|
+
|
|
13
|
+
function unwrap(node) {
|
|
14
|
+
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
function number(node, expected) {
|
|
18
|
+
node = unwrap(node);
|
|
19
|
+
return node && ts.isNumericLiteral(node) && Number(node.text) === expected;
|
|
20
|
+
}
|
|
21
|
+
function named(node, name) { node = unwrap(node); return node && ts.isIdentifier(node) && node.text === name; }
|
|
22
|
+
function firstConjunct(node) {
|
|
23
|
+
node = unwrap(node);
|
|
24
|
+
while (node && ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) node = unwrap(node.left);
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Shared admission for original-source JavaScript and generated Native plans.
|
|
29
|
+
// The literal bound is the first conjunct, so the terminating test cannot run
|
|
30
|
+
// extra application expressions after the counter reaches its cap.
|
|
31
|
+
function inspectBoundedPureLoop(statement, options = {}) {
|
|
32
|
+
const errors = [];
|
|
33
|
+
function fail(node, message) { errors.push({ node, message }); }
|
|
34
|
+
function expression(input, counters, mutation = true) {
|
|
35
|
+
const node = unwrap(input);
|
|
36
|
+
if (!node) return;
|
|
37
|
+
if (ts.isIdentifier(node)) {
|
|
38
|
+
if (node.text === options.ctxName) fail(node, 'Read context values before entering a pure loop.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)
|
|
42
|
+
|| [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return;
|
|
43
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
44
|
+
if (node.questionDotToken) fail(node, 'Optional access is outside the bounded pure loop subset.');
|
|
45
|
+
expression(node.expression, counters, mutation); return;
|
|
46
|
+
}
|
|
47
|
+
if (ts.isElementAccessExpression(node)) {
|
|
48
|
+
if (node.questionDotToken) fail(node, 'Optional access is outside the bounded pure loop subset.');
|
|
49
|
+
expression(node.expression, counters, mutation); expression(node.argumentExpression, counters, mutation); return;
|
|
50
|
+
}
|
|
51
|
+
if (ts.isCallExpression(node)) {
|
|
52
|
+
const target = unwrap(node.expression);
|
|
53
|
+
if (!ts.isPropertyAccessExpression(target) || target.name.text !== 'trim' || node.arguments.length || node.questionDotToken || target.questionDotToken) {
|
|
54
|
+
fail(node, 'Pure loops admit only zero-argument string .trim() calls; effects, helpers and callbacks are separate contracts.'); return;
|
|
55
|
+
}
|
|
56
|
+
expression(target.expression, counters, false); return;
|
|
57
|
+
}
|
|
58
|
+
if (ts.isBinaryExpression(node)) {
|
|
59
|
+
const assignment = node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
|
60
|
+
const target = unwrap(node.left);
|
|
61
|
+
if (assignment && !ts.isIdentifier(target) && !ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) fail(node, 'Destructuring assignment is outside the pure loop subset.');
|
|
62
|
+
if (assignment && (!mutation || [...counters].some(name => named(node.left, name)))) fail(node, 'Loop tests cannot mutate values, and loop counters cannot be assigned in the body.');
|
|
63
|
+
expression(node.left, counters, mutation); expression(node.right, counters, mutation); return;
|
|
64
|
+
}
|
|
65
|
+
if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
|
|
66
|
+
const update = node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken;
|
|
67
|
+
if (update && (!mutation || [...counters].some(name => named(node.operand, name)))) fail(node, 'Loop tests cannot mutate values, and loop counters cannot be updated in the body.');
|
|
68
|
+
expression(node.operand, counters, mutation); return;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isConditionalExpression(node)) {
|
|
71
|
+
expression(node.condition, counters, mutation); expression(node.whenTrue, counters, mutation); expression(node.whenFalse, counters, mutation); return;
|
|
72
|
+
}
|
|
73
|
+
if (ts.isTypeOfExpression(node) || ts.isVoidExpression(node)) { expression(node.expression, counters, mutation); return; }
|
|
74
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
75
|
+
for (const item of node.elements) expression(ts.isSpreadElement(item) ? item.expression : item, counters, mutation);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
79
|
+
for (const item of node.properties) {
|
|
80
|
+
if (ts.isSpreadAssignment(item)) expression(item.expression, counters, mutation);
|
|
81
|
+
else if (ts.isShorthandPropertyAssignment(item)) expression(item.name, counters, mutation);
|
|
82
|
+
else if (ts.isPropertyAssignment(item)) {
|
|
83
|
+
if (ts.isComputedPropertyName(item.name)) expression(item.name.expression, counters, mutation);
|
|
84
|
+
expression(item.initializer, counters, mutation);
|
|
85
|
+
} else fail(item, 'Methods and accessors are outside pure loop values.');
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (ts.isTemplateExpression(node)) { for (const span of node.templateSpans) expression(span.expression, counters, mutation); return; }
|
|
90
|
+
fail(node, `${ts.SyntaxKind[node.kind]} is outside bounded pure loop expressions.`);
|
|
91
|
+
}
|
|
92
|
+
function body(node, counters, product) {
|
|
93
|
+
if (ts.isBlock(node)) { for (const child of node.statements) body(child, counters, product); return; }
|
|
94
|
+
if (ts.isForStatement(node)) { loop(node, counters, product); return; }
|
|
95
|
+
if (ts.isVariableStatement(node)) {
|
|
96
|
+
if (!(node.declarationList.flags & ts.NodeFlags.BlockScoped)) fail(node, 'Pure loop locals must use let or const.');
|
|
97
|
+
for (const declaration of node.declarationList.declarations) {
|
|
98
|
+
if (!ts.isIdentifier(declaration.name) || counters.has(declaration.name.text)) fail(declaration, 'Loop locals require identifiers and cannot shadow an active counter.');
|
|
99
|
+
if (declaration.initializer) expression(declaration.initializer, counters);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (ts.isIfStatement(node)) {
|
|
104
|
+
expression(node.expression, counters); body(node.thenStatement, counters, product);
|
|
105
|
+
if (node.elseStatement) body(node.elseStatement, counters, product); return;
|
|
106
|
+
}
|
|
107
|
+
if (ts.isExpressionStatement(node)) { expression(node.expression, counters); return; }
|
|
108
|
+
if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
|
|
109
|
+
if (node.label) fail(node, 'Pure loop break and continue must be unlabelled.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (ts.isEmptyStatement(node)) return;
|
|
113
|
+
fail(node, 'Pure loop bodies accept locals, value assignments, if/else, nested bounded for, break and continue only.');
|
|
114
|
+
}
|
|
115
|
+
function loop(node, inherited, product) {
|
|
116
|
+
const list = node.initializer;
|
|
117
|
+
const declaration = list && ts.isVariableDeclarationList(list) && list.declarations.length === 1 && list.declarations[0];
|
|
118
|
+
if (!declaration || !(list.flags & ts.NodeFlags.Let) || !ts.isIdentifier(declaration.name) || !number(declaration.initializer, 0)) {
|
|
119
|
+
fail(node, 'A bounded pure loop starts with for (let index = 0; ...; index++).'); return;
|
|
120
|
+
}
|
|
121
|
+
const name = declaration.name.text;
|
|
122
|
+
const bound = firstConjunct(node.condition);
|
|
123
|
+
if (!bound || !ts.isBinaryExpression(bound) || !named(bound.left, name) || bound.operatorToken.kind !== ts.SyntaxKind.LessThanToken || !ts.isNumericLiteral(unwrap(bound.right))) {
|
|
124
|
+
fail(node, 'The first loop condition must be index < a literal iteration cap.'); return;
|
|
125
|
+
}
|
|
126
|
+
const maxIterations = Number(unwrap(bound.right).text);
|
|
127
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 0 || maxIterations > CANONICAL_PURE_LOOP_LIMITS.maxIterations || product * Math.max(1, maxIterations) > CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations) {
|
|
128
|
+
fail(node, 'The literal loop cap or nested iteration product exceeds the portable limit.'); return;
|
|
129
|
+
}
|
|
130
|
+
const increment = unwrap(node.incrementor);
|
|
131
|
+
if (!increment || !((ts.isPostfixUnaryExpression(increment) || ts.isPrefixUnaryExpression(increment)) && increment.operator === ts.SyntaxKind.PlusPlusToken && named(increment.operand, name))
|
|
132
|
+
&& !(ts.isBinaryExpression(increment) && increment.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken && named(increment.left, name) && number(increment.right, 1))) {
|
|
133
|
+
fail(node, 'The loop increment must be index++ or index += 1.'); return;
|
|
134
|
+
}
|
|
135
|
+
if (inherited.has(name)) fail(declaration, 'Nested loop counters cannot shadow active counters.');
|
|
136
|
+
const counters = new Set([...inherited, name]);
|
|
137
|
+
expression(node.condition, counters, false);
|
|
138
|
+
body(node.statement, counters, product * Math.max(1, maxIterations));
|
|
139
|
+
return { name, maxIterations };
|
|
140
|
+
}
|
|
141
|
+
const result = loop(statement, new Set(), 1);
|
|
142
|
+
return { ...result, errors };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { inspectBoundedPureLoop };
|
package/src/spine/handler-ir.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const ts = require('typescript');
|
|
4
|
+
const { inspectBoundedPureLoop } = require('./bounded-pure-loop.js');
|
|
4
5
|
const {
|
|
5
6
|
extractFetchChain,
|
|
6
7
|
extractKvNamespaceDeclaration,
|
|
@@ -185,25 +186,35 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
185
186
|
const continuationSites = [];
|
|
186
187
|
const effectCounters = new Map();
|
|
187
188
|
let continuationIndex = 0;
|
|
188
|
-
|
|
189
|
+
// Offsets belong to a source file. Project-wide contributions can share an
|
|
190
|
+
// offset; only duplicate ranges within the same owning source are invalid.
|
|
191
|
+
const sourceKey = (file, start) => `${String(file).replace(/\\/g, '/')}\u0000${start}`;
|
|
192
|
+
const inputKey = (input, start) => sourceKey(input.loc && input.loc.file || sourceFile.fileName, start);
|
|
193
|
+
const callKey = (call) => {
|
|
194
|
+
const owner = call.getSourceFile() || sourceFile;
|
|
195
|
+
return sourceKey(owner.fileName, call.getStart(owner));
|
|
196
|
+
};
|
|
197
|
+
const packageEffectsBySource = new Map();
|
|
189
198
|
for (const effectInput of options.packageEffects || []) {
|
|
190
199
|
if (!effectInput || effectInput.version !== canonicalRuntimeContract.CANONICAL_PACKAGE_EFFECT_VERSION || !effectInput.range) continue;
|
|
191
200
|
const start = Number(effectInput.range.start);
|
|
192
|
-
|
|
201
|
+
const key = inputKey(effectInput, start);
|
|
202
|
+
if (!Number.isSafeInteger(start) || packageEffectsBySource.has(key)) {
|
|
193
203
|
diagnostics.push(diagnostic(sourceFile, handler, 'PULSE_CANONICAL_PACKAGE_EFFECT_RANGE_INVALID', 'Package-owned canonical effects require unique source ranges.', { effect: effectInput }));
|
|
194
204
|
continue;
|
|
195
205
|
}
|
|
196
|
-
|
|
206
|
+
packageEffectsBySource.set(key, effectInput);
|
|
197
207
|
}
|
|
198
|
-
const
|
|
199
|
-
const
|
|
208
|
+
const packageIntrinsicsBySource = new Map();
|
|
209
|
+
const linkedPackageIntrinsicsBySource = new Map();
|
|
200
210
|
for (const intrinsicInput of options.packageIntrinsics || []) {
|
|
201
211
|
const start = Number(intrinsicInput && intrinsicInput.range && intrinsicInput.range.start);
|
|
202
|
-
|
|
212
|
+
const key = intrinsicInput && inputKey(intrinsicInput, start);
|
|
213
|
+
if (!Number.isSafeInteger(start) || packageIntrinsicsBySource.has(key)) {
|
|
203
214
|
diagnostics.push(diagnostic(sourceFile, handler, 'PULSE_CANONICAL_PACKAGE_INTRINSIC_RANGE_INVALID', 'Package-owned intrinsics require unique source ranges.', { intrinsic: intrinsicInput }));
|
|
204
215
|
continue;
|
|
205
216
|
}
|
|
206
|
-
|
|
217
|
+
packageIntrinsicsBySource.set(key, intrinsicInput);
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
function unwrapPackageCall(expression) {
|
|
@@ -222,7 +233,7 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
222
233
|
return linked;
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
|
-
const effect =
|
|
236
|
+
const effect = packageEffectsBySource.get(callKey(call));
|
|
226
237
|
if (effect) linkPackageIntrinsicsWithin(call);
|
|
227
238
|
return effect;
|
|
228
239
|
}
|
|
@@ -232,9 +243,11 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
232
243
|
if (typeof options.packageIntrinsicForCall === 'function') {
|
|
233
244
|
const linked = options.packageIntrinsicForCall(call);
|
|
234
245
|
if (linked) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
const owner = call.getSourceFile() || sourceFile;
|
|
247
|
+
const start = call.getStart(owner);
|
|
248
|
+
const key = callKey(call);
|
|
249
|
+
if (!linkedPackageIntrinsicsBySource.has(key)) {
|
|
250
|
+
linkedPackageIntrinsicsBySource.set(key, Object.freeze({
|
|
238
251
|
...linked,
|
|
239
252
|
range: Object.freeze({ start, end: call.getEnd() })
|
|
240
253
|
}));
|
|
@@ -242,9 +255,9 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
242
255
|
return linked;
|
|
243
256
|
}
|
|
244
257
|
}
|
|
245
|
-
const
|
|
246
|
-
const intrinsic =
|
|
247
|
-
if (intrinsic && !
|
|
258
|
+
const key = callKey(call);
|
|
259
|
+
const intrinsic = packageIntrinsicsBySource.get(key);
|
|
260
|
+
if (intrinsic && !linkedPackageIntrinsicsBySource.has(key)) linkedPackageIntrinsicsBySource.set(key, intrinsic);
|
|
248
261
|
return intrinsic;
|
|
249
262
|
}
|
|
250
263
|
|
|
@@ -623,8 +636,13 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
623
636
|
return createHandlerOperation('opaque-fetch-return', { statement, chain, site, continuation });
|
|
624
637
|
}
|
|
625
638
|
}
|
|
626
|
-
if (ts.
|
|
627
|
-
|
|
639
|
+
if (ts.isForStatement(statement)) {
|
|
640
|
+
const loop = inspectBoundedPureLoop(statement, { ctxName });
|
|
641
|
+
for (const error of loop.errors) diagnostics.push(diagnostic(sourceFile, error.node, 'PULSE_CANONICAL_PURE_LOOP_UNSUPPORTED', error.message));
|
|
642
|
+
return createHandlerOperation('source-statement', { statement, role: 'bounded-pure-loop' });
|
|
643
|
+
}
|
|
644
|
+
if (ts.isTryStatement(statement) || ts.isForInStatement(statement) || ts.isForOfStatement(statement) || ts.isWhileStatement(statement) || ts.isDoStatement(statement) || ts.isSwitchStatement(statement)) {
|
|
645
|
+
diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_CONTROL_FLOW_UNSUPPORTED', 'Canonical lowering supports if/else and literal-capped pure for loops; other loops, switch, and try/catch are reserved.'));
|
|
628
646
|
}
|
|
629
647
|
return createHandlerOperation('source-statement', { statement, role: 'preserved-source' });
|
|
630
648
|
}
|
|
@@ -709,11 +727,9 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
709
727
|
effectSites: Object.freeze(effectSites),
|
|
710
728
|
continuationSites: Object.freeze(continuationSites),
|
|
711
729
|
packageEffects: Object.freeze([...(options.packageEffects || [])]),
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
: [...(options.packageIntrinsics || [])]
|
|
716
|
-
),
|
|
730
|
+
// The emitter consumes this handler's linked ranges, not the project-wide
|
|
731
|
+
// recognition index. Keep original loc metadata when rebasing a range.
|
|
732
|
+
packageIntrinsics: Object.freeze([...linkedPackageIntrinsicsBySource.values()]),
|
|
717
733
|
packageResultAdapters: Object.freeze([...(options.packageResultAdapters || [])]),
|
|
718
734
|
summary: Object.freeze({
|
|
719
735
|
operationCount: operationSummary.operationCount,
|
|
@@ -174,6 +174,9 @@ function extractProviderCall(expression, ctxName, kvAliases = new Map(), options
|
|
|
174
174
|
|
|
175
175
|
if (receiver && ts.isPropertyAccessExpression(receiver) && isIdentifierNamed(receiver.expression, ctxName, options)) {
|
|
176
176
|
const namespace = receiver.name.text;
|
|
177
|
+
if (namespace === 'time' && method === 'now' && current.arguments.length === 0) {
|
|
178
|
+
return Object.freeze({ call: current, surfaceId: 'ctx.time.now', kind: 'time.now', providerKind: 'time', operation: 'now', capability: 'time.wall-clock', args: Object.freeze([]) });
|
|
179
|
+
}
|
|
177
180
|
if (namespace === 'config' && method === 'get' && current.arguments.length === 1) {
|
|
178
181
|
return Object.freeze({ call: current, surfaceId: 'ctx.config.get', kind: 'config.get', providerKind: 'config', operation: 'get', capability: 'config.get', resource: current.arguments[0], name: current.arguments[0], args: Object.freeze([...current.arguments]) });
|
|
179
182
|
}
|
|
@@ -265,6 +268,7 @@ function recognizeHandlerSurface(node, options = {}) {
|
|
|
265
268
|
param: 'ctx.param',
|
|
266
269
|
json: 'ctx.json',
|
|
267
270
|
encodeJson: 'ctx.encodeJson',
|
|
271
|
+
decodeJson: 'ctx.decodeJson',
|
|
268
272
|
text: 'ctx.text',
|
|
269
273
|
response: 'ctx.response'
|
|
270
274
|
});
|
|
@@ -117,9 +117,11 @@ function analyzeHandler(sourceFile, handler, ctxName, diagnostics, schemaBundleI
|
|
|
117
117
|
let schemaBoundJsonCount = 0;
|
|
118
118
|
const forbiddenCalls = new Set(['require', 'setTimeout', 'setInterval', 'setImmediate', 'queueMicrotask', 'eval', 'Function']);
|
|
119
119
|
const forbiddenRoots = new Set(['process', 'global', 'globalThis', 'Deno', 'Bun', 'Buffer', 'console', 'WebSocket', 'XMLHttpRequest', 'EventSource', 'Worker']);
|
|
120
|
+
const packageEffectRanges = new Set();
|
|
120
121
|
|
|
121
122
|
for (const effect of options.packageEffects || []) {
|
|
122
123
|
if (effect && effect.capability) capabilities.add(String(effect.capability));
|
|
124
|
+
if (effect && effect.range) packageEffectRanges.add(`${effect.range.start}:${effect.range.end}`);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
function reject(node, code, message, detail = {}) {
|
|
@@ -319,7 +321,12 @@ function analyzeHandler(sourceFile, handler, ctxName, diagnostics, schemaBundleI
|
|
|
319
321
|
if (root && forbiddenRoots.has(root)) reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', `Ambient ${root} authority is not available in canonical user scope.`, { identifier: root });
|
|
320
322
|
if (root === 'Date' && node.expression.name.text === 'now') reject(node, 'PULSE_CANONICAL_NONDETERMINISM_UNSUPPORTED', 'Ambient clock access is not available in canonical user scope.');
|
|
321
323
|
if (root === 'Math' && node.expression.name.text === 'random') reject(node, 'PULSE_CANONICAL_NONDETERMINISM_UNSUPPORTED', 'Ambient randomness is not available in canonical user scope.');
|
|
322
|
-
|
|
324
|
+
// A trusted package can own this exact call; its arguments still pass
|
|
325
|
+
// through ordinary ambient-authority validation below.
|
|
326
|
+
const packageEffect = typeof options.packageEffectForCall === 'function'
|
|
327
|
+
? options.packageEffectForCall(node)
|
|
328
|
+
: packageEffectRanges.has(`${node.getStart(sourceFile)}:${node.getEnd()}`);
|
|
329
|
+
if (root === 'crypto' && !packageEffect) reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', 'Ambient crypto authority is not available in canonical user scope.', { identifier: 'crypto' });
|
|
323
330
|
|
|
324
331
|
const target = node.expression;
|
|
325
332
|
const surface = recognizeHandlerSurface(node, { ctxName });
|
|
@@ -335,11 +342,12 @@ function analyzeHandler(sourceFile, handler, ctxName, diagnostics, schemaBundleI
|
|
|
335
342
|
validateFetchSchema(node);
|
|
336
343
|
}
|
|
337
344
|
if (surface.detail.fetch.decoderCall === node) validateDecoderCall(node, 'fetch-decode');
|
|
338
|
-
} else if (surface.surfaceId === 'ctx.encodeJson') {
|
|
345
|
+
} else if (surface.surfaceId === 'ctx.encodeJson' || surface.surfaceId === 'ctx.decodeJson') {
|
|
346
|
+
const usage = surface.surfaceId === 'ctx.decodeJson' ? 'text-decode' : 'value-encode';
|
|
339
347
|
if (node.arguments.length !== 2) {
|
|
340
|
-
reject(node, 'PULSE_CANONICAL_DECODER_ARGUMENTS_UNSUPPORTED',
|
|
348
|
+
reject(node, 'PULSE_CANONICAL_DECODER_ARGUMENTS_UNSUPPORTED', `${surface.surfaceId} requires exactly ${usage === 'text-decode' ? 'JSON text' : 'a value'} and a registered literal schema ID.`, { usage, arguments: node.arguments.length });
|
|
341
349
|
} else {
|
|
342
|
-
recordSchemaReference(node, node.arguments[1],
|
|
350
|
+
recordSchemaReference(node, node.arguments[1], usage);
|
|
343
351
|
schemaBoundJsonCount += 1;
|
|
344
352
|
}
|
|
345
353
|
} else if (['ctx.json', 'ctx.text', 'ctx.response'].includes(surface.surfaceId)) {
|
|
@@ -327,7 +327,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
|
|
|
327
327
|
role,
|
|
328
328
|
nextIndex: entry.nextIndex,
|
|
329
329
|
transferFlag,
|
|
330
|
-
clearNormalMode: call.arguments.length === 0
|
|
330
|
+
clearNormalMode: call.arguments.length === 0,
|
|
331
331
|
errorExpression: call.arguments.length > 0 ? rewrite(call.arguments[0]) : undefined
|
|
332
332
|
});
|
|
333
333
|
}
|