@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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const ts = require('typescript');
|
|
6
|
+
const { attachTerminalPackageNative } = require('./spine/terminal-package-native.js');
|
|
6
7
|
const { buildSchemaJsonCompile } = require('@pulse-compute/wasm-schema-json/compiler/schema-json-compile');
|
|
7
8
|
const { buildCanonicalSchemaBundle } = require('@pulse-compute/wasm-schema-json/compiler/canonical-schema-codecs');
|
|
8
9
|
const {
|
|
@@ -509,6 +510,23 @@ function sourceDiagnosticForPackageEffect(effect, message, detail = {}) {
|
|
|
509
510
|
});
|
|
510
511
|
}
|
|
511
512
|
|
|
513
|
+
// Native must lower every reachable package site. JavaScript executes helpers
|
|
514
|
+
// from the original source graph; only sites owned by normalized Router handlers
|
|
515
|
+
// require a generated counterpart. Keep the full recognition bundle for package
|
|
516
|
+
// inspection, validation and provider requirements on both targets.
|
|
517
|
+
function packageSitesRequiringRouterLink(sites, records, options) {
|
|
518
|
+
if (options.target !== 'javascript') return sites;
|
|
519
|
+
return sites.filter((site) => records.some((record) => {
|
|
520
|
+
const ir = record && record.operationIr;
|
|
521
|
+
if (!ir || !ir.sourceFile || !ir.handler) return false;
|
|
522
|
+
const start = Number(site && site.range && site.range.start);
|
|
523
|
+
const end = Number(site && site.range && site.range.end);
|
|
524
|
+
return sameModulePath(site && site.loc && site.loc.file, ir.sourceFile.fileName || ir.file, options.rootDir)
|
|
525
|
+
&& Number.isSafeInteger(start) && Number.isSafeInteger(end)
|
|
526
|
+
&& start >= ir.handler.getStart(ir.sourceFile) && end <= ir.handler.getEnd();
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
|
|
512
530
|
function routerGeneratedPackageEffectLookup(router, recognition, options = {}) {
|
|
513
531
|
const effects = [...(recognition && recognition.operations || [])].map((operation) => operation.canonicalEffect);
|
|
514
532
|
if (!router || effects.length === 0) return undefined;
|
|
@@ -588,12 +606,15 @@ function routerGeneratedPackageEffectLookup(router, recognition, options = {}) {
|
|
|
588
606
|
}
|
|
589
607
|
}
|
|
590
608
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
609
|
+
// A handler can be registered more than once. Each entry must link all of its
|
|
610
|
+
// generated calls above, while coverage here is over authored source effects.
|
|
611
|
+
// Counting generated sites would reject valid one-to-many source mappings.
|
|
612
|
+
const linked = new Set(generatedByStart.values());
|
|
613
|
+
const required = packageSitesRequiringRouterLink(effects, records, options);
|
|
614
|
+
const missing = required.filter((effect) => !linked.has(effect));
|
|
615
|
+
if (missing.length > 0) {
|
|
595
616
|
throw new CanonicalProjectCompileError(
|
|
596
|
-
`Generated Router source linked ${
|
|
617
|
+
`Generated Router source linked ${linked.size} of ${required.length} required package effect(s).`,
|
|
597
618
|
missing.map((effect) => sourceDiagnosticForPackageEffect(effect, `Reachable package effect ${effect.kind} was not linked into generated Router source.`, {
|
|
598
619
|
contractId: effect.contractId
|
|
599
620
|
})),
|
|
@@ -728,11 +749,13 @@ function routerGeneratedPackageIntrinsicLookup(router, recognition, options = {}
|
|
|
728
749
|
}
|
|
729
750
|
}
|
|
730
751
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
752
|
+
// Intrinsic calls share the same source-to-registration multiplicity as effects.
|
|
753
|
+
const linked = new Set(generatedByStart.values());
|
|
754
|
+
const required = packageSitesRequiringRouterLink(intrinsics, records, options);
|
|
755
|
+
const missing = required.filter((intrinsic) => !linked.has(intrinsic));
|
|
756
|
+
if (missing.length > 0) {
|
|
734
757
|
throw new CanonicalProjectCompileError(
|
|
735
|
-
`Generated Router source linked ${
|
|
758
|
+
`Generated Router source linked ${linked.size} of ${required.length} required package intrinsic(s).`,
|
|
736
759
|
missing.map((intrinsic) => sourceDiagnosticForPackageEffect(
|
|
737
760
|
intrinsic,
|
|
738
761
|
`Reachable package intrinsic ${intrinsic.kind} was not linked into generated Router source.`,
|
|
@@ -1020,6 +1043,7 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
|
|
|
1020
1043
|
try {
|
|
1021
1044
|
managedHandlers = compileManagedHandlerDescriptors({
|
|
1022
1045
|
graphBuild,
|
|
1046
|
+
target: options.target,
|
|
1023
1047
|
descriptors: packageOperationRecognition.managedHandlers,
|
|
1024
1048
|
packageOperationRecognition
|
|
1025
1049
|
});
|
|
@@ -1045,7 +1069,20 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
|
|
|
1045
1069
|
const terminalPackageApplication = terminalPackageIntrinsicApplication(root, packageOperationRecognition);
|
|
1046
1070
|
|
|
1047
1071
|
let linkedProjectModules;
|
|
1048
|
-
|
|
1072
|
+
let localHelperCall = false;
|
|
1073
|
+
if (root?.kind === 'router') {
|
|
1074
|
+
const visit = node => {
|
|
1075
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
1076
|
+
if (options.target !== 'javascript') localHelperCall = true;
|
|
1077
|
+
else for (let parent = node.parent; parent && !ts.isFunctionLike(parent); parent = parent.parent) {
|
|
1078
|
+
if (ts.isForStatement(parent)) localHelperCall = true;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
ts.forEachChild(node, visit);
|
|
1082
|
+
};
|
|
1083
|
+
visit(root.module.sourceFile);
|
|
1084
|
+
}
|
|
1085
|
+
if (multiModule || localHelperCall) {
|
|
1049
1086
|
try {
|
|
1050
1087
|
linkedProjectModules = root.kind === 'router'
|
|
1051
1088
|
? linkProjectRouterModules(graphBuild, { rootDir, target: options.target })
|
|
@@ -1095,13 +1132,13 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
|
|
|
1095
1132
|
? terminalPackageApplication.sourceText
|
|
1096
1133
|
: authoringSourceText;
|
|
1097
1134
|
const generatedPackageEffectForCall = router
|
|
1098
|
-
? routerGeneratedPackageEffectLookup(router, packageOperationRecognition, { rootDir })
|
|
1135
|
+
? routerGeneratedPackageEffectLookup(router, packageOperationRecognition, { rootDir, target: options.target })
|
|
1099
1136
|
: packageEffectLookup(packageOperationRecognition);
|
|
1100
1137
|
const generatedPackageResultAdapterForCall = router
|
|
1101
1138
|
? routerGeneratedPackageResultAdapterLookup(router, packageOperationRecognition, { rootDir })
|
|
1102
1139
|
: packageResultAdapterLookup(packageOperationRecognition);
|
|
1103
1140
|
const generatedPackageIntrinsicForCall = router
|
|
1104
|
-
? routerGeneratedPackageIntrinsicLookup(router, packageOperationRecognition, { rootDir })
|
|
1141
|
+
? routerGeneratedPackageIntrinsicLookup(router, packageOperationRecognition, { rootDir, target: options.target })
|
|
1105
1142
|
: packageIntrinsicLookup(packageOperationRecognition);
|
|
1106
1143
|
const applicationEnvelope = createApplicationEnvelope(router, options.applicationProjectMetadata);
|
|
1107
1144
|
const compileOptions = {
|
|
@@ -1198,6 +1235,10 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
|
|
|
1198
1235
|
].map((file) => path.resolve(file)))])
|
|
1199
1236
|
});
|
|
1200
1237
|
attachPackageOperationRecognition(project, packageOperationRecognition);
|
|
1238
|
+
if (terminalPackageApplication) attachTerminalPackageNative(project, {
|
|
1239
|
+
application: terminalPackageApplication, recognition: packageOperationRecognition,
|
|
1240
|
+
managedHandlers, schemaBundle: schema.bundle, cwd: rootDir, workspaceRoot: options.workspaceRoot
|
|
1241
|
+
});
|
|
1201
1242
|
return project;
|
|
1202
1243
|
}
|
|
1203
1244
|
|
package/src/execution-plan.js
CHANGED
|
@@ -301,6 +301,7 @@ function buildExecutionPlan(routerTreeArtifact, dispatchTable, options = {}) {
|
|
|
301
301
|
pattern: mountPattern,
|
|
302
302
|
scopeId: scopeBefore,
|
|
303
303
|
childRouter: op.router,
|
|
304
|
+
...(op.eligibility ? { eligibility: op.eligibility } : {}),
|
|
304
305
|
childStartIndex: null,
|
|
305
306
|
parentContinueIndex: null,
|
|
306
307
|
loc: op.loc
|
package/src/extractor.js
CHANGED
|
@@ -421,6 +421,29 @@ function parseTimeoutLiteral(sourceFile, node, diagnostics, phase = 'extract') {
|
|
|
421
421
|
return config;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
function parseMountEligibility(sourceFile, node, diagnostics) {
|
|
425
|
+
const result = {};
|
|
426
|
+
let valid = ts.isObjectLiteralExpression(node) && node.properties.length === 2;
|
|
427
|
+
if (valid) {
|
|
428
|
+
for (const property of node.properties) {
|
|
429
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
430
|
+
!(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ||
|
|
431
|
+
!['state', 'equals'].includes(property.name.text) ||
|
|
432
|
+
Object.hasOwn(result, property.name.text) || !ts.isStringLiteral(property.initializer)) {
|
|
433
|
+
valid = false;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
result[property.name.text] = property.initializer.text;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (valid && Object.hasOwn(result, 'state') && Object.hasOwn(result, 'equals')) return result;
|
|
440
|
+
diagnostics.push(diagnostic(sourceFile, node, DIAGNOSTIC_CODES.UNSUPPORTED_MOUNT_ELIGIBILITY,
|
|
441
|
+
'Mount eligibility must be an inline { state: "key", equals: "value" } descriptor with exactly two string literals.',
|
|
442
|
+
'Select a family in earlier middleware. Eligibility cannot call functions, run effects, use getters, spreads, computed keys or dynamic values.',
|
|
443
|
+
{ phase: 'extract' }));
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
424
447
|
function parsePathLiteral(sourceFile, node, diagnostics, usage, phase = 'extract') {
|
|
425
448
|
if (!isStringLiteralLike(node)) {
|
|
426
449
|
diagnostics.push(
|
|
@@ -732,6 +755,7 @@ function parseRouterOperation(sourceFile, step, diagnostics, options = {}) {
|
|
|
732
755
|
return {
|
|
733
756
|
...base(definition.opKind),
|
|
734
757
|
path: stripScopedWildcard(parsePathLiteral(sourceFile, args[signature.pathArg], diagnostics, 'mount')),
|
|
758
|
+
...(signature.eligibilityArg !== undefined ? { eligibility: parseMountEligibility(sourceFile, args[signature.eligibilityArg], diagnostics) } : {}),
|
|
735
759
|
router: ts.isIdentifier(unwrappedRouter.expression) ? unwrappedRouter.expression.text : nodeText(sourceFile, routerArg),
|
|
736
760
|
...(unwrappedRouter.wrappers.length > 0 ? { routerWrappers: unwrappedRouter.wrappers } : {})
|
|
737
761
|
};
|
|
@@ -1625,6 +1649,18 @@ function extractFromSourceFile(sourceFile, options = {}) {
|
|
|
1625
1649
|
}
|
|
1626
1650
|
|
|
1627
1651
|
const ir = buildRouterIR(sourceFile, options);
|
|
1652
|
+
// Historical table/harness emitters have no request-state eligibility ABI.
|
|
1653
|
+
// Keep this new authoring form on the canonical Router pipeline only.
|
|
1654
|
+
const selectedMount = ir.routerIR.flatMap(router => router.ops || []).find(op => op.eligibility);
|
|
1655
|
+
if (selectedMount) {
|
|
1656
|
+
throw new ExtractionError([{
|
|
1657
|
+
code: DIAGNOSTIC_CODES.UNSUPPORTED_MOUNT_ELIGIBILITY,
|
|
1658
|
+
severity: 'error', phase: 'extract', loc: selectedMount.loc,
|
|
1659
|
+
message: 'Selected mounts require the canonical Router compiler; historical table/harness emitters do not support eligibility.',
|
|
1660
|
+
hint: 'Use the canonical Pulse project compile/build workflow.'
|
|
1661
|
+
}], { passes, source });
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1628
1664
|
const indexDiagnostics = ir.diagnostics.filter((diag) => (diag.phase || diag.pass) === 'index');
|
|
1629
1665
|
const extractDiagnostics = ir.diagnostics.filter((diag) => (diag.phase || diag.pass) !== 'index');
|
|
1630
1666
|
passes.push(makePass('index', indexDiagnostics.length === 0 ? 'ok' : 'error', {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Private planning facts about the existing, normalized S3GetTextResult contract.
|
|
4
|
+
// They never change an effect envelope or validate an arbitrary object by shape.
|
|
5
|
+
function bind(local, effect) {
|
|
6
|
+
return local.declaration === 'const' && effect.kind === 's3.getText'
|
|
7
|
+
&& effect.declaredResult === 's3-get-text-result'
|
|
8
|
+
? { ...local, textResult: { invalidated: false }, textFound: false } : local;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function refine(test, truth, scope) {
|
|
12
|
+
if (test.kind === 'unary' && test.operator === '!') return refine(test.value, !truth, scope);
|
|
13
|
+
if (test.kind !== 'binary') return;
|
|
14
|
+
if (test.operator === (truth ? '&&' : '||')) {
|
|
15
|
+
refine(test.left, truth, scope);
|
|
16
|
+
refine(test.right, truth, scope);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!['===', '!=='].includes(test.operator) || truth !== (test.operator === '===')) return;
|
|
20
|
+
const read = test.left.kind === 'literal' && test.left.value === 'found' ? test.right
|
|
21
|
+
: test.right.kind === 'literal' && test.right.value === 'found' ? test.left : undefined;
|
|
22
|
+
if (read?.kind !== 'property' || read.property !== 'status' || read.object.kind !== 'local') return;
|
|
23
|
+
const local = scope.get(read.object.name);
|
|
24
|
+
if (local?.id === read.object.id && local.textResult && !local.textResult.invalidated) {
|
|
25
|
+
scope.set(local.name, { ...local, textFound: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function propertyKind(object, property, scope) {
|
|
30
|
+
if (object.kind !== 'local' || property !== 'text') return;
|
|
31
|
+
const local = scope.get(object.name);
|
|
32
|
+
if (local?.id === object.id && local.textFound && !local.textResult.invalidated) return 'string';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function join(scope, thenScope, elseScope, thenReturns, elseReturns) {
|
|
36
|
+
for (const [name, local] of scope) {
|
|
37
|
+
if (!local.textResult || local.textResult.invalidated) continue;
|
|
38
|
+
const yes = thenScope.get(name)?.textFound;
|
|
39
|
+
const no = elseScope.get(name)?.textFound;
|
|
40
|
+
if (thenReturns && no || elseReturns && yes || yes && no) scope.set(name, { ...local, textFound: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Property writes may use aliases. Conservatively retire all live result facts,
|
|
45
|
+
// including sibling scopes sharing the record, instead of assuming no aliasing.
|
|
46
|
+
function invalidate(scope) {
|
|
47
|
+
for (const local of scope.values()) if (local.textResult) local.textResult.invalidated = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { bind, refine, propertyKind, join, invalidate };
|
|
@@ -561,6 +561,63 @@ function linkProjectRouterModules(graphBuild, options = {}) {
|
|
|
561
561
|
}
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
// Only HTTP handler loop calls enter JavaScript's bounded inspection. Never
|
|
565
|
+
// walk an ordinary dependency function into a new purity requirement.
|
|
566
|
+
const httpFunctions = new Set();
|
|
567
|
+
if (options.target === 'javascript') for (const ref of handlerReferences) {
|
|
568
|
+
if (!['route', 'middleware'].includes(ref.role)) continue;
|
|
569
|
+
const module = context.projectModules.get(ref.module);
|
|
570
|
+
if (!module) continue;
|
|
571
|
+
if (!ref.localName.startsWith('<inline:')) {
|
|
572
|
+
httpFunctions.add(functionForNamedHandler(module.sourceFile, ref.localName));
|
|
573
|
+
} else {
|
|
574
|
+
const visit = node => {
|
|
575
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
576
|
+
const pos = module.sourceFile.getLineAndCharacterOfPosition(node.getStart(module.sourceFile));
|
|
577
|
+
if (pos.line + 1 === ref.source.line && pos.character + 1 === ref.source.column) httpFunctions.add(node);
|
|
578
|
+
}
|
|
579
|
+
ts.forEachChild(node, visit);
|
|
580
|
+
};
|
|
581
|
+
visit(module.sourceFile);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const sourceHelpers = [];
|
|
586
|
+
const helperCalls = new WeakMap();
|
|
587
|
+
{
|
|
588
|
+
const byIdentity = new Map();
|
|
589
|
+
for (const module of context.projectModules.values()) {
|
|
590
|
+
function visit(node) {
|
|
591
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
592
|
+
const call = node;
|
|
593
|
+
const awaited = ts.isAwaitExpression(node.parent);
|
|
594
|
+
const target = resolveLocal(context, indexes, module, call.expression.text, [], [], undefined, consumedImports);
|
|
595
|
+
const fn = target?.module && functionForNamedHandler(target.module.sourceFile, target.localName);
|
|
596
|
+
// JavaScript keeps ordinary calls in its original graph. Only bounded
|
|
597
|
+
// loop calls enter the shared pure-helper inspection contract.
|
|
598
|
+
let inLoop = false;
|
|
599
|
+
let owner = call.parent;
|
|
600
|
+
for (; owner && !ts.isFunctionLike(owner); owner = owner.parent) {
|
|
601
|
+
if (ts.isForStatement(owner)) inLoop = true;
|
|
602
|
+
}
|
|
603
|
+
const inspectPureLoopCall = inLoop && httpFunctions.has(owner) && fn && !fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
|
|
604
|
+
if (fn && (options.target !== 'javascript' || inspectPureLoopCall) && (awaited && fn.parameters.length > 1 || !awaited)) {
|
|
605
|
+
const identity = `${target.module.path}#${target.localName}`;
|
|
606
|
+
let helper = byIdentity.get(identity);
|
|
607
|
+
if (!helper) {
|
|
608
|
+
helper = { pure: !fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), id: `helper:${identity}`, name: `__pulse_helper_${sourceHelpers.length}`, functionNode: fn,
|
|
609
|
+
resolveType: node => require('../spine/pure-helper-source').resolvePureType(context, target.module, node),
|
|
610
|
+
sourceFile: target.module.sourceFile, source: { file: target.module.path, name: target.localName } };
|
|
611
|
+
sourceHelpers.push(helper); byIdentity.set(identity, helper);
|
|
612
|
+
}
|
|
613
|
+
helperCalls.set(call, helper);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
ts.forEachChild(node, visit);
|
|
617
|
+
}
|
|
618
|
+
visit(module.sourceFile);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
564
621
|
const rootGlobalName = routerSymbols.get(`${root.module.path}:${root.localName}`);
|
|
565
622
|
const ordered = orderLinkedRouters(routers, rootGlobalName);
|
|
566
623
|
if (options.target !== 'javascript') validateRuntimeProjectImports(context, consumedImports, diagnostics);
|
|
@@ -616,7 +673,14 @@ function linkProjectRouterModules(graphBuild, options = {}) {
|
|
|
616
673
|
entrySourceFile: entryModule.sourceFile,
|
|
617
674
|
entrySourceText: entryModule.sourceText,
|
|
618
675
|
retainedDeclarations: retained,
|
|
676
|
+
sourceHelpers,
|
|
677
|
+
helperForCall: call => helperCalls.get(ts.getOriginalNode(call)),
|
|
619
678
|
functionNodeForHandler,
|
|
679
|
+
pureTypeForNode(node) {
|
|
680
|
+
const original = ts.getOriginalNode(node);
|
|
681
|
+
const module = [...context.projectModules.values()].find(m => m.sourceFile === original.getSourceFile());
|
|
682
|
+
return module && require('../spine/pure-helper-source').resolvePureType(context, module, original);
|
|
683
|
+
},
|
|
620
684
|
sourceFileForPath(file) {
|
|
621
685
|
const module = context.projectModules.get(file);
|
|
622
686
|
return module && module.sourceFile;
|
|
@@ -139,7 +139,7 @@ function projectRequire(projectRoot) {
|
|
|
139
139
|
function resolveToolchainEntry(selector, packageName, options = {}) {
|
|
140
140
|
const request = `${packageName}/toolchain`;
|
|
141
141
|
const builtin = BUILTIN_PROVIDER_IDS.includes(selector);
|
|
142
|
-
const resolver = builtin ?
|
|
142
|
+
const resolver = projectRequire(builtin ? options.builtinRoot || options.projectRoot : options.projectRoot);
|
|
143
143
|
try {
|
|
144
144
|
return resolver.resolve(request);
|
|
145
145
|
} catch (cause) {
|
|
@@ -263,13 +263,13 @@ function providerTargetAvailability(provider, options = {}) {
|
|
|
263
263
|
|
|
264
264
|
function providerIds(options = {}) {
|
|
265
265
|
return Object.freeze(BUILTIN_PROVIDER_IDS.filter((id) => (
|
|
266
|
-
options.executable !== true || getProviderDriver(id).executable === true
|
|
266
|
+
options.executable !== true || getProviderDriver(id, options).executable === true
|
|
267
267
|
)));
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
function providerConfigReferences() {
|
|
270
|
+
function providerConfigReferences(options = {}) {
|
|
271
271
|
return Object.freeze(BUILTIN_PROVIDER_IDS
|
|
272
|
-
.map((id) => getProviderDriver(id))
|
|
272
|
+
.map((id) => getProviderDriver(id, options))
|
|
273
273
|
.filter((driver) => driver.configReference)
|
|
274
274
|
.map((driver) => driver.configReference));
|
|
275
275
|
}
|
|
@@ -283,7 +283,7 @@ for (const id of BUILTIN_PROVIDER_IDS) {
|
|
|
283
283
|
}
|
|
284
284
|
Object.freeze(drivers);
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
const api = Object.freeze({
|
|
287
287
|
PROVIDER_PACKAGE_SCOPE,
|
|
288
288
|
PROVIDER_PACKAGE_PREFIX,
|
|
289
289
|
BUILTIN_PROVIDER_IDS,
|
|
@@ -297,3 +297,27 @@ module.exports = Object.freeze({
|
|
|
297
297
|
providerIds,
|
|
298
298
|
providerConfigReferences
|
|
299
299
|
});
|
|
300
|
+
|
|
301
|
+
// Bind a composition-owned resolution root without process-global registration.
|
|
302
|
+
// The CLI supplies its own package root for bundled IDs; scoped providers still
|
|
303
|
+
// resolve exactly from the project. A compiler consumer supplies only its project.
|
|
304
|
+
function createProviderResolver(defaults = {}) {
|
|
305
|
+
const options = Object.freeze({ ...defaults });
|
|
306
|
+
const resolver = { ...api };
|
|
307
|
+
for (const name of ['resolveProviderToolchain', 'getProviderDriver', 'providerTargetAvailability']) {
|
|
308
|
+
resolver[name] = (value, input = {}) => api[name](value, { ...input, ...options });
|
|
309
|
+
}
|
|
310
|
+
for (const name of ['getProviderTargetDescriptor', 'getProviderTargetSupportDeclaration']) {
|
|
311
|
+
resolver[name] = (value, target, input = {}) => api[name](value, target, { ...input, ...options });
|
|
312
|
+
}
|
|
313
|
+
resolver.providerIds = (input = {}) => providerIds({ ...input, ...options });
|
|
314
|
+
resolver.providerConfigReferences = () => providerConfigReferences(options);
|
|
315
|
+
const boundDrivers = {};
|
|
316
|
+
for (const id of BUILTIN_PROVIDER_IDS) Object.defineProperty(boundDrivers, id, {
|
|
317
|
+
enumerable: true, get() { return resolver.getProviderDriver(id); }
|
|
318
|
+
});
|
|
319
|
+
resolver.drivers = Object.freeze(boundDrivers);
|
|
320
|
+
return Object.freeze(resolver);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = Object.freeze({ ...api, createProviderResolver });
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
|
|
3
|
+
// These descriptors prove reads of existing handles, never runtime conversions.
|
|
4
|
+
const SCALAR_TYPES = Object.freeze(['string', 'number', 'boolean']);
|
|
5
|
+
const BORROW_LIMITS = Object.freeze({ recordDepth: 2, recordFields: 32 });
|
|
6
|
+
const FORBIDDEN_FIELDS = Object.freeze(['__proto__', 'constructor', 'prototype']);
|
|
7
|
+
const scalar = type => SCALAR_TYPES.includes(type);
|
|
8
|
+
const kind = type => typeof type === 'string' ? type : type?.kind === 'record' ? 'object' : type?.kind === 'number-array' ? 'array' : 'unknown';
|
|
9
|
+
function validType(type, depth = 0) {
|
|
10
|
+
if (scalar(type)) return true;
|
|
11
|
+
if (!type || typeof type !== 'object' || depth > BORROW_LIMITS.recordDepth) return false;
|
|
12
|
+
if (type.kind === 'number-array') return Object.keys(type).length === 1;
|
|
13
|
+
return type.kind === 'record' && depth < BORROW_LIMITS.recordDepth && Object.keys(type).sort().join() === 'fields,kind'
|
|
14
|
+
&& Array.isArray(type.fields) && type.fields.length > 0 && type.fields.length <= BORROW_LIMITS.recordFields
|
|
15
|
+
&& new Set(type.fields.map(f => f?.name)).size === type.fields.length
|
|
16
|
+
&& type.fields.every(f => f && Object.keys(f).sort().join() === 'name,type' && typeof f.name === 'string'
|
|
17
|
+
&& /^[A-Za-z_$][\w$]*$/.test(f.name) && !FORBIDDEN_FIELDS.includes(f.name) && validType(f.type, depth + 1));
|
|
18
|
+
}
|
|
19
|
+
function same(a, b) {
|
|
20
|
+
if (a === undefined && b === undefined) return true;
|
|
21
|
+
if (typeof a === 'string' || typeof b === 'string') return a === b;
|
|
22
|
+
if (!a || !b || a.kind !== b.kind) return false;
|
|
23
|
+
return a.kind === 'number-array' || Array.isArray(a.fields) && Array.isArray(b.fields) && a.fields.length === b.fields.length && a.fields.every(f => same(f.type, b.fields.find(g => g.name === f.name)?.type));
|
|
24
|
+
}
|
|
25
|
+
function member(type, property) {
|
|
26
|
+
if ((type === 'string' || type?.kind === 'number-array') && property === 'length') return 'number';
|
|
27
|
+
return type?.kind === 'record' && Array.isArray(type.fields) ? type.fields.find(f => f.name === property)?.type : undefined;
|
|
28
|
+
}
|
|
29
|
+
// Result facts are not admission. These two existing inference boundaries have
|
|
30
|
+
// different evidence: plan kinds describe dynamic values; readType reconstructs
|
|
31
|
+
// proven structural types. Preserve their differences until policy is changed
|
|
32
|
+
// deliberately. Independent helper validation still owns operand/admission proof.
|
|
33
|
+
const BINARY_RULES = Object.freeze({
|
|
34
|
+
'===': 'comparison', '!==': 'comparison', '==': 'comparison', '!=': 'comparison',
|
|
35
|
+
'<': 'comparison', '<=': 'comparison', '>': 'comparison', '>=': 'comparison',
|
|
36
|
+
'&&': 'logical', '||': 'logical', '??': 'logical',
|
|
37
|
+
'+': 'numeric', '-': 'numeric', '*': 'numeric', '/': 'numeric', '%': 'numeric',
|
|
38
|
+
'**': 'numeric', '&': 'numeric', '|': 'numeric', '^': 'numeric',
|
|
39
|
+
'<<': 'numeric', '>>': 'numeric', '>>>': 'numeric'
|
|
40
|
+
});
|
|
41
|
+
const UNARY_TYPES = Object.freeze({ '!': 'boolean', typeof: 'string', '+': 'number', '-': 'number', '~': 'number' });
|
|
42
|
+
function binaryRule(operator) {
|
|
43
|
+
return Object.hasOwn(BINARY_RULES, operator) ? BINARY_RULES[operator] : undefined;
|
|
44
|
+
}
|
|
45
|
+
function binaryResult(operator, a, b, planKind, optionalStringDefault = true) {
|
|
46
|
+
const rule = binaryRule(operator);
|
|
47
|
+
const unknown = planKind ? 'unknown' : undefined;
|
|
48
|
+
if (rule === 'logical') {
|
|
49
|
+
if (planKind ? a === b : same(a, b)) return a;
|
|
50
|
+
if (optionalStringDefault && ['||', '??'].includes(operator) && (planKind
|
|
51
|
+
? a === 'string-or-undefined' && b === 'string'
|
|
52
|
+
|| operator === '??' && a === 'string' && b === 'string-or-undefined'
|
|
53
|
+
: a === 'string-or-undefined' && b === 'string')) return 'string';
|
|
54
|
+
return unknown;
|
|
55
|
+
}
|
|
56
|
+
if (rule === 'comparison' || planKind && operator === 'in') return 'boolean';
|
|
57
|
+
if (!planKind && (!scalar(a) || !scalar(b))) return undefined;
|
|
58
|
+
if (operator === '+' && (a === 'string' || b === 'string')) return 'string';
|
|
59
|
+
// The proven reader historically reports numeric result facts for scalar
|
|
60
|
+
// operands, even before operator admission. It must not become a validator.
|
|
61
|
+
return !planKind || rule === 'numeric' ? 'number' : unknown;
|
|
62
|
+
}
|
|
63
|
+
function binaryKind(operator, leftKind, rightKind, optionalStringDefault = true) {
|
|
64
|
+
return binaryResult(operator, leftKind, rightKind, true, optionalStringDefault);
|
|
65
|
+
}
|
|
66
|
+
function unaryType(operator) {
|
|
67
|
+
return Object.hasOwn(UNARY_TYPES, operator) ? UNARY_TYPES[operator] : undefined;
|
|
68
|
+
}
|
|
69
|
+
function unaryKind(operator) { return unaryType(operator) || 'number'; }
|
|
70
|
+
function element(type, indexType, literalIndex, optionalStringIndex = false) {
|
|
71
|
+
if (indexType === 'number') {
|
|
72
|
+
if (type === 'string') return optionalStringIndex ? 'string-or-undefined' : 'string';
|
|
73
|
+
if (type?.kind === 'number-array') return 'number';
|
|
74
|
+
}
|
|
75
|
+
if (typeof literalIndex === 'string') return member(type, literalIndex);
|
|
76
|
+
}
|
|
77
|
+
// Projection types are derived from declarations, literals and schema boundaries.
|
|
78
|
+
// They describe existing dynamic handles; they do not validate external data.
|
|
79
|
+
function schemaType(node) {
|
|
80
|
+
if (['string','boolean'].includes(node?.kind)) return node.kind;
|
|
81
|
+
if (['i32','u32','f64'].includes(node?.kind)) return 'number';
|
|
82
|
+
if (node?.kind === 'array' && schemaType(node.element) === 'number') return {kind:'number-array'};
|
|
83
|
+
if (node?.kind === 'object') return {kind:'record', ...(node.open || node.additionalProperties ? {open:true} : {}), fields: node.fields.map(f => ({name:f.name,
|
|
84
|
+
type: f.required ? schemaType(f.value) : schemaType(f.value) === 'string' ? 'string-or-undefined' : undefined}))};
|
|
85
|
+
}
|
|
86
|
+
function schemaRead(id, registry) {
|
|
87
|
+
return id?.kind === 'literal' && typeof id.value === 'string' ? schemaType(registry?.schemas?.find(s => s.id === id.value)?.root) : undefined;
|
|
88
|
+
}
|
|
89
|
+
function effectType(effect, registry) {
|
|
90
|
+
if (effect.kind === 'kv.getVersioned' && effect.borrowedValue?.version === CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION
|
|
91
|
+
&& validType(effect.borrowedValue.type)) return {kind:'record', fields:[
|
|
92
|
+
{name:'status',type:'string'}, {name:'value',type:effect.borrowedValue.type}]};
|
|
93
|
+
if (effect.result?.decoder?.kind === 'json') return schemaRead(effect.result.decoder.arguments?.[0], registry);
|
|
94
|
+
}
|
|
95
|
+
function readType(e, types, optionalStringIndex = false) {
|
|
96
|
+
if (!e) return;
|
|
97
|
+
const read = value => readType(value, types, optionalStringIndex);
|
|
98
|
+
if (e.kind === 'local') return types?.get(e.id);
|
|
99
|
+
if (e.kind === 'literal' && scalar(typeof e.value)) return typeof e.value;
|
|
100
|
+
if (e.kind === 'property') return member(read(e.object), e.property);
|
|
101
|
+
if (e.kind === 'element') {
|
|
102
|
+
const type = element(read(e.object), read(e.index), e.index?.kind === 'literal' ? e.index.value : undefined, optionalStringIndex);
|
|
103
|
+
if (type) return type;
|
|
104
|
+
}
|
|
105
|
+
if (e.kind === 'intrinsic' && e.name === 'request.text' && Array.isArray(e.arguments) && e.arguments.length === 0) return 'string';
|
|
106
|
+
if (e.kind === 'intrinsic' && e.name === 'state.get' && Array.isArray(e.arguments)
|
|
107
|
+
&& e.arguments.length === 1 && read(e.arguments[0]) === 'string') return 'string-or-undefined';
|
|
108
|
+
if (e.kind === 'intrinsic' && ['request.json','schema.decode.text'].includes(e.name)) return schemaRead(e.arguments?.[e.name === 'request.json' ? 0 : 1], types?.schemas);
|
|
109
|
+
if (e.kind === 'array' && e.items.every(item => read(item) === 'number')) return {kind:'number-array'};
|
|
110
|
+
if (e.kind === 'object') {
|
|
111
|
+
if (!e.entries.some(entry => entry.kind === 'spread')) {
|
|
112
|
+
if (e.entries.every(f => f.kind === 'property' && f.key?.kind === 'literal' && typeof f.key.value === 'string')
|
|
113
|
+
&& new Set(e.entries.map(f => f.key.value)).size === e.entries.length)
|
|
114
|
+
return {kind:'record',fields:e.entries.map(f=>({name:f.key.value,type:read(f.value)}))};
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const fields = new Map(), explicit = new Set();
|
|
118
|
+
for (const entry of e.entries) {
|
|
119
|
+
if (entry.kind === 'spread') {
|
|
120
|
+
const spread = read(entry.value);
|
|
121
|
+
if (spread?.kind !== 'record' || !validType(spread)) return;
|
|
122
|
+
for (const field of spread.fields) fields.set(field.name, field.type);
|
|
123
|
+
} else if (entry.kind === 'property' && entry.key?.kind === 'literal' && typeof entry.key.value === 'string'
|
|
124
|
+
&& !explicit.has(entry.key.value)) {
|
|
125
|
+
explicit.add(entry.key.value);
|
|
126
|
+
fields.set(entry.key.value, read(entry.value));
|
|
127
|
+
} else return;
|
|
128
|
+
}
|
|
129
|
+
const result = {kind:'record', fields:[...fields].map(([name,type]) => ({name,type}))};
|
|
130
|
+
if (validType(result)) return result;
|
|
131
|
+
}
|
|
132
|
+
if (e.kind === 'conditional') { const a=read(e.whenTrue),b=read(e.whenFalse); if(same(a,b))return a; }
|
|
133
|
+
if (e.kind === 'binary') return binaryResult(e.operator, read(e.left), read(e.right), false);
|
|
134
|
+
if (e.kind === 'unary') return unaryType(e.operator);
|
|
135
|
+
if (e.kind === 'template') return 'string';
|
|
136
|
+
}
|
|
137
|
+
module.exports = { SCALAR_TYPES, BORROW_LIMITS, FORBIDDEN_FIELDS, scalar, kind, validType, same, member,
|
|
138
|
+
binaryRule, binaryKind, unaryKind, element, readType, schemaType, schemaRead, effectType };
|