@pulse-compute/wasm-compiler 1.0.0-beta.4 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/bin/provider-proof-composition.js +29 -31
- package/bin/pulsewasm-extract.js +1 -1
- package/package.json +9 -11
- package/src/canonical-api-compiler.js +1 -9
- package/src/canonical-native-compiler.js +40 -62
- package/src/canonical-native-plan.js +513 -81
- package/src/canonical-project-compiler.js +52 -12
- package/src/execution-plan.js +1 -0
- package/src/extractor.js +36 -0
- package/src/javascript-application-plan.js +4 -1
- package/src/native-result-facts.js +50 -0
- package/src/project/router-module-linker.js +64 -0
- package/src/project-target-support.js +10 -2
- package/src/provider-toolchain.js +29 -5
- package/src/pure-helper-values.js +138 -0
- package/src/shared-stage-plan.js +239 -0
- package/src/source-helper-plan.js +482 -0
- package/src/spine/async-surface-normalizer.js +17 -1
- package/src/spine/bounded-pure-loop.js +210 -0
- package/src/spine/canonical-handler-ir.js +3 -1
- package/src/spine/guest-unit-stage.js +11 -2
- package/src/spine/handler-ir-emitter.js +20 -0
- package/src/spine/handler-ir.js +129 -26
- package/src/spine/handler-surface-authority.js +13 -0
- package/src/spine/incoming-body-source.js +48 -0
- package/src/spine/plain-handler-frontend.js +17 -4
- package/src/spine/pure-helper-source.js +123 -0
- package/src/spine/router-handler-frontend.js +39 -2
- package/src/spine/router-handler-ir.js +71 -3
- package/src/spine/router-topology-frontend.js +3 -1
- package/src/spine/terminal-package-native.js +37 -0
|
@@ -0,0 +1,210 @@
|
|
|
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, CANONICAL_READ_LOOP_CONTRACT } = 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
|
+
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 || [])] : []);
|
|
40
|
+
function fail(node, message) { errors.push({ node, message }); }
|
|
41
|
+
function expression(input, counters, mutation = true, effectRoot = false) {
|
|
42
|
+
let node = unwrap(input);
|
|
43
|
+
if (!node) return;
|
|
44
|
+
if (readBody() && effectRoot && ts.isAwaitExpression(node)) node = unwrap(node.expression);
|
|
45
|
+
if (ts.isIdentifier(node)) {
|
|
46
|
+
if (node.text === options.ctxName) fail(node, 'Read context values before entering a pure loop.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)
|
|
50
|
+
|| [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return;
|
|
51
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
52
|
+
if (node.questionDotToken) fail(node, 'Optional access is outside the bounded pure loop subset.');
|
|
53
|
+
expression(node.expression, counters, mutation); return;
|
|
54
|
+
}
|
|
55
|
+
if (ts.isElementAccessExpression(node)) {
|
|
56
|
+
if (node.questionDotToken) fail(node, 'Optional access is outside the bounded pure loop subset.');
|
|
57
|
+
expression(node.expression, counters, mutation); expression(node.argumentExpression, counters, mutation); return;
|
|
58
|
+
}
|
|
59
|
+
if (ts.isCallExpression(node)) {
|
|
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
|
+
}
|
|
99
|
+
if (!ts.isPropertyAccessExpression(target) || target.name.text !== 'trim' || node.arguments.length || node.questionDotToken || target.questionDotToken) {
|
|
100
|
+
fail(node, 'Pure loops admit only zero-argument string .trim() calls; effects, helpers and callbacks are separate contracts.'); return;
|
|
101
|
+
}
|
|
102
|
+
expression(target.expression, counters, false); return;
|
|
103
|
+
}
|
|
104
|
+
if (ts.isBinaryExpression(node)) {
|
|
105
|
+
const assignment = node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
|
106
|
+
const target = unwrap(node.left);
|
|
107
|
+
if (assignment && !ts.isIdentifier(target) && !ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) fail(node, 'Destructuring assignment is outside the pure loop subset.');
|
|
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.');
|
|
109
|
+
expression(node.left, counters, mutation); expression(node.right, counters, mutation); return;
|
|
110
|
+
}
|
|
111
|
+
if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
|
|
112
|
+
const update = node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken;
|
|
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.');
|
|
114
|
+
expression(node.operand, counters, mutation); return;
|
|
115
|
+
}
|
|
116
|
+
if (ts.isConditionalExpression(node)) {
|
|
117
|
+
expression(node.condition, counters, mutation); expression(node.whenTrue, counters, mutation); expression(node.whenFalse, counters, mutation); return;
|
|
118
|
+
}
|
|
119
|
+
if (ts.isTypeOfExpression(node) || ts.isVoidExpression(node)) { expression(node.expression, counters, mutation); return; }
|
|
120
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
121
|
+
for (const item of node.elements) expression(ts.isSpreadElement(item) ? item.expression : item, counters, mutation);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
125
|
+
for (const item of node.properties) {
|
|
126
|
+
if (ts.isSpreadAssignment(item)) expression(item.expression, counters, mutation);
|
|
127
|
+
else if (ts.isShorthandPropertyAssignment(item)) expression(item.name, counters, mutation);
|
|
128
|
+
else if (ts.isPropertyAssignment(item)) {
|
|
129
|
+
if (ts.isComputedPropertyName(item.name)) expression(item.name.expression, counters, mutation);
|
|
130
|
+
expression(item.initializer, counters, mutation);
|
|
131
|
+
} else fail(item, 'Methods and accessors are outside pure loop values.');
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (ts.isTemplateExpression(node)) { for (const span of node.templateSpans) expression(span.expression, counters, mutation); return; }
|
|
136
|
+
fail(node, `${ts.SyntaxKind[node.kind]} is outside bounded pure loop expressions.`);
|
|
137
|
+
}
|
|
138
|
+
function body(node, counters, product) {
|
|
139
|
+
if (ts.isBlock(node)) { for (const child of node.statements) body(child, counters, product); return; }
|
|
140
|
+
if (ts.isForStatement(node)) { loop(node, counters, product); return; }
|
|
141
|
+
if (ts.isVariableStatement(node)) {
|
|
142
|
+
if (!(node.declarationList.flags & ts.NodeFlags.BlockScoped)) fail(node, 'Pure loop locals must use let or const.');
|
|
143
|
+
for (const declaration of node.declarationList.declarations) {
|
|
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);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (ts.isIfStatement(node)) {
|
|
150
|
+
expression(node.expression, counters); body(node.thenStatement, counters, product);
|
|
151
|
+
if (node.elseStatement) body(node.elseStatement, counters, product); return;
|
|
152
|
+
}
|
|
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; }
|
|
155
|
+
if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
|
|
156
|
+
if (node.label) fail(node, 'Pure loop break and continue must be unlabelled.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (ts.isEmptyStatement(node)) return;
|
|
160
|
+
fail(node, 'Pure loop bodies accept locals, value assignments, if/else, nested bounded for, break and continue only.');
|
|
161
|
+
}
|
|
162
|
+
function loop(node, inherited, product) {
|
|
163
|
+
const isReadLoop = options.readLoop === true && loopDepth === 0;
|
|
164
|
+
const list = node.initializer;
|
|
165
|
+
const declaration = list && ts.isVariableDeclarationList(list) && list.declarations.length === 1 && list.declarations[0];
|
|
166
|
+
if (!declaration || !(list.flags & ts.NodeFlags.Let) || !ts.isIdentifier(declaration.name) || !number(declaration.initializer, 0)) {
|
|
167
|
+
fail(node, 'A bounded pure loop starts with for (let index = 0; ...; index++).'); return;
|
|
168
|
+
}
|
|
169
|
+
const name = declaration.name.text;
|
|
170
|
+
const bound = firstConjunct(node.condition);
|
|
171
|
+
if (!bound || !ts.isBinaryExpression(bound) || !named(bound.left, name) || bound.operatorToken.kind !== ts.SyntaxKind.LessThanToken || !ts.isNumericLiteral(unwrap(bound.right))) {
|
|
172
|
+
fail(node, 'The first loop condition must be index < a literal iteration cap.'); return;
|
|
173
|
+
}
|
|
174
|
+
const maxIterations = Number(unwrap(bound.right).text);
|
|
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) {
|
|
176
|
+
fail(node, 'The literal loop cap or nested iteration product exceeds the portable limit.'); return;
|
|
177
|
+
}
|
|
178
|
+
const increment = unwrap(node.incrementor);
|
|
179
|
+
if (!increment || !((ts.isPostfixUnaryExpression(increment) || ts.isPrefixUnaryExpression(increment)) && increment.operator === ts.SyntaxKind.PlusPlusToken && named(increment.operand, name))
|
|
180
|
+
&& !(ts.isBinaryExpression(increment) && increment.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken && named(increment.left, name) && number(increment.right, 1))) {
|
|
181
|
+
fail(node, 'The loop increment must be index++ or index += 1.'); return;
|
|
182
|
+
}
|
|
183
|
+
if (inherited.has(name) || protectedNames.has(name)) fail(declaration, 'Nested loop counters cannot shadow active counters or context/namespace aliases.');
|
|
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;
|
|
188
|
+
expression(node.condition, counters, false);
|
|
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
|
+
}
|
|
199
|
+
return { name, maxIterations };
|
|
200
|
+
}
|
|
201
|
+
const result = loop(statement, new Set(), 1);
|
|
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 });
|
|
208
|
+
}
|
|
209
|
+
|
|
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,6 +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');
|
|
5
|
+
const { inspectBoundedPureLoop, inspectBoundedReadLoop } = require('./bounded-pure-loop.js');
|
|
4
6
|
const {
|
|
5
7
|
extractFetchChain,
|
|
6
8
|
extractKvNamespaceDeclaration,
|
|
@@ -30,6 +32,7 @@ const HANDLER_IR_OPERATION_KINDS = Object.freeze([
|
|
|
30
32
|
'source-statement',
|
|
31
33
|
'block',
|
|
32
34
|
'if',
|
|
35
|
+
'read-loop',
|
|
33
36
|
'fetch-single',
|
|
34
37
|
'fetch-group',
|
|
35
38
|
'provider-variable',
|
|
@@ -40,7 +43,11 @@ const HANDLER_IR_OPERATION_KINDS = Object.freeze([
|
|
|
40
43
|
'opaque-fetch-return'
|
|
41
44
|
]);
|
|
42
45
|
const HANDLER_IR_EXTENSION_OPERATION_KINDS = Object.freeze([
|
|
43
|
-
'parallel-group'
|
|
46
|
+
'parallel-group',
|
|
47
|
+
'router-body',
|
|
48
|
+
'router-body-call',
|
|
49
|
+
'helper-body',
|
|
50
|
+
'helper-call'
|
|
44
51
|
]);
|
|
45
52
|
const ROUTER_HANDLER_IR_OPERATION_KINDS = Object.freeze([
|
|
46
53
|
'router-transfer',
|
|
@@ -159,6 +166,7 @@ function summarizeHandlerOperation(body) {
|
|
|
159
166
|
if (entry.elseOperation) countOperation(entry.elseOperation);
|
|
160
167
|
}
|
|
161
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);
|
|
162
170
|
}
|
|
163
171
|
countOperation(body);
|
|
164
172
|
return Object.freeze({
|
|
@@ -185,25 +193,53 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
185
193
|
const continuationSites = [];
|
|
186
194
|
const effectCounters = new Map();
|
|
187
195
|
let continuationIndex = 0;
|
|
188
|
-
const
|
|
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]));
|
|
214
|
+
// Offsets belong to a source file. Project-wide contributions can share an
|
|
215
|
+
// offset; only duplicate ranges within the same owning source are invalid.
|
|
216
|
+
const sourceKey = (file, start) => `${String(file).replace(/\\/g, '/')}\u0000${start}`;
|
|
217
|
+
const inputKey = (input, start) => sourceKey(input.loc && input.loc.file || sourceFile.fileName, start);
|
|
218
|
+
const callKey = (call) => {
|
|
219
|
+
const owner = call.getSourceFile() || sourceFile;
|
|
220
|
+
return sourceKey(owner.fileName, call.getStart(owner));
|
|
221
|
+
};
|
|
222
|
+
const packageEffectsBySource = new Map();
|
|
189
223
|
for (const effectInput of options.packageEffects || []) {
|
|
190
224
|
if (!effectInput || effectInput.version !== canonicalRuntimeContract.CANONICAL_PACKAGE_EFFECT_VERSION || !effectInput.range) continue;
|
|
191
225
|
const start = Number(effectInput.range.start);
|
|
192
|
-
|
|
226
|
+
const key = inputKey(effectInput, start);
|
|
227
|
+
if (!Number.isSafeInteger(start) || packageEffectsBySource.has(key)) {
|
|
193
228
|
diagnostics.push(diagnostic(sourceFile, handler, 'PULSE_CANONICAL_PACKAGE_EFFECT_RANGE_INVALID', 'Package-owned canonical effects require unique source ranges.', { effect: effectInput }));
|
|
194
229
|
continue;
|
|
195
230
|
}
|
|
196
|
-
|
|
231
|
+
packageEffectsBySource.set(key, effectInput);
|
|
197
232
|
}
|
|
198
|
-
const
|
|
199
|
-
const
|
|
233
|
+
const packageIntrinsicsBySource = new Map();
|
|
234
|
+
const linkedPackageIntrinsicsBySource = new Map();
|
|
200
235
|
for (const intrinsicInput of options.packageIntrinsics || []) {
|
|
201
236
|
const start = Number(intrinsicInput && intrinsicInput.range && intrinsicInput.range.start);
|
|
202
|
-
|
|
237
|
+
const key = intrinsicInput && inputKey(intrinsicInput, start);
|
|
238
|
+
if (!Number.isSafeInteger(start) || packageIntrinsicsBySource.has(key)) {
|
|
203
239
|
diagnostics.push(diagnostic(sourceFile, handler, 'PULSE_CANONICAL_PACKAGE_INTRINSIC_RANGE_INVALID', 'Package-owned intrinsics require unique source ranges.', { intrinsic: intrinsicInput }));
|
|
204
240
|
continue;
|
|
205
241
|
}
|
|
206
|
-
|
|
242
|
+
packageIntrinsicsBySource.set(key, intrinsicInput);
|
|
207
243
|
}
|
|
208
244
|
|
|
209
245
|
function unwrapPackageCall(expression) {
|
|
@@ -221,8 +257,11 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
221
257
|
linkPackageIntrinsicsWithin(call);
|
|
222
258
|
return linked;
|
|
223
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;
|
|
224
263
|
}
|
|
225
|
-
const effect =
|
|
264
|
+
const effect = packageEffectsBySource.get(callKey(call));
|
|
226
265
|
if (effect) linkPackageIntrinsicsWithin(call);
|
|
227
266
|
return effect;
|
|
228
267
|
}
|
|
@@ -232,19 +271,22 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
232
271
|
if (typeof options.packageIntrinsicForCall === 'function') {
|
|
233
272
|
const linked = options.packageIntrinsicForCall(call);
|
|
234
273
|
if (linked) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
const owner = call.getSourceFile() || sourceFile;
|
|
275
|
+
const start = call.getStart(owner);
|
|
276
|
+
const key = callKey(call);
|
|
277
|
+
if (!linkedPackageIntrinsicsBySource.has(key)) {
|
|
278
|
+
linkedPackageIntrinsicsBySource.set(key, Object.freeze({
|
|
238
279
|
...linked,
|
|
239
280
|
range: Object.freeze({ start, end: call.getEnd() })
|
|
240
281
|
}));
|
|
241
282
|
}
|
|
242
283
|
return linked;
|
|
243
284
|
}
|
|
285
|
+
if (options.internalGeneratedHandler) return undefined;
|
|
244
286
|
}
|
|
245
|
-
const
|
|
246
|
-
const intrinsic =
|
|
247
|
-
if (intrinsic && !
|
|
287
|
+
const key = callKey(call);
|
|
288
|
+
const intrinsic = packageIntrinsicsBySource.get(key);
|
|
289
|
+
if (intrinsic && !linkedPackageIntrinsicsBySource.has(key)) linkedPackageIntrinsicsBySource.set(key, intrinsic);
|
|
248
290
|
return intrinsic;
|
|
249
291
|
}
|
|
250
292
|
|
|
@@ -265,6 +307,7 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
265
307
|
if (typeof options.packageResultAdapterForCall === 'function') {
|
|
266
308
|
const linked = options.packageResultAdapterForCall(call);
|
|
267
309
|
if (linked) return linked;
|
|
310
|
+
if (options.internalGeneratedHandler) return undefined;
|
|
268
311
|
}
|
|
269
312
|
return packageResultAdaptersByStart.get(call.getStart(sourceFile));
|
|
270
313
|
}
|
|
@@ -303,10 +346,13 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
303
346
|
}
|
|
304
347
|
|
|
305
348
|
function nextProviderEffectSite(providerOperation, grouped = false, groupKey) {
|
|
306
|
-
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;
|
|
307
352
|
const site = Object.freeze({
|
|
308
353
|
id: nextEffectId(prefix),
|
|
309
354
|
kind: providerOperation.kind,
|
|
355
|
+
...(valueType ? { borrowedValue: { version: CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION, type: valueType } } : {}),
|
|
310
356
|
providerKind: providerOperation.providerKind,
|
|
311
357
|
operation: providerOperation.operation,
|
|
312
358
|
grouped: Boolean(grouped),
|
|
@@ -347,7 +393,8 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
347
393
|
payload: Object.freeze({ ...(effect.payload || {}) }),
|
|
348
394
|
result: String(effect.result || 'value'),
|
|
349
395
|
resource: effect.resource || Object.freeze({ kind: 'none' }),
|
|
350
|
-
position: packageEffectPosition(effect, call)
|
|
396
|
+
position: packageEffectPosition(effect, call),
|
|
397
|
+
...(options.internalGeneratedHandler ? { generatedPosition: positionFor(sourceFile, call) } : {})
|
|
351
398
|
});
|
|
352
399
|
effectSites.push(site);
|
|
353
400
|
return site;
|
|
@@ -437,6 +484,10 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
437
484
|
continue;
|
|
438
485
|
}
|
|
439
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
|
+
}
|
|
440
491
|
if (provider) {
|
|
441
492
|
members.push(Object.freeze({ key, kind: 'provider', property, operation: provider }));
|
|
442
493
|
continue;
|
|
@@ -581,13 +632,37 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
581
632
|
index += 1;
|
|
582
633
|
continue;
|
|
583
634
|
}
|
|
584
|
-
out.push(buildStatement(statement, aliases));
|
|
635
|
+
out.push(buildStatement(statement, aliases, true));
|
|
585
636
|
index += 1;
|
|
586
637
|
}
|
|
587
638
|
return Object.freeze(out.flat().filter(Boolean));
|
|
588
639
|
}
|
|
589
640
|
|
|
590
|
-
|
|
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
|
+
}
|
|
591
666
|
if (ts.isBlock(statement)) return createHandlerOperation('block', { statement, statements: buildStatementList(statement.statements, aliases) });
|
|
592
667
|
if (ts.isIfStatement(statement)) {
|
|
593
668
|
const thenOperation = buildStatement(statement.thenStatement, new Map(aliases));
|
|
@@ -623,8 +698,34 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
623
698
|
return createHandlerOperation('opaque-fetch-return', { statement, chain, site, continuation });
|
|
624
699
|
}
|
|
625
700
|
}
|
|
626
|
-
if (ts.
|
|
627
|
-
|
|
701
|
+
if (ts.isForStatement(statement)) {
|
|
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 });
|
|
724
|
+
for (const error of loop.errors) diagnostics.push(diagnostic(sourceFile, error.node, 'PULSE_CANONICAL_PURE_LOOP_UNSUPPORTED', error.message));
|
|
725
|
+
return createHandlerOperation('source-statement', { statement, role: 'bounded-pure-loop' });
|
|
726
|
+
}
|
|
727
|
+
if (ts.isTryStatement(statement) || ts.isForInStatement(statement) || ts.isForOfStatement(statement) || ts.isWhileStatement(statement) || ts.isDoStatement(statement) || ts.isSwitchStatement(statement)) {
|
|
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.'));
|
|
628
729
|
}
|
|
629
730
|
return createHandlerOperation('source-statement', { statement, role: 'preserved-source' });
|
|
630
731
|
}
|
|
@@ -670,6 +771,9 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
670
771
|
const aliases = new Map(inheritedAliases);
|
|
671
772
|
for (let index = 0; index < statements.length; index += 1) {
|
|
672
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
|
+
}
|
|
673
777
|
const namespace = extractKvNamespaceDeclaration(statement, ctxName);
|
|
674
778
|
if (namespace) { aliases.set(namespace.variableName, namespace.store); continue; }
|
|
675
779
|
if (parallelInvocationStatement(statement, ctxName)) continue;
|
|
@@ -682,6 +786,7 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
682
786
|
if (ts.isReturnStatement(statement) && packageEffectForCall(unwrapPackageCall(statement.expression))) continue;
|
|
683
787
|
if (ts.isReturnStatement(statement) && extractFetchChain(statement.expression, ctxName)) continue;
|
|
684
788
|
if (ts.isBlock(statement)) { scanOriginalStatements(statement.statements, aliases); continue; }
|
|
789
|
+
if (ts.isForStatement(statement)) { scanOriginalStatements([statement.statement], aliases); continue; }
|
|
685
790
|
if (ts.isIfStatement(statement)) {
|
|
686
791
|
scanOriginalStatements(ts.isBlock(statement.thenStatement) ? statement.thenStatement.statements : [statement.thenStatement], aliases);
|
|
687
792
|
if (statement.elseStatement) scanOriginalStatements(ts.isBlock(statement.elseStatement) ? statement.elseStatement.statements : [statement.elseStatement], aliases);
|
|
@@ -709,11 +814,9 @@ function buildPlainHandlerIr(frontend, options = {}) {
|
|
|
709
814
|
effectSites: Object.freeze(effectSites),
|
|
710
815
|
continuationSites: Object.freeze(continuationSites),
|
|
711
816
|
packageEffects: Object.freeze([...(options.packageEffects || [])]),
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
: [...(options.packageIntrinsics || [])]
|
|
716
|
-
),
|
|
817
|
+
// The emitter consumes this handler's linked ranges, not the project-wide
|
|
818
|
+
// recognition index. Keep original loc metadata when rebasing a range.
|
|
819
|
+
packageIntrinsics: Object.freeze([...linkedPackageIntrinsicsBySource.values()]),
|
|
717
820
|
packageResultAdapters: Object.freeze([...(options.packageResultAdapters || [])]),
|
|
718
821
|
summary: Object.freeze({
|
|
719
822
|
operationCount: operationSummary.operationCount,
|
|
@@ -174,6 +174,15 @@ 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
|
+
}
|
|
183
|
+
if (namespace === 'time' && method === 'now' && current.arguments.length === 0) {
|
|
184
|
+
return Object.freeze({ call: current, surfaceId: 'ctx.time.now', kind: 'time.now', providerKind: 'time', operation: 'now', capability: 'time.wall-clock', args: Object.freeze([]) });
|
|
185
|
+
}
|
|
177
186
|
if (namespace === 'config' && method === 'get' && current.arguments.length === 1) {
|
|
178
187
|
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
188
|
}
|
|
@@ -197,6 +206,7 @@ function extractProviderCall(expression, ctxName, kvAliases = new Map(), options
|
|
|
197
206
|
return Object.freeze({
|
|
198
207
|
call: current,
|
|
199
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,
|
|
200
210
|
kind: `kv.${method}`,
|
|
201
211
|
providerKind: 'kv',
|
|
202
212
|
operation: method,
|
|
@@ -265,6 +275,7 @@ function recognizeHandlerSurface(node, options = {}) {
|
|
|
265
275
|
param: 'ctx.param',
|
|
266
276
|
json: 'ctx.json',
|
|
267
277
|
encodeJson: 'ctx.encodeJson',
|
|
278
|
+
decodeJson: 'ctx.decodeJson',
|
|
268
279
|
text: 'ctx.text',
|
|
269
280
|
response: 'ctx.response'
|
|
270
281
|
});
|
|
@@ -274,8 +285,10 @@ function recognizeHandlerSurface(node, options = {}) {
|
|
|
274
285
|
if (receiver && ts.isPropertyAccessExpression(receiver) && isIdentifierNamed(receiver.expression, ctxName, options)) {
|
|
275
286
|
const namespace = receiver.name.text;
|
|
276
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);
|
|
277
289
|
if (namespace === 'req' && method === 'header') return createRecognition('ctx.req.header', current, { arguments: Object.freeze([...current.arguments]) }, options);
|
|
278
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);
|
|
279
292
|
if (namespace === 'req' && method === 'json') {
|
|
280
293
|
return createRecognition(current.arguments.length === 0 ? 'ctx.req.json.generic' : 'ctx.req.json.schema', current, { arguments: Object.freeze([...current.arguments]) }, options);
|
|
281
294
|
}
|