@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.
- 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 +44 -65
- package/src/canonical-native-plan.js +450 -101
- package/src/canonical-project-compiler.js +53 -12
- package/src/execution-plan.js +1 -0
- package/src/extractor.js +36 -0
- package/src/native-result-facts.js +50 -0
- package/src/project/router-module-linker.js +64 -0
- package/src/provider-toolchain.js +29 -5
- package/src/pure-helper-values.js +138 -0
- package/src/shared-stage-plan.js +239 -0
- package/src/source-helper-plan.js +482 -0
- package/src/spine/async-surface-normalizer.js +17 -1
- package/src/spine/bounded-pure-loop.js +78 -13
- package/src/spine/canonical-handler-ir.js +3 -1
- package/src/spine/guest-unit-stage.js +11 -2
- package/src/spine/handler-ir-emitter.js +20 -0
- package/src/spine/handler-ir-managed.js +19 -6
- package/src/spine/handler-ir.js +95 -8
- package/src/spine/handler-surface-authority.js +9 -0
- package/src/spine/incoming-body-source.js +48 -0
- package/src/spine/plain-handler-frontend.js +5 -0
- package/src/spine/pure-helper-source.js +123 -0
- package/src/spine/router-handler-frontend.js +38 -1
- package/src/spine/router-handler-ir.js +71 -3
- package/src/spine/router-topology-frontend.js +3 -1
- package/src/spine/terminal-package-native.js +37 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
const values = require('../pure-helper-values');
|
|
5
|
+
|
|
6
|
+
// Admission only; lowering remains in Handler IR and the canonical plan.
|
|
7
|
+
function validatePureHelperSource(helper) {
|
|
8
|
+
const fn = helper.functionNode, source = helper.sourceFile, diagnostics = [];
|
|
9
|
+
const scalar = node => node && values.scalar(ts.tokenToString(node.kind));
|
|
10
|
+
helper.parameterTypes = fn.parameters.map(p => helper.resolveType?.(p.type) || (scalar(p.type) ? p.type.getText(source) : undefined));
|
|
11
|
+
function fail(node, reason, message) {
|
|
12
|
+
const pos = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
13
|
+
diagnostics.push({ code: `PULSE_NATIVE_PURE_HELPER_${reason}_UNSUPPORTED`, severity: 'error', message,
|
|
14
|
+
file: helper.source.file, position: { line: pos.line + 1, column: pos.character + 1 } });
|
|
15
|
+
}
|
|
16
|
+
if (!ts.isFunctionDeclaration(fn) || fn.asteriskToken || fn.typeParameters?.length || !fn.body || !ts.isBlock(fn.body)
|
|
17
|
+
|| fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) || !scalar(fn.type)
|
|
18
|
+
|| fn.parameters.some(p => !ts.isIdentifier(p.name) || p.name.text === 'ctx' || !helper.parameterTypes[fn.parameters.indexOf(p)] || p.initializer || p.dotDotDotToken || p.questionToken)
|
|
19
|
+
|| new Set(fn.parameters.map(p => p.name.getText(source))).size !== fn.parameters.length) {
|
|
20
|
+
fail(fn, 'SIGNATURE', 'Pure helpers require synchronous function declarations with explicit bounded scalar/record/number-array parameters and a scalar result.');
|
|
21
|
+
return diagnostics;
|
|
22
|
+
}
|
|
23
|
+
// Function declarations are mutable bindings in JS; reject writes anywhere in
|
|
24
|
+
// the defining module. Caller shadowing is checked at the resolved call site.
|
|
25
|
+
function assignments(node) {
|
|
26
|
+
const target = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
27
|
+
? node.left : (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) ? node.operand : undefined;
|
|
28
|
+
if (target && ts.isIdentifier(target) && target.text === fn.name.text) fail(node, 'IDENTITY', 'Pure helper identities cannot be reassigned.');
|
|
29
|
+
ts.forEachChild(node, assignments);
|
|
30
|
+
}
|
|
31
|
+
assignments(source);
|
|
32
|
+
const params = new Set(fn.parameters.map(p => p.name.text));
|
|
33
|
+
const names = new Set(params);
|
|
34
|
+
function collect(node) {
|
|
35
|
+
if (node !== fn && ts.isFunctionLike(node)) { fail(node, 'CAPTURE', 'Nested functions and closures are outside pure helpers.'); return; }
|
|
36
|
+
if (ts.isVariableDeclaration(node)) {
|
|
37
|
+
if (!ts.isIdentifier(node.name) || names.has(node.name.text)) fail(node, 'LOCAL', 'Pure helper locals must have unique identifier bindings.');
|
|
38
|
+
else names.add(node.name.text);
|
|
39
|
+
}
|
|
40
|
+
ts.forEachChild(node, collect);
|
|
41
|
+
}
|
|
42
|
+
collect(fn.body);
|
|
43
|
+
function inspect(node) {
|
|
44
|
+
if (ts.isCallExpression(node)) { fail(node, 'CALL', 'Pure helpers cannot call helpers, callbacks, methods, effects or Router operations.'); return; }
|
|
45
|
+
if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isThrowStatement(node) || ts.isTryStatement(node)) {
|
|
46
|
+
fail(node, 'CONTROL', 'Async operations and exception syntax are outside pure helpers.'); return;
|
|
47
|
+
}
|
|
48
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
49
|
+
if (node.questionDotToken) fail(node, 'SHAPE', 'Optional access is outside pure helpers.');
|
|
50
|
+
inspect(node.expression); return;
|
|
51
|
+
}
|
|
52
|
+
if (ts.isElementAccessExpression(node)) {
|
|
53
|
+
if (node.questionDotToken) fail(node, 'SHAPE', 'Optional access is outside pure helpers.');
|
|
54
|
+
inspect(node.expression); inspect(node.argumentExpression); return;
|
|
55
|
+
}
|
|
56
|
+
if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)
|
|
57
|
+
|| ts.isNewExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node)) {
|
|
58
|
+
fail(node, 'VALUE', 'Pure helpers cannot allocate structured values or use type assertions.'); return;
|
|
59
|
+
}
|
|
60
|
+
if (ts.isIdentifier(node) && !names.has(node.text)) fail(node, 'CAPTURE', `Pure helper cannot capture ${node.text}.`);
|
|
61
|
+
const target = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
62
|
+
? node.left : ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) ? node.operand : undefined;
|
|
63
|
+
if (target && (!ts.isIdentifier(target) || params.has(target.text))) fail(node, 'MUTATION', 'Pure helper parameters are read-only.');
|
|
64
|
+
if (ts.isVariableDeclaration(node)) {
|
|
65
|
+
if (!node.initializer || node.type && !scalar(node.type) && !helper.resolveType?.(node.type)) fail(node, 'LOCAL', 'Pure helper locals require supported initializers.');
|
|
66
|
+
if (node.initializer) inspect(node.initializer);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
ts.forEachChild(node, inspect);
|
|
70
|
+
}
|
|
71
|
+
inspect(fn.body);
|
|
72
|
+
return diagnostics;
|
|
73
|
+
}
|
|
74
|
+
// Resolve only structural declarations in the already contained project graph.
|
|
75
|
+
// No TypeScript checker widening, ambient types, package lookup or runtime import.
|
|
76
|
+
function resolvePureType(context, module, node, seen = new Set(), depth = 0) {
|
|
77
|
+
if (!node || depth > values.BORROW_LIMITS.recordDepth) return;
|
|
78
|
+
if (values.scalar(ts.tokenToString(node.kind))) return node.getText(module.sourceFile);
|
|
79
|
+
if (ts.isArrayTypeNode(node) && node.elementType.kind === ts.SyntaxKind.NumberKeyword) return { kind: 'number-array' };
|
|
80
|
+
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments?.length) {
|
|
81
|
+
const name = node.typeName.text, key = module.path + '#' + name;
|
|
82
|
+
if (seen.has(key)) return;
|
|
83
|
+
const next = new Set([...seen, key]);
|
|
84
|
+
const declarations = module.sourceFile.statements.filter(s => (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) && s.name.text === name);
|
|
85
|
+
if (declarations.length === 1) {
|
|
86
|
+
const decl = declarations[0];
|
|
87
|
+
if (decl.typeParameters?.length || decl.heritageClauses?.length) return;
|
|
88
|
+
return resolvePureType(context, module, ts.isTypeAliasDeclaration(decl) ? decl.type : decl, next, depth);
|
|
89
|
+
}
|
|
90
|
+
for (const relation of module.imports) {
|
|
91
|
+
const binding = relation.bindings.find(b => b.localName === name);
|
|
92
|
+
if (!binding || binding.importedName === '*') continue;
|
|
93
|
+
const resolution = module.resolutions.find(r => r.relation === relation);
|
|
94
|
+
const target = resolution && context.projectModules.get(resolution.targetKey);
|
|
95
|
+
if (!target) return;
|
|
96
|
+
const exported = target.sourceFile.statements.find(s => (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) && s.name.text === binding.importedName && s.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword));
|
|
97
|
+
const targetName = target.localExports.get(binding.importedName) || exported?.name.text;
|
|
98
|
+
if (!targetName) return;
|
|
99
|
+
const decl = target.sourceFile.statements.find(s => (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) && s.name.text === targetName);
|
|
100
|
+
if (!decl || decl.typeParameters?.length || decl.heritageClauses?.length) return;
|
|
101
|
+
return resolvePureType(context, target, ts.isTypeAliasDeclaration(decl) ? decl.type : decl, next, depth);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeLiteralNode(node)) && depth < values.BORROW_LIMITS.recordDepth) {
|
|
106
|
+
const fields = [];
|
|
107
|
+
for (const member of node.members) {
|
|
108
|
+
if (!ts.isPropertySignature(member) || !ts.isIdentifier(member.name) || member.questionToken || member.initializer) return;
|
|
109
|
+
const name = member.name.text;
|
|
110
|
+
if (values.FORBIDDEN_FIELDS.includes(name) || fields.some(f => f.name === name)) return;
|
|
111
|
+
const type = resolvePureType(context, module, member.type, seen, depth + 1);
|
|
112
|
+
if (!type) return;
|
|
113
|
+
fields.push({ name, type });
|
|
114
|
+
}
|
|
115
|
+
if (fields.length && fields.length <= values.BORROW_LIMITS.recordFields) return { kind: 'record', fields };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function typeNode(type) {
|
|
119
|
+
if (typeof type === 'string') return ts.factory.createKeywordTypeNode({string:ts.SyntaxKind.StringKeyword,number:ts.SyntaxKind.NumberKeyword,boolean:ts.SyntaxKind.BooleanKeyword}[type]);
|
|
120
|
+
if (type.kind === 'number-array') return ts.factory.createArrayTypeNode(typeNode('number'));
|
|
121
|
+
return ts.factory.createTypeLiteralNode(type.fields.map(f => ts.factory.createPropertySignature(undefined, f.name, undefined, typeNode(f.type))));
|
|
122
|
+
}
|
|
123
|
+
module.exports = { validatePureHelperSource, resolvePureType, typeNode };
|
|
@@ -130,6 +130,7 @@ function analysisForSurfaceFacts(surfaceFacts) {
|
|
|
130
130
|
const capabilities = new Set();
|
|
131
131
|
let fetchCount = 0;
|
|
132
132
|
for (const fact of facts) {
|
|
133
|
+
if (fact.surfaceId === 'ctx.req.body') { capabilities.add('request.body.forward'); continue; }
|
|
133
134
|
if (fact.surfaceId.startsWith('ctx.fetch.')) { capabilities.add('fetch'); fetchCount += 1; continue; }
|
|
134
135
|
if (fact.surfaceId.startsWith('ctx.req.')) capabilities.add(fact.surfaceId.slice('ctx.'.length));
|
|
135
136
|
else if (fact.surfaceId.startsWith('ctx.config.')) capabilities.add('config.get');
|
|
@@ -197,6 +198,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
|
|
|
197
198
|
handlerAuthoring: topology.options && topology.options.handlerAuthoring,
|
|
198
199
|
requireAsync: topology.options && topology.options.requireAsync === true,
|
|
199
200
|
requireEffectAwait: topology.options && topology.options.requireEffectAwait === true,
|
|
201
|
+
helperForCall: topology.options?.linkedProjectModules?.helperForCall,
|
|
200
202
|
packageEffectForCall: topology.options && topology.options.packageEffectForCall
|
|
201
203
|
|| ((call) => packageEffectsByStart.get(call.getStart(sourceFile)))
|
|
202
204
|
});
|
|
@@ -230,7 +232,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
|
|
|
230
232
|
}
|
|
231
233
|
const contextSurface = recognizeHandlerSurface(node, { ctxName, unwrap: true });
|
|
232
234
|
if (contextSurface) {
|
|
233
|
-
const httpOnly = contextSurface.surfaceId === 'ctx.param'
|
|
235
|
+
const httpOnly = contextSurface.surfaceId.startsWith('ctx.output.') || contextSurface.surfaceId === 'ctx.param'
|
|
234
236
|
|| contextSurface.surfaceId.startsWith('ctx.req.')
|
|
235
237
|
|| ['ctx.json', 'ctx.text', 'ctx.response'].includes(contextSurface.surfaceId);
|
|
236
238
|
const eventOnly = contextSurface.surfaceId.startsWith('ctx.event.');
|
|
@@ -258,6 +260,41 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
|
|
|
258
260
|
|
|
259
261
|
const rewriteTransformer = (context) => {
|
|
260
262
|
const visit = (node) => {
|
|
263
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
|
|
264
|
+
&& ts.isIdentifier(node.expression.expression) && node.expression.expression.text === ctxName
|
|
265
|
+
&& node.expression.name.text === 'kv' && node.typeArguments?.length === 1) {
|
|
266
|
+
const type = topology.options?.linkedProjectModules?.pureTypeForNode?.(node.typeArguments[0]);
|
|
267
|
+
if (type) return ts.factory.updateCallExpression(node, node.expression,
|
|
268
|
+
[require('./pure-helper-source').typeNode(type)], node.arguments.map(arg => ts.visitNode(arg, visit)));
|
|
269
|
+
}
|
|
270
|
+
const helper = ts.isCallExpression(node) && topology.options?.linkedProjectModules?.helperForCall?.(node);
|
|
271
|
+
if (helper) {
|
|
272
|
+
const original = ts.getOriginalNode(node);
|
|
273
|
+
const awaited = original.parent && ts.isAwaitExpression(original.parent);
|
|
274
|
+
const binding = awaited && original.parent.parent;
|
|
275
|
+
let shadowed = false;
|
|
276
|
+
const inspect = item => {
|
|
277
|
+
if ((ts.isVariableDeclaration(item) || ts.isParameter(item) || ts.isFunctionDeclaration(item)) && ts.isIdentifier(item.name) && item.name.text === original.expression.text) shadowed = true;
|
|
278
|
+
ts.forEachChild(item, inspect);
|
|
279
|
+
};
|
|
280
|
+
inspect(authoredFunctionNode);
|
|
281
|
+
if (helper.pure) {
|
|
282
|
+
if (awaited || shadowed || node.typeArguments?.length || node.questionDotToken
|
|
283
|
+
|| node.arguments.length !== helper.functionNode.parameters.length || !['route', 'middleware'].includes(role)) {
|
|
284
|
+
diagnostics.push(diagnostic(sourceFile, original, 'PULSE_NATIVE_PURE_HELPER_CALL_UNSUPPORTED',
|
|
285
|
+
'Pure helpers require a static unshadowed synchronous pure call in an HTTP handler.'));
|
|
286
|
+
}
|
|
287
|
+
return ts.factory.updateCallExpression(node, ts.factory.createIdentifier(helper.name), undefined,
|
|
288
|
+
node.arguments.map(arg => ts.visitNode(arg, visit)));
|
|
289
|
+
}
|
|
290
|
+
if (!binding || !ts.isVariableDeclaration(binding) || !ts.isIdentifier(binding.name)
|
|
291
|
+
|| shadowed || node.arguments.length !== helper.functionNode.parameters.length
|
|
292
|
+
|| !ts.isIdentifier(node.arguments[0]) || node.arguments[0].text !== ctxName
|
|
293
|
+
|| !['route', 'middleware'].includes(role)) {
|
|
294
|
+
diagnostics.push(diagnostic(sourceFile, original, 'PULSE_NATIVE_HELPER_CALL_UNSUPPORTED', 'A source helper requires an unshadowed, directly awaited local binding and the current request context in an HTTP handler.'));
|
|
295
|
+
}
|
|
296
|
+
return ts.factory.updateCallExpression(node, ts.factory.createIdentifier(helper.name), undefined, node.arguments.slice(1));
|
|
297
|
+
}
|
|
261
298
|
if (errorName && ts.isShorthandPropertyAssignment(node) && node.name.text === errorName) {
|
|
262
299
|
return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(errorName), ts.factory.createIdentifier(ERROR));
|
|
263
300
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const ts = require('typescript');
|
|
3
|
+
const { normalizeManagedHandler } = require('./async-surface-normalizer');
|
|
4
|
+
const { CanonicalRouterCompileError } = require('./router-topology-frontend');
|
|
2
5
|
|
|
3
6
|
const {
|
|
4
7
|
buildRouterHandlerIr
|
|
@@ -142,15 +145,73 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
|
|
|
142
145
|
const topology = prepared.topology;
|
|
143
146
|
const operationByEntry = new Map();
|
|
144
147
|
const emittedByEntry = new Map();
|
|
148
|
+
const bodyByEntry = new Map();
|
|
149
|
+
const authoredHandlers = new Map(topology.handlerTable.handlers.map(handler => [handler.id, handler]));
|
|
145
150
|
for (const handler of prepared.handlers) {
|
|
146
151
|
const operationIr = buildRouterHandlerIr(handler);
|
|
147
152
|
operationByEntry.set(handler.entry.stableId, operationIr);
|
|
148
153
|
emittedByEntry.set(handler.entry.stableId, emitCanonicalRouterHandlerBody(operationIr));
|
|
154
|
+
// The first private body family is a terminal HTTP route. Transfer-capable
|
|
155
|
+
// handlers retain the existing cursor/mode lowering until their own pass.
|
|
156
|
+
if (handler.entry.kind === 'route' && !operationIr.summary.operationKinds['router-transfer']) {
|
|
157
|
+
const authored = authoredHandlers.get(handler.entry.handlerId);
|
|
158
|
+
bodyByEntry.set(handler.entry.stableId, Object.freeze({
|
|
159
|
+
version: 'pulse.router-native-body.v1',
|
|
160
|
+
family: 'terminal-route',
|
|
161
|
+
name: `__pulse_body_${handler.entry.stableId}`,
|
|
162
|
+
source: authored && authored.loc
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
const header = topology.retainedDeclarations;
|
|
152
168
|
let synthetic = header ? `${header}\n\n` : '';
|
|
153
169
|
synthetic += 'export default function __pulse_router_entry(ctx) {\n';
|
|
170
|
+
const helpers = [];
|
|
171
|
+
const helperRecords = [];
|
|
172
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
173
|
+
for (const helper of topology.options?.linkedProjectModules?.sourceHelpers || []) {
|
|
174
|
+
const fn = helper.functionNode;
|
|
175
|
+
if (helper.pure) {
|
|
176
|
+
const diagnostics = require('./pure-helper-source').validatePureHelperSource(helper);
|
|
177
|
+
if (diagnostics.length) throw new CanonicalRouterCompileError('Unsupported pure source helper.', diagnostics);
|
|
178
|
+
const start = synthetic.length;
|
|
179
|
+
synthetic += `function ${helper.name}(${fn.parameters.map(p => printer.printNode(ts.EmitHint.Unspecified, p, helper.sourceFile)).join(',')}) ${printer.printNode(ts.EmitHint.Unspecified, fn.body, helper.sourceFile)}\n`;
|
|
180
|
+
const generatedRange = { start, end: synthetic.length };
|
|
181
|
+
helpers.push({ id: helper.id, name: helper.name, pure: true, source: helper.source,
|
|
182
|
+
resultKind: fn.type.getText(helper.sourceFile),
|
|
183
|
+
parameters: fn.parameters.map((p, i) => { const type = helper.parameterTypes[i]; return { name: p.name.text, valueKind: typeof type === 'string' ? type : type.kind === 'record' ? 'object' : 'array', ...(typeof type === 'object' ? { borrow: { version: require('@pulse-compute/wasm-contracts/handler/canonical-native-plan').CANONICAL_NATIVE_PURE_BORROW_VERSION, type } } : {}) }; }), generatedRange });
|
|
184
|
+
helperRecords.push({ entryStableId: helper.id, operationIr: { sourceFile: helper.sourceFile, handler: fn },
|
|
185
|
+
canonicalIr: { router: { entry: { generatedRange } } } });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const declaration = fn.parent;
|
|
189
|
+
const immutable = !ts.isVariableDeclaration(declaration) || Boolean(declaration.parent.flags & ts.NodeFlags.Const);
|
|
190
|
+
let reassigned = false;
|
|
191
|
+
const scanAssignments = node => {
|
|
192
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
193
|
+
&& ts.isIdentifier(node.left) && node.left.text === helper.source.name) reassigned = true;
|
|
194
|
+
ts.forEachChild(node, scanAssignments);
|
|
195
|
+
};
|
|
196
|
+
scanAssignments(helper.sourceFile);
|
|
197
|
+
const valid = immutable && !reassigned && ts.isBlock(fn.body) && fn.parameters[0]?.name?.text === 'ctx'
|
|
198
|
+
&& fn.parameters.every((p, i) => ts.isIdentifier(p.name) && !p.initializer && !p.dotDotDotToken && !p.questionToken
|
|
199
|
+
&& (i === 0 || [ts.SyntaxKind.StringKeyword, ts.SyntaxKind.NumberKeyword, ts.SyntaxKind.BooleanKeyword].includes(p.type?.kind)))
|
|
200
|
+
&& new Set(fn.parameters.map(p => p.name.text)).size === fn.parameters.length
|
|
201
|
+
&& fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
|
|
202
|
+
if (!valid) throw new CanonicalRouterCompileError('Unsupported source helper signature.', [{ code: 'PULSE_NATIVE_HELPER_SIGNATURE_UNSUPPORTED', severity: 'error', message: 'Helpers require async block bodies, ctx first and explicit scalar inputs.', file: helper.source.file }]);
|
|
203
|
+
const normalized = normalizeManagedHandler(fn, { sourceFile: helper.sourceFile, ctxName: 'ctx', frontend: 'canonical-router',
|
|
204
|
+
target: 'native', requireAsync: true, packageEffectForCall: topology.options.packageEffectForCall });
|
|
205
|
+
if (normalized.diagnostics.length) throw new CanonicalRouterCompileError('Unsupported source helper body.', normalized.diagnostics);
|
|
206
|
+
const start = synthetic.length;
|
|
207
|
+
const parameters = fn.parameters.slice(1).map(p => printer.printNode(ts.EmitHint.Unspecified, p, helper.sourceFile)).join(',');
|
|
208
|
+
synthetic += `function ${helper.name}(${parameters}) ${printer.printNode(ts.EmitHint.Unspecified, normalized.functionNode.body, helper.sourceFile)}\n`;
|
|
209
|
+
const generatedRange = { start, end: synthetic.length };
|
|
210
|
+
helpers.push({ id: helper.id, name: helper.name, source: helper.source,
|
|
211
|
+
parameters: fn.parameters.slice(1).map(p => ({ name: p.name.text, valueKind: p.type.getText(helper.sourceFile) })), generatedRange });
|
|
212
|
+
helperRecords.push({ entryStableId: helper.id, operationIr: { sourceFile: helper.sourceFile, handler: fn },
|
|
213
|
+
canonicalIr: { router: { entry: { generatedRange } } } });
|
|
214
|
+
}
|
|
154
215
|
|
|
155
216
|
const eventReachable = Boolean(topology.eventTopology && topology.eventCatalog && topology.eventCatalog.events.length > 0);
|
|
156
217
|
const eventTopology = realizedEventTopology(topology, prepared);
|
|
@@ -198,7 +259,9 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
|
|
|
198
259
|
|
|
199
260
|
if (entry.kind === 'mount') {
|
|
200
261
|
const pattern = rawEntry.pattern && rawEntry.pattern.normalized || `${rawEntry.path || '/'}/*`;
|
|
201
|
-
|
|
262
|
+
const eligibility = rawEntry.eligibility;
|
|
263
|
+
const condition = eligibility ? ` && ctx.state.get(${JSON.stringify(eligibility.state)}) === ${JSON.stringify(eligibility.equals)}` : '';
|
|
264
|
+
synthetic += ` if (${MODE} === 0 && __pulse_router_match(ctx.req.path, ${JSON.stringify(pattern)})${condition}) {\n`;
|
|
202
265
|
synthetic += ` ${CURSOR} = ${rawEntry.childStartIndex};\n`;
|
|
203
266
|
synthetic += ' } else {\n';
|
|
204
267
|
synthetic += ` ${CURSOR} = ${rawEntry.parentContinueIndex};\n`;
|
|
@@ -213,10 +276,13 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
|
|
|
213
276
|
}
|
|
214
277
|
if (entry.kind === 'route') matchCondition += ` && ctx.req.method === ${JSON.stringify(entry.method)} && __pulse_router_match(ctx.req.path, ${JSON.stringify(entry.path)})`;
|
|
215
278
|
synthetic += ` if (${matchCondition}) {\n`;
|
|
279
|
+
const nativeBody = bodyByEntry.get(entry.stableId);
|
|
280
|
+
if (nativeBody) synthetic += ` function ${nativeBody.name}() {\n`;
|
|
216
281
|
const start = synthetic.length;
|
|
217
|
-
synthetic += `${indent(emitted.sourceText, 6)}\n`;
|
|
282
|
+
synthetic += `${indent(emitted.sourceText, nativeBody ? 8 : 6)}\n`;
|
|
218
283
|
const end = synthetic.length;
|
|
219
284
|
generatedRange = Object.freeze({ start, end });
|
|
285
|
+
if (nativeBody) synthetic += ` }\n return ${nativeBody.name}();\n`;
|
|
220
286
|
synthetic += ' } else {\n';
|
|
221
287
|
synthetic += ` ${CURSOR} = ${entry.nextIndex};\n`;
|
|
222
288
|
synthetic += ' }\n';
|
|
@@ -227,6 +293,7 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
|
|
|
227
293
|
|
|
228
294
|
const record = Object.freeze({
|
|
229
295
|
...entry,
|
|
296
|
+
...(bodyByEntry.has(entry.stableId) ? { nativeBody: bodyByEntry.get(entry.stableId) } : {}),
|
|
230
297
|
...(eventReachable ? { plane: 'http' } : {}),
|
|
231
298
|
generatedRange,
|
|
232
299
|
generatedBlockRange: Object.freeze({ start: openingStart, end: synthetic.length })
|
|
@@ -354,6 +421,7 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
|
|
|
354
421
|
}),
|
|
355
422
|
routes: Object.freeze(routeRecords),
|
|
356
423
|
entries: Object.freeze(entryRecords),
|
|
424
|
+
...(helpers.length ? { helpers: Object.freeze(helpers) } : {}),
|
|
357
425
|
...(eventReachable ? { applicationEntries: Object.freeze(entryRecords) } : {})
|
|
358
426
|
}),
|
|
359
427
|
diagnostics: Object.freeze([])
|
|
@@ -362,7 +430,7 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
|
|
|
362
430
|
return Object.freeze({
|
|
363
431
|
version: ROUTER_HANDLER_IR_BUNDLE_VERSION,
|
|
364
432
|
output,
|
|
365
|
-
handlers: Object.freeze(handlerRecords)
|
|
433
|
+
handlers: Object.freeze([...helperRecords, ...handlerRecords])
|
|
366
434
|
});
|
|
367
435
|
}
|
|
368
436
|
|
|
@@ -188,7 +188,8 @@ function stableEntryId(entry) {
|
|
|
188
188
|
method: entry.method,
|
|
189
189
|
path: entry.path,
|
|
190
190
|
handlerId: entry.handler && (entry.handler.id || entry.handler.handlerId),
|
|
191
|
-
childRouter: entry.childRouter
|
|
191
|
+
childRouter: entry.childRouter,
|
|
192
|
+
...(entry.eligibility ? { eligibility: entry.eligibility } : {})
|
|
192
193
|
});
|
|
193
194
|
return `router_entry_${crypto.createHash('sha256').update(input).digest('hex').slice(0, 24)}`;
|
|
194
195
|
}
|
|
@@ -233,6 +234,7 @@ function cleanExecutionEntry(entry) {
|
|
|
233
234
|
routeRuntimeId: Number.isInteger(entry.runtimeId) ? entry.runtimeId : undefined,
|
|
234
235
|
handlerId: entry.handler && (entry.handler.id || entry.handler.handlerId) || undefined,
|
|
235
236
|
childRouter: entry.childRouter || undefined,
|
|
237
|
+
...(entry.eligibility ? { eligibility: Object.freeze({ ...entry.eligibility }) } : {}),
|
|
236
238
|
childStartIndex: Number.isInteger(entry.childStartIndex) ? entry.childStartIndex : undefined,
|
|
237
239
|
parentContinueIndex: Number.isInteger(entry.parentContinueIndex) ? entry.parentContinueIndex : undefined
|
|
238
240
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { findLowerableManifestRecord } = require('@pulse-compute/wasm-library-kit/compiler/package-lowering');
|
|
4
|
+
const { loadLowerableCompilerBuilder } = require('@pulse-compute/wasm-library-kit/compiler/handler-library-contracts');
|
|
5
|
+
const { compileManagedHandlerNativeBundle } = require('./handler-ir-managed.js');
|
|
6
|
+
const applications = new WeakMap();
|
|
7
|
+
|
|
8
|
+
function attachTerminalPackageNative(compiled, inputs) {
|
|
9
|
+
applications.set(compiled, inputs);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function terminalPackageNativeForCompiled(compiled) {
|
|
13
|
+
const inputs = applications.get(compiled);
|
|
14
|
+
if (!inputs) return null;
|
|
15
|
+
const { application, recognition, managedHandlers, schemaBundle, cwd, workspaceRoot } = inputs;
|
|
16
|
+
const selected = recognition.packages.find(entry => entry.contractId === application.contractId);
|
|
17
|
+
if (!selected || selected.builder.trust !== 'first-party') throw new TypeError('Native application requires its selected trusted lowerer.');
|
|
18
|
+
const { record } = findLowerableManifestRecord({ cwd, workspaceRoot, contractId: selected.contractId, npmPackage: selected.package });
|
|
19
|
+
const exportName = record?.manifest?.compiler?.nativeApplicationExport;
|
|
20
|
+
if (!exportName) return null;
|
|
21
|
+
const loaded = loadLowerableCompilerBuilder(record);
|
|
22
|
+
const builder = Object.hasOwn(loaded.module, exportName) ? loaded.module[exportName] : null;
|
|
23
|
+
if (typeof builder !== 'function') throw new TypeError('Trusted Native application builder is missing.');
|
|
24
|
+
const intrinsic = recognition.intrinsics.find(entry => entry.intrinsic === application.intrinsic);
|
|
25
|
+
const result = builder({
|
|
26
|
+
plan: intrinsic.staticArguments[0],
|
|
27
|
+
managedHandlerNativeBundle: compileManagedHandlerNativeBundle(managedHandlers),
|
|
28
|
+
schemaBundle
|
|
29
|
+
});
|
|
30
|
+
if (result.version !== 'pulse.package-native-application.v1' || result.contractId !== selected.contractId
|
|
31
|
+
|| result.package !== selected.package || result.intrinsic !== application.intrinsic || result.automaticFallback !== false) {
|
|
32
|
+
throw new TypeError('Trusted Native application result does not match its selected package.');
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { attachTerminalPackageNative, terminalPackageNativeForCompiled };
|