@pulse-compute/wasm-compiler 1.0.0-beta.5 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/bin/provider-proof-composition.js +29 -31
- package/bin/pulsewasm-extract.js +1 -1
- package/package.json +9 -11
- package/src/canonical-api-compiler.js +1 -9
- package/src/canonical-native-compiler.js +37 -64
- package/src/canonical-native-plan.js +450 -101
- package/src/canonical-project-compiler.js +52 -12
- package/src/execution-plan.js +1 -0
- package/src/extractor.js +36 -0
- package/src/native-result-facts.js +50 -0
- package/src/project/router-module-linker.js +64 -0
- package/src/provider-toolchain.js +29 -5
- package/src/pure-helper-values.js +138 -0
- package/src/shared-stage-plan.js +239 -0
- package/src/source-helper-plan.js +482 -0
- package/src/spine/async-surface-normalizer.js +17 -1
- package/src/spine/bounded-pure-loop.js +78 -13
- package/src/spine/canonical-handler-ir.js +3 -1
- package/src/spine/guest-unit-stage.js +11 -2
- package/src/spine/handler-ir-emitter.js +20 -0
- package/src/spine/handler-ir.js +95 -8
- package/src/spine/handler-surface-authority.js +9 -0
- package/src/spine/incoming-body-source.js +48 -0
- package/src/spine/plain-handler-frontend.js +5 -0
- package/src/spine/pure-helper-source.js +123 -0
- package/src/spine/router-handler-frontend.js +38 -1
- package/src/spine/router-handler-ir.js +71 -3
- package/src/spine/router-topology-frontend.js +3 -1
- package/src/spine/terminal-package-native.js +37 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Native plan owner for shared, transfer-capable HTTP route and middleware stages. Admission
|
|
4
|
+
// is conservative: an unsupported shape keeps the ordinary Native lowering.
|
|
5
|
+
function loadStageContract() {
|
|
6
|
+
try { return require('@pulse-compute/wasm-contracts/handler/canonical-native-plan'); }
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../contracts/src/handler/canonical-native-plan');
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const contract = loadStageContract().CANONICAL_NATIVE_STAGE_CONTRACT;
|
|
13
|
+
const VERSION = contract.version;
|
|
14
|
+
const OUTPUTS = contract.outputs;
|
|
15
|
+
const MAX_SITES = contract.maxEffectSites;
|
|
16
|
+
const excluded = Symbol('not a shared stage');
|
|
17
|
+
const demand = (condition, reason = 'unsupported-shape') => {
|
|
18
|
+
if (!condition) throw { [excluded]: true, reason };
|
|
19
|
+
};
|
|
20
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
21
|
+
function mapTree(node, replace) {
|
|
22
|
+
const replaced = replace(node);
|
|
23
|
+
if (replaced !== node) return replaced;
|
|
24
|
+
if (Array.isArray(node)) return node.map(value => mapTree(value, replace));
|
|
25
|
+
if (!node || typeof node !== 'object') return node;
|
|
26
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, mapTree(value, replace)]));
|
|
27
|
+
}
|
|
28
|
+
function statements(body, visit, loopDepth = 0) {
|
|
29
|
+
for (const statement of body || []) {
|
|
30
|
+
visit(statement, loopDepth);
|
|
31
|
+
if (statement.kind === 'if') { statements(statement.then, visit, loopDepth); statements(statement.else, visit, loopDepth); }
|
|
32
|
+
else if (statement.kind === 'pure-loop') statements(statement.body, visit, loopDepth + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function effectShape(effect) {
|
|
36
|
+
return Object.fromEntries(['kind', 'providerKind', 'operation', 'capability', 'resource', 'package', 'contractId',
|
|
37
|
+
'declaredResult', 'borrowedValue', 'grouped', 'groupIndex', 'decoder', 'inputs', 'result'].map(key => [key, effect[key]]));
|
|
38
|
+
}
|
|
39
|
+
function eligibleEffect(effect) {
|
|
40
|
+
return effect && !effect.grouped && effect.groupIndex === undefined && !effect.helperId && !effect.borrowedValue
|
|
41
|
+
&& effect.result?.mode === 'bind' && (effect.kind === 'fetch' && effect.decoder === 'text'
|
|
42
|
+
|| ['time.now', 'crypto.digestText'].includes(effect.kind) && effect.decoder === null && !effect.result.decoder);
|
|
43
|
+
}
|
|
44
|
+
function rowFor(plan, entry, router) {
|
|
45
|
+
demand(['route', 'use'].includes(entry.kind) && !entry.nativeBody, 'entry-kind');
|
|
46
|
+
const outer = plan.entry.body.find(s => s.kind === 'if' && s.test?.kind === 'binary'
|
|
47
|
+
&& s.test.operator === '===' && s.test.left?.id === router.cursor && s.test.right?.value === entry.index);
|
|
48
|
+
demand(outer?.then?.length === 1 && outer.then[0].kind === 'if');
|
|
49
|
+
const branch = outer.then[0], body = branch.then, localIds = [], effectIds = [];
|
|
50
|
+
statements(body, (s, loopDepth) => {
|
|
51
|
+
demand(s.kind !== 'helper-call', 'nested-helper-call');
|
|
52
|
+
demand((loopDepth ? ['local', 'expression', 'if', 'pure-loop', 'break', 'continue']
|
|
53
|
+
: ['local', 'expression', 'return', 'if', 'effect', 'pure-loop']).includes(s.kind), 'unsupported-statement:' + s.kind);
|
|
54
|
+
if (s.kind === 'local' || s.kind === 'pure-loop') localIds.push(s.localId);
|
|
55
|
+
if (s.kind === 'effect') {
|
|
56
|
+
demand(s.result?.mode === 'bind', 'effect-result-mode'); localIds.push(s.result.localId); effectIds.push(s.effectId);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
demand(new Set(localIds).size === localIds.length && effectIds.length > 0 && effectIds.length <= MAX_SITES, 'locals-or-effect-count');
|
|
60
|
+
const effects = effectIds.map(id => plan.effects.find(e => e.id === id));
|
|
61
|
+
demand(effects.every(e => e?.routerEntryStableId === entry.stableId && eligibleEffect(e)), 'effect-kind-or-ownership');
|
|
62
|
+
const locals = new Map(localIds.map((id, i) => [id, `local-${i}`]));
|
|
63
|
+
const ids = new Map(effects.flatMap((e, i) => [[e.id, `effect-${i}`], [e.continuationId, `continuation-${i}`]]));
|
|
64
|
+
let transfers = 0;
|
|
65
|
+
function normalize(node) {
|
|
66
|
+
if (Array.isArray(node)) return node.map(normalize);
|
|
67
|
+
if (!node || typeof node !== 'object') return node;
|
|
68
|
+
// Helper ownership is still tied to the original Router entry, not a stage.
|
|
69
|
+
demand(node.kind !== 'pure-helper-call' && node.kind !== 'helper-call', 'nested-helper-call');
|
|
70
|
+
if (node.kind === 'local' && node.id) demand(locals.has(node.id) || Object.values(router).includes(node.id));
|
|
71
|
+
if (node.kind === 'assignment' && node.target?.id === router.cursor && node.value !== null) {
|
|
72
|
+
demand(node.operator === '=' && node.value?.kind === 'literal' && node.value.value === entry.nextIndex);
|
|
73
|
+
transfers++;
|
|
74
|
+
return { ...normalize({ ...node, value: null }), value: { kind: 'stage-next-cursor' } };
|
|
75
|
+
}
|
|
76
|
+
return Object.fromEntries(Object.entries(node).filter(([key]) => !['statementPath', 'localName'].includes(key)
|
|
77
|
+
&& !(key === 'name' && node.kind === 'local')).map(([key, value]) => [key,
|
|
78
|
+
['id', 'localId'].includes(key) && locals.has(value) ? locals.get(value)
|
|
79
|
+
: ['effectId', 'continuationId'].includes(key) && ids.has(value) ? ids.get(value) : normalize(value)]));
|
|
80
|
+
}
|
|
81
|
+
const signature = JSON.stringify({ body: normalize(body), effects: effects.map(e => normalize(effectShape(e))) });
|
|
82
|
+
demand(transfers > 0);
|
|
83
|
+
return { entry, branch, body, localIds, effects, signature };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function lowerSharedStages(plan, { onExcluded } = {}) {
|
|
87
|
+
const router = Object.fromEntries(['cursor', 'mode', 'error'].map(name => [name,
|
|
88
|
+
plan.locals.find(local => local.name === `__pulse_router_${name}`)?.id]));
|
|
89
|
+
if (!Object.values(router).every(Boolean)) return plan;
|
|
90
|
+
const families = new Map();
|
|
91
|
+
for (const entry of plan.routing?.entries || []) {
|
|
92
|
+
if (!families.has(entry.handlerId)) families.set(entry.handlerId, []);
|
|
93
|
+
families.get(entry.handlerId).push(entry);
|
|
94
|
+
}
|
|
95
|
+
const continuationOwners = new Map();
|
|
96
|
+
const stages = [], replacement = new Map(), localOwners = new Map(), removedLocals = new Set(), effectReplacements = new Map();
|
|
97
|
+
for (const entries of families.values()) {
|
|
98
|
+
let rows;
|
|
99
|
+
try {
|
|
100
|
+
rows = entries.map(entry => rowFor(plan, entry, router));
|
|
101
|
+
demand(rows.every(row => row.signature === rows[0].signature), 'registration-shape-mismatch');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!error?.[excluded]) throw error;
|
|
104
|
+
// Optional internal inspection; never changes the valid fallback plan/hash.
|
|
105
|
+
if (onExcluded && entries[0].handlerId) onExcluded({
|
|
106
|
+
handlerId: entries[0].handlerId, entryIds: entries.map(entry => entry.stableId), reason: error.reason
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const first = rows[0], id = `stage:${first.entry.stableId}`, nextCursorLocalId = `${id}:next`;
|
|
111
|
+
const body = mapTree(first.body, node => node?.kind === 'assignment' && node.target?.id === router.cursor
|
|
112
|
+
? { ...node, value: { kind: 'local', id: nextCursorLocalId, valueKind: 'number' } } : node);
|
|
113
|
+
const registrations = rows.map(row => {
|
|
114
|
+
replacement.set(row.branch, { ...row.branch, then: [{ kind: 'stage-call', stageId: id, registrationId: row.entry.stableId }] });
|
|
115
|
+
row.localIds.forEach(local => removedLocals.add(local));
|
|
116
|
+
row.effects.forEach((effect, site) => {
|
|
117
|
+
effectReplacements.set(effect.id, { ...effect, stageId: id, stageSite: site, inputs: first.effects[site].inputs, result: first.effects[site].result });
|
|
118
|
+
continuationOwners.set(effect.continuationId, { stageId: id, stageSite: site });
|
|
119
|
+
});
|
|
120
|
+
return { entryId: row.entry.stableId, nextIndex: row.entry.nextIndex,
|
|
121
|
+
effectIds: row.effects.map(e => e.id), continuationIds: row.effects.map(e => e.continuationId) };
|
|
122
|
+
});
|
|
123
|
+
first.localIds.forEach(local => { removedLocals.delete(local); localOwners.set(local, id); });
|
|
124
|
+
stages.push({ version: VERSION, id, handlerId: first.entry.handlerId,
|
|
125
|
+
inputs: { context: 'request', nextCursorLocalId, routerLocals: router }, outputs: OUTPUTS,
|
|
126
|
+
frame: contract.frame,
|
|
127
|
+
localIds: first.localIds, effectIds: first.effects.map(e => e.id), body, registrations });
|
|
128
|
+
}
|
|
129
|
+
if (!stages.length) return plan;
|
|
130
|
+
const rewriteBranches = body => body.map(s => replacement.get(s) || (s.kind === 'if'
|
|
131
|
+
? { ...s, then: rewriteBranches(s.then), else: rewriteBranches(s.else) } : s));
|
|
132
|
+
const locals = plan.locals.filter(local => !removedLocals.has(local.id)).map(local => localOwners.has(local.id)
|
|
133
|
+
? { ...local, scopeId: localOwners.get(local.id) } : local);
|
|
134
|
+
for (const stage of stages) locals.push({ id: stage.inputs.nextCursorLocalId, scopeId: stage.id, name: '__pulse_stage_next_cursor', valueKind: 'number' });
|
|
135
|
+
return { ...plan, stages, entry: { ...plan.entry, body: rewriteBranches(plan.entry.body) }, locals,
|
|
136
|
+
effects: plan.effects.map(e => effectReplacements.get(e.id) || e),
|
|
137
|
+
continuations: plan.continuations.map(c => continuationOwners.has(c.id) ? { ...c, ...continuationOwners.get(c.id) } : c) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateSharedStages(plan, fail, walkStatements, walkExpression, stableStringify) {
|
|
141
|
+
if (plan.stages !== undefined && !Array.isArray(plan.stages)) { fail('stages must be an array'); return; }
|
|
142
|
+
const stages = new Map(), bindings = new Map(), calls = new Map();
|
|
143
|
+
const entries = new Map((plan.routing?.entries || []).map(e => [e.stableId, e]));
|
|
144
|
+
const effects = new Map(plan.effects.map(e => [e.id, e]));
|
|
145
|
+
const continuations = new Map(plan.continuations.map(c => [c.id, c]));
|
|
146
|
+
const locals = new Map(plan.locals.map(l => [l.id, l]));
|
|
147
|
+
for (const stage of plan.stages || []) {
|
|
148
|
+
if (!stage || typeof stage.id !== 'string' || !stage.id.startsWith('stage:')
|
|
149
|
+
|| (plan.handlers || []).some(handler => handler.id === stage.id) || stages.has(stage.id) || stage.version !== VERSION || !Array.isArray(stage.body)
|
|
150
|
+
|| !Array.isArray(stage.localIds) || !Array.isArray(stage.effectIds) || !Array.isArray(stage.registrations)
|
|
151
|
+
|| !stage.inputs || !same(stage.outputs, OUTPUTS)
|
|
152
|
+
|| !same(stage.frame, contract.frame)) {
|
|
153
|
+
fail('shared stage contract is invalid'); continue;
|
|
154
|
+
}
|
|
155
|
+
stages.set(stage.id, stage);
|
|
156
|
+
const router = stage.inputs.routerLocals || {};
|
|
157
|
+
if (stage.inputs.context !== 'request' || !['cursor', 'mode', 'error'].every(name =>
|
|
158
|
+
locals.get(router[name])?.name === `__pulse_router_${name}` && locals.get(router[name])?.scopeId === 'entry')) fail('stage requires explicit request and Router inputs');
|
|
159
|
+
const own = new Set([...stage.localIds, stage.inputs.nextCursorLocalId]);
|
|
160
|
+
if (own.size !== stage.localIds.length + 1 || [...own].some(id => locals.get(id)?.scopeId !== stage.id)
|
|
161
|
+
|| plan.locals.filter(l => l.scopeId === stage.id).length !== own.size
|
|
162
|
+
|| locals.get(stage.inputs.nextCursorLocalId)?.valueKind !== 'number') fail('stage local ownership is invalid');
|
|
163
|
+
const allowed = new Set([...own, ...Object.values(router)]);
|
|
164
|
+
const observedEffects = [], declaredLocals = [];
|
|
165
|
+
const visitor = s => {
|
|
166
|
+
// The common plan validator enforces pure-loop caps, purity and transfers;
|
|
167
|
+
// this visitor also discovers induction and nested-body local ownership.
|
|
168
|
+
if (!['local', 'expression', 'return', 'if', 'effect', 'pure-loop', 'break', 'continue'].includes(s.kind)) fail('shared stage contains unsupported control flow');
|
|
169
|
+
if (s.kind === 'local' || s.kind === 'pure-loop') declaredLocals.push(s.localId);
|
|
170
|
+
if (s.kind === 'effect') {
|
|
171
|
+
observedEffects.push(s.effectId); declaredLocals.push(s.result?.localId);
|
|
172
|
+
const effect = effects.get(s.effectId);
|
|
173
|
+
if (!effect || s.continuationId !== effect.continuationId || !same(s.result, effect.result)) fail('stage statement effect binding is invalid');
|
|
174
|
+
}
|
|
175
|
+
if (s.localId && !own.has(s.localId)) fail('stage statement crosses local ownership');
|
|
176
|
+
};
|
|
177
|
+
visitor.expression = expr => {
|
|
178
|
+
if (expr.kind === 'pure-helper-call') fail('shared stage contains unsupported helper call');
|
|
179
|
+
if (expr.kind === 'local' && !allowed.has(expr.id)) fail('stage captures another lexical owner');
|
|
180
|
+
if (['assignment', 'update'].includes(expr.kind) && expr.target?.id === stage.inputs.nextCursorLocalId) fail('stage return input is read-only');
|
|
181
|
+
if (expr.kind === 'assignment' && expr.target?.id === router.cursor
|
|
182
|
+
&& (expr.operator !== '=' || expr.value?.kind !== 'local' || expr.value.id !== stage.inputs.nextCursorLocalId)) fail('stage transfer must use its selected return cursor');
|
|
183
|
+
};
|
|
184
|
+
walkStatements(stage.body, visitor);
|
|
185
|
+
if (!same(observedEffects, stage.effectIds) || !same(declaredLocals, stage.localIds)
|
|
186
|
+
|| !stage.effectIds.length || stage.effectIds.length > MAX_SITES) fail('stage sites or locals do not match its body');
|
|
187
|
+
const first = stage.registrations[0];
|
|
188
|
+
if (!first || !same(stage.effectIds, first.effectIds)) fail('stage requires a representative registration');
|
|
189
|
+
for (const row of stage.registrations) {
|
|
190
|
+
const entry = entries.get(row?.entryId);
|
|
191
|
+
if (!row || bindings.has(row.entryId) || !['route', 'use'].includes(entry?.kind) || entry.nativeBody
|
|
192
|
+
|| entry.handlerId !== stage.handlerId || row.nextIndex !== entry.nextIndex
|
|
193
|
+
|| !Array.isArray(row.effectIds) || !Array.isArray(row.continuationIds)
|
|
194
|
+
|| row.effectIds.length !== stage.effectIds.length || row.continuationIds.length !== stage.effectIds.length) {
|
|
195
|
+
fail('stage registration ownership is invalid'); continue;
|
|
196
|
+
}
|
|
197
|
+
bindings.set(row.entryId, stage.id);
|
|
198
|
+
row.effectIds.forEach((id, site) => {
|
|
199
|
+
const effect = effects.get(id), representative = effects.get(stage.effectIds[site]);
|
|
200
|
+
const continuation = continuations.get(row.continuationIds[site]);
|
|
201
|
+
if (!effect || effect.stageId !== stage.id || effect.stageSite !== site || effect.routerEntryStableId !== row.entryId
|
|
202
|
+
|| !eligibleEffect(effect)
|
|
203
|
+
|| effect.continuationId !== row.continuationIds[site] || continuation?.routerEntryStableId !== row.entryId
|
|
204
|
+
|| continuation?.stageId !== stage.id || continuation?.stageSite !== site
|
|
205
|
+
|| !same(continuation?.effectIds, [id]) || !representative
|
|
206
|
+
|| stableStringify(effectShape(effect)) !== stableStringify(effectShape(representative))) fail('stage effect/continuation binding is invalid');
|
|
207
|
+
for (const input of effect?.inputs || []) walkExpression(input.value, visitor.expression);
|
|
208
|
+
for (const argument of effect?.result?.decoder?.arguments || []) walkExpression(argument, visitor.expression);
|
|
209
|
+
if (!own.has(effect?.result?.localId)) fail('stage result crosses local ownership');
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Calls may occur only in the matching registration's admitted branch.
|
|
214
|
+
function checkCalls(body, entryId, admission = false) {
|
|
215
|
+
for (const s of body || []) {
|
|
216
|
+
if (s.kind === 'stage-call') {
|
|
217
|
+
if (!stages.has(s.stageId) || !admission || entryId !== s.registrationId || bindings.get(s.registrationId) !== s.stageId) fail('stage call is outside its registration admission');
|
|
218
|
+
calls.set(s.registrationId, (calls.get(s.registrationId) || 0) + 1);
|
|
219
|
+
}
|
|
220
|
+
if (s.kind === 'if') {
|
|
221
|
+
const entry = s.test?.kind === 'binary' && s.test.operator === '===' && s.test.left?.kind === 'local'
|
|
222
|
+
&& locals.get(s.test.left.id)?.name === '__pulse_router_cursor'
|
|
223
|
+
&& [...entries.values()].find(e => e.index === s.test.right?.value);
|
|
224
|
+
checkCalls(s.then, entry ? entry.stableId : entryId, !entry && Boolean(entryId));
|
|
225
|
+
checkCalls(s.else, undefined);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
checkCalls(plan.entry?.body);
|
|
230
|
+
for (const id of bindings.keys()) if (calls.get(id) !== 1) fail('stage registration requires one static call');
|
|
231
|
+
for (const effect of plan.effects) if (effect.stageId !== undefined
|
|
232
|
+
&& (bindings.get(effect.routerEntryStableId) !== effect.stageId
|
|
233
|
+
|| !stages.get(effect.stageId)?.registrations.some(row => row.effectIds[effect.stageSite] === effect.id))) fail('orphan stage effect binding');
|
|
234
|
+
for (const continuation of plan.continuations) if (continuation.stageId !== undefined
|
|
235
|
+
&& (bindings.get(continuation.routerEntryStableId) !== continuation.stageId
|
|
236
|
+
|| !stages.get(continuation.stageId)?.registrations.some(row => row.continuationIds[continuation.stageSite] === continuation.id))) fail('orphan stage continuation binding');
|
|
237
|
+
return { stages, bindings };
|
|
238
|
+
}
|
|
239
|
+
module.exports = { VERSION, OUTPUTS, MAX_SITES, lowerSharedStages, validateSharedStages };
|