@pulse-compute/wasm-compiler 1.0.0-beta.5 → 1.0.0-beta.7

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.
@@ -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
- const node = unwrap(input);
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 loop counters cannot be assigned in the body.');
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 loop counters cannot be updated in the body.');
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 an active counter.');
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
- body(node.statement, counters, product * Math.max(1, maxIterations));
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
- return { ...result, errors };
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
- const owner = ownerForPosition(site && site.position);
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
- wat: result.wat,
71
- guestUnits: result.guestUnits,
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);
@@ -815,7 +815,7 @@ function effectPlanForHandler(options) {
815
815
  });
816
816
  }
817
817
 
818
- function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageRecognition) {
818
+ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageRecognition, options = {}) {
819
819
  const descriptor = resolved.descriptor;
820
820
  const sourceFile = resolved.sourceModule.sourceFile;
821
821
  const original = resolved.functionNode;
@@ -868,10 +868,18 @@ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageR
868
868
  frontend: 'canonical-source',
869
869
  role: MANAGED_HANDLER_ROLE,
870
870
  strict: true,
871
- requireEffectAwait: false,
871
+ requireEffectAwait: options.target === 'javascript',
872
+ target: options.target,
872
873
  packageEffectForCall
873
874
  });
874
875
  diagnostics.push(...normalizer.diagnostics);
876
+ // JavaScript executes the original source graph. Native expression admission
877
+ // is independent, and cannot prevent inspection of that explicit target.
878
+ const nativeDiagnostics = options.target === 'javascript' ? [] : diagnostics;
879
+ if (options.target === 'javascript') nativeDiagnostics.push(...normalizeManagedHandler(original, {
880
+ sourceFile, ctxName, frontend: 'canonical-source', role: MANAGED_HANDLER_ROLE,
881
+ strict: true, requireEffectAwait: false, packageEffectForCall
882
+ }).diagnostics);
875
883
  const handler = normalizer.functionNode;
876
884
 
877
885
  const schemaReferences = Object.freeze([
@@ -896,6 +904,7 @@ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageR
896
904
  })
897
905
  };
898
906
  const rawOperationIr = buildPlainHandlerIr(frontend, {
907
+ target: options.target,
899
908
  packageEffects: packageInputs.effects,
900
909
  packageIntrinsics: packageInputs.intrinsics,
901
910
  packageResultAdapters: packageInputs.resultAdapters,
@@ -916,7 +925,7 @@ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageR
916
925
  const compileExpression = createExpressionCompiler({
917
926
  sourceFile,
918
927
  descriptor,
919
- diagnostics,
928
+ diagnostics: nativeDiagnostics,
920
929
  ctxName,
921
930
  inputName,
922
931
  handlerTargets,
@@ -938,7 +947,7 @@ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageR
938
947
  ]);
939
948
 
940
949
  function fail(node, code, message, detail = {}) {
941
- diagnostics.push(diagnostic({ descriptor, sourceFile, node, code, message, detail }));
950
+ nativeDiagnostics.push(diagnostic({ descriptor, sourceFile, node, code, message, detail }));
942
951
  }
943
952
 
944
953
  function registerEffectRuntimeInputs(site, candidate, scope) {
@@ -1377,6 +1386,9 @@ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageR
1377
1386
  packageEffects: packageInputs.effects,
1378
1387
  packageLowering: packageInputs.lowering
1379
1388
  });
1389
+ if (options.target === 'javascript') diagnostics.push(...nativeDiagnostics.filter(entry => [
1390
+ 'PULSE_MANAGED_HANDLER_RECURSION_UNSUPPORTED', 'PULSE_MANAGED_HANDLER_DIRECT_CALL_UNSUPPORTED'
1391
+ ].includes(entry.code)));
1380
1392
  const semantic = {
1381
1393
  version: managedHandlerIrVersion,
1382
1394
  kind: 'managed-handler',
@@ -1408,7 +1420,8 @@ function compileHandler(resolved, graphBuild, allResolved, diagnostics, packageR
1408
1420
  inspected: true,
1409
1421
  packageTargetPromotion: false,
1410
1422
  javascript: Object.freeze({ eligible: true, blockers: Object.freeze([]) }),
1411
- native: Object.freeze({ eligible: true, blockers: Object.freeze([]) })
1423
+ native: Object.freeze({ eligible: options.target !== 'javascript' || nativeDiagnostics.length === 0,
1424
+ blockers: Object.freeze(options.target === 'javascript' ? sortedUnique(nativeDiagnostics.map(entry => entry.code)) : []) })
1412
1425
  })
1413
1426
  };
1414
1427
  const record = deepFreeze({ ...semantic, handlerHash: sha256Hex(stableStringify(semantic)) });
@@ -1465,7 +1478,7 @@ function compileManagedHandlerDescriptors(inputs = {}) {
1465
1478
  const resolved = resolveDescriptors(inputs.graphBuild, descriptors, diagnostics);
1466
1479
  const handlers = [];
1467
1480
  for (const entry of resolved) {
1468
- const handler = compileHandler(entry, inputs.graphBuild, resolved, diagnostics, packageRecognition);
1481
+ const handler = compileHandler(entry, inputs.graphBuild, resolved, diagnostics, packageRecognition, { target: inputs.target });
1469
1482
  if (handler) handlers.push(handler);
1470
1483
  }
1471
1484
  if (diagnostics.length > 0) {
@@ -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 prefix = providerOperation.providerKind === 'kv' ? `kv-${providerOperation.operation}` : providerOperation.providerKind;
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
- function buildStatement(statement, aliases) {
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 loop = inspectBoundedPureLoop(statement, { ctxName });
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 and literal-capped pure for loops; other loops, switch, and try/catch are reserved.'));
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,