@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.2
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 +53 -1
- package/bin/provider-proof-composition.js +34 -0
- package/bin/pulsewasm-extract.js +20 -0
- package/package.json +60 -5
- package/src/artifacts-dir.js +13 -0
- package/src/ast-json.js +52 -0
- package/src/build-manifest.js +354 -0
- package/src/canonical-api-compiler.js +269 -0
- package/src/canonical-native-compiler.js +411 -0
- package/src/canonical-native-plan.js +1478 -0
- package/src/canonical-project-compiler.js +1224 -0
- package/src/canonical-router-compiler.js +25 -0
- package/src/cli-intents.js +1235 -0
- package/src/cli.js +1927 -0
- package/src/codegen/assemblyscript-compile.js +3 -0
- package/src/codegen/assemblyscript-core.js +3 -0
- package/src/codegen/assemblyscript-shape.js +3 -0
- package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
- package/src/codegen/backend-capabilities.js +3 -0
- package/src/codegen/channel-broadcaster.js +3 -0
- package/src/codegen/compiled-handlers.js +3 -0
- package/src/codegen/compiled-wasm-runtime.js +3 -0
- package/src/codegen/config-references.js +248 -0
- package/src/codegen/dispatch-ts.js +145 -0
- package/src/codegen/effect-composition.js +331 -0
- package/src/codegen/effect-runtime.js +418 -0
- package/src/codegen/execution-harness-ts.js +467 -0
- package/src/codegen/handler-bindings-ts.js +298 -0
- package/src/codegen/handler-library-contracts.js +3 -0
- package/src/codegen/host-capabilities.js +3 -0
- package/src/codegen/host-runtime-kernel.js +3 -0
- package/src/codegen/integrated-compiled-app.js +3 -0
- package/src/codegen/json-body.js +3 -0
- package/src/codegen/library-sidecars.js +3 -0
- package/src/codegen/local-harness-ts.js +453 -0
- package/src/codegen/pulse-wrapper.js +217 -0
- package/src/codegen/request-result-headers.js +3 -0
- package/src/codegen/schema-json-compile.js +3 -0
- package/src/codegen/schema-json-sidecar-v2.js +3 -0
- package/src/codegen/schema-json-sidecar.js +3 -0
- package/src/codegen/streaming-passthrough.js +3 -0
- package/src/codegen/wasm-host-abi.js +3 -0
- package/src/codegen/wasm-host-bridge.js +3 -0
- package/src/compiled-wasm-host-runtime-kv.js +3 -0
- package/src/config-resolver.js +813 -0
- package/src/crypto-requirement-planner.js +89 -0
- package/src/definitions/config-schema.js +14 -0
- package/src/definitions/handler-roles.js +14 -0
- package/src/definitions/path-grammar.js +14 -0
- package/src/definitions/router-api.js +14 -0
- package/src/diagnostics/codes.js +14 -0
- package/src/diagnostics/reporter.js +14 -0
- package/src/diagnostics.js +14 -0
- package/src/dispatch-table.js +400 -0
- package/src/events/event-emit.js +265 -0
- package/src/events/event-topology.js +127 -0
- package/src/execution-plan.js +463 -0
- package/src/extractor.js +2816 -0
- package/src/handler-eval.js +1154 -0
- package/src/handler-table.js +326 -0
- package/src/index.js +19 -0
- package/src/javascript-application-plan.js +181 -0
- package/src/kv-provider.js +3 -0
- package/src/path-table.js +60 -0
- package/src/path.js +14 -0
- package/src/patterns/config-define.js +30 -0
- package/src/patterns/dependency-call.js +18 -0
- package/src/patterns/env-lookup.js +13 -0
- package/src/patterns/handler-reference.js +29 -0
- package/src/patterns/path-literal.js +35 -0
- package/src/patterns/result.js +15 -0
- package/src/patterns/router-chain-call.js +50 -0
- package/src/patterns/router-construction.js +19 -0
- package/src/project/package-reachability.js +782 -0
- package/src/project/reachable-graph-builder.js +992 -0
- package/src/project/reachable-graph-contract.js +54 -0
- package/src/project/reachable-graph-implementation.js +36 -0
- package/src/project/router-module-linker.js +710 -0
- package/src/project-config-compiler.js +222 -0
- package/src/project-target-support.js +500 -0
- package/src/provider-toolchain.js +299 -0
- package/src/spine/async-surface-normalizer.js +328 -0
- package/src/spine/canonical-handler-ir.js +336 -0
- package/src/spine/canonical-native-module.js +87 -0
- package/src/spine/canonical-native-plan.js +89 -0
- package/src/spine/canonical-project.js +76 -0
- package/src/spine/canonical-router.js +155 -0
- package/src/spine/canonical-source.js +165 -0
- package/src/spine/diagnostic-authority.js +290 -0
- package/src/spine/equivalence.js +262 -0
- package/src/spine/guest-unit-stage.js +87 -0
- package/src/spine/handler-ir-emitter.js +455 -0
- package/src/spine/handler-ir-managed.js +1598 -0
- package/src/spine/handler-ir.js +797 -0
- package/src/spine/handler-surface-authority.js +588 -0
- package/src/spine/package-operation-seam.js +1015 -0
- package/src/spine/pipeline.js +202 -0
- package/src/spine/plain-handler-frontend.js +545 -0
- package/src/spine/provider-requirement-authority.js +208 -0
- package/src/spine/router-control-contract.js +17 -0
- package/src/spine/router-handler-frontend.js +514 -0
- package/src/spine/router-handler-ir.js +372 -0
- package/src/spine/router-topology-frontend.js +638 -0
- package/src/stable-id.js +14 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { createRequire } = require('node:module');
|
|
5
|
+
const {
|
|
6
|
+
PROVIDER_DRIVER_VERSION,
|
|
7
|
+
PROVIDER_TARGET_RESULT_VERSION,
|
|
8
|
+
assertProviderToolchain,
|
|
9
|
+
assertProviderDriver,
|
|
10
|
+
normalizeProviderTargetResult
|
|
11
|
+
} = require('@pulse-compute/wasm-contracts/provider/toolchain');
|
|
12
|
+
|
|
13
|
+
const PROVIDER_PACKAGE_SCOPE = '@pulse-compute';
|
|
14
|
+
const PROVIDER_PACKAGE_PREFIX = 'provider-';
|
|
15
|
+
const BUILTIN_PROVIDER_IDS = Object.freeze(['node', 'fastly', 'none']);
|
|
16
|
+
const loadedByEntry = new Map();
|
|
17
|
+
|
|
18
|
+
const reportingTargetCapability = Object.freeze({
|
|
19
|
+
supported: true,
|
|
20
|
+
default: 'info',
|
|
21
|
+
levels: Object.freeze(['off', 'error', 'warn', 'info', 'debug'])
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const noneDriver = assertProviderDriver({
|
|
25
|
+
version: PROVIDER_DRIVER_VERSION,
|
|
26
|
+
id: 'none',
|
|
27
|
+
descriptor: undefined,
|
|
28
|
+
executable: false,
|
|
29
|
+
localExecution: false,
|
|
30
|
+
deployable: false,
|
|
31
|
+
deploymentValidated: false,
|
|
32
|
+
sourcePackage: false,
|
|
33
|
+
compiledWasm: true,
|
|
34
|
+
defaultBuildMode: 'compile-only',
|
|
35
|
+
sourceOnlySupported: false,
|
|
36
|
+
normalizeConfig() {
|
|
37
|
+
return Object.freeze({ kind: 'none', bindings: Object.freeze({}), local: Object.freeze({}) });
|
|
38
|
+
},
|
|
39
|
+
defaultLocalNetworkFetch: false,
|
|
40
|
+
projectConfigDocument() { return undefined; },
|
|
41
|
+
initTemplate() {
|
|
42
|
+
return Object.freeze({ profileFragment: '', dependencies: Object.freeze({}) });
|
|
43
|
+
},
|
|
44
|
+
targets: Object.freeze({
|
|
45
|
+
native: Object.freeze({
|
|
46
|
+
version: 'pulse.provider-target-descriptor.v1',
|
|
47
|
+
target: 'native',
|
|
48
|
+
targetId: 'portable-native-wasm',
|
|
49
|
+
runtimeClass: 'native',
|
|
50
|
+
status: 'compile-only',
|
|
51
|
+
automaticFallback: false,
|
|
52
|
+
reporting: reportingTargetCapability,
|
|
53
|
+
commands: Object.freeze({ compile: true, inspect: true, doctor: true, build: false, test: false, dev: false })
|
|
54
|
+
}),
|
|
55
|
+
javascript: Object.freeze({
|
|
56
|
+
version: 'pulse.javascript-target-descriptor.v2',
|
|
57
|
+
target: 'javascript',
|
|
58
|
+
targetId: 'none-javascript',
|
|
59
|
+
runtimeClass: 'javascript',
|
|
60
|
+
status: 'unsupported',
|
|
61
|
+
sourceApplication: true,
|
|
62
|
+
automaticFallback: false,
|
|
63
|
+
hostBridge: false,
|
|
64
|
+
applicationLoader: false,
|
|
65
|
+
requestAdapter: false,
|
|
66
|
+
lifecycle: false,
|
|
67
|
+
reporting: reportingTargetCapability,
|
|
68
|
+
commands: Object.freeze({ compile: true, inspect: true, doctor: true, build: false, test: false, dev: false })
|
|
69
|
+
})
|
|
70
|
+
}),
|
|
71
|
+
inspectRealization(invocation) {
|
|
72
|
+
const artifact = invocation.nativeArtifact;
|
|
73
|
+
return normalizeProviderTargetResult({
|
|
74
|
+
version: PROVIDER_TARGET_RESULT_VERSION,
|
|
75
|
+
action: 'inspect-native',
|
|
76
|
+
provider: 'none',
|
|
77
|
+
target: 'native',
|
|
78
|
+
status: 'inspected',
|
|
79
|
+
automaticFallback: false,
|
|
80
|
+
files: Object.freeze({}),
|
|
81
|
+
build: null,
|
|
82
|
+
realization: Object.freeze({
|
|
83
|
+
target: 'portable-native-wasm',
|
|
84
|
+
provider: null,
|
|
85
|
+
providerNeutralWasm: true,
|
|
86
|
+
nativeWasm: true,
|
|
87
|
+
javascriptRuntime: false,
|
|
88
|
+
jsComputeRuntime: false,
|
|
89
|
+
wasm: artifact.manifest.wasm,
|
|
90
|
+
wat: artifact.manifest.wat,
|
|
91
|
+
importModules: artifact.manifest.importModules,
|
|
92
|
+
imports: artifact.manifest.imports,
|
|
93
|
+
exports: artifact.manifest.exports
|
|
94
|
+
}),
|
|
95
|
+
packaging: null
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function providerError(code, message, detail = {}, cause) {
|
|
101
|
+
const error = new TypeError(message, cause ? { cause } : undefined);
|
|
102
|
+
error.code = code;
|
|
103
|
+
error.detail = Object.freeze({ ...detail });
|
|
104
|
+
return error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeSelector(value) {
|
|
108
|
+
const selector = String(value || '').trim();
|
|
109
|
+
if (!selector) throw providerError('PULSE_PROVIDER_UNSUPPORTED', 'Pulse provider selection must not be empty.');
|
|
110
|
+
return selector.startsWith('@') ? selector : selector.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertPackageName(value) {
|
|
114
|
+
const packageName = String(value);
|
|
115
|
+
const segment = '[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?';
|
|
116
|
+
const valid = new RegExp(`^(?:${segment}|@${segment}/${segment})$`).test(packageName);
|
|
117
|
+
if (!valid || packageName === '.' || packageName === '..') {
|
|
118
|
+
throw providerError(
|
|
119
|
+
'PULSE_PROVIDER_UNSUPPORTED',
|
|
120
|
+
`Pulse provider "${packageName}" must be an exact npm package name.`,
|
|
121
|
+
{ selector: packageName }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return packageName;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function providerPackageName(value) {
|
|
128
|
+
const selector = normalizeSelector(value);
|
|
129
|
+
if (selector === 'none') return null;
|
|
130
|
+
if (selector.startsWith('@')) return assertPackageName(selector);
|
|
131
|
+
return `${PROVIDER_PACKAGE_SCOPE}/${PROVIDER_PACKAGE_PREFIX}${assertPackageName(selector)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function projectRequire(projectRoot) {
|
|
135
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
136
|
+
return createRequire(path.join(root, 'package.json'));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveToolchainEntry(selector, packageName, options = {}) {
|
|
140
|
+
const request = `${packageName}/toolchain`;
|
|
141
|
+
const builtin = BUILTIN_PROVIDER_IDS.includes(selector);
|
|
142
|
+
const resolver = builtin ? require : projectRequire(options.projectRoot);
|
|
143
|
+
try {
|
|
144
|
+
return resolver.resolve(request);
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
throw providerError(
|
|
147
|
+
'PULSE_PROVIDER_PACKAGE_NOT_FOUND',
|
|
148
|
+
`Pulse provider package "${packageName}" does not expose ${request}.`,
|
|
149
|
+
{
|
|
150
|
+
selector,
|
|
151
|
+
packageName,
|
|
152
|
+
projectRoot: path.resolve(options.projectRoot || process.cwd()),
|
|
153
|
+
entrypoint: './toolchain'
|
|
154
|
+
},
|
|
155
|
+
cause
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function loadProviderToolchain(selector, packageName, entry) {
|
|
161
|
+
const cached = loadedByEntry.get(entry);
|
|
162
|
+
if (cached) return cached;
|
|
163
|
+
let declared;
|
|
164
|
+
try {
|
|
165
|
+
declared = require(entry);
|
|
166
|
+
} catch (cause) {
|
|
167
|
+
throw providerError(
|
|
168
|
+
'PULSE_PROVIDER_TOOLCHAIN_LOAD_FAILED',
|
|
169
|
+
`Pulse could not load provider toolchain "${packageName}/toolchain".`,
|
|
170
|
+
{ selector, packageName, entrypoint: './toolchain' },
|
|
171
|
+
cause
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
let toolchain;
|
|
175
|
+
try {
|
|
176
|
+
toolchain = assertProviderToolchain(declared, {
|
|
177
|
+
packageName,
|
|
178
|
+
id: BUILTIN_PROVIDER_IDS.includes(selector) ? selector : undefined
|
|
179
|
+
});
|
|
180
|
+
} catch (cause) {
|
|
181
|
+
throw providerError(
|
|
182
|
+
'PULSE_PROVIDER_TOOLCHAIN_INVALID',
|
|
183
|
+
`Provider package "${packageName}" does not implement the Pulse provider toolchain contract.`,
|
|
184
|
+
{ selector, packageName, entrypoint: './toolchain' },
|
|
185
|
+
cause
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
let driver;
|
|
189
|
+
try {
|
|
190
|
+
driver = assertProviderDriver(toolchain.createDriver(), { id: toolchain.id });
|
|
191
|
+
} catch (cause) {
|
|
192
|
+
if (cause && cause.code === 'PULSE_PROVIDER_TOOLCHAIN_INVALID') throw cause;
|
|
193
|
+
throw providerError(
|
|
194
|
+
'PULSE_PROVIDER_TOOLCHAIN_LOAD_FAILED',
|
|
195
|
+
`Provider package "${packageName}" failed while creating its Pulse driver.`,
|
|
196
|
+
{ selector, packageName, entrypoint: './toolchain' },
|
|
197
|
+
cause
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const loaded = Object.freeze({ toolchain, driver });
|
|
201
|
+
loadedByEntry.set(entry, loaded);
|
|
202
|
+
return loaded;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveProviderToolchain(value, options = {}) {
|
|
206
|
+
const selector = normalizeSelector(value);
|
|
207
|
+
if (selector === 'none') {
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
selector,
|
|
210
|
+
id: 'none',
|
|
211
|
+
packageName: null,
|
|
212
|
+
packageVersion: null,
|
|
213
|
+
contractVersion: null,
|
|
214
|
+
entrypoint: null,
|
|
215
|
+
official: true,
|
|
216
|
+
driver: noneDriver
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const packageName = providerPackageName(selector);
|
|
220
|
+
const entry = resolveToolchainEntry(selector, packageName, options);
|
|
221
|
+
const loaded = loadProviderToolchain(selector, packageName, entry);
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
selector,
|
|
224
|
+
id: loaded.driver.id,
|
|
225
|
+
packageName: loaded.toolchain.packageName,
|
|
226
|
+
packageVersion: loaded.toolchain.packageVersion,
|
|
227
|
+
contractVersion: loaded.toolchain.version,
|
|
228
|
+
entrypoint: './toolchain',
|
|
229
|
+
official: BUILTIN_PROVIDER_IDS.includes(selector),
|
|
230
|
+
driver: loaded.driver
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getProviderDriver(value, options = {}) {
|
|
235
|
+
return resolveProviderToolchain(value, options).driver;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getProviderTargetDescriptor(provider, target = 'native', options = {}) {
|
|
239
|
+
const driver = typeof provider === 'string' ? getProviderDriver(provider, options) : provider;
|
|
240
|
+
const descriptor = driver.targets && driver.targets[String(target || 'native').toLowerCase()];
|
|
241
|
+
if (!descriptor) throw new TypeError(`Unknown Pulse target ${String(target)} for provider ${driver.id}.`);
|
|
242
|
+
return descriptor;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getProviderTargetSupportDeclaration(provider, target = 'native', options = {}) {
|
|
246
|
+
const driver = typeof provider === 'string' ? getProviderDriver(provider, options) : provider;
|
|
247
|
+
return driver.targetSupport && driver.targetSupport[String(target || 'native').toLowerCase()] || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function providerTargetAvailability(provider, options = {}) {
|
|
251
|
+
const driver = typeof provider === 'string' ? getProviderDriver(provider, options) : provider;
|
|
252
|
+
const native = driver.targets && driver.targets.native;
|
|
253
|
+
const javascript = driver.targets && driver.targets.javascript;
|
|
254
|
+
const javascriptSupport = getProviderTargetSupportDeclaration(driver, 'javascript');
|
|
255
|
+
return Object.freeze({
|
|
256
|
+
native: Boolean(native && ['supported', 'compile-only'].includes(native.status)),
|
|
257
|
+
javascript: javascriptSupport
|
|
258
|
+
? javascriptSupport.availability.generalAvailable === true
|
|
259
|
+
: Boolean(javascript && javascript.status === 'supported'),
|
|
260
|
+
descriptors: Object.freeze({ native, javascript })
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function providerIds(options = {}) {
|
|
265
|
+
return Object.freeze(BUILTIN_PROVIDER_IDS.filter((id) => (
|
|
266
|
+
options.executable !== true || getProviderDriver(id).executable === true
|
|
267
|
+
)));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function providerConfigReferences() {
|
|
271
|
+
return Object.freeze(BUILTIN_PROVIDER_IDS
|
|
272
|
+
.map((id) => getProviderDriver(id))
|
|
273
|
+
.filter((driver) => driver.configReference)
|
|
274
|
+
.map((driver) => driver.configReference));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const drivers = {};
|
|
278
|
+
for (const id of BUILTIN_PROVIDER_IDS) {
|
|
279
|
+
Object.defineProperty(drivers, id, {
|
|
280
|
+
enumerable: true,
|
|
281
|
+
get() { return getProviderDriver(id); }
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
Object.freeze(drivers);
|
|
285
|
+
|
|
286
|
+
module.exports = Object.freeze({
|
|
287
|
+
PROVIDER_PACKAGE_SCOPE,
|
|
288
|
+
PROVIDER_PACKAGE_PREFIX,
|
|
289
|
+
BUILTIN_PROVIDER_IDS,
|
|
290
|
+
drivers,
|
|
291
|
+
providerPackageName,
|
|
292
|
+
resolveProviderToolchain,
|
|
293
|
+
getProviderDriver,
|
|
294
|
+
getProviderTargetDescriptor,
|
|
295
|
+
getProviderTargetSupportDeclaration,
|
|
296
|
+
providerTargetAvailability,
|
|
297
|
+
providerIds,
|
|
298
|
+
providerConfigReferences
|
|
299
|
+
});
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
const {
|
|
5
|
+
HANDLER_AUTHORING_MODES
|
|
6
|
+
} = require('@pulse-compute/wasm-contracts/handler/surface-contract');
|
|
7
|
+
const {
|
|
8
|
+
recognizeHandlerSurface,
|
|
9
|
+
extractKvNamespaceDeclaration,
|
|
10
|
+
unwrapExpression
|
|
11
|
+
} = require('./handler-surface-authority.js');
|
|
12
|
+
const {
|
|
13
|
+
createCanonicalDiagnostic,
|
|
14
|
+
createHandlerDiagnostic
|
|
15
|
+
} = require('./diagnostic-authority.js');
|
|
16
|
+
|
|
17
|
+
const ASYNC_SURFACE_NORMALIZER_VERSION = 'pulse.async-surface-normalizer.v2';
|
|
18
|
+
|
|
19
|
+
function warning(frontend, sourceFile, node, code, message, detail = {}) {
|
|
20
|
+
return createCanonicalDiagnostic({
|
|
21
|
+
frontend,
|
|
22
|
+
sourceFile,
|
|
23
|
+
node,
|
|
24
|
+
code,
|
|
25
|
+
message,
|
|
26
|
+
detail,
|
|
27
|
+
severity: 'warning'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function modifiersWithoutAsync(node) {
|
|
32
|
+
const current = node.modifiers || [];
|
|
33
|
+
const next = current.filter((modifier) => modifier.kind !== ts.SyntaxKind.AsyncKeyword);
|
|
34
|
+
return next.length === current.length ? node.modifiers : next;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function updateFunctionWithoutAsync(node, body) {
|
|
38
|
+
const modifiers = modifiersWithoutAsync(node);
|
|
39
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
40
|
+
return ts.factory.updateFunctionDeclaration(node, modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body);
|
|
41
|
+
}
|
|
42
|
+
if (ts.isFunctionExpression(node)) {
|
|
43
|
+
return ts.factory.updateFunctionExpression(node, modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body);
|
|
44
|
+
}
|
|
45
|
+
if (ts.isArrowFunction(node)) {
|
|
46
|
+
return ts.factory.updateArrowFunction(node, modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body);
|
|
47
|
+
}
|
|
48
|
+
if (ts.isMethodDeclaration(node)) {
|
|
49
|
+
return ts.factory.updateMethodDeclaration(node, modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, body);
|
|
50
|
+
}
|
|
51
|
+
return node;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function directReturnExpression(node) {
|
|
55
|
+
let current = node;
|
|
56
|
+
let parent = current && current.parent;
|
|
57
|
+
while (parent && (
|
|
58
|
+
ts.isParenthesizedExpression(parent)
|
|
59
|
+
|| ts.isAsExpression(parent)
|
|
60
|
+
|| ts.isSatisfiesExpression?.(parent)
|
|
61
|
+
|| ts.isNonNullExpression(parent)
|
|
62
|
+
|| ts.isTypeAssertionExpression(parent)
|
|
63
|
+
)) {
|
|
64
|
+
current = parent;
|
|
65
|
+
parent = current.parent;
|
|
66
|
+
}
|
|
67
|
+
return Boolean(parent && ts.isReturnStatement(parent) && parent.expression === current);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveHandlerAuthoringMode(options = {}) {
|
|
71
|
+
const explicit = options.handlerAuthoring;
|
|
72
|
+
if (explicit !== undefined) {
|
|
73
|
+
if (!Object.values(HANDLER_AUTHORING_MODES).includes(explicit)) {
|
|
74
|
+
throw new TypeError(`Unknown Pulse handler authoring mode: ${explicit}`);
|
|
75
|
+
}
|
|
76
|
+
return explicit;
|
|
77
|
+
}
|
|
78
|
+
return options.requireAsync === true
|
|
79
|
+
? HANDLER_AUTHORING_MODES.ASYNC_REQUIRED
|
|
80
|
+
: HANDLER_AUTHORING_MODES.SYNCHRONOUS_COMPATIBILITY;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeManagedHandler(functionNode, options = {}) {
|
|
84
|
+
if (!functionNode || !ts.isFunctionLike(functionNode)) throw new TypeError('normalizeManagedHandler requires a function-like node.');
|
|
85
|
+
const sourceFile = options.sourceFile;
|
|
86
|
+
const ctxName = options.ctxName || 'ctx';
|
|
87
|
+
const nextName = options.nextName;
|
|
88
|
+
const frontend = options.frontend || 'canonical-source';
|
|
89
|
+
const strict = options.strict !== false;
|
|
90
|
+
const handlerAuthoring = resolveHandlerAuthoringMode(options);
|
|
91
|
+
const requireAsync = handlerAuthoring === HANDLER_AUTHORING_MODES.ASYNC_REQUIRED;
|
|
92
|
+
const requireEffectAwait = options.requireEffectAwait === true;
|
|
93
|
+
const diagnostics = [];
|
|
94
|
+
const warnings = [];
|
|
95
|
+
const changes = [];
|
|
96
|
+
let kvAliases = new Map();
|
|
97
|
+
const awaitedRoots = new WeakSet();
|
|
98
|
+
const parallelMemberRoots = new WeakSet();
|
|
99
|
+
const userAuthoredAsync = (functionNode.modifiers || []).some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword);
|
|
100
|
+
const enforceEffectAwait = userAuthoredAsync || requireEffectAwait;
|
|
101
|
+
let awaitCount = 0;
|
|
102
|
+
let effectAwaitCount = 0;
|
|
103
|
+
let syncAwaitCount = 0;
|
|
104
|
+
let missingEffectAwaitCount = 0;
|
|
105
|
+
|
|
106
|
+
if (requireAsync && !userAuthoredAsync) {
|
|
107
|
+
diagnostics.push(createHandlerDiagnostic({
|
|
108
|
+
frontend,
|
|
109
|
+
issue: 'handler.async.required',
|
|
110
|
+
sourceFile,
|
|
111
|
+
node: functionNode,
|
|
112
|
+
values: { role: options.role || 'handler' },
|
|
113
|
+
detail: { role: options.role || 'handler', handlerAuthoring }
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function surfaceFor(node, position) {
|
|
118
|
+
return recognizeHandlerSurface(node, {
|
|
119
|
+
ctxName,
|
|
120
|
+
kvAliases,
|
|
121
|
+
nextName,
|
|
122
|
+
role: options.role,
|
|
123
|
+
strict,
|
|
124
|
+
packageEffectForCall: options.packageEffectForCall,
|
|
125
|
+
unwrap: true,
|
|
126
|
+
position
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const transformer = (context) => {
|
|
131
|
+
function visit(node) {
|
|
132
|
+
if (ts.isBlock(node)) {
|
|
133
|
+
const inherited = kvAliases; kvAliases = new Map(inherited);
|
|
134
|
+
try { return ts.visitEachChild(node, visit, context); } finally { kvAliases = inherited; }
|
|
135
|
+
}
|
|
136
|
+
if (ts.isVariableStatement(node)) {
|
|
137
|
+
const namespace = extractKvNamespaceDeclaration(node, ctxName, { unwrap: true });
|
|
138
|
+
for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name)) kvAliases.delete(declaration.name.text);
|
|
139
|
+
if (namespace) kvAliases.set(namespace.variableName, namespace.store);
|
|
140
|
+
}
|
|
141
|
+
if (node !== functionNode && ts.isFunctionLike(node)) {
|
|
142
|
+
const nestedAsync = (node.modifiers || []).some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword);
|
|
143
|
+
if (nestedAsync) diagnostics.push(createHandlerDiagnostic({
|
|
144
|
+
frontend,
|
|
145
|
+
issue: 'handler.nested-async-function.unsupported',
|
|
146
|
+
sourceFile,
|
|
147
|
+
node
|
|
148
|
+
}));
|
|
149
|
+
return node;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (ts.isAwaitExpression(node)) {
|
|
153
|
+
awaitCount += 1;
|
|
154
|
+
const inner = unwrapExpression(node.expression);
|
|
155
|
+
const surface = surfaceFor(inner, 'await-expression');
|
|
156
|
+
if (!surface || surface.surfaceId === 'javascript.await' || surface.class === 'javascript-only') {
|
|
157
|
+
// The explicit JavaScript project path uses this normalized tree only
|
|
158
|
+
// for inspection. Its original async source executes through the
|
|
159
|
+
// application loader; Native compilation still rejects this boundary.
|
|
160
|
+
if (options.target === 'javascript') return ts.visitNode(node.expression, visit);
|
|
161
|
+
diagnostics.push(createHandlerDiagnostic({
|
|
162
|
+
frontend,
|
|
163
|
+
issue: 'handler.await.native-unsupported',
|
|
164
|
+
sourceFile,
|
|
165
|
+
node,
|
|
166
|
+
values: { expression: inner && inner.getText ? inner.getText(sourceFile) : '<unknown>' },
|
|
167
|
+
detail: { selectedTarget: options.target || 'native', automaticFallback: false }
|
|
168
|
+
}));
|
|
169
|
+
return ts.visitNode(node.expression, visit);
|
|
170
|
+
}
|
|
171
|
+
awaitedRoots.add(inner);
|
|
172
|
+
if (surface.surfaceId === 'ctx.parallel') {
|
|
173
|
+
const record = surface.detail && surface.detail.parallel && surface.detail.parallel.record;
|
|
174
|
+
if (record && ts.isObjectLiteralExpression(record)) {
|
|
175
|
+
for (const property of record.properties) {
|
|
176
|
+
if (ts.isPropertyAssignment(property)) parallelMemberRoots.add(unwrapExpression(property.initializer));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (surface.classification && surface.classification.ok === false) {
|
|
181
|
+
diagnostics.push(createCanonicalDiagnostic({
|
|
182
|
+
frontend,
|
|
183
|
+
sourceFile,
|
|
184
|
+
node,
|
|
185
|
+
code: surface.classification.code,
|
|
186
|
+
message: surface.classification.message,
|
|
187
|
+
detail: { surfaceId: surface.surfaceId, strict }
|
|
188
|
+
}));
|
|
189
|
+
return ts.visitNode(node.expression, visit);
|
|
190
|
+
}
|
|
191
|
+
if (surface.surfaceId === 'router.next') {
|
|
192
|
+
diagnostics.push(createCanonicalDiagnostic({
|
|
193
|
+
frontend,
|
|
194
|
+
sourceFile,
|
|
195
|
+
node,
|
|
196
|
+
code: 'PULSE_ROUTER_NEXT_TERMINAL',
|
|
197
|
+
message: 'next() is a terminal Router control transfer and must be returned directly.',
|
|
198
|
+
detail: { surfaceId: surface.surfaceId }
|
|
199
|
+
}));
|
|
200
|
+
return ts.visitNode(node.expression, visit);
|
|
201
|
+
}
|
|
202
|
+
if (surface.awaitPolicy === 'synchronous-statement-only') {
|
|
203
|
+
diagnostics.push(createCanonicalDiagnostic({
|
|
204
|
+
frontend,
|
|
205
|
+
sourceFile,
|
|
206
|
+
node,
|
|
207
|
+
code: 'PULSE_LOG_AWAIT_FORBIDDEN',
|
|
208
|
+
message: `${surface.surfaceId} is synchronous, returns void, and must not be awaited.`,
|
|
209
|
+
detail: { surfaceId: surface.surfaceId }
|
|
210
|
+
}));
|
|
211
|
+
return ts.visitNode(node.expression, visit);
|
|
212
|
+
}
|
|
213
|
+
if (surface.class === 'effect' || surface.class === 'package-owned') {
|
|
214
|
+
effectAwaitCount += 1;
|
|
215
|
+
changes.push(Object.freeze({ kind: 'effect-await-erased', surfaceId: surface.surfaceId }));
|
|
216
|
+
return ts.visitNode(node.expression, visit);
|
|
217
|
+
}
|
|
218
|
+
if (surface.class === 'sync') {
|
|
219
|
+
syncAwaitCount += 1;
|
|
220
|
+
warnings.push(warning(
|
|
221
|
+
frontend,
|
|
222
|
+
sourceFile,
|
|
223
|
+
node,
|
|
224
|
+
'PULSE_AWAIT_SYNC_REDUNDANT',
|
|
225
|
+
`await is redundant for synchronous Pulse surface ${surface.surfaceId}; native lowering erases it.`,
|
|
226
|
+
{ surfaceId: surface.surfaceId }
|
|
227
|
+
));
|
|
228
|
+
changes.push(Object.freeze({ kind: 'sync-await-erased', surfaceId: surface.surfaceId }));
|
|
229
|
+
return ts.visitNode(node.expression, visit);
|
|
230
|
+
}
|
|
231
|
+
diagnostics.push(createHandlerDiagnostic({
|
|
232
|
+
frontend,
|
|
233
|
+
issue: 'handler.await.native-unsupported',
|
|
234
|
+
sourceFile,
|
|
235
|
+
node,
|
|
236
|
+
values: { expression: inner && inner.getText ? inner.getText(sourceFile) : '<unknown>' }
|
|
237
|
+
}));
|
|
238
|
+
return ts.visitNode(node.expression, visit);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (ts.isCallExpression(node)) {
|
|
242
|
+
const surface = surfaceFor(node, directReturnExpression(node) ? 'return' : 'expression');
|
|
243
|
+
if (surface) {
|
|
244
|
+
const awaited = awaitedRoots.has(unwrapExpression(node));
|
|
245
|
+
if (surface.classification && surface.classification.ok === false && !awaited) {
|
|
246
|
+
diagnostics.push(createCanonicalDiagnostic({
|
|
247
|
+
frontend,
|
|
248
|
+
sourceFile,
|
|
249
|
+
node,
|
|
250
|
+
code: surface.classification.code,
|
|
251
|
+
message: surface.classification.message,
|
|
252
|
+
detail: { surfaceId: surface.surfaceId, strict }
|
|
253
|
+
}));
|
|
254
|
+
} else if (surface.surfaceId === 'router.next') {
|
|
255
|
+
// Router terminal validation owns the exact return-position checks.
|
|
256
|
+
} else if (enforceEffectAwait && (surface.class === 'effect' || surface.class === 'package-owned')
|
|
257
|
+
&& !awaitedRoots.has(unwrapExpression(node)) && !parallelMemberRoots.has(unwrapExpression(node))) {
|
|
258
|
+
const returnAdopted = directReturnExpression(node) && (
|
|
259
|
+
surface.awaitPolicy === 'return-adopted-or-required'
|
|
260
|
+
|| (surface.class === 'package-owned' && surface.detail && surface.detail.packageEffect && surface.detail.packageEffect.placement === 'return')
|
|
261
|
+
);
|
|
262
|
+
if (!returnAdopted) {
|
|
263
|
+
missingEffectAwaitCount += 1;
|
|
264
|
+
diagnostics.push(createCanonicalDiagnostic({
|
|
265
|
+
frontend,
|
|
266
|
+
sourceFile,
|
|
267
|
+
node,
|
|
268
|
+
code: 'PULSE_EFFECT_AWAIT_REQUIRED',
|
|
269
|
+
message: `Pulse effect ${surface.surfaceId} must be awaited before its value is consumed.`,
|
|
270
|
+
detail: { surfaceId: surface.surfaceId, awaitPolicy: surface.awaitPolicy }
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (surface.surfaceId === 'ctx.parallel') {
|
|
275
|
+
// The outer combinator owns its statically keyed effect members. Visiting
|
|
276
|
+
// those calls as independent expressions would incorrectly diagnose each
|
|
277
|
+
// member as a missing await before Handler IR can validate the group.
|
|
278
|
+
return node;
|
|
279
|
+
}
|
|
280
|
+
// The recognized call owns its receiver chain. Visit only arguments so nested
|
|
281
|
+
// effects in argument expressions still receive deterministic diagnostics.
|
|
282
|
+
const argumentsArray = node.arguments.map((argument) => ts.visitNode(argument, visit));
|
|
283
|
+
if (argumentsArray.every((argument, index) => argument === node.arguments[index])) return node;
|
|
284
|
+
return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, argumentsArray);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return ts.visitEachChild(node, visit, context);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return (root) => {
|
|
292
|
+
const body = root.body ? ts.visitNode(root.body, visit) : root.body;
|
|
293
|
+
const updated = updateFunctionWithoutAsync(root, body);
|
|
294
|
+
if (updated !== root && userAuthoredAsync) changes.push(Object.freeze({ kind: 'async-wrapper-erased' }));
|
|
295
|
+
return updated;
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const result = ts.transform(functionNode, [transformer]);
|
|
300
|
+
const transformed = result.transformed[0];
|
|
301
|
+
result.dispose();
|
|
302
|
+
const normalized = changes.length > 0 ? transformed : functionNode;
|
|
303
|
+
|
|
304
|
+
return Object.freeze({
|
|
305
|
+
version: ASYNC_SURFACE_NORMALIZER_VERSION,
|
|
306
|
+
functionNode: normalized,
|
|
307
|
+
diagnostics: Object.freeze(diagnostics),
|
|
308
|
+
warnings: Object.freeze(warnings),
|
|
309
|
+
summary: Object.freeze({
|
|
310
|
+
changed: changes.length > 0,
|
|
311
|
+
userAuthoredAsync,
|
|
312
|
+
handlerAuthoring,
|
|
313
|
+
requireAsync,
|
|
314
|
+
requireEffectAwait: enforceEffectAwait,
|
|
315
|
+
awaitCount,
|
|
316
|
+
effectAwaitCount,
|
|
317
|
+
syncAwaitCount,
|
|
318
|
+
missingEffectAwaitCount,
|
|
319
|
+
changes: Object.freeze(changes)
|
|
320
|
+
})
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = Object.freeze({
|
|
325
|
+
ASYNC_SURFACE_NORMALIZER_VERSION,
|
|
326
|
+
resolveHandlerAuthoringMode,
|
|
327
|
+
normalizeManagedHandler
|
|
328
|
+
});
|