@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
|
@@ -8,7 +8,7 @@ function loadContract() {
|
|
|
8
8
|
throw error;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
const { CANONICAL_PURE_LOOP_LIMITS } = loadContract();
|
|
11
|
+
const { CANONICAL_PURE_LOOP_LIMITS, CANONICAL_READ_LOOP_CONTRACT } = loadContract();
|
|
12
12
|
|
|
13
13
|
function unwrap(node) {
|
|
14
14
|
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
|
|
@@ -30,10 +30,18 @@ function firstConjunct(node) {
|
|
|
30
30
|
// extra application expressions after the counter reaches its cap.
|
|
31
31
|
function inspectBoundedPureLoop(statement, options = {}) {
|
|
32
32
|
const errors = [];
|
|
33
|
+
let loopDepth = 0;
|
|
34
|
+
let effectCount = 0;
|
|
35
|
+
let checkingHeader = false;
|
|
36
|
+
let activeProduct = 1;
|
|
37
|
+
let maxIterationProduct = 1;
|
|
38
|
+
const readBody = () => options.readLoop === true && loopDepth === 1 && !checkingHeader;
|
|
39
|
+
const protectedNames = new Set(options.readLoop ? [options.ctxName, ...(options.namespaceAliases || [])] : []);
|
|
33
40
|
function fail(node, message) { errors.push({ node, message }); }
|
|
34
|
-
function expression(input, counters, mutation = true) {
|
|
35
|
-
|
|
41
|
+
function expression(input, counters, mutation = true, effectRoot = false) {
|
|
42
|
+
let node = unwrap(input);
|
|
36
43
|
if (!node) return;
|
|
44
|
+
if (readBody() && effectRoot && ts.isAwaitExpression(node)) node = unwrap(node.expression);
|
|
37
45
|
if (ts.isIdentifier(node)) {
|
|
38
46
|
if (node.text === options.ctxName) fail(node, 'Read context values before entering a pure loop.');
|
|
39
47
|
return;
|
|
@@ -50,6 +58,44 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
50
58
|
}
|
|
51
59
|
if (ts.isCallExpression(node)) {
|
|
52
60
|
const target = unwrap(node.expression);
|
|
61
|
+
const pure = !checkingHeader && options.pureHelperForCall?.(node);
|
|
62
|
+
if (pure) {
|
|
63
|
+
if (activeProduct * (pure.maxIterationProduct || 1) > CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations) fail(node, 'Pure helper exceeds the combined caller/callee iteration bound.');
|
|
64
|
+
if (node.questionDotToken || node.typeArguments?.length) fail(node, 'Pure loop calls require a static synchronous helper.');
|
|
65
|
+
for (const argument of node.arguments) expression(argument, counters, false);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const helper = readBody() && options.helperForCall && options.helperForCall(node);
|
|
69
|
+
if (helper) {
|
|
70
|
+
if (!effectRoot) { fail(node, 'Read-loop helpers require a directly bound sequential call.'); return; }
|
|
71
|
+
effectCount += 1;
|
|
72
|
+
for (const argument of node.arguments) {
|
|
73
|
+
// Original-source JavaScript calls retain their explicit context input.
|
|
74
|
+
if (!named(argument, options.ctxName)) expression(argument, counters, false);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const effect = readBody() && options.effectForCall && options.effectForCall(node);
|
|
79
|
+
if (effect) {
|
|
80
|
+
if (!effectRoot || !CANONICAL_READ_LOOP_CONTRACT.effectKinds.includes(effect.kind)) {
|
|
81
|
+
fail(node, 'Read loops admit only directly bound or discarded sequential s3.getText, kv.getVersioned, crypto.digestText, output.write and req.readTextChunk effects.'); return;
|
|
82
|
+
}
|
|
83
|
+
effectCount += 1;
|
|
84
|
+
for (const argument of node.arguments) {
|
|
85
|
+
// Package facades receive the context as their first argument.
|
|
86
|
+
if (!named(argument, options.ctxName)) expression(argument, counters, false);
|
|
87
|
+
}
|
|
88
|
+
// A direct KV namespace expression is also evaluated at the call site.
|
|
89
|
+
if (ts.isPropertyAccessExpression(target) && ts.isCallExpression(unwrap(target.expression))) {
|
|
90
|
+
for (const argument of unwrap(target.expression).arguments) expression(argument, counters, false);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (readBody() && ts.isPropertyAccessExpression(target) && named(target.expression, options.ctxName)
|
|
95
|
+
&& ['decodeJson', 'encodeJson', 'text', 'json', 'response'].includes(target.name.text) && !node.questionDotToken && !target.questionDotToken) {
|
|
96
|
+
for (const argument of node.arguments) expression(argument, counters, mutation);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
53
99
|
if (!ts.isPropertyAccessExpression(target) || target.name.text !== 'trim' || node.arguments.length || node.questionDotToken || target.questionDotToken) {
|
|
54
100
|
fail(node, 'Pure loops admit only zero-argument string .trim() calls; effects, helpers and callbacks are separate contracts.'); return;
|
|
55
101
|
}
|
|
@@ -59,12 +105,12 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
59
105
|
const assignment = node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
|
60
106
|
const target = unwrap(node.left);
|
|
61
107
|
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
|
|
108
|
+
if (assignment && (!mutation || [...counters, ...protectedNames].some(name => named(node.left, name)))) fail(node, 'Loop tests cannot mutate values, and active counters or context/namespace aliases cannot be assigned in the body.');
|
|
63
109
|
expression(node.left, counters, mutation); expression(node.right, counters, mutation); return;
|
|
64
110
|
}
|
|
65
111
|
if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
|
|
66
112
|
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
|
|
113
|
+
if (update && (!mutation || [...counters, ...protectedNames].some(name => named(node.operand, name)))) fail(node, 'Loop tests cannot mutate values, and active counters or context/namespace aliases cannot be updated in the body.');
|
|
68
114
|
expression(node.operand, counters, mutation); return;
|
|
69
115
|
}
|
|
70
116
|
if (ts.isConditionalExpression(node)) {
|
|
@@ -95,8 +141,8 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
95
141
|
if (ts.isVariableStatement(node)) {
|
|
96
142
|
if (!(node.declarationList.flags & ts.NodeFlags.BlockScoped)) fail(node, 'Pure loop locals must use let or const.');
|
|
97
143
|
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
|
|
99
|
-
if (declaration.initializer) expression(declaration.initializer, counters);
|
|
144
|
+
if (!ts.isIdentifier(declaration.name) || counters.has(declaration.name.text) || protectedNames.has(declaration.name.text)) fail(declaration, 'Loop locals require identifiers and cannot shadow active counters or context/namespace aliases.');
|
|
145
|
+
if (declaration.initializer) expression(declaration.initializer, counters, true, readBody() && node.declarationList.declarations.length === 1);
|
|
100
146
|
}
|
|
101
147
|
return;
|
|
102
148
|
}
|
|
@@ -104,7 +150,8 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
104
150
|
expression(node.expression, counters); body(node.thenStatement, counters, product);
|
|
105
151
|
if (node.elseStatement) body(node.elseStatement, counters, product); return;
|
|
106
152
|
}
|
|
107
|
-
if (ts.isExpressionStatement(node)) { expression(node.expression, counters); return; }
|
|
153
|
+
if (ts.isExpressionStatement(node)) { expression(node.expression, counters, true, readBody()); return; }
|
|
154
|
+
if ((readBody() || options.pureHelper === true) && ts.isReturnStatement(node)) { expression(node.expression, counters); return; }
|
|
108
155
|
if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
|
|
109
156
|
if (node.label) fail(node, 'Pure loop break and continue must be unlabelled.');
|
|
110
157
|
return;
|
|
@@ -113,6 +160,7 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
113
160
|
fail(node, 'Pure loop bodies accept locals, value assignments, if/else, nested bounded for, break and continue only.');
|
|
114
161
|
}
|
|
115
162
|
function loop(node, inherited, product) {
|
|
163
|
+
const isReadLoop = options.readLoop === true && loopDepth === 0;
|
|
116
164
|
const list = node.initializer;
|
|
117
165
|
const declaration = list && ts.isVariableDeclarationList(list) && list.declarations.length === 1 && list.declarations[0];
|
|
118
166
|
if (!declaration || !(list.flags & ts.NodeFlags.Let) || !ts.isIdentifier(declaration.name) || !number(declaration.initializer, 0)) {
|
|
@@ -124,7 +172,7 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
124
172
|
fail(node, 'The first loop condition must be index < a literal iteration cap.'); return;
|
|
125
173
|
}
|
|
126
174
|
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) {
|
|
175
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 0 || maxIterations > (isReadLoop ? CANONICAL_READ_LOOP_CONTRACT.maxIterations : CANONICAL_PURE_LOOP_LIMITS.maxIterations) || product * Math.max(1, maxIterations) > CANONICAL_PURE_LOOP_LIMITS.maxNestedIterations) {
|
|
128
176
|
fail(node, 'The literal loop cap or nested iteration product exceeds the portable limit.'); return;
|
|
129
177
|
}
|
|
130
178
|
const increment = unwrap(node.incrementor);
|
|
@@ -132,14 +180,31 @@ function inspectBoundedPureLoop(statement, options = {}) {
|
|
|
132
180
|
&& !(ts.isBinaryExpression(increment) && increment.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken && named(increment.left, name) && number(increment.right, 1))) {
|
|
133
181
|
fail(node, 'The loop increment must be index++ or index += 1.'); return;
|
|
134
182
|
}
|
|
135
|
-
if (inherited.has(name)) fail(declaration, 'Nested loop counters cannot shadow active counters.');
|
|
183
|
+
if (inherited.has(name) || protectedNames.has(name)) fail(declaration, 'Nested loop counters cannot shadow active counters or context/namespace aliases.');
|
|
136
184
|
const counters = new Set([...inherited, name]);
|
|
185
|
+
// Header tests retain the pure value contract, including no effects.
|
|
186
|
+
const previousHeader = checkingHeader;
|
|
187
|
+
checkingHeader = true;
|
|
137
188
|
expression(node.condition, counters, false);
|
|
138
|
-
|
|
189
|
+
checkingHeader = previousHeader;
|
|
190
|
+
if (!options.headerOnly) {
|
|
191
|
+
loopDepth += 1;
|
|
192
|
+
const previousProduct = activeProduct;
|
|
193
|
+
activeProduct = product * Math.max(1, maxIterations);
|
|
194
|
+
maxIterationProduct = Math.max(maxIterationProduct, activeProduct);
|
|
195
|
+
body(node.statement, counters, activeProduct);
|
|
196
|
+
activeProduct = previousProduct;
|
|
197
|
+
loopDepth -= 1;
|
|
198
|
+
}
|
|
139
199
|
return { name, maxIterations };
|
|
140
200
|
}
|
|
141
201
|
const result = loop(statement, new Set(), 1);
|
|
142
|
-
|
|
202
|
+
if (options.readLoop && !options.headerOnly && !effectCount) fail(statement, 'A bounded read loop requires at least one admitted sequential effect site.');
|
|
203
|
+
return { ...result, errors, effectCount, maxIterationProduct };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function inspectBoundedReadLoop(statement, options = {}) {
|
|
207
|
+
return inspectBoundedPureLoop(statement, { ...options, readLoop: true });
|
|
143
208
|
}
|
|
144
209
|
|
|
145
|
-
module.exports = { inspectBoundedPureLoop };
|
|
210
|
+
module.exports = { inspectBoundedPureLoop, inspectBoundedReadLoop };
|
|
@@ -39,7 +39,9 @@ function annotateSites(effectSites, continuationSites, routerMetadata) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function annotate(site) {
|
|
42
|
-
|
|
42
|
+
// Package diagnostics retain authored locations, including imported files.
|
|
43
|
+
// Router ownership instead follows the mapped call in generated source.
|
|
44
|
+
const owner = ownerForPosition(site && (site.generatedPosition || site.position));
|
|
43
45
|
if (!owner) return site;
|
|
44
46
|
const ownerKind = owner.kind || 'route';
|
|
45
47
|
const plane = owner.plane || (ownerKind === 'event' ? 'event' : 'http');
|
|
@@ -32,6 +32,10 @@ const {
|
|
|
32
32
|
|
|
33
33
|
function optimizationPosture(value) {
|
|
34
34
|
if (value === undefined || value === null || value === false) return 'native-default';
|
|
35
|
+
if (
|
|
36
|
+
value === 'experimental-native-bounded-size'
|
|
37
|
+
|| (value && typeof value === 'object' && value.mode === 'experimental-native-bounded-size')
|
|
38
|
+
) return 'native-default';
|
|
35
39
|
if (
|
|
36
40
|
value === true
|
|
37
41
|
|| value === 'experimental-native-size'
|
|
@@ -67,8 +71,13 @@ function realizeSelectedGuestUnits(realization, guestUnits, options = {}) {
|
|
|
67
71
|
return Object.freeze({
|
|
68
72
|
...realization,
|
|
69
73
|
wasm: result.wasm,
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
// Guest linking retains its mandatory independent disassembly audit.
|
|
75
|
+
// Only an explicitly requested diagnostic becomes an output artifact.
|
|
76
|
+
wat: realization.textEmitted ? result.wat : '',
|
|
77
|
+
// Preserve trusted package roots for subsequent provider-specific linking.
|
|
78
|
+
guestUnits: Object.freeze(result.guestUnits.map(unit => Object.freeze({
|
|
79
|
+
...unit, packageRoot: selected.find(selection => selection.id === unit.id).packageRoot
|
|
80
|
+
}))),
|
|
72
81
|
guestLink: Object.freeze({
|
|
73
82
|
version: result.version,
|
|
74
83
|
plan: result.plan,
|
|
@@ -66,6 +66,8 @@ function providerEffectExpression(factory, sourceFile, site, providerOperation)
|
|
|
66
66
|
properties.push(literalProperty(factory, 'key', providerOperation.key));
|
|
67
67
|
if (providerOperation.generation) properties.push(literalProperty(factory, 'generation', providerOperation.generation));
|
|
68
68
|
if (providerOperation.value) properties.push(literalProperty(factory, 'value', providerOperation.value));
|
|
69
|
+
} else if (providerOperation.providerKind === 'output') {
|
|
70
|
+
if (providerOperation.args[0]) properties.push(literalProperty(factory, 'argument0', providerOperation.args[0]));
|
|
69
71
|
} else if (providerOperation.kind === 'event.emit') {
|
|
70
72
|
properties.push(literalProperty(factory, 'type', providerOperation.type));
|
|
71
73
|
properties.push(literalProperty(factory, 'emission', providerOperation.emission));
|
|
@@ -262,6 +264,22 @@ function emitCanonicalHandlerGenerator(ir) {
|
|
|
262
264
|
|
|
263
265
|
function emitOperation(entry) {
|
|
264
266
|
switch (entry.kind) {
|
|
267
|
+
case 'helper-body':
|
|
268
|
+
return factory.updateFunctionDeclaration(entry.statement, entry.statement.modifiers,
|
|
269
|
+
entry.pure ? undefined : factory.createToken(ts.SyntaxKind.AsteriskToken), entry.statement.name,
|
|
270
|
+
undefined, entry.statement.parameters, undefined, emitOperation(entry.body));
|
|
271
|
+
case 'helper-call':
|
|
272
|
+
return factory.updateVariableStatement(entry.statement, entry.statement.modifiers,
|
|
273
|
+
factory.updateVariableDeclarationList(entry.statement.declarationList, [factory.updateVariableDeclaration(
|
|
274
|
+
entry.declaration, entry.declaration.name, undefined, entry.declaration.type,
|
|
275
|
+
factory.createYieldExpression(factory.createToken(ts.SyntaxKind.AsteriskToken), entry.call))]));
|
|
276
|
+
case 'router-body':
|
|
277
|
+
return factory.updateFunctionDeclaration(entry.statement, entry.statement.modifiers,
|
|
278
|
+
factory.createToken(ts.SyntaxKind.AsteriskToken), entry.statement.name,
|
|
279
|
+
undefined, [], undefined, emitOperation(entry.body));
|
|
280
|
+
case 'router-body-call':
|
|
281
|
+
return factory.updateReturnStatement(entry.statement,
|
|
282
|
+
factory.createYieldExpression(factory.createToken(ts.SyntaxKind.AsteriskToken), entry.statement.expression));
|
|
265
283
|
case 'source-statement':
|
|
266
284
|
return rewritePackageIntrinsics(entry.statement);
|
|
267
285
|
case 'block':
|
|
@@ -276,6 +294,8 @@ function emitCanonicalHandlerGenerator(ir) {
|
|
|
276
294
|
emitOperation(entry.thenOperation),
|
|
277
295
|
entry.elseOperation ? emitOperation(entry.elseOperation) : undefined
|
|
278
296
|
);
|
|
297
|
+
case 'read-loop':
|
|
298
|
+
return factory.updateForStatement(entry.statement, entry.initializer, entry.test, entry.increment, emitOperation(entry.body));
|
|
279
299
|
case 'fetch-single': {
|
|
280
300
|
const { candidate, site, continuation } = entry;
|
|
281
301
|
const marker = fetchEffectExpression(factory, sourceFile, site, candidate.chain);
|
package/src/spine/handler-ir.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
|
|
2
3
|
|
|
3
4
|
const ts = require('typescript');
|
|
4
|
-
const { inspectBoundedPureLoop } = require('./bounded-pure-loop.js');
|
|
5
|
+
const { inspectBoundedPureLoop, inspectBoundedReadLoop } = require('./bounded-pure-loop.js');
|
|
5
6
|
const {
|
|
6
7
|
extractFetchChain,
|
|
7
8
|
extractKvNamespaceDeclaration,
|
|
@@ -31,6 +32,7 @@ const HANDLER_IR_OPERATION_KINDS = Object.freeze([
|
|
|
31
32
|
'source-statement',
|
|
32
33
|
'block',
|
|
33
34
|
'if',
|
|
35
|
+
'read-loop',
|
|
34
36
|
'fetch-single',
|
|
35
37
|
'fetch-group',
|
|
36
38
|
'provider-variable',
|
|
@@ -41,7 +43,11 @@ const HANDLER_IR_OPERATION_KINDS = Object.freeze([
|
|
|
41
43
|
'opaque-fetch-return'
|
|
42
44
|
]);
|
|
43
45
|
const HANDLER_IR_EXTENSION_OPERATION_KINDS = Object.freeze([
|
|
44
|
-
'parallel-group'
|
|
46
|
+
'parallel-group',
|
|
47
|
+
'router-body',
|
|
48
|
+
'router-body-call',
|
|
49
|
+
'helper-body',
|
|
50
|
+
'helper-call'
|
|
45
51
|
]);
|
|
46
52
|
const ROUTER_HANDLER_IR_OPERATION_KINDS = Object.freeze([
|
|
47
53
|
'router-transfer',
|
|
@@ -160,6 +166,7 @@ function summarizeHandlerOperation(body) {
|
|
|
160
166
|
if (entry.elseOperation) countOperation(entry.elseOperation);
|
|
161
167
|
}
|
|
162
168
|
if (entry.kind === 'router-guard') countOperation(entry.body);
|
|
169
|
+
if (entry.kind === 'read-loop' || entry.kind === 'router-body' || entry.kind === 'helper-body') countOperation(entry.body);
|
|
163
170
|
}
|
|
164
171
|
countOperation(body);
|
|
165
172
|
return Object.freeze({
|
|
@@ -186,6 +193,24 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
186
193
|
const continuationSites = [];
|
|
187
194
|
const effectCounters = new Map();
|
|
188
195
|
let continuationIndex = 0;
|
|
196
|
+
const helperBodies = new Map((options.internalGeneratedHandler && options.metadataExtensions?.router?.helpers || []).map(helper => [helper.name, helper]));
|
|
197
|
+
const pureHelperBounds = new Map();
|
|
198
|
+
for (const declaration of handler.body.statements || []) {
|
|
199
|
+
if (!ts.isFunctionDeclaration(declaration) || !helperBodies.get(declaration.name?.text)?.pure) continue;
|
|
200
|
+
let maxIterationProduct = 1;
|
|
201
|
+
const visit = node => {
|
|
202
|
+
if (ts.isForStatement(node)) {
|
|
203
|
+
const loop = inspectBoundedPureLoop(node, { pureHelper: true });
|
|
204
|
+
maxIterationProduct = Math.max(maxIterationProduct, loop.maxIterationProduct);
|
|
205
|
+
return; // The shared inspector includes all nested bounds.
|
|
206
|
+
}
|
|
207
|
+
ts.forEachChild(node, visit);
|
|
208
|
+
};
|
|
209
|
+
visit(declaration.body);
|
|
210
|
+
pureHelperBounds.set(declaration.name.text, { maxIterationProduct });
|
|
211
|
+
}
|
|
212
|
+
const routerBodies = new Map((options.internalGeneratedHandler && options.metadataExtensions?.router?.entries || [])
|
|
213
|
+
.filter(entry => entry.nativeBody).map(entry => [entry.nativeBody.name, entry]));
|
|
189
214
|
// Offsets belong to a source file. Project-wide contributions can share an
|
|
190
215
|
// offset; only duplicate ranges within the same owning source are invalid.
|
|
191
216
|
const sourceKey = (file, start) => `${String(file).replace(/\\/g, '/')}\u0000${start}`;
|
|
@@ -232,6 +257,9 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
232
257
|
linkPackageIntrinsicsWithin(call);
|
|
233
258
|
return linked;
|
|
234
259
|
}
|
|
260
|
+
// Generated Router offsets belong to a different source tree. A miss in
|
|
261
|
+
// its explicit mapping must not fall back to authored-source offsets.
|
|
262
|
+
if (options.internalGeneratedHandler) return undefined;
|
|
235
263
|
}
|
|
236
264
|
const effect = packageEffectsBySource.get(callKey(call));
|
|
237
265
|
if (effect) linkPackageIntrinsicsWithin(call);
|
|
@@ -254,6 +282,7 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
254
282
|
}
|
|
255
283
|
return linked;
|
|
256
284
|
}
|
|
285
|
+
if (options.internalGeneratedHandler) return undefined;
|
|
257
286
|
}
|
|
258
287
|
const key = callKey(call);
|
|
259
288
|
const intrinsic = packageIntrinsicsBySource.get(key);
|
|
@@ -278,6 +307,7 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
278
307
|
if (typeof options.packageResultAdapterForCall === 'function') {
|
|
279
308
|
const linked = options.packageResultAdapterForCall(call);
|
|
280
309
|
if (linked) return linked;
|
|
310
|
+
if (options.internalGeneratedHandler) return undefined;
|
|
281
311
|
}
|
|
282
312
|
return packageResultAdaptersByStart.get(call.getStart(sourceFile));
|
|
283
313
|
}
|
|
@@ -316,10 +346,13 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
316
346
|
}
|
|
317
347
|
|
|
318
348
|
function nextProviderEffectSite(providerOperation, grouped = false, groupKey) {
|
|
319
|
-
const
|
|
349
|
+
const valueType = providerOperation.valueTypeNode && require('./pure-helper-source').resolvePureType(
|
|
350
|
+
{ projectModules: new Map() }, { sourceFile, path: sourceFile.fileName, imports: [] }, providerOperation.valueTypeNode);
|
|
351
|
+
const prefix = providerOperation.providerKind === 'kv' ? `kv-${providerOperation.operation}` : providerOperation.providerKind;
|
|
320
352
|
const site = Object.freeze({
|
|
321
353
|
id: nextEffectId(prefix),
|
|
322
354
|
kind: providerOperation.kind,
|
|
355
|
+
...(valueType ? { borrowedValue: { version: CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION, type: valueType } } : {}),
|
|
323
356
|
providerKind: providerOperation.providerKind,
|
|
324
357
|
operation: providerOperation.operation,
|
|
325
358
|
grouped: Boolean(grouped),
|
|
@@ -360,7 +393,8 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
360
393
|
payload: Object.freeze({ ...(effect.payload || {}) }),
|
|
361
394
|
result: String(effect.result || 'value'),
|
|
362
395
|
resource: effect.resource || Object.freeze({ kind: 'none' }),
|
|
363
|
-
position: packageEffectPosition(effect, call)
|
|
396
|
+
position: packageEffectPosition(effect, call),
|
|
397
|
+
...(options.internalGeneratedHandler ? { generatedPosition: positionFor(sourceFile, call) } : {})
|
|
364
398
|
});
|
|
365
399
|
effectSites.push(site);
|
|
366
400
|
return site;
|
|
@@ -450,6 +484,10 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
450
484
|
continue;
|
|
451
485
|
}
|
|
452
486
|
const provider = extractProviderCall(property.initializer, ctxName, aliases, { unwrap: true });
|
|
487
|
+
if (provider && provider.providerKind === 'output') {
|
|
488
|
+
diagnostics.push(diagnostic(sourceFile, property.initializer, 'PULSE_OUTPUT_PARALLEL_FORBIDDEN', 'Output operations cannot be members of ctx.parallel.'));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
453
491
|
if (provider) {
|
|
454
492
|
members.push(Object.freeze({ key, kind: 'provider', property, operation: provider }));
|
|
455
493
|
continue;
|
|
@@ -594,13 +632,37 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
594
632
|
index += 1;
|
|
595
633
|
continue;
|
|
596
634
|
}
|
|
597
|
-
out.push(buildStatement(statement, aliases));
|
|
635
|
+
out.push(buildStatement(statement, aliases, true));
|
|
598
636
|
index += 1;
|
|
599
637
|
}
|
|
600
638
|
return Object.freeze(out.flat().filter(Boolean));
|
|
601
639
|
}
|
|
602
640
|
|
|
603
|
-
|
|
641
|
+
let activePureHelper = false;
|
|
642
|
+
function buildStatement(statement, aliases, fromList = false) {
|
|
643
|
+
if (ts.isFunctionDeclaration(statement) && helperBodies.has(statement.name?.text)) {
|
|
644
|
+
const previous = activePureHelper;
|
|
645
|
+
activePureHelper = helperBodies.get(statement.name.text).pure === true;
|
|
646
|
+
const body = buildStatement(statement.body, new Map());
|
|
647
|
+
activePureHelper = previous;
|
|
648
|
+
return createHandlerOperation('helper-body', { statement, pure: helperBodies.get(statement.name.text).pure === true, body });
|
|
649
|
+
}
|
|
650
|
+
if (ts.isVariableStatement(statement) && statement.declarationList.declarations.length === 1) {
|
|
651
|
+
const declaration = statement.declarationList.declarations[0], call = declaration.initializer;
|
|
652
|
+
if (call && ts.isCallExpression(call) && ts.isIdentifier(call.expression) && helperBodies.has(call.expression.text) && !helperBodies.get(call.expression.text).pure)
|
|
653
|
+
return createHandlerOperation('helper-call', { statement, declaration, call });
|
|
654
|
+
}
|
|
655
|
+
if (ts.isFunctionDeclaration(statement) && routerBodies.has(statement.name?.text)) {
|
|
656
|
+
return createHandlerOperation('router-body', { statement, body: buildStatement(statement.body, new Map()) });
|
|
657
|
+
}
|
|
658
|
+
if (ts.isReturnStatement(statement) && statement.expression && ts.isCallExpression(statement.expression)
|
|
659
|
+
&& ts.isIdentifier(statement.expression.expression) && routerBodies.has(statement.expression.expression.text)) {
|
|
660
|
+
return createHandlerOperation('router-body-call', { statement });
|
|
661
|
+
}
|
|
662
|
+
if (!fromList && (ts.isExpressionStatement(statement) || ts.isVariableStatement(statement))) {
|
|
663
|
+
const statements = buildStatementList([statement], aliases);
|
|
664
|
+
return statements.length === 1 ? statements[0] : createHandlerOperation('block', { statement: ts.factory.createBlock([statement], true), statements });
|
|
665
|
+
}
|
|
604
666
|
if (ts.isBlock(statement)) return createHandlerOperation('block', { statement, statements: buildStatementList(statement.statements, aliases) });
|
|
605
667
|
if (ts.isIfStatement(statement)) {
|
|
606
668
|
const thenOperation = buildStatement(statement.thenStatement, new Map(aliases));
|
|
@@ -637,12 +699,33 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
637
699
|
}
|
|
638
700
|
}
|
|
639
701
|
if (ts.isForStatement(statement)) {
|
|
640
|
-
const
|
|
702
|
+
const effectForCall = call => extractProviderCall(call, ctxName, aliases) || packageEffectForCall(call);
|
|
703
|
+
const pureHelperForCall = call => ts.isIdentifier(call.expression) && pureHelperBounds.get(call.expression.text);
|
|
704
|
+
const helperForCall = call => ts.isIdentifier(call.expression) && (
|
|
705
|
+
(helperBodies.get(call.expression.text)?.pure ? undefined : helperBodies.get(call.expression.text))
|
|
706
|
+
// JavaScript inspection retains ordinary ctx-first source calls. The
|
|
707
|
+
// original graph executes them; this is not Native callee admission.
|
|
708
|
+
|| options.target === 'javascript' && call.arguments.length > 0 && ts.isIdentifier(call.arguments[0]) && call.arguments[0].text === ctxName);
|
|
709
|
+
let hasEffect = false;
|
|
710
|
+
function findEffect(node) {
|
|
711
|
+
if (ts.isCallExpression(node) && (effectForCall(node) || helperForCall(node))) hasEffect = true;
|
|
712
|
+
ts.forEachChild(node, findEffect);
|
|
713
|
+
}
|
|
714
|
+
findEffect(statement);
|
|
715
|
+
if (hasEffect) {
|
|
716
|
+
const loop = inspectBoundedReadLoop(statement, { ctxName, effectForCall, helperForCall, pureHelperForCall, namespaceAliases: [...aliases.keys()] });
|
|
717
|
+
for (const error of loop.errors) diagnostics.push(diagnostic(sourceFile, error.node, 'PULSE_CANONICAL_READ_LOOP_UNSUPPORTED', error.message));
|
|
718
|
+
if (loop.errors.length) return createHandlerOperation('source-statement', { statement, role: 'read-loop-rejected' });
|
|
719
|
+
return createHandlerOperation('read-loop', { statement, counter: loop.name, maxIterations: loop.maxIterations,
|
|
720
|
+
initializer: statement.initializer, test: statement.condition, increment: statement.incrementor,
|
|
721
|
+
body: buildStatement(statement.statement, new Map(aliases)) });
|
|
722
|
+
}
|
|
723
|
+
const loop = inspectBoundedPureLoop(statement, { ctxName, pureHelper: activePureHelper, pureHelperForCall });
|
|
641
724
|
for (const error of loop.errors) diagnostics.push(diagnostic(sourceFile, error.node, 'PULSE_CANONICAL_PURE_LOOP_UNSUPPORTED', error.message));
|
|
642
725
|
return createHandlerOperation('source-statement', { statement, role: 'bounded-pure-loop' });
|
|
643
726
|
}
|
|
644
727
|
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
|
|
728
|
+
diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_CONTROL_FLOW_UNSUPPORTED', 'Canonical lowering supports if/else, literal-capped pure for loops and bounded sequential read loops; other loops, switch, and try/catch are reserved.'));
|
|
646
729
|
}
|
|
647
730
|
return createHandlerOperation('source-statement', { statement, role: 'preserved-source' });
|
|
648
731
|
}
|
|
@@ -688,6 +771,9 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
688
771
|
const aliases = new Map(inheritedAliases);
|
|
689
772
|
for (let index = 0; index < statements.length; index += 1) {
|
|
690
773
|
const statement = statements[index];
|
|
774
|
+
if (ts.isFunctionDeclaration(statement) && (routerBodies.has(statement.name?.text) || helperBodies.has(statement.name?.text))) {
|
|
775
|
+
scanOriginalStatements(statement.body.statements); continue;
|
|
776
|
+
}
|
|
691
777
|
const namespace = extractKvNamespaceDeclaration(statement, ctxName);
|
|
692
778
|
if (namespace) { aliases.set(namespace.variableName, namespace.store); continue; }
|
|
693
779
|
if (parallelInvocationStatement(statement, ctxName)) continue;
|
|
@@ -700,6 +786,7 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
700
786
|
if (ts.isReturnStatement(statement) && packageEffectForCall(unwrapPackageCall(statement.expression))) continue;
|
|
701
787
|
if (ts.isReturnStatement(statement) && extractFetchChain(statement.expression, ctxName)) continue;
|
|
702
788
|
if (ts.isBlock(statement)) { scanOriginalStatements(statement.statements, aliases); continue; }
|
|
789
|
+
if (ts.isForStatement(statement)) { scanOriginalStatements([statement.statement], aliases); continue; }
|
|
703
790
|
if (ts.isIfStatement(statement)) {
|
|
704
791
|
scanOriginalStatements(ts.isBlock(statement.thenStatement) ? statement.thenStatement.statements : [statement.thenStatement], aliases);
|
|
705
792
|
if (statement.elseStatement) scanOriginalStatements(ts.isBlock(statement.elseStatement) ? statement.elseStatement.statements : [statement.elseStatement], aliases);
|
|
@@ -174,6 +174,12 @@ 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 === 'req' && method === 'readTextChunk' && current.arguments.length === 0) {
|
|
178
|
+
return Object.freeze({ call: current, surfaceId: 'ctx.req.readTextChunk', kind: 'output.readTextChunk', providerKind: 'output', operation: 'readTextChunk', capability: 'request.body.transform', args: Object.freeze([]) });
|
|
179
|
+
}
|
|
180
|
+
if (namespace === 'output' && ((method === 'start' && current.arguments.length <= 1) || (method === 'write' && current.arguments.length === 1))) {
|
|
181
|
+
return Object.freeze({ call: current, surfaceId: `ctx.output.${method}`, kind: `output.${method}`, providerKind: 'output', operation: method, capability: 'response.output', args: Object.freeze([...current.arguments]) });
|
|
182
|
+
}
|
|
177
183
|
if (namespace === 'time' && method === 'now' && current.arguments.length === 0) {
|
|
178
184
|
return Object.freeze({ call: current, surfaceId: 'ctx.time.now', kind: 'time.now', providerKind: 'time', operation: 'now', capability: 'time.wall-clock', args: Object.freeze([]) });
|
|
179
185
|
}
|
|
@@ -200,6 +206,7 @@ function extractProviderCall(expression, ctxName, kvAliases = new Map(), options
|
|
|
200
206
|
return Object.freeze({
|
|
201
207
|
call: current,
|
|
202
208
|
surfaceId: `ctx.kv.${method}`,
|
|
209
|
+
valueTypeNode: method === 'getVersioned' && (ts.isCallExpression(receiver) ? receiver : store.parent)?.typeArguments?.length === 1 ? (ts.isCallExpression(receiver) ? receiver : store.parent).typeArguments[0] : undefined,
|
|
203
210
|
kind: `kv.${method}`,
|
|
204
211
|
providerKind: 'kv',
|
|
205
212
|
operation: method,
|
|
@@ -278,8 +285,10 @@ function recognizeHandlerSurface(node, options = {}) {
|
|
|
278
285
|
if (receiver && ts.isPropertyAccessExpression(receiver) && isIdentifierNamed(receiver.expression, ctxName, options)) {
|
|
279
286
|
const namespace = receiver.name.text;
|
|
280
287
|
const method = target.name.text;
|
|
288
|
+
if (namespace === 'output' && method === 'close' && current.arguments.length === 0) return createRecognition('ctx.output.close', current, { arguments: Object.freeze([]) }, options);
|
|
281
289
|
if (namespace === 'req' && method === 'header') return createRecognition('ctx.req.header', current, { arguments: Object.freeze([...current.arguments]) }, options);
|
|
282
290
|
if (namespace === 'req' && method === 'text') return createRecognition('ctx.req.text', current, { arguments: Object.freeze([...current.arguments]) }, options);
|
|
291
|
+
if (namespace === 'req' && method === 'body') return createRecognition('ctx.req.body', current, { arguments: Object.freeze([...current.arguments]) }, options);
|
|
283
292
|
if (namespace === 'req' && method === 'json') {
|
|
284
293
|
return createRecognition(current.arguments.length === 0 ? 'ctx.req.json.generic' : 'ctx.req.json.schema', current, { arguments: Object.freeze([...current.arguments]) }, options);
|
|
285
294
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const ts = require('typescript');
|
|
3
|
+
const { createCanonicalDiagnostic } = require('./diagnostic-authority.js');
|
|
4
|
+
|
|
5
|
+
function validateIncomingBodySource(handler, options) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
let calls = 0, marker, structuredRead = false;
|
|
8
|
+
const ctx = options.ctxName || 'ctx';
|
|
9
|
+
function reject(node, code, message) {
|
|
10
|
+
diagnostics.push(createCanonicalDiagnostic({ ...options, node, code, message }));
|
|
11
|
+
}
|
|
12
|
+
function visit(node) {
|
|
13
|
+
if (ts.isTypeNode(node)) return;
|
|
14
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
|
|
15
|
+
&& ['text', 'json'].includes(node.expression.name.text)
|
|
16
|
+
&& ts.isPropertyAccessExpression(node.expression.expression)
|
|
17
|
+
&& node.expression.expression.name.text === 'req'
|
|
18
|
+
&& ts.isIdentifier(node.expression.expression.expression)
|
|
19
|
+
&& node.expression.expression.expression.text === ctx) structuredRead = true;
|
|
20
|
+
const property = ts.isPropertyAccessExpression(node) && node.name.text === 'body';
|
|
21
|
+
const computed = ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && node.argumentExpression.text === 'body';
|
|
22
|
+
if ((property || computed) && ts.isPropertyAccessExpression(node.expression)
|
|
23
|
+
&& node.expression.name.text === 'req' && ts.isIdentifier(node.expression.expression)
|
|
24
|
+
&& node.expression.expression.text === ctx) {
|
|
25
|
+
const call = node.parent, field = call?.parent, init = field?.parent, fetch = init?.parent;
|
|
26
|
+
const named = name => init && ts.isObjectLiteralExpression(init) && init.properties.find(p =>
|
|
27
|
+
ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === name);
|
|
28
|
+
const method = named('method');
|
|
29
|
+
calls++;
|
|
30
|
+
marker = node;
|
|
31
|
+
if (computed || !call || !ts.isCallExpression(call) || call.expression !== node || call.arguments.length
|
|
32
|
+
|| !field || !ts.isPropertyAssignment(field) || field.initializer !== call || field.name.getText(options.sourceFile) !== 'body'
|
|
33
|
+
|| !init || !ts.isObjectLiteralExpression(init) || init.properties.some(p => ts.isSpreadAssignment(p))
|
|
34
|
+
|| !fetch || !ts.isCallExpression(fetch) || fetch.arguments[1] !== init
|
|
35
|
+
|| !ts.isPropertyAccessExpression(fetch.expression) || !ts.isIdentifier(fetch.expression.expression)
|
|
36
|
+
|| fetch.expression.expression.text !== ctx || fetch.expression.name.text !== 'fetch'
|
|
37
|
+
|| !method || !ts.isStringLiteral(method.initializer) || method.initializer.text !== 'POST'
|
|
38
|
+
|| named('json') || named('schema') || calls > 1) {
|
|
39
|
+
reject(node, 'PULSE_REQUEST_FORWARDING_FORM_UNSUPPORTED', 'Use one ctx.req.body() inline as the body of a literal outbound POST; markers cannot escape, be awaited, or be duplicated.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
ts.forEachChild(node, visit);
|
|
43
|
+
}
|
|
44
|
+
visit(handler.body || handler);
|
|
45
|
+
if (marker && structuredRead) reject(marker, 'PULSE_REQUEST_BODY_OWNERSHIP', 'A forwarding handler cannot also project the incoming body as text or JSON.');
|
|
46
|
+
return diagnostics;
|
|
47
|
+
}
|
|
48
|
+
module.exports = { validateIncomingBodySource };
|
|
@@ -360,6 +360,10 @@ function analyzeHandler(sourceFile, handler, ctxName, diagnostics, schemaBundleI
|
|
|
360
360
|
} else if (surface.surfaceId === 'ctx.req.text') {
|
|
361
361
|
capabilities.add('request.text');
|
|
362
362
|
validateDecoderCall(node, 'request-decode');
|
|
363
|
+
} else if (surface.surfaceId === 'ctx.output.close') {
|
|
364
|
+
capabilities.add('response.output');
|
|
365
|
+
} else if (surface.surfaceId === 'ctx.req.body') {
|
|
366
|
+
capabilities.add('request.body.forward');
|
|
363
367
|
} else if (surface.surfaceId === 'ctx.state.get' || surface.surfaceId === 'ctx.state.set') {
|
|
364
368
|
capabilities.add(surface.surfaceId.slice('ctx.'.length));
|
|
365
369
|
} else if (surface.surfaceId.startsWith('ctx.log.')) {
|
|
@@ -508,6 +512,7 @@ function preparePlainHandlerSource(sourceText, options = {}, recognition) {
|
|
|
508
512
|
role: 'handler',
|
|
509
513
|
strict: options.strict === true,
|
|
510
514
|
frontend: 'canonical-source',
|
|
515
|
+
internalGeneratedHandler: options.internalGeneratedHandler === true,
|
|
511
516
|
target: options.target,
|
|
512
517
|
handlerAuthoring: options.handlerAuthoring,
|
|
513
518
|
requireAsync: options.requireAsync === true,
|