@pulse-compute/wasm-compiler 1.0.0-beta.3 → 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 +7 -2
- package/src/canonical-native-plan.js +88 -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 +5 -0
- package/src/spine/plain-handler-frontend.js +17 -2
- 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',
|
|
@@ -254,7 +259,7 @@ function realizeCanonicalNativePlan(plan, options = {}, providerRequirements) {
|
|
|
254
259
|
maxBuffer: 16 * 1024 * 1024,
|
|
255
260
|
timeout: Number(options.timeoutMs || 120000),
|
|
256
261
|
env: schemaCodecsActive
|
|
257
|
-
? { ...process.env, JSON_STRICT: 'true', JSON_USE_FAST_PATH: '0' }
|
|
262
|
+
? { ...process.env, JSON_STRICT: 'true', JSON_USE_FAST_PATH: '0', JSON_MODE: 'NAIVE' }
|
|
258
263
|
: process.env
|
|
259
264
|
});
|
|
260
265
|
const durationMs = Date.now() - startedAt;
|
|
@@ -308,7 +313,7 @@ function verifyCanonicalNativeRealization(realization) {
|
|
|
308
313
|
assemblyScript: Object.freeze({ package: 'assemblyscript', version: realization.assemblyScriptVersion }),
|
|
309
314
|
optimization: resolveNativeOptimization(realization.optimization),
|
|
310
315
|
jsonAs: realization.jsonAsVersion
|
|
311
|
-
? Object.freeze({ package: 'json-as', version: realization.jsonAsVersion, transform: true, strict: true })
|
|
316
|
+
? Object.freeze({ package: 'json-as', version: realization.jsonAsVersion, transform: true, strict: true, mode: 'NAIVE', fastPath: false })
|
|
312
317
|
: null,
|
|
313
318
|
wasm: Object.freeze({ bytes: inspection.bytes, sha256: inspection.sha256, magic: inspection.magic }),
|
|
314
319
|
wat: Object.freeze({ bytes: Buffer.byteLength(realization.wat), sha256: sha256(realization.wat) }),
|
|
@@ -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'); }
|
|
@@ -202,6 +203,8 @@ function intrinsicForContextCall(parts) {
|
|
|
202
203
|
'req.text': ['request.text', 'string'],
|
|
203
204
|
'req.json': ['request.json', 'json'],
|
|
204
205
|
json: ['response.json', 'pulse-result'],
|
|
206
|
+
encodeJson: ['schema.encode.text', 'string'],
|
|
207
|
+
decodeJson: ['schema.decode.text', 'json'],
|
|
205
208
|
text: ['response.text', 'pulse-result'],
|
|
206
209
|
response: ['response.custom', 'pulse-result'],
|
|
207
210
|
kv: ['kv.namespace', 'kv-namespace'],
|
|
@@ -220,6 +223,7 @@ function resultKindForEffect(site, decoder, continuation, resultMode) {
|
|
|
220
223
|
if (site.result === 'ack') return 'ack';
|
|
221
224
|
if (site.kind === 'fetch') return 'fetch-response';
|
|
222
225
|
if (site.kind === 'config.get' || site.kind === 'secret.get') return 'string-or-undefined';
|
|
226
|
+
if (site.kind === 'time.now') return 'json';
|
|
223
227
|
if (site.kind === 'kv.get') return 'json-or-undefined';
|
|
224
228
|
if (site.kind === 'kv.put') return 'ack';
|
|
225
229
|
if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(site.kind)) return 'json';
|
|
@@ -442,7 +446,7 @@ class NativePlanBuilder {
|
|
|
442
446
|
if (ts.isBinaryExpression(current)) {
|
|
443
447
|
const operator = current.operatorToken.getText(this.sourceFile);
|
|
444
448
|
if (ASSIGNMENT_OPERATORS.has(operator)) {
|
|
445
|
-
const target = this.
|
|
449
|
+
const target = this.assignmentTarget(current.left, scope);
|
|
446
450
|
const value = this.expression(current.right, scope);
|
|
447
451
|
return Object.freeze({ kind: 'assignment', operator, target, value, valueKind: value.valueKind || 'unknown' });
|
|
448
452
|
}
|
|
@@ -457,7 +461,7 @@ class NativePlanBuilder {
|
|
|
457
461
|
if (ts.isPrefixUnaryExpression(current)) {
|
|
458
462
|
const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(0, 1);
|
|
459
463
|
if (UPDATE_OPERATORS.has(operator)) {
|
|
460
|
-
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' });
|
|
461
465
|
}
|
|
462
466
|
if (!PREFIX_OPERATORS.has(operator)) {
|
|
463
467
|
this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Prefix operator ${operator} is outside the canonical native value model.`, { operator });
|
|
@@ -469,7 +473,7 @@ class NativePlanBuilder {
|
|
|
469
473
|
if (ts.isPostfixUnaryExpression(current)) {
|
|
470
474
|
const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(-2);
|
|
471
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 });
|
|
472
|
-
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' });
|
|
473
477
|
}
|
|
474
478
|
|
|
475
479
|
if (ts.isConditionalExpression(current)) {
|
|
@@ -586,6 +590,9 @@ class NativePlanBuilder {
|
|
|
586
590
|
if (ts.isPropertyAccessExpression(target)) {
|
|
587
591
|
const receiver = this.expression(target.expression, scope);
|
|
588
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
|
+
}
|
|
589
596
|
if (!FETCH_RESPONSE_METHODS.has(method) || receiver.valueKind !== 'fetch-response') {
|
|
590
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 });
|
|
591
598
|
}
|
|
@@ -606,6 +613,14 @@ class NativePlanBuilder {
|
|
|
606
613
|
return Object.freeze({ kind: 'intrinsic', name: 'unsupported:call', arguments: Object.freeze([]), valueKind: 'unknown' });
|
|
607
614
|
}
|
|
608
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
|
+
|
|
609
624
|
allocateLocal(name, valueKind, statementPath, declaration) {
|
|
610
625
|
this.localIndex += 1;
|
|
611
626
|
const local = Object.freeze({
|
|
@@ -899,10 +914,38 @@ class NativePlanBuilder {
|
|
|
899
914
|
|
|
900
915
|
if (ts.isVariableStatement(statement)) return this.lowerVariableStatement(statement, scope, pathParts, depth);
|
|
901
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
|
+
|
|
902
935
|
if (ts.isIfStatement(statement)) {
|
|
903
936
|
this.summary.branchCount += 1;
|
|
904
937
|
const thenScope = new Map(scope);
|
|
905
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
|
+
}
|
|
906
949
|
const thenBody = ts.isBlock(statement.thenStatement)
|
|
907
950
|
? this.statementList(statement.thenStatement.statements, thenScope, [...pathParts, 'then'], depth + 1)
|
|
908
951
|
: this.statement(statement.thenStatement, thenScope, [...pathParts, 'then', 0], depth + 1);
|
|
@@ -913,7 +956,7 @@ class NativePlanBuilder {
|
|
|
913
956
|
: this.statement(statement.elseStatement, elseScope, [...pathParts, 'else', 0], depth + 1);
|
|
914
957
|
return [Object.freeze({
|
|
915
958
|
kind: 'if',
|
|
916
|
-
test
|
|
959
|
+
test,
|
|
917
960
|
then: Object.freeze(thenBody),
|
|
918
961
|
else: Object.freeze(elseBody),
|
|
919
962
|
statementPath
|
|
@@ -1159,6 +1202,9 @@ function walkStatements(statements, visitor) {
|
|
|
1159
1202
|
walkExpression(statement.test, visitor.expression);
|
|
1160
1203
|
walkStatements(statement.then, visitor);
|
|
1161
1204
|
walkStatements(statement.else, visitor);
|
|
1205
|
+
} else if (statement.kind === 'pure-loop') {
|
|
1206
|
+
walkExpression(statement.test, visitor.expression);
|
|
1207
|
+
walkStatements(statement.body, visitor);
|
|
1162
1208
|
} else if (statement.kind === 'return') walkExpression(statement.value, visitor.expression);
|
|
1163
1209
|
else if (statement.kind === 'expression') walkExpression(statement.expression, visitor.expression);
|
|
1164
1210
|
}
|
|
@@ -1193,6 +1239,9 @@ function summarizeNativePlan(body, locals, effects, continuations) {
|
|
|
1193
1239
|
summary.expressionCount += countExpression(statement.test);
|
|
1194
1240
|
visitStatements(statement.then, depth + 1);
|
|
1195
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);
|
|
1196
1245
|
} else if (statement.kind === 'return') {
|
|
1197
1246
|
summary.returnCount += 1;
|
|
1198
1247
|
summary.expressionCount += countExpression(statement.value);
|
|
@@ -1237,6 +1286,41 @@ function validateResult(result, fail, localIds, detail = {}) {
|
|
|
1237
1286
|
}
|
|
1238
1287
|
|
|
1239
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);
|
|
1240
1324
|
const visitor = (statement) => {
|
|
1241
1325
|
if (!contract.CANONICAL_NATIVE_STATEMENT_KINDS.includes(statement.kind)) fail('statement kind is unknown', { kind: statement.kind });
|
|
1242
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
|
}
|
|
@@ -264,6 +267,8 @@ function recognizeHandlerSurface(node, options = {}) {
|
|
|
264
267
|
const direct = Object.freeze({
|
|
265
268
|
param: 'ctx.param',
|
|
266
269
|
json: 'ctx.json',
|
|
270
|
+
encodeJson: 'ctx.encodeJson',
|
|
271
|
+
decodeJson: 'ctx.decodeJson',
|
|
267
272
|
text: 'ctx.text',
|
|
268
273
|
response: 'ctx.response'
|
|
269
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 = {}) {
|
|
@@ -173,7 +175,7 @@ function analyzeHandler(sourceFile, handler, ctxName, diagnostics, schemaBundleI
|
|
|
173
175
|
);
|
|
174
176
|
return;
|
|
175
177
|
}
|
|
176
|
-
const encode = usage === 'response-encode' || usage === 'fetch-encode';
|
|
178
|
+
const encode = usage === 'response-encode' || usage === 'fetch-encode' || usage === 'value-encode';
|
|
177
179
|
schemaReferences.push(Object.freeze({
|
|
178
180
|
id: schemaId,
|
|
179
181
|
usage,
|
|
@@ -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,6 +342,14 @@ 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');
|
|
345
|
+
} else if (surface.surfaceId === 'ctx.encodeJson' || surface.surfaceId === 'ctx.decodeJson') {
|
|
346
|
+
const usage = surface.surfaceId === 'ctx.decodeJson' ? 'text-decode' : 'value-encode';
|
|
347
|
+
if (node.arguments.length !== 2) {
|
|
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 });
|
|
349
|
+
} else {
|
|
350
|
+
recordSchemaReference(node, node.arguments[1], usage);
|
|
351
|
+
schemaBoundJsonCount += 1;
|
|
352
|
+
}
|
|
338
353
|
} else if (['ctx.json', 'ctx.text', 'ctx.response'].includes(surface.surfaceId)) {
|
|
339
354
|
const method = surface.surfaceId.slice('ctx.'.length);
|
|
340
355
|
capabilities.add(`response.${method}`);
|
|
@@ -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
|
}
|