@intentius/chant 0.28.0 → 0.30.0
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/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +5 -3
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/config.d.ts +46 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +257 -0
- package/dist/deep-observation.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +153 -17
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +3 -2
- package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
- package/dist/env.d.ts +5 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +35 -3
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +9 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +73 -4
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +27 -0
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon.d.ts +78 -6
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +26 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-diff.d.ts +103 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -0
- package/dist/lifecycle/deep-observe.d.ts +62 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts +25 -1
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +118 -0
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -0
- package/dist/lifecycle/observe.d.ts +4 -2
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +26 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +8 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +92 -0
- package/dist/live-endpoint.d.ts.map +1 -0
- package/dist/observation.d.ts +123 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.test.ts +63 -4
- package/src/cli/handlers/components.ts +78 -35
- package/src/cli/handlers/graph.test.ts +155 -6
- package/src/cli/handlers/graph.ts +124 -29
- package/src/cli/handlers/lifecycle.test.ts +410 -6
- package/src/cli/handlers/lifecycle.ts +475 -182
- package/src/cli/main.test.ts +6 -0
- package/src/cli/main.ts +12 -0
- package/src/cli/registry.ts +14 -0
- package/src/config.test.ts +75 -0
- package/src/config.ts +61 -3
- package/src/deep-observation.test.ts +234 -0
- package/src/deep-observation.ts +489 -0
- package/src/discovery/fold-composite.test.ts +594 -0
- package/src/discovery/fold-import.test.ts +372 -1
- package/src/discovery/fold-import.ts +1216 -116
- package/src/discovery/sandbox/config-wire.ts +3 -2
- package/src/env.test.ts +12 -0
- package/src/env.ts +12 -4
- package/src/fold/fold.test.ts +105 -0
- package/src/fold/fold.ts +100 -20
- package/src/fold/subset.test.ts +38 -7
- package/src/fold/subset.ts +9 -0
- package/src/graph-ir-live.test.ts +28 -1
- package/src/graph-ir.ts +115 -12
- package/src/index.ts +2 -0
- package/src/kubectl-context.ts +81 -0
- package/src/lexicon.ts +100 -6
- package/src/lifecycle/change-set.test.ts +93 -1
- package/src/lifecycle/change-set.ts +65 -13
- package/src/lifecycle/deep-diff.test.ts +157 -0
- package/src/lifecycle/deep-diff.ts +213 -0
- package/src/lifecycle/deep-observe.test.ts +174 -0
- package/src/lifecycle/deep-observe.ts +173 -0
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/live-diff.test.ts +39 -0
- package/src/lifecycle/live-diff.ts +51 -5
- package/src/lifecycle/observation-baseline.test.ts +99 -0
- package/src/lifecycle/observation-baseline.ts +217 -0
- package/src/lifecycle/observe.test.ts +74 -3
- package/src/lifecycle/observe.ts +82 -22
- package/src/lifecycle/snapshot.test.ts +39 -1
- package/src/lifecycle/snapshot.ts +40 -20
- package/src/lifecycle/status.test.ts +89 -8
- package/src/lifecycle/status.ts +53 -3
- package/src/lifecycle/types.ts +8 -0
- package/src/live-endpoint.test.ts +115 -0
- package/src/live-endpoint.ts +148 -0
- package/src/observation.test.ts +96 -0
- package/src/observation.ts +213 -0
- package/src/stack-output.test.ts +55 -0
- package/src/stack-output.ts +41 -20
|
@@ -5,13 +5,14 @@ import { dirname, basename, join, sep, isAbsolute, resolve as resolvePath } from
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
8
|
-
import { isCompositeInstance, type CompositeInstance } from "../composite";
|
|
8
|
+
import { Composite, isCompositeInstance, type CompositeInstance, type CompositeMembers } from "../composite";
|
|
9
9
|
import { isAttrRefLike } from "../utils";
|
|
10
10
|
import { isIntrinsic } from "../intrinsic";
|
|
11
11
|
import {
|
|
12
12
|
collectConsts,
|
|
13
13
|
foldResource,
|
|
14
14
|
fold,
|
|
15
|
+
propName,
|
|
15
16
|
FoldError,
|
|
16
17
|
locate,
|
|
17
18
|
type FoldedResource,
|
|
@@ -21,7 +22,18 @@ import {
|
|
|
21
22
|
type SymbolicValue,
|
|
22
23
|
} from "../fold/fold";
|
|
23
24
|
import { isChantOwnedSpecifier, isFoldableHelperName } from "../fold/foldable-helpers";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
briefNodeText,
|
|
27
|
+
callExpressionMessage,
|
|
28
|
+
computedPropertyNameMessage,
|
|
29
|
+
isLiteralElementKey,
|
|
30
|
+
unsupportedBinaryMessage,
|
|
31
|
+
unsupportedExpressionMessage,
|
|
32
|
+
UNSUPPORTED_OBJECT_MEMBER_MESSAGE,
|
|
33
|
+
UNSUPPORTED_UNARY_MESSAGE,
|
|
34
|
+
SUPPORTED_BINARY_OPERATORS,
|
|
35
|
+
SUPPORTED_UNARY_OPERATORS,
|
|
36
|
+
} from "../fold/subset";
|
|
25
37
|
import { importModule } from "./import";
|
|
26
38
|
import type { IntrinsicDef } from "../lexicon";
|
|
27
39
|
import type { BuildParamValue } from "../build-params";
|
|
@@ -44,28 +56,37 @@ import type { BuildParamValue } from "../build-params";
|
|
|
44
56
|
* class/function; the only thing skipped here is executing the file's *own*
|
|
45
57
|
* statements.
|
|
46
58
|
*
|
|
47
|
-
* chant #
|
|
48
|
-
*
|
|
59
|
+
* chant #1022 extends this from leaf resources (`new Type(...)`) to composite
|
|
60
|
+
* factory calls — `SomeComposite({...})`,
|
|
49
61
|
* `propagate(SomeComposite({...}), {...})`, member access on the result
|
|
50
62
|
* (`web.deployment`), and destructuring (`const { a, b } = SomeComposite(...)`
|
|
51
63
|
* or `export const { a, b } = SomeComposite(...)`). A composite factory is a
|
|
52
64
|
* pure function of its props (EVL009/EVL010 guarantee its body only
|
|
53
65
|
* references props, sibling members, and imports), so — exactly like a
|
|
54
|
-
* resource constructor — it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* resource constructor — it is resolved through the file's imports and
|
|
67
|
+
* INVOKED with statically-folded props, and the RESULT is what matters: if it
|
|
68
|
+
* satisfies {@link isCompositeInstance} (or, for a plain resource-returning
|
|
69
|
+
* helper, {@link isDeclarable}), it's used. Nested composites and
|
|
70
|
+
* `propagate()`'d shared props need no special-casing there: a nested
|
|
71
|
+
* composite is just another member the real factory call already produced,
|
|
72
|
+
* and `propagate` is just another resolvable imported function that receives a
|
|
73
|
+
* live `CompositeInstance` plus folded shared props and returns it —
|
|
74
|
+
* `expandComposite()` (invoked downstream by `collectEntities`, unchanged)
|
|
75
|
+
* does the recursive expansion and the shared-prop merge exactly as it does
|
|
76
|
+
* for the run path.
|
|
77
|
+
*
|
|
78
|
+
* chant #1023 (epic #1019 Phase 5) removes that invocation where it can. When
|
|
79
|
+
* the callee is a composite defined in a PROJECT file as
|
|
80
|
+
* `Composite(<fn>, "<name>")` and `<fn>`'s body stays inside a closed,
|
|
81
|
+
* documented subset, its body is INTERPRETED instead — the defining module is
|
|
82
|
+
* never imported, and the members are built here, by the lexicon's own
|
|
83
|
+
* constructors, from the same folded props. That closes the last place a file
|
|
84
|
+
* reported as `[fold:fold]` could still execute project-authored code in the
|
|
85
|
+
* CLI's process (#1093), and lets such a file fold under `--sandbox` rather
|
|
86
|
+
* than being demoted to the child (#1111). A factory outside the subset keeps
|
|
87
|
+
* invoking, unchanged. See the contract block above
|
|
88
|
+
* {@link resolveInterpretableFactory} for the five admissibility rules and
|
|
89
|
+
* what interpretation preserves.
|
|
69
90
|
*/
|
|
70
91
|
|
|
71
92
|
/** One exported `const` name folded to a real, constructed `Declarable` or `CompositeInstance`. */
|
|
@@ -217,6 +238,25 @@ export interface FoldSession {
|
|
|
217
238
|
* only the fold half would buy nothing there.
|
|
218
239
|
*/
|
|
219
240
|
readonly sandbox: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* chant #1023 — per-build memo for {@link readFactoryModule}, keyed by the
|
|
243
|
+
* resolved absolute path of a module that DEFINES a composite. A composite
|
|
244
|
+
* defined once and called from a dozen sibling files is parsed, and its
|
|
245
|
+
* imports resolved, exactly once per build — the same reason
|
|
246
|
+
* {@link FoldSession.cache} exists for the files discovery folds directly.
|
|
247
|
+
*
|
|
248
|
+
* Separate from `cache` because the two ask different questions of the same
|
|
249
|
+
* file: `cache` asks "what are this module's exported VALUES" (and fails
|
|
250
|
+
* outright for a module that exports a function declaration, which a
|
|
251
|
+
* composite-defining module very often does); this asks "what is this
|
|
252
|
+
* module's static SCOPE" — its consts, its imports, and those imports'
|
|
253
|
+
* already-resolved values — which is well-defined even when the module as a
|
|
254
|
+
* whole doesn't fold. `lexicons/aws/examples/lambda-api/src/lambda-api.ts`
|
|
255
|
+
* is exactly that case: it exports two plain functions alongside its
|
|
256
|
+
* `Composite`, so it never folds, and its `LambdaApi` is interpretable
|
|
257
|
+
* regardless.
|
|
258
|
+
*/
|
|
259
|
+
readonly factoryModules: Map<string, Promise<FactoryModuleScope | undefined>>;
|
|
220
260
|
}
|
|
221
261
|
|
|
222
262
|
/**
|
|
@@ -256,6 +296,7 @@ export function createFoldSession(
|
|
|
256
296
|
buildParams,
|
|
257
297
|
lexiconPackages: new Set(lexicons.map(lexiconPackageName)),
|
|
258
298
|
sandbox,
|
|
299
|
+
factoryModules: new Map(),
|
|
259
300
|
};
|
|
260
301
|
}
|
|
261
302
|
|
|
@@ -392,6 +433,60 @@ function hasExportModifier(node: { modifiers?: ts.NodeArray<ts.ModifierLike> }):
|
|
|
392
433
|
return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
393
434
|
}
|
|
394
435
|
|
|
436
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
437
|
+
// Execution accounting (chant #1023).
|
|
438
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* What the fold pass actually EXECUTED, and what it interpreted instead — the
|
|
442
|
+
* number chant #1023 is measured by, and the one the #1093/#1111
|
|
443
|
+
* execution-boundary report could not state before: `--sandbox` proves
|
|
444
|
+
* *nothing project-owned ran in this process*, but plain `--fold` still
|
|
445
|
+
* invoked composite factories in-process, and nothing counted them.
|
|
446
|
+
*
|
|
447
|
+
* Purely observational. Two integer increments on paths that were already
|
|
448
|
+
* about to perform a dynamic `import()` or parse a module, so it costs
|
|
449
|
+
* nothing measurable and changes no decision.
|
|
450
|
+
*/
|
|
451
|
+
export interface FoldExecutionCounts {
|
|
452
|
+
/**
|
|
453
|
+
* Composite-factory / wrapper calls {@link resolveCallExpression} resolved
|
|
454
|
+
* by importing the defining module and CALLING it in this process.
|
|
455
|
+
*/
|
|
456
|
+
factoryInvocations: number;
|
|
457
|
+
/**
|
|
458
|
+
* Of {@link factoryInvocations}, the ones whose callee came from a PROJECT
|
|
459
|
+
* FILE (a relative/absolute specifier) rather than a lexicon package or
|
|
460
|
+
* chant's own — i.e. the ones that execute project-authored code here. Text
|
|
461
|
+
* only ({@link isProjectFileSpecifier}); no resolution is performed to
|
|
462
|
+
* classify.
|
|
463
|
+
*/
|
|
464
|
+
projectFactoryInvocations: number;
|
|
465
|
+
/**
|
|
466
|
+
* Composite factory bodies {@link interpretCompositeFactory} evaluated
|
|
467
|
+
* statically instead — each one an invocation that did NOT happen.
|
|
468
|
+
*/
|
|
469
|
+
factoryInterpretations: number;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const executionCounts: FoldExecutionCounts = {
|
|
473
|
+
factoryInvocations: 0,
|
|
474
|
+
projectFactoryInvocations: 0,
|
|
475
|
+
factoryInterpretations: 0,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/** A snapshot of {@link FoldExecutionCounts}. Process-wide and monotonic — a caller wanting a per-build figure calls {@link resetFoldExecutionCounts} first. */
|
|
479
|
+
export function foldExecutionCounts(): Readonly<FoldExecutionCounts> {
|
|
480
|
+
return { ...executionCounts };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Zero {@link foldExecutionCounts}, for a caller measuring one build. */
|
|
484
|
+
export function resetFoldExecutionCounts(): void {
|
|
485
|
+
executionCounts.factoryInvocations = 0;
|
|
486
|
+
executionCounts.projectFactoryInvocations = 0;
|
|
487
|
+
executionCounts.factoryInterpretations = 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
395
490
|
/**
|
|
396
491
|
* True for any non-null, non-array object — a real `CompositeInstance`, a
|
|
397
492
|
* real `Declarable`, or (chant #1020) the synthetic plain object a
|
|
@@ -1049,6 +1144,23 @@ interface ResolveCtx {
|
|
|
1049
1144
|
lexiconPackages: ReadonlySet<string>;
|
|
1050
1145
|
/** chant #1093 — see {@link FoldSession.sandbox}. */
|
|
1051
1146
|
sandbox: boolean;
|
|
1147
|
+
/**
|
|
1148
|
+
* chant #1023 — the whole build session, for the two things composite-factory
|
|
1149
|
+
* interpretation needs that a per-file context cannot carry: the
|
|
1150
|
+
* {@link FoldSession.factoryModules} memo (a composite's defining module is
|
|
1151
|
+
* parsed once per BUILD, not once per calling file) and
|
|
1152
|
+
* {@link FoldSession.stack} (so a factory whose module is already being
|
|
1153
|
+
* resolved further up the same chain is a detected cycle, not a hang).
|
|
1154
|
+
*/
|
|
1155
|
+
session: FoldSession;
|
|
1156
|
+
/**
|
|
1157
|
+
* chant #1023 — how many composite factory bodies are being interpreted
|
|
1158
|
+
* around this context, 0 at a file's own top level. A composite that
|
|
1159
|
+
* (directly or through its members) calls itself has no fixpoint and no file
|
|
1160
|
+
* boundary for {@link FoldSession.stack} to notice, so this is what
|
|
1161
|
+
* terminates it — see {@link MAX_INTERPRETATION_DEPTH}.
|
|
1162
|
+
*/
|
|
1163
|
+
interpretDepth: number;
|
|
1052
1164
|
}
|
|
1053
1165
|
|
|
1054
1166
|
/** `{ value }` when `node`'s shape was recognized and resolved (value may itself be `undefined`/`null` — e.g. an optional composite member that wasn't created); `undefined` when the shape isn't one the live resolver understands (a plain literal, etc.) — callers fall back to the original, unchanged handling for that shape. */
|
|
@@ -1127,7 +1239,7 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
|
|
|
1127
1239
|
}
|
|
1128
1240
|
|
|
1129
1241
|
/**
|
|
1130
|
-
* Resolve
|
|
1242
|
+
* Resolve a bare call expression — a composite factory call
|
|
1131
1243
|
* (`SomeComposite({...})`) or a wrapper that takes a composite instance and
|
|
1132
1244
|
* returns one (`propagate(SomeComposite({...}), {...})`). The callee must be
|
|
1133
1245
|
* a plain identifier bound by this file's own `import` (a namespace-import
|
|
@@ -1135,13 +1247,25 @@ async function resolveLiveValue(node: ts.Expression, ctx: ResolveCtx): Promise<L
|
|
|
1135
1247
|
* same file, can't be resolved without running the file — falls back, same
|
|
1136
1248
|
* as an unresolvable resource constructor).
|
|
1137
1249
|
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1250
|
+
* Two ways to get the value, tried in that order:
|
|
1251
|
+
*
|
|
1252
|
+
* 1. **Interpretation** (chant #1023) — the callee is an interpretable
|
|
1253
|
+
* registered `Composite` defined in a PROJECT file, so its body is
|
|
1254
|
+
* evaluated statically and nothing is imported or run. See
|
|
1255
|
+
* {@link resolveInterpretableFactory} for the exact admissible subset.
|
|
1256
|
+
* 2. **Invocation** (chant #1022) — everything else. Each argument is
|
|
1257
|
+
* resolved (recursively, for a nested composite-call/member-access
|
|
1258
|
+
* argument like `propagate`'s first one) or folded (for a plain props
|
|
1259
|
+
* object literal via {@link fold}), the real function is imported and
|
|
1260
|
+
* invoked, and the RESULT is what matters to the caller —
|
|
1261
|
+
* {@link resolveLiveValue}'s callers decide what shape they need (a
|
|
1262
|
+
* `CompositeInstance` for member access, an
|
|
1263
|
+
* `isDeclarable`/`isCompositeInstance` value for a top-level export).
|
|
1264
|
+
* No pre-check verifies the resolved callee is "really" a composite.
|
|
1265
|
+
*
|
|
1266
|
+
* Arm 2 is untouched by #1023, including for a factory arm 1 STARTED and then
|
|
1267
|
+
* declined on: interpretation is all-or-nothing per call, and a decline is not
|
|
1268
|
+
* a fold failure — it lands exactly where the code landed before this existed.
|
|
1145
1269
|
*/
|
|
1146
1270
|
async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx): Promise<unknown> {
|
|
1147
1271
|
// chant #1054 — reuses ../fold/subset's `callExpressionMessage` (the SAME
|
|
@@ -1158,6 +1282,43 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1158
1282
|
throw cheapError(callExpressionMessage(node));
|
|
1159
1283
|
}
|
|
1160
1284
|
|
|
1285
|
+
// chant #1023 — is this callee a composite whose body can be INTERPRETED
|
|
1286
|
+
// rather than run? Purely a static question (does the defining module parse,
|
|
1287
|
+
// does it declare this export as `Composite(<fn>, "<name>")`, is `<fn>`'s
|
|
1288
|
+
// body inside the admissible subset) — no argument has been evaluated yet
|
|
1289
|
+
// and nothing has been imported, so a `undefined` here has cost nothing and
|
|
1290
|
+
// changed nothing.
|
|
1291
|
+
const factory = await resolveInterpretableFactory(binding, ctx);
|
|
1292
|
+
|
|
1293
|
+
if (factory) {
|
|
1294
|
+
const args = await resolveCallArguments(node, calleeName, ctx);
|
|
1295
|
+
const interpreted = await interpretCompositeFactory(factory, args, ctx);
|
|
1296
|
+
if (interpreted) return interpreted.value;
|
|
1297
|
+
// Declined — the body's shape passed but something in it did not resolve
|
|
1298
|
+
// (an identifier the module's own imports don't reach, a constructor
|
|
1299
|
+
// --sandbox refuses). Fall through to arm 2 with the arguments ALREADY
|
|
1300
|
+
// evaluated, so a nested composite-call argument is not built twice.
|
|
1301
|
+
return invokeImportedCallee(node, calleeName, binding, ctx, args);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return invokeImportedCallee(node, calleeName, binding, ctx);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Arm 2 — import the module the callee came from and call it, in this process.
|
|
1309
|
+
* Unchanged from #1022 in both behavior and ORDER (refusal, resolve, import,
|
|
1310
|
+
* callable check, arguments, call); #1023 only factored it out so the
|
|
1311
|
+
* interpretation arm above can fall back into it.
|
|
1312
|
+
*
|
|
1313
|
+
* @param args - Already-evaluated arguments, when the caller has them.
|
|
1314
|
+
*/
|
|
1315
|
+
async function invokeImportedCallee(
|
|
1316
|
+
node: ts.CallExpression,
|
|
1317
|
+
calleeName: string,
|
|
1318
|
+
binding: ImportBinding,
|
|
1319
|
+
ctx: ResolveCtx,
|
|
1320
|
+
args?: readonly unknown[],
|
|
1321
|
+
): Promise<unknown> {
|
|
1161
1322
|
// chant #1093 — THE gap this check exists for. Invoking the callee runs
|
|
1162
1323
|
// project code (the factory body, and its whole module's top level) in the
|
|
1163
1324
|
// CLI's own process; under --sandbox that has to happen in the child
|
|
@@ -1202,21 +1363,43 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1202
1363
|
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a function`);
|
|
1203
1364
|
}
|
|
1204
1365
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1366
|
+
const callArgs = args ?? (await resolveCallArguments(node, calleeName, ctx));
|
|
1367
|
+
|
|
1368
|
+
executionCounts.factoryInvocations += 1;
|
|
1369
|
+
if (isProjectFileSpecifier(binding.specifier)) executionCounts.projectFactoryInvocations += 1;
|
|
1370
|
+
|
|
1371
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(...callArgs);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Resolve a call's arguments to the real values the callee would receive —
|
|
1376
|
+
* factored out of {@link resolveCallExpression} (chant #1023) so the
|
|
1377
|
+
* interpretation and invocation arms consume ONE evaluation of them. Anything
|
|
1378
|
+
* else would evaluate a nested composite-call argument twice and hand the
|
|
1379
|
+
* callee the second instance while the first was already wired into an
|
|
1380
|
+
* `AttrRef`.
|
|
1381
|
+
*
|
|
1382
|
+
* chant #1112 — ONE rule for a registered authoring helper's arguments,
|
|
1383
|
+
* applied at BOTH sites that can invoke one. `reviveHelperCall` (the
|
|
1384
|
+
* nested-value site, #1082) already revives a helper's arguments with
|
|
1385
|
+
* `requireLiveRefs`, because a helper reads THROUGH its ref (`output()`
|
|
1386
|
+
* derefs the `WeakRef` parent) and a look-alike `{__attrRef}` envelope
|
|
1387
|
+
* makes it produce a wrong result rather than none. This site — a
|
|
1388
|
+
* top-level `export const oArn = output(bucket.Arn, "oArn")` — took the
|
|
1389
|
+
* composite-factory rule (`false`) instead, which is right for a factory
|
|
1390
|
+
* (its props keep the envelope, and the serializer's own walker resolves
|
|
1391
|
+
* it) and wrong for a helper. It went unnoticed while the resulting
|
|
1392
|
+
* `LexiconOutput` was being discarded anyway; with the export namespace
|
|
1393
|
+
* now collected in full, a same-file `output(...)` would reach the
|
|
1394
|
+
* serializer holding an inert envelope where the run path has a real
|
|
1395
|
+
* reference. Rejected here instead, which falls the file back to run —
|
|
1396
|
+
* absent output, never a wrong one.
|
|
1397
|
+
*/
|
|
1398
|
+
async function resolveCallArguments(
|
|
1399
|
+
node: ts.CallExpression,
|
|
1400
|
+
calleeName: string,
|
|
1401
|
+
ctx: ResolveCtx,
|
|
1402
|
+
): Promise<unknown[]> {
|
|
1220
1403
|
const helperArgs = isFoldableHelperName(calleeName);
|
|
1221
1404
|
const args: unknown[] = [];
|
|
1222
1405
|
for (const argNode of node.arguments) {
|
|
@@ -1231,8 +1414,773 @@ async function resolveCallExpression(node: ts.CallExpression, ctx: ResolveCtx):
|
|
|
1231
1414
|
: await reviveFoldedValue(fold(argNode, ctx.consts, ctx.intrinsics, ctx.externals), ctx, helperArgs),
|
|
1232
1415
|
);
|
|
1233
1416
|
}
|
|
1417
|
+
return args;
|
|
1418
|
+
}
|
|
1234
1419
|
|
|
1235
|
-
|
|
1420
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1421
|
+
// Composite factory interpretation (chant #1023, epic #1019 Phase 5).
|
|
1422
|
+
//
|
|
1423
|
+
// THE CONTRACT — the admissible factory subset, in ../fold/subset.ts's style.
|
|
1424
|
+
//
|
|
1425
|
+
// A composite is a factory function, and #1022 got its value the only way it
|
|
1426
|
+
// could: by importing the defining module and CALLING it. That is the last
|
|
1427
|
+
// place `chant build --fold` executes project-authored code in the CLI's own
|
|
1428
|
+
// process (chant #1093), and the reason `--sandbox` has to demote such a file
|
|
1429
|
+
// to the sandboxed child instead of folding it (chant #1111). This section
|
|
1430
|
+
// removes the call for the factories whose bodies can be evaluated instead.
|
|
1431
|
+
//
|
|
1432
|
+
// A call is interpreted when ALL of the following hold. Every one of them is
|
|
1433
|
+
// checked BEFORE any argument is evaluated or anything is constructed, so a
|
|
1434
|
+
// factory outside the subset costs a parse and nothing else:
|
|
1435
|
+
//
|
|
1436
|
+
// 1. **The callee is bound by an `import` from a PROJECT FILE** (a
|
|
1437
|
+
// relative/absolute specifier). A lexicon-package composite is
|
|
1438
|
+
// deliberately NOT interpreted, for two independent reasons: an installed
|
|
1439
|
+
// lexicon ships compiled JS, so whether its factory bodies were
|
|
1440
|
+
// interpretable would depend on whether the package happened to ship
|
|
1441
|
+
// `.ts` — fold coverage must not vary with a dependency's build shape —
|
|
1442
|
+
// and a lexicon package is on {@link isTrustedExecutableBinding}'s
|
|
1443
|
+
// allowlist already, so calling it is exactly as safe under `--sandbox` as
|
|
1444
|
+
// the `loadPlugins` import the CLI performed before discovery began. There
|
|
1445
|
+
// is nothing to buy and a build-shape dependency to lose.
|
|
1446
|
+
// 2. **The defining module declares that export as `Composite(<fn>, "<name>")`**
|
|
1447
|
+
// — a top-level `export const <name> = Composite(...)`, where `Composite`
|
|
1448
|
+
// is bound in THAT module to an import of chant's own
|
|
1449
|
+
// ({@link isChantOwnedHelperBinding}, the same provenance question #1082
|
|
1450
|
+
// asks of an authoring helper, and the same answer). This is #1023's
|
|
1451
|
+
// "recognize the callee is a registered Composite": a bare call to
|
|
1452
|
+
// anything else — a project helper function, a `withDefaults(...)`
|
|
1453
|
+
// wrapper, a re-exported binding — is not recognized and is not
|
|
1454
|
+
// interpreted.
|
|
1455
|
+
// 3. **`<fn>` is an arrow/function expression taking at most one parameter**,
|
|
1456
|
+
// bound as a plain identifier or a simple object binding pattern (no
|
|
1457
|
+
// default, rest, or nested pattern).
|
|
1458
|
+
// 4. **Its body is a single expression, or a block of `const` declarations
|
|
1459
|
+
// followed by one `return`** — and nothing else. No `if`, no `throw`, no
|
|
1460
|
+
// loop, no `let`/`var`, no nested function declaration, no bare expression
|
|
1461
|
+
// statement. This is the line loomster's `composites/*.ts` fall outside
|
|
1462
|
+
// (module-level `buildXxx()` helpers with `if`/`throw` and `.map()`), and
|
|
1463
|
+
// they are meant to: they keep invoking, exactly as before.
|
|
1464
|
+
// 5. **Every expression in it is inside the fold subset, extended with the
|
|
1465
|
+
// two things a factory body exists to do**: `new Type(...)` in ANY value
|
|
1466
|
+
// position (a member, a nested property object, an array element), and a
|
|
1467
|
+
// call through a bare identifier. Both are shape-admissible here and
|
|
1468
|
+
// resolution-checked at evaluation, the same permissive-shape /
|
|
1469
|
+
// strict-evaluation split ../fold/subset.ts documents for identifier
|
|
1470
|
+
// binding and helper provenance.
|
|
1471
|
+
//
|
|
1472
|
+
// Everything else is out: a computed key, a method call (`naming.name(...)` —
|
|
1473
|
+
// the callee is a property access, not an identifier), an array method, an
|
|
1474
|
+
// operator `fold()` doesn't implement, `await`, a class expression, a template
|
|
1475
|
+
// with a computed tag.
|
|
1476
|
+
//
|
|
1477
|
+
// WHY THIS IS NOT IN ../fold/subset.ts. That module is the shared, shape-only
|
|
1478
|
+
// predicate `fold()` and EVL both read, and its own contract is that it may
|
|
1479
|
+
// only ever be PERMISSIVE relative to `fold()`, never stricter. Admissibility
|
|
1480
|
+
// here is not a shape question at all: rules 1 and 2 need the module graph —
|
|
1481
|
+
// which file a name came from, and what that file declares — exactly like
|
|
1482
|
+
// #1082's authoring-helper provenance, which subset.ts documents (point 2b)
|
|
1483
|
+
// as deliberately out for the same reason. Putting rules 3-5 there alone would
|
|
1484
|
+
// describe a subset no caller could act on without also answering 1 and 2, and
|
|
1485
|
+
// putting all five there would mean teaching a syntax-only lint rule to
|
|
1486
|
+
// resolve imports. The shape half stays checkable by anything that wants it —
|
|
1487
|
+
// {@link findFactorySubsetViolation} takes a lone `ts` node — but the shared
|
|
1488
|
+
// predicate is not made stricter, or wider, by this issue.
|
|
1489
|
+
//
|
|
1490
|
+
// WHAT IS PRESERVED. Interpretation is not a second construction path:
|
|
1491
|
+
//
|
|
1492
|
+
// - **The instances are real, and made by the lexicon's own constructors.**
|
|
1493
|
+
// A `new Role({...})` in the body resolves `Role` through the DEFINING
|
|
1494
|
+
// module's imports and calls that class, the same class the run path would
|
|
1495
|
+
// have called from the same resolved module path.
|
|
1496
|
+
// - **Sibling references are live, not symbolic.** `role.Arn` reads the
|
|
1497
|
+
// attribute off the `Role` instance this interpretation just built, so it
|
|
1498
|
+
// is a genuine `AttrRef` wired to a genuine parent — identical to running
|
|
1499
|
+
// the factory, and strictly better than a top-level fold's `{__attrRef}`
|
|
1500
|
+
// envelope (an intrinsic that inspects its argument, `Sub`/`Ref`, gets what
|
|
1501
|
+
// it expects here).
|
|
1502
|
+
// - **The instance is assembled by `Composite()` itself.** The members go
|
|
1503
|
+
// back through chant's own {@link Composite} (../composite.ts) rather than
|
|
1504
|
+
// through a hand-built look-alike, so member validation, the non-enumerable
|
|
1505
|
+
// `members`/`_definition` layout `propagate()` and `expandComposite()`
|
|
1506
|
+
// depend on, and provenance stamping are the run path's, by construction
|
|
1507
|
+
// and not by resemblance.
|
|
1508
|
+
// - **`propagate()` still works, untouched.** It is a chant-owned import, so
|
|
1509
|
+
// `propagate(SomeComposite({...}), {...})` invokes the real `propagate` on
|
|
1510
|
+
// the interpreted instance and mutates it in place exactly as it does a run
|
|
1511
|
+
// one — which is why instance identity has to be a real `CompositeInstance`
|
|
1512
|
+
// and not a copy (chant #1097).
|
|
1513
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* A backstop for a composite that calls itself, directly or through a member.
|
|
1517
|
+
* {@link FoldSession.stack} cannot see it — the recursion happens entirely
|
|
1518
|
+
* inside one module's source, crossing no file boundary — so this is what
|
|
1519
|
+
* terminates it. Deliberately small: real composite nesting is 2-3 deep, and a
|
|
1520
|
+
* chain past this is a bug, not a design.
|
|
1521
|
+
*/
|
|
1522
|
+
const MAX_INTERPRETATION_DEPTH = 16;
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* The static scope of a module that DEFINES composites — everything
|
|
1526
|
+
* interpreting one of its factory bodies needs, computed once per module per
|
|
1527
|
+
* build (see {@link FoldSession.factoryModules}).
|
|
1528
|
+
*
|
|
1529
|
+
* Note what is NOT here: the module's exported VALUES. Interpreting a factory
|
|
1530
|
+
* never needs them, which is the whole point — a module can define a perfectly
|
|
1531
|
+
* interpretable composite and still be unfoldable as a module (an exported
|
|
1532
|
+
* function declaration alongside it, say), and those two facts are
|
|
1533
|
+
* independent.
|
|
1534
|
+
*/
|
|
1535
|
+
interface FactoryModuleScope {
|
|
1536
|
+
file: string;
|
|
1537
|
+
sourceFile: ts.SourceFile;
|
|
1538
|
+
/**
|
|
1539
|
+
* The module's top-level `const`s, MINUS every one that resolves to a
|
|
1540
|
+
* `new Type(...)` resource.
|
|
1541
|
+
*
|
|
1542
|
+
* The exclusion is the load-bearing part. `fold()` turns a property access
|
|
1543
|
+
* on a resource-valued const into a symbolic `{__attrRef, entity: "<the
|
|
1544
|
+
* const's name>"}` — a name resolved much later, against the entity table of
|
|
1545
|
+
* the file being COLLECTED, which is the calling file and not this one. A
|
|
1546
|
+
* module-level resource shared by every call of a factory is also a
|
|
1547
|
+
* singleton whose identity the run path shares and interpretation would not.
|
|
1548
|
+
* Dropping those consts makes any reference to one an ordinary "unresolved
|
|
1549
|
+
* identifier" failure, which declines the interpretation and invokes
|
|
1550
|
+
* instead — the answer that is right on both counts.
|
|
1551
|
+
*/
|
|
1552
|
+
consts: Map<string, ts.Expression>;
|
|
1553
|
+
imports: Map<string, ImportBinding>;
|
|
1554
|
+
namespaceImports: Map<string, NamespaceImportBinding>;
|
|
1555
|
+
/**
|
|
1556
|
+
* This module's own imports, resolved to their real cross-file values (see
|
|
1557
|
+
* {@link ResolveCtx.externals}) — LAZILY, and memoized here once built.
|
|
1558
|
+
*
|
|
1559
|
+
* Laziness is not an optimization detail, it is what keeps this issue from
|
|
1560
|
+
* paying #1020's cost all over again. `resolveCallExpression` reaches
|
|
1561
|
+
* {@link resolveInterpretableFactory} for EVERY call through a project-file
|
|
1562
|
+
* import, the overwhelming majority of which are not composites at all. Only
|
|
1563
|
+
* the parse is spent finding that out; resolving a module's whole import
|
|
1564
|
+
* graph — which recursively folds every project file it names — is spent
|
|
1565
|
+
* only by a call that is actually about to be interpreted.
|
|
1566
|
+
*/
|
|
1567
|
+
resolved?: Promise<{ externals: Map<string, unknown>; failures: Map<string, string> }>;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/** An admissible composite factory: where it lives, what it is called, and the function to interpret. */
|
|
1571
|
+
interface InterpretableFactory {
|
|
1572
|
+
scope: FactoryModuleScope;
|
|
1573
|
+
fn: ts.ArrowFunction | ts.FunctionExpression;
|
|
1574
|
+
/** `Composite()`'s second argument, or `"anonymous"` when it has none — matching {@link Composite}'s own default. */
|
|
1575
|
+
compositeName: string;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Parse a composite-defining module into its static scope, memoized per build.
|
|
1580
|
+
* Returns `undefined` when the file can't be read or parsed — a decline, never
|
|
1581
|
+
* a throw: the caller falls through to the invocation arm, which will produce
|
|
1582
|
+
* its own (identical, pre-#1023) error if the module is genuinely broken.
|
|
1583
|
+
*
|
|
1584
|
+
* Reads and parses only. Nothing is imported, nothing is executed, and the
|
|
1585
|
+
* module's own imports are not resolved yet — see
|
|
1586
|
+
* {@link FactoryModuleScope.resolved}.
|
|
1587
|
+
*/
|
|
1588
|
+
function readFactoryModule(modulePath: string, session: FoldSession): Promise<FactoryModuleScope | undefined> {
|
|
1589
|
+
const cached = session.factoryModules.get(modulePath);
|
|
1590
|
+
if (cached) return cached;
|
|
1591
|
+
const promise = readFactoryModuleCore(modulePath);
|
|
1592
|
+
session.factoryModules.set(modulePath, promise);
|
|
1593
|
+
return promise;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
async function readFactoryModuleCore(modulePath: string): Promise<FactoryModuleScope | undefined> {
|
|
1597
|
+
let sourceFile: ts.SourceFile;
|
|
1598
|
+
try {
|
|
1599
|
+
const source = await readFile(modulePath, "utf-8");
|
|
1600
|
+
sourceFile = ts.createSourceFile(modulePath, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
1601
|
+
} catch {
|
|
1602
|
+
// Not a readable TypeScript source — an installed package's compiled
|
|
1603
|
+
// entry point reached through a path specifier, a missing file. Decline.
|
|
1604
|
+
return undefined;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const collected = collectImports(sourceFile);
|
|
1608
|
+
const consts = collectConsts(sourceFile);
|
|
1609
|
+
for (const [name] of [...consts]) {
|
|
1610
|
+
if (constResolvesToResource(consts, name, new Set())) consts.delete(name);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
return {
|
|
1614
|
+
file: modulePath,
|
|
1615
|
+
sourceFile,
|
|
1616
|
+
consts,
|
|
1617
|
+
imports: collected.named,
|
|
1618
|
+
namespaceImports: collected.namespaces,
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Resolve a composite-defining module's own imports, once per module per
|
|
1624
|
+
* build. Guarded by {@link FoldSession.stack}, exactly like
|
|
1625
|
+
* {@link foldFileMemoized}: a module already being resolved further up the
|
|
1626
|
+
* same chain is a genuine cycle, and gets an empty scope (every identifier
|
|
1627
|
+
* then reads as unresolved, which declines the interpretation) rather than a
|
|
1628
|
+
* re-entry.
|
|
1629
|
+
*/
|
|
1630
|
+
function factoryModuleScopeResolved(
|
|
1631
|
+
scope: FactoryModuleScope,
|
|
1632
|
+
session: FoldSession,
|
|
1633
|
+
): Promise<{ externals: Map<string, unknown>; failures: Map<string, string> }> {
|
|
1634
|
+
if (scope.resolved) return scope.resolved;
|
|
1635
|
+
if (session.stack.includes(scope.file) || session.stack.length >= MAX_RESOLUTION_DEPTH) {
|
|
1636
|
+
return Promise.resolve({ externals: new Map(), failures: new Map() });
|
|
1637
|
+
}
|
|
1638
|
+
session.stack.push(scope.file);
|
|
1639
|
+
scope.resolved = buildExternals(scope.file, scope.imports, scope.namespaceImports, session).finally(() => {
|
|
1640
|
+
const idx = session.stack.lastIndexOf(scope.file);
|
|
1641
|
+
if (idx !== -1) session.stack.splice(idx, 1);
|
|
1642
|
+
});
|
|
1643
|
+
return scope.resolved;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* True when a module-level `const` is (transitively) bound to a
|
|
1648
|
+
* `new Type(...)`. Mirrors `fold()`'s own `resolvesToResource`, but follows an
|
|
1649
|
+
* identifier chain (`const a = new T(); const b = a;`) so aliasing can't smuggle
|
|
1650
|
+
* a module-level resource into a factory body — see
|
|
1651
|
+
* {@link FactoryModuleScope.consts}.
|
|
1652
|
+
*/
|
|
1653
|
+
function constResolvesToResource(
|
|
1654
|
+
consts: Map<string, ts.Expression>,
|
|
1655
|
+
name: string,
|
|
1656
|
+
seen: Set<string>,
|
|
1657
|
+
): boolean {
|
|
1658
|
+
if (seen.has(name)) return false;
|
|
1659
|
+
seen.add(name);
|
|
1660
|
+
const init = consts.get(name);
|
|
1661
|
+
if (init === undefined) return false;
|
|
1662
|
+
if (ts.isNewExpression(init)) return true;
|
|
1663
|
+
if (ts.isIdentifier(init)) return constResolvesToResource(consts, init.text, seen);
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Find `export const <exportName> = Composite(<fn>, "<name>")` at the top level
|
|
1669
|
+
* of `scope`'s module, verifying that `Composite` really is chant's own in that
|
|
1670
|
+
* module. Returns `undefined` for every other shape — see rule 2 of the
|
|
1671
|
+
* contract above.
|
|
1672
|
+
*/
|
|
1673
|
+
function findCompositeDefinition(
|
|
1674
|
+
scope: FactoryModuleScope,
|
|
1675
|
+
exportName: string,
|
|
1676
|
+
ctx: ResolveCtx,
|
|
1677
|
+
): { fn: ts.ArrowFunction | ts.FunctionExpression; compositeName: string } | undefined {
|
|
1678
|
+
for (const statement of scope.sourceFile.statements) {
|
|
1679
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
1680
|
+
if (!hasExportModifier(statement)) continue;
|
|
1681
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
1682
|
+
|
|
1683
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1684
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== exportName) continue;
|
|
1685
|
+
const init = decl.initializer;
|
|
1686
|
+
if (!init || !ts.isCallExpression(init) || !ts.isIdentifier(init.expression)) return undefined;
|
|
1687
|
+
|
|
1688
|
+
// `Composite` must be bound, in THIS module, to an import of chant's
|
|
1689
|
+
// own — the identical provenance question #1082 asks of an authoring
|
|
1690
|
+
// helper, answered by the identical predicate. A project-local
|
|
1691
|
+
// `function Composite(...)` shadowing the name is not chant's, and a
|
|
1692
|
+
// call to it is not a registered composite.
|
|
1693
|
+
const compositeBinding = scope.imports.get(init.expression.text);
|
|
1694
|
+
if (!compositeBinding || compositeBinding.imported !== "Composite") return undefined;
|
|
1695
|
+
if (!isChantOwnedHelperBinding(compositeBinding, { ...ctx, file: scope.file })) return undefined;
|
|
1696
|
+
|
|
1697
|
+
const [fnArg, nameArg] = init.arguments;
|
|
1698
|
+
if (!fnArg || (!ts.isArrowFunction(fnArg) && !ts.isFunctionExpression(fnArg))) return undefined;
|
|
1699
|
+
// `Composite()`'s own default when the name is omitted (../composite.ts).
|
|
1700
|
+
// COR017 requires the literal in practice; anything that is not a plain
|
|
1701
|
+
// string literal is not something to guess at.
|
|
1702
|
+
if (nameArg !== undefined && !ts.isStringLiteral(nameArg)) return undefined;
|
|
1703
|
+
return { fn: fnArg, compositeName: nameArg ? nameArg.text : "anonymous" };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Decide whether `binding`'s callee is an interpretable composite — rules 1-5
|
|
1711
|
+
* of the contract above, in that order, with no evaluation and no import.
|
|
1712
|
+
* `undefined` means "not interpretable", never "broken".
|
|
1713
|
+
*/
|
|
1714
|
+
async function resolveInterpretableFactory(
|
|
1715
|
+
binding: ImportBinding,
|
|
1716
|
+
ctx: ResolveCtx,
|
|
1717
|
+
): Promise<InterpretableFactory | undefined> {
|
|
1718
|
+
// Rule 1 — project files only. A text check; no resolution performed for a
|
|
1719
|
+
// bare specifier, so this costs nothing for the (common) lexicon case.
|
|
1720
|
+
if (!isProjectFileSpecifier(binding.specifier)) return undefined;
|
|
1721
|
+
if (ctx.interpretDepth >= MAX_INTERPRETATION_DEPTH) return undefined;
|
|
1722
|
+
|
|
1723
|
+
let modulePath: string;
|
|
1724
|
+
try {
|
|
1725
|
+
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return undefined;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const scope = await readFactoryModule(modulePath, ctx.session);
|
|
1731
|
+
if (!scope) return undefined;
|
|
1732
|
+
|
|
1733
|
+
// Rule 2.
|
|
1734
|
+
const definition = findCompositeDefinition(scope, binding.imported, ctx);
|
|
1735
|
+
if (!definition) return undefined;
|
|
1736
|
+
|
|
1737
|
+
// Rules 3-5.
|
|
1738
|
+
if (findFactorySubsetViolation(definition.fn) !== undefined) return undefined;
|
|
1739
|
+
|
|
1740
|
+
return { scope, fn: definition.fn, compositeName: definition.compositeName };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* The SHAPE half of the contract (rules 3-5) — a lone `ts` node in, a reason
|
|
1745
|
+
* string out, or `undefined` when the factory is admissible. Deliberately
|
|
1746
|
+
* takes nothing but the node: it is the half a caller with no module graph
|
|
1747
|
+
* could evaluate, and keeping it separable is what lets the doc above claim
|
|
1748
|
+
* the shape rules are checkable without the provenance ones.
|
|
1749
|
+
*/
|
|
1750
|
+
export function findFactorySubsetViolation(fn: ts.ArrowFunction | ts.FunctionExpression): string | undefined {
|
|
1751
|
+
// Rule 3 — at most one parameter, bound plainly.
|
|
1752
|
+
if (fn.parameters.length > 1) return "a composite factory takes a single props parameter";
|
|
1753
|
+
const param = fn.parameters[0];
|
|
1754
|
+
if (param) {
|
|
1755
|
+
if (param.dotDotDotToken) return "a rest parameter is not interpretable";
|
|
1756
|
+
if (param.initializer) return "a defaulted parameter is not interpretable";
|
|
1757
|
+
if (ts.isObjectBindingPattern(param.name)) {
|
|
1758
|
+
for (const el of param.name.elements) {
|
|
1759
|
+
if (bindingElementPropKey(el) === undefined) {
|
|
1760
|
+
return "a destructured props parameter with a rest, default, or nested element is not interpretable";
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
} else if (!ts.isIdentifier(param.name)) {
|
|
1764
|
+
return "an array-destructured props parameter is not interpretable";
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Rule 4 — a concise expression body, or `const`s then one `return`.
|
|
1769
|
+
if (!ts.isBlock(fn.body)) return checkFactoryExpression(fn.body);
|
|
1770
|
+
|
|
1771
|
+
const statements = fn.body.statements;
|
|
1772
|
+
if (statements.length === 0) return "an empty composite factory body has no members to interpret";
|
|
1773
|
+
for (let i = 0; i < statements.length; i += 1) {
|
|
1774
|
+
const statement = statements[i];
|
|
1775
|
+
const last = i === statements.length - 1;
|
|
1776
|
+
|
|
1777
|
+
if (ts.isReturnStatement(statement)) {
|
|
1778
|
+
if (!last) return "an early `return` is not interpretable";
|
|
1779
|
+
if (!statement.expression) return "a composite factory must return its members";
|
|
1780
|
+
const violation = checkFactoryExpression(statement.expression);
|
|
1781
|
+
if (violation) return violation;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (last) return "a composite factory body must end in `return`";
|
|
1785
|
+
|
|
1786
|
+
if (!ts.isVariableStatement(statement)) {
|
|
1787
|
+
return `\`${ts.SyntaxKind[statement.kind]}\` in a composite factory body is not interpretable`;
|
|
1788
|
+
}
|
|
1789
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
1790
|
+
return "`let`/`var` in a composite factory body is not interpretable";
|
|
1791
|
+
}
|
|
1792
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1793
|
+
if (!decl.initializer) return "an uninitialized `const` in a composite factory body is not interpretable";
|
|
1794
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
1795
|
+
for (const el of decl.name.elements) {
|
|
1796
|
+
if (bindingElementPropKey(el) === undefined) {
|
|
1797
|
+
return "a destructured `const` with a rest, default, or nested element is not interpretable";
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} else if (!ts.isIdentifier(decl.name)) {
|
|
1801
|
+
return "an array-destructured `const` is not interpretable";
|
|
1802
|
+
}
|
|
1803
|
+
const violation = checkFactoryExpression(decl.initializer);
|
|
1804
|
+
if (violation) return violation;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return undefined;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Rule 5 — the fold subset, extended with `new Type(...)` in any value
|
|
1812
|
+
* position and a call through a bare identifier.
|
|
1813
|
+
*
|
|
1814
|
+
* Shape only, in ../fold/subset.ts's sense: whether a `new`'s constructor or a
|
|
1815
|
+
* call's callee actually resolves to something invocable is settled at
|
|
1816
|
+
* evaluation, where the module graph is available. Every operator/key/member
|
|
1817
|
+
* rule is read from ../fold/subset.ts's own exported sets rather than
|
|
1818
|
+
* re-listed, so widening `fold()` widens this in the same commit.
|
|
1819
|
+
*/
|
|
1820
|
+
function checkFactoryExpression(node: ts.Expression): string | undefined {
|
|
1821
|
+
if (
|
|
1822
|
+
ts.isParenthesizedExpression(node) ||
|
|
1823
|
+
ts.isAsExpression(node) ||
|
|
1824
|
+
ts.isSatisfiesExpression(node) ||
|
|
1825
|
+
ts.isNonNullExpression(node) ||
|
|
1826
|
+
ts.isTypeAssertionExpression(node)
|
|
1827
|
+
) {
|
|
1828
|
+
return checkFactoryExpression(node.expression);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (
|
|
1832
|
+
ts.isStringLiteral(node) ||
|
|
1833
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
1834
|
+
ts.isNumericLiteral(node) ||
|
|
1835
|
+
ts.isIdentifier(node) ||
|
|
1836
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
1837
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
1838
|
+
node.kind === ts.SyntaxKind.NullKeyword
|
|
1839
|
+
) {
|
|
1840
|
+
return undefined;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (ts.isNewExpression(node) || ts.isCallExpression(node)) {
|
|
1844
|
+
// A bare-identifier callee only — `ns.Foo(...)`, `arr.map(...)` and
|
|
1845
|
+
// `props.factory(...)` are all property accesses and all stay out, which
|
|
1846
|
+
// is what keeps a data transform (chant EVL010) from sneaking in.
|
|
1847
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
1848
|
+
return ts.isNewExpression(node)
|
|
1849
|
+
? `\`new ${briefNodeText(node.expression)}(...)\` needs a plain imported constructor to interpret`
|
|
1850
|
+
: callExpressionMessage(node);
|
|
1851
|
+
}
|
|
1852
|
+
for (const arg of node.arguments ?? []) {
|
|
1853
|
+
const violation = checkFactoryExpression(arg);
|
|
1854
|
+
if (violation) return violation;
|
|
1855
|
+
}
|
|
1856
|
+
return undefined;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
1860
|
+
if (!ts.isIdentifier(node.tag)) return unsupportedExpressionMessage(node);
|
|
1861
|
+
if (ts.isNoSubstitutionTemplateLiteral(node.template)) return undefined;
|
|
1862
|
+
for (const span of node.template.templateSpans) {
|
|
1863
|
+
const violation = checkFactoryExpression(span.expression);
|
|
1864
|
+
if (violation) return violation;
|
|
1865
|
+
}
|
|
1866
|
+
return undefined;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
if (ts.isTemplateExpression(node)) {
|
|
1870
|
+
for (const span of node.templateSpans) {
|
|
1871
|
+
const violation = checkFactoryExpression(span.expression);
|
|
1872
|
+
if (violation) return violation;
|
|
1873
|
+
}
|
|
1874
|
+
return undefined;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1878
|
+
for (const prop of node.properties) {
|
|
1879
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
1880
|
+
if (!isLiteralPropertyNameNode(prop.name)) return computedPropertyNameMessage(prop.name);
|
|
1881
|
+
const violation = checkFactoryExpression(prop.initializer);
|
|
1882
|
+
if (violation) return violation;
|
|
1883
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1884
|
+
if (prop.objectAssignmentInitializer) return UNSUPPORTED_OBJECT_MEMBER_MESSAGE;
|
|
1885
|
+
} else if (ts.isSpreadAssignment(prop)) {
|
|
1886
|
+
const violation = checkFactoryExpression(prop.expression);
|
|
1887
|
+
if (violation) return violation;
|
|
1888
|
+
} else {
|
|
1889
|
+
return UNSUPPORTED_OBJECT_MEMBER_MESSAGE;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1896
|
+
for (const el of node.elements) {
|
|
1897
|
+
const violation = checkFactoryExpression(ts.isSpreadElement(el) ? el.expression : el);
|
|
1898
|
+
if (violation) return violation;
|
|
1899
|
+
}
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (ts.isPropertyAccessExpression(node)) return checkFactoryExpression(node.expression);
|
|
1904
|
+
|
|
1905
|
+
if (ts.isElementAccessExpression(node)) {
|
|
1906
|
+
if (!isLiteralElementKey(node.argumentExpression)) {
|
|
1907
|
+
return `dynamic element access [${briefNodeText(node.argumentExpression)}] is not interpretable`;
|
|
1908
|
+
}
|
|
1909
|
+
return checkFactoryExpression(node.expression);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
1913
|
+
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) return UNSUPPORTED_UNARY_MESSAGE;
|
|
1914
|
+
return checkFactoryExpression(node.operand);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (ts.isBinaryExpression(node)) {
|
|
1918
|
+
if (!SUPPORTED_BINARY_OPERATORS.has(node.operatorToken.kind)) {
|
|
1919
|
+
return unsupportedBinaryMessage(node.operatorToken.kind);
|
|
1920
|
+
}
|
|
1921
|
+
return checkFactoryExpression(node.left) ?? checkFactoryExpression(node.right);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (ts.isConditionalExpression(node)) {
|
|
1925
|
+
return (
|
|
1926
|
+
checkFactoryExpression(node.condition) ??
|
|
1927
|
+
checkFactoryExpression(node.whenTrue) ??
|
|
1928
|
+
checkFactoryExpression(node.whenFalse)
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return unsupportedExpressionMessage(node);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/** `isLiteralPropertyName` narrowed to the node type {@link propName} accepts, without importing the type predicate's generic form twice. */
|
|
1936
|
+
function isLiteralPropertyNameNode(node: ts.PropertyName): boolean {
|
|
1937
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/**
|
|
1941
|
+
* Interpret an admissible factory's body against `args`, and assemble the
|
|
1942
|
+
* result through chant's own {@link Composite}.
|
|
1943
|
+
*
|
|
1944
|
+
* Returns `undefined` — a DECLINE, not a failure — when anything in the body
|
|
1945
|
+
* fails to resolve (an identifier the defining module's imports don't reach, a
|
|
1946
|
+
* constructor `--sandbox` refuses, a nested call that isn't a composite). The
|
|
1947
|
+
* caller then invokes for real, landing exactly where it landed before #1023.
|
|
1948
|
+
* That asymmetry is deliberate: interpretation may only ever REMOVE an
|
|
1949
|
+
* execution, never introduce a fold failure that wasn't there.
|
|
1950
|
+
*/
|
|
1951
|
+
async function interpretCompositeFactory(
|
|
1952
|
+
factory: InterpretableFactory,
|
|
1953
|
+
args: readonly unknown[],
|
|
1954
|
+
ctx: ResolveCtx,
|
|
1955
|
+
): Promise<{ value: unknown } | undefined> {
|
|
1956
|
+
// A `Composite()` factory takes exactly one props argument. Anything else at
|
|
1957
|
+
// the call site means the callee is being used as something this doesn't
|
|
1958
|
+
// model.
|
|
1959
|
+
if (args.length > 1) return undefined;
|
|
1960
|
+
|
|
1961
|
+
const { scope, fn } = factory;
|
|
1962
|
+
const resolved = await factoryModuleScopeResolved(scope, ctx.session);
|
|
1963
|
+
// The defining module's own scope, with the factory's locals layered on top.
|
|
1964
|
+
// `locals` is deliberately EMPTY: `resolveLiveValue`'s local-binding branch
|
|
1965
|
+
// exists to re-navigate a file's own top-level composite spine, which is not
|
|
1966
|
+
// what a name inside a factory body means. Body bindings live in `externals`
|
|
1967
|
+
// instead, where an identifier resolves to the value already computed for it
|
|
1968
|
+
// — which is what makes `role.Arn` a live `AttrRef` on the real instance.
|
|
1969
|
+
const consts = new Map(scope.consts);
|
|
1970
|
+
const externals = new Map(resolved.externals);
|
|
1971
|
+
const bodyCtx: ResolveCtx = {
|
|
1972
|
+
file: scope.file,
|
|
1973
|
+
consts,
|
|
1974
|
+
locals: new Map(),
|
|
1975
|
+
imports: scope.imports,
|
|
1976
|
+
namespaceImports: scope.namespaceImports,
|
|
1977
|
+
memo: new Map(),
|
|
1978
|
+
intrinsics: ctx.intrinsics,
|
|
1979
|
+
externals,
|
|
1980
|
+
crossFileFailures: resolved.failures,
|
|
1981
|
+
importCache: ctx.importCache,
|
|
1982
|
+
resolvePathCache: ctx.resolvePathCache,
|
|
1983
|
+
lexiconPackages: ctx.lexiconPackages,
|
|
1984
|
+
sandbox: ctx.sandbox,
|
|
1985
|
+
session: ctx.session,
|
|
1986
|
+
interpretDepth: ctx.interpretDepth + 1,
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
const bind = (name: string, value: unknown): void => {
|
|
1990
|
+
// A body binding SHADOWS a module-level const of the same name, so the
|
|
1991
|
+
// const has to go — `fold()` consults `consts` before `externals`.
|
|
1992
|
+
consts.delete(name);
|
|
1993
|
+
externals.set(name, value);
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
try {
|
|
1997
|
+
const param = fn.parameters[0];
|
|
1998
|
+
if (param) {
|
|
1999
|
+
const props = args[0];
|
|
2000
|
+
if (ts.isIdentifier(param.name)) {
|
|
2001
|
+
bind(param.name.text, props);
|
|
2002
|
+
} else if (ts.isObjectBindingPattern(param.name)) {
|
|
2003
|
+
if (!isIndexableObject(props)) return undefined;
|
|
2004
|
+
for (const el of param.name.elements) {
|
|
2005
|
+
const key = bindingElementPropKey(el);
|
|
2006
|
+
if (key === undefined) return undefined;
|
|
2007
|
+
bind(el.name.getText(), (props as Record<string, unknown>)[key]);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const members = await interpretFactoryBody(fn, bodyCtx, bind);
|
|
2013
|
+
if (!isIndexableObject(members)) return undefined;
|
|
2014
|
+
|
|
2015
|
+
// Assembled by chant's own `Composite()` — see the contract's "what is
|
|
2016
|
+
// preserved" note. One definition object per interpreted call (rather than
|
|
2017
|
+
// one per module, as the run path has) is the single visible difference:
|
|
2018
|
+
// `_id` is a fresh symbol, which nothing outside `CompositeRegistry` — used
|
|
2019
|
+
// only by tests — reads.
|
|
2020
|
+
const definition = Composite<void, CompositeMembers>(() => members as CompositeMembers, factory.compositeName);
|
|
2021
|
+
const instance = definition();
|
|
2022
|
+
executionCounts.factoryInterpretations += 1;
|
|
2023
|
+
return { value: instance };
|
|
2024
|
+
} catch {
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/** Evaluate an admissible factory body's statements in order, binding each `const`, and return the `return` expression's value. */
|
|
2030
|
+
async function interpretFactoryBody(
|
|
2031
|
+
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
2032
|
+
ctx: ResolveCtx,
|
|
2033
|
+
bind: (name: string, value: unknown) => void,
|
|
2034
|
+
): Promise<unknown> {
|
|
2035
|
+
if (!ts.isBlock(fn.body)) return interpretExpression(fn.body, ctx);
|
|
2036
|
+
|
|
2037
|
+
for (const statement of fn.body.statements) {
|
|
2038
|
+
if (ts.isReturnStatement(statement)) {
|
|
2039
|
+
// `findFactorySubsetViolation` has already established this is the last
|
|
2040
|
+
// statement and has an expression.
|
|
2041
|
+
return interpretExpression(statement.expression as ts.Expression, ctx);
|
|
2042
|
+
}
|
|
2043
|
+
const declarations = (statement as ts.VariableStatement).declarationList.declarations;
|
|
2044
|
+
for (const decl of declarations) {
|
|
2045
|
+
const value = await interpretExpression(decl.initializer as ts.Expression, ctx);
|
|
2046
|
+
if (ts.isIdentifier(decl.name)) {
|
|
2047
|
+
bind(decl.name.text, value);
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
if (!isIndexableObject(value)) {
|
|
2051
|
+
throw cheapError(`destructured \`const\` source in "${briefNodeText(decl.name)}" is not an object`);
|
|
2052
|
+
}
|
|
2053
|
+
for (const el of (decl.name as ts.ObjectBindingPattern).elements) {
|
|
2054
|
+
bind(el.name.getText(), (value as Record<string, unknown>)[bindingElementPropKey(el) as string]);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// Unreachable: rule 4 requires a trailing `return`.
|
|
2059
|
+
throw cheapError("composite factory body did not return");
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/**
|
|
2063
|
+
* Evaluate one expression of a factory body.
|
|
2064
|
+
*
|
|
2065
|
+
* Only the cases that can CONTAIN a construction or a composite call are
|
|
2066
|
+
* handled here; everything else delegates to {@link resolveDeclaratorValue},
|
|
2067
|
+
* which is the same `resolveLiveValue` -> `fold()` -> revive pipeline every
|
|
2068
|
+
* other value in a fold takes. That split is the point: the interpreter owns
|
|
2069
|
+
* as little evaluation semantics as it possibly can, so `fold()` stays the one
|
|
2070
|
+
* definition of what an expression means, and the operators/short-circuiting
|
|
2071
|
+
* duplicated below are duplicated because they must not evaluate a branch
|
|
2072
|
+
* `fold()` would not have evaluated.
|
|
2073
|
+
*/
|
|
2074
|
+
async function interpretExpression(node: ts.Expression, ctx: ResolveCtx): Promise<unknown> {
|
|
2075
|
+
if (
|
|
2076
|
+
ts.isParenthesizedExpression(node) ||
|
|
2077
|
+
ts.isAsExpression(node) ||
|
|
2078
|
+
ts.isSatisfiesExpression(node) ||
|
|
2079
|
+
ts.isNonNullExpression(node)
|
|
2080
|
+
) {
|
|
2081
|
+
return interpretExpression(node.expression, ctx);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (ts.isNewExpression(node)) return interpretNewExpression(node, ctx);
|
|
2085
|
+
|
|
2086
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
2087
|
+
const obj: Record<string, unknown> = {};
|
|
2088
|
+
for (const prop of node.properties) {
|
|
2089
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
2090
|
+
obj[propName(prop.name)] = await interpretExpression(prop.initializer, ctx);
|
|
2091
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
2092
|
+
obj[prop.name.text] = await interpretExpression(prop.name, ctx);
|
|
2093
|
+
} else {
|
|
2094
|
+
const src = await interpretExpression((prop as ts.SpreadAssignment).expression, ctx);
|
|
2095
|
+
if (src === null || typeof src !== "object") throw cheapError("spread source not an object");
|
|
2096
|
+
Object.assign(obj, src);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return obj;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2103
|
+
const arr: unknown[] = [];
|
|
2104
|
+
for (const el of node.elements) {
|
|
2105
|
+
if (ts.isSpreadElement(el)) {
|
|
2106
|
+
const src = await interpretExpression(el.expression, ctx);
|
|
2107
|
+
if (!Array.isArray(src)) throw cheapError("spread source not an array");
|
|
2108
|
+
arr.push(...src);
|
|
2109
|
+
} else {
|
|
2110
|
+
arr.push(await interpretExpression(el, ctx));
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return arr;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (ts.isConditionalExpression(node)) {
|
|
2117
|
+
return (await interpretExpression(node.condition, ctx))
|
|
2118
|
+
? interpretExpression(node.whenTrue, ctx)
|
|
2119
|
+
: interpretExpression(node.whenFalse, ctx);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (ts.isBinaryExpression(node)) {
|
|
2123
|
+
const S = ts.SyntaxKind;
|
|
2124
|
+
const opKind = node.operatorToken.kind;
|
|
2125
|
+
if (opKind === S.AmpersandAmpersandToken) {
|
|
2126
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2127
|
+
return left ? interpretExpression(node.right, ctx) : left;
|
|
2128
|
+
}
|
|
2129
|
+
if (opKind === S.BarBarToken) {
|
|
2130
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2131
|
+
return left ? left : interpretExpression(node.right, ctx);
|
|
2132
|
+
}
|
|
2133
|
+
if (opKind === S.QuestionQuestionToken) {
|
|
2134
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2135
|
+
return left === null || left === undefined ? interpretExpression(node.right, ctx) : left;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
return (await resolveDeclaratorValue(node, ctx)).value;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/**
|
|
2143
|
+
* Construct a real resource from a `new Type(...)` anywhere inside a factory
|
|
2144
|
+
* body — a member, a nested property object, an array element.
|
|
2145
|
+
*
|
|
2146
|
+
* Deliberately NOT routed through {@link resolveResourceEntity}: a factory body
|
|
2147
|
+
* evaluates against the DEFINING module's scope with the caller's props already
|
|
2148
|
+
* bound to live values, so each argument is interpreted recursively (through
|
|
2149
|
+
* {@link interpretExpression}, which can produce a live composite instance a
|
|
2150
|
+
* plain `fold()` has no representation for) rather than folded and revived.
|
|
2151
|
+
*
|
|
2152
|
+
* chant #1169 removed the asymmetry that used to motivate this comment: a
|
|
2153
|
+
* nested `new` in a TOP-LEVEL value position now constructs too, via
|
|
2154
|
+
* {@link constructFoldedResource}. The two paths reach the same place — the
|
|
2155
|
+
* class named by an `import`, called with the arguments the source wrote — by
|
|
2156
|
+
* different routes, because a factory body and a file's own top level start
|
|
2157
|
+
* from different scopes.
|
|
2158
|
+
*/
|
|
2159
|
+
async function interpretNewExpression(node: ts.NewExpression, ctx: ResolveCtx): Promise<unknown> {
|
|
2160
|
+
// Guaranteed an identifier by {@link checkFactoryExpression}; re-checked so a
|
|
2161
|
+
// future caller can't reach this with a dotted callee and get a silent miss.
|
|
2162
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
2163
|
+
throw cheapError(`\`new ${briefNodeText(node.expression)}(...)\` needs a plain imported constructor`);
|
|
2164
|
+
}
|
|
2165
|
+
const typeName = node.expression.text;
|
|
2166
|
+
const binding = ctx.imports.get(typeName);
|
|
2167
|
+
if (!binding) throw cheapError(`constructor "${typeName}" is not a resolvable import`);
|
|
2168
|
+
|
|
2169
|
+
// chant #1093 — same gate, same reason, as every other site that imports a
|
|
2170
|
+
// module in order to execute something from it.
|
|
2171
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, typeName, "constructor");
|
|
2172
|
+
if (refusal) throw cheapError(refusal);
|
|
2173
|
+
|
|
2174
|
+
const modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2175
|
+
const mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
2176
|
+
const Ctor = mod[binding.imported];
|
|
2177
|
+
if (typeof Ctor !== "function") {
|
|
2178
|
+
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a constructor`);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
const ctorArgs: unknown[] = [];
|
|
2182
|
+
for (const arg of node.arguments ?? []) ctorArgs.push(await interpretExpression(arg, ctx));
|
|
2183
|
+
return new (Ctor as new (...ctorArguments: unknown[]) => unknown)(...ctorArgs);
|
|
1236
2184
|
}
|
|
1237
2185
|
|
|
1238
2186
|
/**
|
|
@@ -1293,10 +2241,16 @@ function applyResolvedValue(
|
|
|
1293
2241
|
// envelope structurally (see ../serializer-walker.ts) with no revival
|
|
1294
2242
|
// needed. But an intrinsic's OWN implementation (e.g. `SubIntrinsic`) needs
|
|
1295
2243
|
// a genuine `AttrRef` instance internally (`instanceof` checks), which would
|
|
1296
|
-
// require wiring a live `WeakRef` to the sibling entity — out of scope here
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
2244
|
+
// require wiring a live `WeakRef` to the sibling entity — out of scope here:
|
|
2245
|
+
// reject (fall back to run) rather than risk silently wrong output.
|
|
2246
|
+
//
|
|
2247
|
+
// chant #1169 adds the fourth envelope this walk revives, and the one that
|
|
2248
|
+
// closes the corpus's largest fold gate: `{__resource}`, a nested
|
|
2249
|
+
// `new Type(...)` used as a value. It is revived the same way and for the same
|
|
2250
|
+
// reason as the other three — the real class is resolved through the folding
|
|
2251
|
+
// file's own imports and called for real — so what the outer constructor
|
|
2252
|
+
// receives is the instance the run path would have handed it, not a look-alike.
|
|
2253
|
+
// See {@link constructFoldedResource}.
|
|
1300
2254
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1301
2255
|
|
|
1302
2256
|
/** Resolve a bare name bound by this file's own `import` to its real, live export — the same two-step (resolve module path, then `importModule`) `resolveResourceEntity`/`resolveCallExpression` already use for constructors and composite factories. */
|
|
@@ -1448,9 +2402,24 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, requireLiv
|
|
|
1448
2402
|
}
|
|
1449
2403
|
|
|
1450
2404
|
if ("__resource" in value) {
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1453
|
-
|
|
2405
|
+
// chant #1169 — a nested `new Type(...)` used as a value. This is the
|
|
2406
|
+
// branch that makes the envelope safe: it is replaced, here, by a REAL
|
|
2407
|
+
// instance of the class the source named, built by the same
|
|
2408
|
+
// resolve-through-this-file's-imports machinery `resolveResourceEntity`
|
|
2409
|
+
// uses for a top-level resource and `interpretNewExpression` (#1023) uses
|
|
2410
|
+
// for a construction inside a factory body. Nothing symbolic reaches the
|
|
2411
|
+
// serializer — see {@link fold}'s `new` branch for why that is the whole
|
|
2412
|
+
// safety argument.
|
|
2413
|
+
//
|
|
2414
|
+
// `requireLiveRefs` is propagated rather than reset: a construction in an
|
|
2415
|
+
// ordinary prop position keeps the top-level rule (a `{__attrRef}` inside
|
|
2416
|
+
// it stays an envelope, which the serializer's own walker resolves by name
|
|
2417
|
+
// through `propertyDeclarable` exactly as it does for a top-level
|
|
2418
|
+
// resource's props), while a construction inside an intrinsic's interior
|
|
2419
|
+
// keeps the stricter one and rejects — the receiving implementation
|
|
2420
|
+
// inspects what it is handed, and this is the direction that falls back to
|
|
2421
|
+
// run rather than emitting something wrong.
|
|
2422
|
+
return constructFoldedResource(value as FoldedResource, ctx, requireLiveRefs);
|
|
1454
2423
|
}
|
|
1455
2424
|
|
|
1456
2425
|
const revived: Record<string, unknown> = {};
|
|
@@ -1638,89 +2607,101 @@ function chantCoreRoot(): string {
|
|
|
1638
2607
|
async function reviveFoldedProps(
|
|
1639
2608
|
props: { [key: string]: FoldedValue },
|
|
1640
2609
|
ctx: ResolveCtx,
|
|
2610
|
+
requireLiveRefs: boolean,
|
|
1641
2611
|
): Promise<Record<string, unknown>> {
|
|
1642
2612
|
const revived: Record<string, unknown> = {};
|
|
1643
2613
|
for (const [key, value] of Object.entries(props)) {
|
|
1644
|
-
revived[key] = await reviveFoldedValue(value, ctx,
|
|
2614
|
+
revived[key] = await reviveFoldedValue(value, ctx, requireLiveRefs);
|
|
1645
2615
|
}
|
|
1646
2616
|
return revived;
|
|
1647
2617
|
}
|
|
1648
2618
|
|
|
1649
2619
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1650
|
-
// Resource construction —
|
|
1651
|
-
// via `fold()`,
|
|
1652
|
-
//
|
|
2620
|
+
// Resource construction — #1022's mechanism (fold the ctor call's arguments
|
|
2621
|
+
// via `fold()`, resolve the constructor through this file's imports,
|
|
2622
|
+
// construct the real Declarable), split by chant #1169 into two reusable
|
|
2623
|
+
// halves so a NESTED `new Type(...)` used as a value is built by exactly the
|
|
2624
|
+
// same code as a file's own top-level resource declaration, not by a second
|
|
2625
|
+
// implementation that could quietly differ.
|
|
2626
|
+
//
|
|
2627
|
+
// `resolveResourceEntity` below is the top-level entry point and keeps its
|
|
2628
|
+
// per-declarator reason strings verbatim; `reviveResourceCtorArgs` and
|
|
2629
|
+
// `instantiateFoldedResource` are the shared halves, and
|
|
2630
|
+
// {@link constructFoldedResource} is the one-call composition of the two that
|
|
2631
|
+
// `reviveFoldedValue` uses for a nested construction.
|
|
1653
2632
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1654
2633
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2634
|
+
/**
|
|
2635
|
+
* Revive a folded constructor call's arguments into the real values the class
|
|
2636
|
+
* will receive.
|
|
2637
|
+
*
|
|
2638
|
+
* chant #1039 — replays any folded intrinsic/symbol envelopes into their real
|
|
2639
|
+
* runtime values before the entity is constructed. A no-op walk when the file
|
|
2640
|
+
* used no registered intrinsics (the overwhelming majority of cases today).
|
|
2641
|
+
*
|
|
2642
|
+
* chant #1082 — when `spec.args` is present the constructor's argument list
|
|
2643
|
+
* isn't the classic `(props)`/`(props, attributes)` shape (AWS's `Parameter` is
|
|
2644
|
+
* `(type, props)`), so the whole list is revived and spread by the caller
|
|
2645
|
+
* instead of `spec.props`, which in that case is only a view onto one of its
|
|
2646
|
+
* entries and would be double-counted.
|
|
2647
|
+
*/
|
|
2648
|
+
async function reviveResourceCtorArgs(
|
|
2649
|
+
spec: FoldedResource,
|
|
1658
2650
|
ctx: ResolveCtx,
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
return { ok: false, reason: `"${name}" is not foldable: ${describeFoldFailure(err, ctx)}` };
|
|
1666
|
-
}
|
|
1667
|
-
throw err;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// chant #1039 — replay any folded intrinsic/symbol envelopes into their
|
|
1671
|
-
// real runtime values before constructing the entity. A no-op walk when
|
|
1672
|
-
// this file used no registered intrinsics (the overwhelming majority of
|
|
1673
|
-
// cases today).
|
|
1674
|
-
//
|
|
1675
|
-
// chant #1082 — when `spec.args` is present the constructor's argument list
|
|
1676
|
-
// isn't the classic `(props)`/`(props, attributes)` shape (AWS's `Parameter`
|
|
1677
|
-
// is `(type, props)`), so revive the whole list and spread it below instead
|
|
1678
|
-
// of reviving `spec.props`, which in that case is only a view onto one of
|
|
1679
|
-
// its entries and would be double-counted.
|
|
1680
|
-
let ctorArgs: unknown[];
|
|
1681
|
-
try {
|
|
1682
|
-
if (spec.args) {
|
|
1683
|
-
ctorArgs = [];
|
|
1684
|
-
for (const arg of spec.args) ctorArgs.push(await reviveFoldedValue(arg, ctx, false));
|
|
1685
|
-
} else {
|
|
1686
|
-
const props = await reviveFoldedProps(spec.props, ctx);
|
|
1687
|
-
// The runtime constructor's optional second argument (`attributes` —
|
|
1688
|
-
// CFN's DependsOn/Condition/DeletionPolicy/…, see createResource in
|
|
1689
|
-
// ../runtime.ts) is only present in `spec` when the source actually
|
|
1690
|
-
// passed one (see foldResource in ../fold/fold.ts). Passing `undefined`
|
|
1691
|
-
// when it's absent matches the run path's own default
|
|
1692
|
-
// (`attributes ?? {}` inside the constructor).
|
|
1693
|
-
ctorArgs = [props, spec.attributes ? await reviveFoldedProps(spec.attributes, ctx) : undefined];
|
|
1694
|
-
}
|
|
1695
|
-
} catch (err) {
|
|
1696
|
-
return {
|
|
1697
|
-
ok: false,
|
|
1698
|
-
reason: `"${name}" is not foldable: ${describeFoldFailure(err, ctx)}`,
|
|
1699
|
-
};
|
|
2651
|
+
requireLiveRefs: boolean,
|
|
2652
|
+
): Promise<unknown[]> {
|
|
2653
|
+
if (spec.args) {
|
|
2654
|
+
const revived: unknown[] = [];
|
|
2655
|
+
for (const arg of spec.args) revived.push(await reviveFoldedValue(arg, ctx, requireLiveRefs));
|
|
2656
|
+
return revived;
|
|
1700
2657
|
}
|
|
2658
|
+
const props = await reviveFoldedProps(spec.props, ctx, requireLiveRefs);
|
|
2659
|
+
// The runtime constructor's optional second argument (`attributes` — CFN's
|
|
2660
|
+
// DependsOn/Condition/DeletionPolicy/…, see createResource in ../runtime.ts)
|
|
2661
|
+
// is only present in `spec` when the source actually passed one (see
|
|
2662
|
+
// foldResource in ../fold/fold.ts). Passing `undefined` when it's absent
|
|
2663
|
+
// matches the run path's own default (`attributes ?? {}` inside the
|
|
2664
|
+
// constructor).
|
|
2665
|
+
return [props, spec.attributes ? await reviveFoldedProps(spec.attributes, ctx, requireLiveRefs) : undefined];
|
|
2666
|
+
}
|
|
1701
2667
|
|
|
1702
|
-
|
|
2668
|
+
/**
|
|
2669
|
+
* Resolve a folded constructor's NAME through `ctx`'s own `import`
|
|
2670
|
+
* declarations and call the real class with `ctorArgs`.
|
|
2671
|
+
*
|
|
2672
|
+
* Throws a {@link cheapError} for every failure; `resolveResourceEntity` turns
|
|
2673
|
+
* those into its per-declarator `reason` strings and the nested path lets them
|
|
2674
|
+
* fall the whole file back to run. `forClause` is the ` for "<export name>"`
|
|
2675
|
+
* fragment the top-level messages carry and a nested construction has no name
|
|
2676
|
+
* for — the only difference between the two callers' diagnostics.
|
|
2677
|
+
*/
|
|
2678
|
+
async function instantiateFoldedResource(
|
|
2679
|
+
typeName: string,
|
|
2680
|
+
ctorArgs: readonly unknown[],
|
|
2681
|
+
ctx: ResolveCtx,
|
|
2682
|
+
forClause: string,
|
|
2683
|
+
): Promise<unknown> {
|
|
1703
2684
|
const binding = ctx.imports.get(typeName);
|
|
1704
2685
|
if (!binding) {
|
|
1705
|
-
|
|
2686
|
+
throw cheapError(`constructor "${typeName}"${forClause} is not a resolvable import`);
|
|
1706
2687
|
}
|
|
1707
2688
|
|
|
1708
2689
|
// chant #1093 — a resource class is a lexicon export in every corpus entry
|
|
1709
2690
|
// today, but nothing forces that: `new Thing(...)` where `Thing` comes from
|
|
1710
2691
|
// a project file (or an arbitrary dependency) would import and run that
|
|
1711
2692
|
// module here, in the CLI's process. Same refusal as the composite-factory
|
|
1712
|
-
// path above
|
|
2693
|
+
// path above, and the reason a nested construction cannot widen the #1093
|
|
2694
|
+
// boundary: it reaches its class by exactly this gate.
|
|
1713
2695
|
const refusal = sandboxedExecutionRefusal(binding, ctx, typeName, "constructor");
|
|
1714
|
-
if (refusal)
|
|
2696
|
+
if (refusal) throw cheapError(refusal);
|
|
1715
2697
|
|
|
1716
2698
|
let modulePath: string;
|
|
1717
2699
|
try {
|
|
1718
2700
|
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1719
2701
|
} catch (err) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
};
|
|
2702
|
+
throw cheapError(
|
|
2703
|
+
`could not resolve import "${binding.specifier}" for "${typeName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2704
|
+
);
|
|
1724
2705
|
}
|
|
1725
2706
|
|
|
1726
2707
|
// Same import mechanism as `resolveCallExpression` above — see its comment.
|
|
@@ -1728,22 +2709,124 @@ async function resolveResourceEntity(
|
|
|
1728
2709
|
try {
|
|
1729
2710
|
mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
1730
2711
|
} catch (err) {
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
};
|
|
2712
|
+
throw cheapError(
|
|
2713
|
+
`could not import "${binding.specifier}" to resolve "${typeName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2714
|
+
);
|
|
1735
2715
|
}
|
|
1736
2716
|
|
|
1737
2717
|
const Ctor = mod[binding.imported];
|
|
1738
2718
|
if (typeof Ctor !== "function") {
|
|
1739
|
-
|
|
2719
|
+
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a constructor`);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// Constructed with exactly the arguments the source wrote.
|
|
2723
|
+
return new (Ctor as new (...ctorArguments: unknown[]) => unknown)(...ctorArgs);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* chant #1169 — build a real instance from a nested `{__resource}` envelope:
|
|
2728
|
+
* revive its arguments, then resolve its class and call it. The composition
|
|
2729
|
+
* {@link reviveFoldedValue} reaches for; identical in every step to what
|
|
2730
|
+
* {@link resolveResourceEntity} does for a top-level declaration, which is the
|
|
2731
|
+
* point — a nested `new Image({...})` and a top-level `export const image = new
|
|
2732
|
+
* Image({...})` produce the same object, from the same class, from the same
|
|
2733
|
+
* resolved module path.
|
|
2734
|
+
*/
|
|
2735
|
+
async function constructFoldedResource(
|
|
2736
|
+
spec: FoldedResource,
|
|
2737
|
+
ctx: ResolveCtx,
|
|
2738
|
+
requireLiveRefs: boolean,
|
|
2739
|
+
): Promise<unknown> {
|
|
2740
|
+
const ctorArgs = await reviveResourceCtorArgs(spec, ctx, requireLiveRefs);
|
|
2741
|
+
return instantiateFoldedResource(spec.__resource, ctorArgs, ctx, "");
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
/**
|
|
2745
|
+
* chant #1169 — construct every top-level `const x = new Type(...)` in the file
|
|
2746
|
+
* ONCE, in source order, before any exported declarator is resolved, and put
|
|
2747
|
+
* each instance in `ctx.externals` under its own name.
|
|
2748
|
+
*
|
|
2749
|
+
* This is what makes a same-file resource usable as a VALUE — `image:
|
|
2750
|
+
* nodeImage`, `DependsOn: [dbCluster]`, `export { app }` — and it is the half
|
|
2751
|
+
* of the #1169 gate the nested-`new` lift alone does not reach: the
|
|
2752
|
+
* `{__resource}` envelope covers a construction written INLINE at the value
|
|
2753
|
+
* position, while the far more common authoring shape names it once and refers
|
|
2754
|
+
* to it. `fold()` cannot answer that reference on its own (it is synchronous,
|
|
2755
|
+
* and constructing needs the module graph), so it defers to `externals` — see
|
|
2756
|
+
* its identifier branch for the full argument.
|
|
2757
|
+
*
|
|
2758
|
+
* ONE instance, and identity is the whole point. Every reference in the file
|
|
2759
|
+
* reads this map, and the exported-declarator loop reuses the same object
|
|
2760
|
+
* through `prebuilt` rather than constructing a second one, so a resource
|
|
2761
|
+
* referenced by name and the entity discovery registers are the same object —
|
|
2762
|
+
* which is what makes the serializer's `Ref`/`DependsOn` resolution land on a
|
|
2763
|
+
* logical name at all. Running the module top-to-bottom produces exactly this:
|
|
2764
|
+
* every top-level `const` evaluated once, in order, later ones seeing earlier
|
|
2765
|
+
* ones. Order matters and is preserved — `collectConsts` yields source order, so
|
|
2766
|
+
* `const b = new Thing({ x: a })` finds `a` already built.
|
|
2767
|
+
*
|
|
2768
|
+
* A construction that FAILS is skipped silently rather than failing the file:
|
|
2769
|
+
* the name stays absent from `externals`, so a reference to it rejects with the
|
|
2770
|
+
* identical message it produced before this existed, and an EXPORTED one falls
|
|
2771
|
+
* through to {@link resolveResourceEntity}, which reproduces the failure with
|
|
2772
|
+
* its own located reason. Strictly additive.
|
|
2773
|
+
*
|
|
2774
|
+
* Under `--sandbox` every construction here goes through the same
|
|
2775
|
+
* {@link sandboxedExecutionRefusal} as every other one, so a project-defined
|
|
2776
|
+
* class refuses, the name stays unresolved, and the file demotes to the
|
|
2777
|
+
* sandboxed child exactly as before. Under plain `--fold` this can import a
|
|
2778
|
+
* constructor's module for a const that is never exported — work the RUN path
|
|
2779
|
+
* performs unconditionally for the same file, and through the same
|
|
2780
|
+
* already-memoized `importModule`.
|
|
2781
|
+
*/
|
|
2782
|
+
async function preresolveResourceConsts(ctx: ResolveCtx): Promise<Map<ts.Expression, unknown>> {
|
|
2783
|
+
const built = new Map<ts.Expression, unknown>();
|
|
2784
|
+
for (const [name, initializer] of ctx.consts) {
|
|
2785
|
+
if (!ts.isNewExpression(initializer) || !ts.isIdentifier(initializer.expression)) continue;
|
|
2786
|
+
try {
|
|
2787
|
+
const spec = foldResource(initializer, ctx.consts, ctx.intrinsics, ctx.externals);
|
|
2788
|
+
const instance = await constructFoldedResource(spec, ctx, false);
|
|
2789
|
+
built.set(initializer, instance);
|
|
2790
|
+
ctx.externals.set(name, instance);
|
|
2791
|
+
} catch {
|
|
2792
|
+
// Not constructible here (an unresolvable constructor import, a prop
|
|
2793
|
+
// outside the fold subset, a --sandbox refusal). Leave the name alone.
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return built;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
async function resolveResourceEntity(
|
|
2800
|
+
name: string,
|
|
2801
|
+
node: ts.NewExpression,
|
|
2802
|
+
ctx: ResolveCtx,
|
|
2803
|
+
): Promise<{ ok: true; entity: Declarable } | { ok: false; reason: string }> {
|
|
2804
|
+
let spec: FoldedResource;
|
|
2805
|
+
try {
|
|
2806
|
+
spec = foldResource(node, ctx.consts, ctx.intrinsics, ctx.externals);
|
|
2807
|
+
} catch (err) {
|
|
2808
|
+
if (err instanceof FoldError) {
|
|
2809
|
+
return { ok: false, reason: `"${name}" is not foldable: ${describeFoldFailure(err, ctx)}` };
|
|
2810
|
+
}
|
|
2811
|
+
throw err;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
let ctorArgs: unknown[];
|
|
2815
|
+
try {
|
|
2816
|
+
ctorArgs = await reviveResourceCtorArgs(spec, ctx, false);
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
return {
|
|
2819
|
+
ok: false,
|
|
2820
|
+
reason: `"${name}" is not foldable: ${describeFoldFailure(err, ctx)}`,
|
|
2821
|
+
};
|
|
1740
2822
|
}
|
|
1741
2823
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2824
|
+
try {
|
|
2825
|
+
const entity = (await instantiateFoldedResource(spec.__resource, ctorArgs, ctx, ` for "${name}"`)) as Declarable;
|
|
2826
|
+
return { ok: true, entity };
|
|
2827
|
+
} catch (err) {
|
|
2828
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
2829
|
+
}
|
|
1747
2830
|
}
|
|
1748
2831
|
|
|
1749
2832
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -2085,13 +3168,30 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
2085
3168
|
resolvePathCache: session.resolvePathCache,
|
|
2086
3169
|
lexiconPackages: session.lexiconPackages,
|
|
2087
3170
|
sandbox: session.sandbox,
|
|
3171
|
+
session,
|
|
3172
|
+
interpretDepth: 0,
|
|
2088
3173
|
};
|
|
2089
3174
|
|
|
3175
|
+
// chant #1169 — every same-file `const x = new Type(...)`, built once, in
|
|
3176
|
+
// source order, before anything references one. See
|
|
3177
|
+
// {@link preresolveResourceConsts}.
|
|
3178
|
+
const prebuiltResources = await preresolveResourceConsts(ctx);
|
|
3179
|
+
|
|
2090
3180
|
const entities: FoldedEntity[] = [];
|
|
2091
3181
|
const exportedValues = new Map<string, unknown>();
|
|
2092
3182
|
|
|
2093
3183
|
for (const decl of scan.declarators) {
|
|
2094
3184
|
if (decl.kind === "resource") {
|
|
3185
|
+
// chant #1169 — the pre-pass already built this exact node. Reuse that
|
|
3186
|
+
// instance rather than constructing a second one: a sibling prop that
|
|
3187
|
+
// referenced this resource by name holds the pre-pass object, and if
|
|
3188
|
+
// the entity discovery registers were a different one, the reference
|
|
3189
|
+
// would have no logical name to resolve against.
|
|
3190
|
+
const prebuilt = prebuiltResources.get(decl.node);
|
|
3191
|
+
if (prebuilt !== undefined) {
|
|
3192
|
+
applyResolvedValue(decl.name, prebuilt, entities, exportedValues);
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
2095
3195
|
const result = await resolveResourceEntity(decl.name, decl.node, ctx);
|
|
2096
3196
|
if (!result.ok) return result;
|
|
2097
3197
|
applyResolvedValue(decl.name, result.entity, entities, exportedValues);
|