@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.
@@ -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
- 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);
@@ -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 packageEffectsByStart = new Map();
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
- if (!Number.isSafeInteger(start) || packageEffectsByStart.has(start)) {
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
- packageEffectsByStart.set(start, effectInput);
231
+ packageEffectsBySource.set(key, effectInput);
197
232
  }
198
- const packageIntrinsicsByStart = new Map();
199
- const linkedPackageIntrinsicsByStart = new Map();
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
- if (!Number.isSafeInteger(start) || packageIntrinsicsByStart.has(start)) {
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
- packageIntrinsicsByStart.set(start, intrinsicInput);
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 = packageEffectsByStart.get(call.getStart(sourceFile));
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 start = call.getStart(sourceFile);
236
- if (!linkedPackageIntrinsicsByStart.has(start)) {
237
- linkedPackageIntrinsicsByStart.set(start, Object.freeze({
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 start = call.getStart(sourceFile);
246
- const intrinsic = packageIntrinsicsByStart.get(start);
247
- if (intrinsic && !linkedPackageIntrinsicsByStart.has(start)) linkedPackageIntrinsicsByStart.set(start, 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 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;
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
- 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
+ }
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.isTryStatement(statement) || ts.isForStatement(statement) || ts.isForOfStatement(statement) || ts.isWhileStatement(statement) || ts.isDoStatement(statement) || ts.isSwitchStatement(statement)) {
627
- diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_CONTROL_FLOW_UNSUPPORTED', 'The current canonical lowering supports straight-line code and if/else branching; loops, switch, and try/catch are reserved.'));
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
- packageIntrinsics: Object.freeze(
713
- typeof options.packageIntrinsicForCall === 'function'
714
- ? [...linkedPackageIntrinsicsByStart.values()]
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
  }