@intentius/chant 0.20.0 → 0.21.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/commands/check-lexicon-intrinsics.d.ts +17 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +30 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +89 -16
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +35 -3
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +73 -23
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +35 -1
- package/src/cli/commands/check-lexicon-intrinsics.ts +38 -2
- package/src/cli/commands/check-lexicon.ts +18 -0
- package/src/codegen/docs-sections.test.ts +7 -1
- package/src/codegen/docs-sections.ts +1 -1
- package/src/codegen/docs-types.ts +2 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/entity-wire-codec.ts +9 -7
- package/src/discovery/fold-import.test.ts +572 -0
- package/src/discovery/fold-import.ts +229 -36
- package/src/discovery/index.ts +9 -0
- package/src/fold/fold.test.ts +277 -0
- package/src/fold/fold.ts +213 -56
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +27 -0
- package/src/fold/subset.test.ts +111 -0
- package/src/fold/subset.ts +109 -28
- package/src/lexicon-schema.test.ts +43 -0
- package/src/lexicon-schema.ts +5 -0
- package/src/lexicon.ts +74 -24
- package/src/runtime.ts +11 -2
package/src/fold/fold.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
-
import {
|
|
2
|
+
import { intrinsicCallFolds, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
3
3
|
import {
|
|
4
4
|
SUPPORTED_BINARY_OPERATORS,
|
|
5
5
|
SUPPORTED_UNARY_OPERATORS,
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
dynamicElementAccessMessage,
|
|
11
11
|
isLiteralElementKey,
|
|
12
12
|
isLiteralPropertyName,
|
|
13
|
-
resourceCtorArgMessage,
|
|
14
13
|
unsupportedBinaryMessage,
|
|
15
14
|
unsupportedExpressionMessage,
|
|
16
15
|
type SubsetRuleId,
|
|
17
16
|
} from "./subset";
|
|
17
|
+
import { isFoldableHelperName } from "./foldable-helpers";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* fold — static AST value reducer (chant #1026/#1021/#1024, part of epic #1019)
|
|
@@ -32,9 +32,22 @@ import {
|
|
|
32
32
|
* ({@link "../lint/rules/evl001-non-literal-expression"}), so the linted
|
|
33
33
|
* subset and the folded subset can never drift apart (#1024).
|
|
34
34
|
*
|
|
35
|
-
* A `CallExpression` has no case — a function call as a value is
|
|
35
|
+
* A `CallExpression` has almost no case — a function call as a value is
|
|
36
36
|
* structurally unrepresentable, not merely linted against. Composite
|
|
37
|
-
* factory calls are out of scope here (epic Phase 5, #1023).
|
|
37
|
+
* factory calls are out of scope here (epic Phase 5, #1023). There are
|
|
38
|
+
* exactly two exceptions, both closed allowlists of names declared
|
|
39
|
+
* somewhere a human had to write them down, and both reducing to a symbolic
|
|
40
|
+
* envelope that executes nothing here:
|
|
41
|
+
*
|
|
42
|
+
* - a call to a REGISTERED chant authoring helper
|
|
43
|
+
* ({@link "./foldable-helpers"}, chant #1082) → {@link FoldedHelperCall};
|
|
44
|
+
* - a call to a lexicon intrinsic whose lexicon registered it AND opted
|
|
45
|
+
* its call form in ({@link intrinsicCallFolds}, ../lexicon.ts, chant
|
|
46
|
+
* #1044) → {@link FoldedIntrinsicCall}, the same `{__intrinsic}` family
|
|
47
|
+
* the tagged-template form already reduces to.
|
|
48
|
+
*
|
|
49
|
+
* Everything else — a user's function, a method call, an array `.map`, a
|
|
50
|
+
* registered name shadowed by a local binding — still throws.
|
|
38
51
|
*
|
|
39
52
|
* Cross-file identifier resolution (chant #1020): `consts` alone is always
|
|
40
53
|
* this file's own top-level bindings — that part stays single-file, and
|
|
@@ -69,6 +82,7 @@ export type FoldedValue =
|
|
|
69
82
|
| { [key: string]: FoldedValue }
|
|
70
83
|
| AttrRefValue
|
|
71
84
|
| FoldedIntrinsic
|
|
85
|
+
| FoldedHelperCall
|
|
72
86
|
| SymbolicValue
|
|
73
87
|
| FoldedResource;
|
|
74
88
|
|
|
@@ -96,12 +110,50 @@ export interface AttrRefValue {
|
|
|
96
110
|
* Mirrors the runtime call shape `Tag(strings, ...values)` so a later
|
|
97
111
|
* build path can replay it into the real intrinsic object (#1022).
|
|
98
112
|
*/
|
|
99
|
-
export
|
|
113
|
+
export type FoldedIntrinsic = FoldedIntrinsicTag | FoldedIntrinsicCall;
|
|
114
|
+
|
|
115
|
+
/** The tagged-template form: `Sub\`${x}-y\`` — see {@link FoldedIntrinsic}. */
|
|
116
|
+
export interface FoldedIntrinsicTag {
|
|
100
117
|
__intrinsic: string;
|
|
101
118
|
strings: string[];
|
|
102
119
|
values: FoldedValue[];
|
|
103
120
|
}
|
|
104
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The plain-call form of a registered, opted-in lexicon intrinsic
|
|
124
|
+
* (chant #1044) — `Ref(bucket)`, `Concat("a", b)`, `GetAtt("Fn", "Arn")`.
|
|
125
|
+
* Same `__intrinsic` key as the tagged-template form, so the same revival
|
|
126
|
+
* branch handles both and nothing downstream learns a new envelope; the
|
|
127
|
+
* payload differs because the call shape does — positional `args` mirroring
|
|
128
|
+
* `Name(...args)`, where the tag form mirrors `Name(strings, ...values)`.
|
|
129
|
+
*
|
|
130
|
+
* Symbolic, exactly like the tag form: `fold()` executes nothing, it only
|
|
131
|
+
* records that a registered intrinsic was called and with what. The real
|
|
132
|
+
* function is resolved through the folding file's own imports and invoked by
|
|
133
|
+
* `../discovery/fold-import.ts`'s `reviveFoldedValue`.
|
|
134
|
+
*/
|
|
135
|
+
export interface FoldedIntrinsicCall {
|
|
136
|
+
__intrinsic: string;
|
|
137
|
+
args: FoldedValue[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The result of folding a call to a registered chant authoring helper
|
|
142
|
+
* (chant #1082) — e.g. `phase("Apply", [...])`, `output(ref, "oX")`. Holds
|
|
143
|
+
* the helper's name and its folded arguments, in source order.
|
|
144
|
+
*
|
|
145
|
+
* Symbolic, exactly like {@link FoldedIntrinsic}: `fold()` executes nothing,
|
|
146
|
+
* it only records that a registered helper was called and with what. The real
|
|
147
|
+
* function is resolved through the folding file's own imports and invoked by
|
|
148
|
+
* `../discovery/fold-import.ts`'s `reviveFoldedValue`, which is also where
|
|
149
|
+
* the "is this name actually bound to chant's own helper" check lives. See
|
|
150
|
+
* {@link "./foldable-helpers"} for the allowlist and why it is closed.
|
|
151
|
+
*/
|
|
152
|
+
export interface FoldedHelperCall {
|
|
153
|
+
__helper: string;
|
|
154
|
+
args: FoldedValue[];
|
|
155
|
+
}
|
|
156
|
+
|
|
105
157
|
/**
|
|
106
158
|
* A sub-expression inside a folded intrinsic that fold could not reduce to
|
|
107
159
|
* a value without resolving an identifier from outside this file — e.g. an
|
|
@@ -129,6 +181,18 @@ export interface FoldedResource {
|
|
|
129
181
|
* ../runtime.ts). Present only when the source actually passed one.
|
|
130
182
|
*/
|
|
131
183
|
attributes?: { [key: string]: FoldedValue };
|
|
184
|
+
/**
|
|
185
|
+
* chant #1082 — every constructor argument, folded, in source order. Present
|
|
186
|
+
* only when the argument list is NOT the classic `(props)` / `(props,
|
|
187
|
+
* attributes)` shape — most often because the props object isn't first
|
|
188
|
+
* (`new Parameter("String", {...})`, whose signature is `(type, props)`).
|
|
189
|
+
*
|
|
190
|
+
* When present this is authoritative: the entity is constructed by spreading
|
|
191
|
+
* it, so the constructor receives exactly what the source wrote. `props`
|
|
192
|
+
* alongside it is the first object-literal argument, reported for readers,
|
|
193
|
+
* never re-passed (which would double-count it).
|
|
194
|
+
*/
|
|
195
|
+
args?: FoldedValue[];
|
|
132
196
|
}
|
|
133
197
|
|
|
134
198
|
/**
|
|
@@ -257,26 +321,49 @@ function resolvesToResource(consts: Map<string, ts.Expression>, ident: ts.Identi
|
|
|
257
321
|
|
|
258
322
|
/**
|
|
259
323
|
* True when `node` is an identifier, or a dotted/bracketed access chain
|
|
260
|
-
* rooted at an identifier, that
|
|
261
|
-
* `AWS.StackName` from an imported pseudo-parameter namespace
|
|
262
|
-
*
|
|
263
|
-
*
|
|
324
|
+
* rooted at an identifier, that neither `consts` nor `externals` can resolve
|
|
325
|
+
* — e.g. `AWS.StackName` from an imported pseudo-parameter namespace inside
|
|
326
|
+
* a lexicon package (still #1063). Nothing here can say what it refers to,
|
|
327
|
+
* so an intrinsic's interior keeps it symbolically rather than rejecting it.
|
|
328
|
+
*
|
|
329
|
+
* `externals` (chant #1020) is consulted so a root that fold CAN resolve is
|
|
330
|
+
* not treated as unresolved: `Ref(environment)`, where `environment` is a
|
|
331
|
+
* `Parameter` imported from a sibling project file, must fold to the REAL,
|
|
332
|
+
* already-constructed Declarable the fold session made for that file, not to
|
|
333
|
+
* a `{__symbol}` the bridge later re-imports — re-importing the defining
|
|
334
|
+
* module builds a second, differently-identified instance of the same
|
|
335
|
+
* resource, which is exactly the shared-identity property #1020 exists to
|
|
336
|
+
* preserve (see fold-import.ts's module doc).
|
|
264
337
|
*/
|
|
265
|
-
function isUnresolvedSymbolChain(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
338
|
+
function isUnresolvedSymbolChain(
|
|
339
|
+
node: ts.Expression,
|
|
340
|
+
consts: Map<string, ts.Expression>,
|
|
341
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
342
|
+
): boolean {
|
|
343
|
+
if (ts.isIdentifier(node)) {
|
|
344
|
+
return node.text !== "undefined" && !consts.has(node.text) && !externals?.has(node.text);
|
|
345
|
+
}
|
|
346
|
+
if (ts.isPropertyAccessExpression(node)) return isUnresolvedSymbolChain(node.expression, consts, externals);
|
|
347
|
+
if (ts.isElementAccessExpression(node)) return isUnresolvedSymbolChain(node.expression, consts, externals);
|
|
348
|
+
if (ts.isNonNullExpression(node)) return isUnresolvedSymbolChain(node.expression, consts, externals);
|
|
270
349
|
return false;
|
|
271
350
|
}
|
|
272
351
|
|
|
273
352
|
/**
|
|
274
|
-
* Fold one
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
353
|
+
* Fold one sub-expression of a registered intrinsic's interior — an
|
|
354
|
+
* interpolation of its tagged-template form, or (chant #1044) an argument of
|
|
355
|
+
* its plain-call form. Identical to {@link fold}, except a symbol chain
|
|
356
|
+
* nothing can resolve (a pseudo-parameter-style access into a lexicon
|
|
357
|
+
* package, `AWS.StackName`) folds to a {@link SymbolicValue} instead of
|
|
358
|
+
* throwing — the run path resolves it once the module actually imports and
|
|
359
|
+
* runs; fold preserves it symbolically rather than stringifying or rejecting
|
|
360
|
+
* it, and fold-import.ts's `resolveSymbolicValue` resolves it for real
|
|
361
|
+
* before the intrinsic is constructed.
|
|
362
|
+
*
|
|
363
|
+
* `externals` (chant #1020) takes precedence over the symbolic path: see
|
|
364
|
+
* {@link isUnresolvedSymbolChain} for why an already-resolved cross-file
|
|
365
|
+
* binding must reach the intrinsic as the real, shared object rather than as
|
|
366
|
+
* a symbol the bridge re-imports.
|
|
280
367
|
*/
|
|
281
368
|
function foldIntrinsicValue(
|
|
282
369
|
node: ts.Expression,
|
|
@@ -284,16 +371,7 @@ function foldIntrinsicValue(
|
|
|
284
371
|
intrinsics: readonly IntrinsicDef[],
|
|
285
372
|
externals?: ReadonlyMap<string, unknown>,
|
|
286
373
|
): FoldedValue {
|
|
287
|
-
|
|
288
|
-
// identifier here still folds to a symbolic `{__symbol}` node exactly as
|
|
289
|
-
// before #1020, revived later by fold-import.ts's own
|
|
290
|
-
// `resolveSymbolicValue` (a real, trusted import of the defining module —
|
|
291
|
-
// fine for a plain pseudo-parameter-style namespace, the only shape this
|
|
292
|
-
// path exists for). Wiring `externals` in here too would mean two
|
|
293
|
-
// different mechanisms resolving the same cross-file name inside an
|
|
294
|
-
// intrinsic interpolation; out of scope for #1020, which only needs
|
|
295
|
-
// identifiers/property access OUTSIDE tagged templates to resolve.
|
|
296
|
-
if (isUnresolvedSymbolChain(node, consts)) {
|
|
374
|
+
if (isUnresolvedSymbolChain(node, consts, externals)) {
|
|
297
375
|
return { __symbol: node.getText() };
|
|
298
376
|
}
|
|
299
377
|
return fold(node, consts, intrinsics, externals);
|
|
@@ -301,9 +379,13 @@ function foldIntrinsicValue(
|
|
|
301
379
|
|
|
302
380
|
/**
|
|
303
381
|
* Fold a `TaggedTemplateExpression` whose tag is a registered, foldable
|
|
304
|
-
* lexicon intrinsic ({@link
|
|
382
|
+
* lexicon intrinsic ({@link intrinsicTagFolds}, `../lexicon.ts`) to its node
|
|
305
383
|
* form. An unregistered — or registered-but-not-foldable — tag throws a
|
|
306
384
|
* located {@link FoldError}.
|
|
385
|
+
*
|
|
386
|
+
* Checks the TAG-form predicate specifically (chant #1044): an intrinsic
|
|
387
|
+
* whose lexicon opted its plain-call form in is not thereby usable as a
|
|
388
|
+
* tagged template, and `` Ref`...` `` stays a rejection.
|
|
307
389
|
*/
|
|
308
390
|
function foldTaggedTemplate(
|
|
309
391
|
node: ts.TaggedTemplateExpression,
|
|
@@ -312,7 +394,7 @@ function foldTaggedTemplate(
|
|
|
312
394
|
externals?: ReadonlyMap<string, unknown>,
|
|
313
395
|
): FoldedIntrinsic {
|
|
314
396
|
const tagName = node.tag.getText();
|
|
315
|
-
const isRegistered = intrinsics.some((i) => i.name === tagName &&
|
|
397
|
+
const isRegistered = intrinsics.some((i) => i.name === tagName && intrinsicTagFolds(i));
|
|
316
398
|
if (!isRegistered) {
|
|
317
399
|
throw foldError(node, `unregistered tagged template intrinsic: ${tagName}\`...\``);
|
|
318
400
|
}
|
|
@@ -332,13 +414,15 @@ function foldTaggedTemplate(
|
|
|
332
414
|
/**
|
|
333
415
|
* Fold a single expression node to a value. Throws {@link FoldError} for
|
|
334
416
|
* anything outside the supported subset — including any `CallExpression`
|
|
335
|
-
* that
|
|
417
|
+
* that is neither a registered chant authoring helper nor a registered,
|
|
418
|
+
* call-form-opted-in lexicon intrinsic (see the module doc).
|
|
336
419
|
*
|
|
337
|
-
* @param intrinsics -
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
420
|
+
* @param intrinsics - The active lexicons' registered intrinsics. A tagged
|
|
421
|
+
* template whose tag isn't in this list, or is in it without
|
|
422
|
+
* {@link intrinsicTagFolds}, is rejected; a plain call is rejected unless
|
|
423
|
+
* its callee is in this list with {@link intrinsicCallFolds} (chant
|
|
424
|
+
* #1044). Defaults to none — pass the target lexicon's manifest
|
|
425
|
+
* `intrinsics` to recognize either form.
|
|
342
426
|
* @param externals - chant #1020: pre-resolved imported bindings, consulted
|
|
343
427
|
* only when an identifier isn't in `consts`. See the module doc above.
|
|
344
428
|
* `undefined` (the default) preserves the exact pre-#1020 single-file
|
|
@@ -555,6 +639,59 @@ export function fold(
|
|
|
555
639
|
}
|
|
556
640
|
|
|
557
641
|
if (ts.isCallExpression(node)) {
|
|
642
|
+
// chant #1082 — the ONE call shape that folds: a registered chant
|
|
643
|
+
// authoring helper ({@link FOLDABLE_AUTHORING_HELPERS}), called through a
|
|
644
|
+
// bare identifier that this file hasn't shadowed with its own `const`.
|
|
645
|
+
// Nothing is executed here — the call reduces to a symbolic
|
|
646
|
+
// {@link FoldedHelperCall} envelope, exactly as a registered intrinsic
|
|
647
|
+
// tagged template reduces to a {@link FoldedIntrinsic} one, and for the
|
|
648
|
+
// same reason: the real function lives in another module, and resolving
|
|
649
|
+
// + invoking it is the async bridge's job (../discovery/fold-import.ts's
|
|
650
|
+
// `reviveFoldedValue`), which also verifies the name is actually bound to
|
|
651
|
+
// an import of chant's own before invoking anything. Every other call —
|
|
652
|
+
// a user's function, a method call, a call to something declared in this
|
|
653
|
+
// file — still has no case and throws, unchanged.
|
|
654
|
+
if (
|
|
655
|
+
ts.isIdentifier(node.expression) &&
|
|
656
|
+
isFoldableHelperName(node.expression.text) &&
|
|
657
|
+
!consts.has(node.expression.text)
|
|
658
|
+
) {
|
|
659
|
+
return {
|
|
660
|
+
__helper: node.expression.text,
|
|
661
|
+
args: node.arguments.map((arg) => fold(arg, consts, intrinsics, externals)),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// chant #1044 — the other call shape that folds: a lexicon intrinsic in
|
|
666
|
+
// PLAIN-CALL form (`Ref(bucket)`, `Concat("a", b)`), where that lexicon
|
|
667
|
+
// registered it AND opted its call form in ({@link intrinsicCallFolds},
|
|
668
|
+
// ../lexicon.ts — default off, never inferred). Reduces to the same
|
|
669
|
+
// `{__intrinsic}` envelope family the tagged-template form produces, with
|
|
670
|
+
// positional `args`; nothing is executed here, for the same reason as the
|
|
671
|
+
// tag form — the real function lives in the lexicon module, and resolving
|
|
672
|
+
// it through this file's own imports and invoking it is the async
|
|
673
|
+
// bridge's job (../discovery/fold-import.ts's `reviveFoldedValue`).
|
|
674
|
+
//
|
|
675
|
+
// Arguments fold through {@link foldIntrinsicValue}, exactly like a tag's
|
|
676
|
+
// interpolations: an intrinsic's interior is where a pseudo-parameter
|
|
677
|
+
// chain (`GetAZs(AWS.Region)`) legitimately appears, and it stays
|
|
678
|
+
// symbolic rather than rejecting.
|
|
679
|
+
//
|
|
680
|
+
// The door does not open any wider than this. A bare-identifier callee
|
|
681
|
+
// only, so `ns.Ref(...)` and `arr.map(...)` are untouched; the file's own
|
|
682
|
+
// `const` shadowing wins, so a local `Ref` is not the lexicon's; and a
|
|
683
|
+
// name absent from the active lexicons' registered set — or registered
|
|
684
|
+
// without the opt-in — falls straight through to the throw below.
|
|
685
|
+
if (ts.isIdentifier(node.expression) && !consts.has(node.expression.text)) {
|
|
686
|
+
const calleeName = node.expression.text;
|
|
687
|
+
if (intrinsics.some((i) => i.name === calleeName && intrinsicCallFolds(i))) {
|
|
688
|
+
return {
|
|
689
|
+
__intrinsic: calleeName,
|
|
690
|
+
args: node.arguments.map((arg) => foldIntrinsicValue(arg, consts, intrinsics, externals)),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
558
695
|
throw foldError(node, callExpressionMessage(node));
|
|
559
696
|
}
|
|
560
697
|
|
|
@@ -562,12 +699,23 @@ export function fold(
|
|
|
562
699
|
}
|
|
563
700
|
|
|
564
701
|
/**
|
|
565
|
-
* Fold a resource constructor call
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* {
|
|
569
|
-
* `DependsOn`, `Condition`, `DeletionPolicy`, …)
|
|
570
|
-
* `
|
|
702
|
+
* Fold a resource constructor call to its spec.
|
|
703
|
+
*
|
|
704
|
+
* The common `createResource` shape (../runtime.ts) is `new Type({ ...props
|
|
705
|
+
* })` or `new Type({ ...props }, { ...attributes })` — CFN-style resource
|
|
706
|
+
* attributes (`DependsOn`, `Condition`, `DeletionPolicy`, …) second — and
|
|
707
|
+
* that shape reduces to `props` (+ `attributes`) exactly as before.
|
|
708
|
+
*
|
|
709
|
+
* chant #1082 — but that is a convention, not a rule every lexicon class
|
|
710
|
+
* follows. AWS's deploy-time `Parameter` is `(type, props)`
|
|
711
|
+
* (lexicons/aws/src/parameter.ts): the props object is the SECOND argument
|
|
712
|
+
* and the first is a plain string. `foldResource` used to require argument 0
|
|
713
|
+
* to be an object literal, so no `new Parameter(...)` anywhere could ever
|
|
714
|
+
* fold, whatever surrounded it. The general case now folds every argument in
|
|
715
|
+
* source order into {@link FoldedResource.args}, which the caller constructs
|
|
716
|
+
* the entity from verbatim — no positional assumption at all. `props` is
|
|
717
|
+
* still reported (the first object-literal argument, for callers that read
|
|
718
|
+
* it) but is a VIEW onto `args`, not the thing constructed from.
|
|
571
719
|
*/
|
|
572
720
|
export function foldResource(
|
|
573
721
|
node: ts.NewExpression,
|
|
@@ -576,24 +724,33 @@ export function foldResource(
|
|
|
576
724
|
externals?: ReadonlyMap<string, unknown>,
|
|
577
725
|
): FoldedResource {
|
|
578
726
|
const typeName = node.expression.getText();
|
|
579
|
-
const
|
|
727
|
+
const args = node.arguments ?? ([] as unknown as ts.NodeArray<ts.Expression>);
|
|
728
|
+
const [firstArg, secondArg] = args;
|
|
729
|
+
const foldArg = (arg: ts.Expression) => fold(arg, consts, intrinsics, externals);
|
|
580
730
|
|
|
581
731
|
if (!firstArg) {
|
|
582
732
|
return { __resource: typeName, props: {} };
|
|
583
733
|
}
|
|
584
|
-
if (!ts.isObjectLiteralExpression(firstArg)) {
|
|
585
|
-
throw foldError(firstArg, resourceCtorArgMessage(typeName));
|
|
586
|
-
}
|
|
587
|
-
const props = fold(firstArg, consts, intrinsics, externals) as { [key: string]: FoldedValue };
|
|
588
734
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
735
|
+
// The classic (props) / (props, attributes) shape — reported without an
|
|
736
|
+
// `args` list so the spec of an ordinary resource is unchanged.
|
|
737
|
+
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
738
|
+
const props = foldArg(firstArg) as { [key: string]: FoldedValue };
|
|
739
|
+
if (args.length === 1) {
|
|
740
|
+
return { __resource: typeName, props };
|
|
741
|
+
}
|
|
742
|
+
if (args.length === 2 && ts.isObjectLiteralExpression(secondArg)) {
|
|
743
|
+
return { __resource: typeName, props, attributes: foldArg(secondArg) as { [key: string]: FoldedValue } };
|
|
744
|
+
}
|
|
594
745
|
}
|
|
595
|
-
|
|
596
|
-
|
|
746
|
+
|
|
747
|
+
// Anything else: fold every argument positionally. Each one still has to be
|
|
748
|
+
// in the fold subset on its own terms — a non-foldable argument throws from
|
|
749
|
+
// `fold()` exactly as a non-foldable prop value does.
|
|
750
|
+
const folded = args.map(foldArg);
|
|
751
|
+
const propsIndex = args.findIndex((arg) => ts.isObjectLiteralExpression(arg));
|
|
752
|
+
const props = (propsIndex === -1 ? {} : folded[propsIndex]) as { [key: string]: FoldedValue };
|
|
753
|
+
return { __resource: typeName, props, args: folded };
|
|
597
754
|
}
|
|
598
755
|
|
|
599
756
|
function hasExportModifier(statement: ts.VariableStatement): boolean {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foldable-helpers — the closed, declared allowlist of chant's OWN authoring
|
|
3
|
+
* helpers that a call expression may fold through (chant #1082, epic #1019).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* `fold()` has no general case for a `CallExpression`: a function call as a
|
|
8
|
+
* value is structurally unrepresentable there, because folding it would mean
|
|
9
|
+
* executing code, which is the one thing the fold path exists to avoid. That
|
|
10
|
+
* rule is right for user code and stays right — a user's own function, an
|
|
11
|
+
* arrow function, a method call (`naming.name(...)`) all keep failing exactly
|
|
12
|
+
* as before.
|
|
13
|
+
*
|
|
14
|
+
* But it also blocks chant's own documented authoring API. `phase("Apply",
|
|
15
|
+
* [...])` is how the component contract says to write a component; `output(ref,
|
|
16
|
+
* "oX")` is how a lexicon output is authored. An application cannot avoid them
|
|
17
|
+
* and still use components or outputs, so every file that uses one falls back
|
|
18
|
+
* to run no matter how statically evaluable the rest of it is.
|
|
19
|
+
*
|
|
20
|
+
* #1044 already settled the shape of the answer one level down, for lexicon
|
|
21
|
+
* intrinsics: a call-shaped thing may fold ONLY when it is registered, opt-in,
|
|
22
|
+
* per-helper — never because it merely looks like a call. This module is the
|
|
23
|
+
* same decision applied one level up, to chant's own helpers, with the
|
|
24
|
+
* registration written here by hand instead of coming from a lexicon manifest.
|
|
25
|
+
*
|
|
26
|
+
* ## What "registered" buys, and what still has to be true
|
|
27
|
+
*
|
|
28
|
+
* A name in this list is NOT permission to invoke whatever it happens to be
|
|
29
|
+
* bound to. Folding a helper call is a two-key operation:
|
|
30
|
+
*
|
|
31
|
+
* 1. **Shape + name** (here, and in `fold()`/`findSubsetViolation`): the
|
|
32
|
+
* callee is a bare identifier whose text is in {@link
|
|
33
|
+
* FOLDABLE_AUTHORING_HELPERS} and which is not shadowed by a local
|
|
34
|
+
* `const`. `fold()` reduces the call to a symbolic
|
|
35
|
+
* `{ __helper, args }` envelope — it still executes nothing.
|
|
36
|
+
* 2. **Provenance + invocation** (`../discovery/fold-import.ts`): the
|
|
37
|
+
* envelope is revived by resolving that name through the folding FILE'S
|
|
38
|
+
* OWN `import` bindings and checking the import actually comes from chant
|
|
39
|
+
* ({@link isChantOwnedSpecifier}, or a path inside chant-core's own tree
|
|
40
|
+
* for in-repo/absolute-specifier callers). Only then is the real function
|
|
41
|
+
* invoked, with the real folded arguments. A same-named helper imported
|
|
42
|
+
* from somewhere else, or declared in the file itself, resolves to
|
|
43
|
+
* nothing chant owns and the whole file falls back to run.
|
|
44
|
+
*
|
|
45
|
+
* So the function that runs is always the same function the run path would
|
|
46
|
+
* have called, from the same module the source itself imported — fold does not
|
|
47
|
+
* substitute its own reimplementation, which is why this list cannot drift
|
|
48
|
+
* from the helpers' real behavior.
|
|
49
|
+
*
|
|
50
|
+
* ## Admission criteria
|
|
51
|
+
*
|
|
52
|
+
* A helper belongs here only if all of these hold:
|
|
53
|
+
*
|
|
54
|
+
* - chant owns and documents it as authoring surface;
|
|
55
|
+
* - it is a pure function of its arguments — no I/O, no `process.env`, no
|
|
56
|
+
* module-level mutable state, no observable side effect;
|
|
57
|
+
* - calling it early (at fold time) is indistinguishable from calling it
|
|
58
|
+
* during a real run of the file.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately NOT admitted — see this module's tests and the #1082 PR body:
|
|
61
|
+
*
|
|
62
|
+
* - `env()` (`../env.ts`) reads `process.env`. Folding it would bake one
|
|
63
|
+
* run's environment into a statically-derived value. It is exactly the
|
|
64
|
+
* kind of call fold must keep rejecting.
|
|
65
|
+
* - `Op()` (`../op/builders.ts`) constructs an `OpResource` — a `Declarable`.
|
|
66
|
+
* `fold()` already rejects a nested `new Type(...)` used as a value for a
|
|
67
|
+
* real, differential-caught reason (the envelope leaks into
|
|
68
|
+
* serialization); a factory that returns one is the same hazard wearing a
|
|
69
|
+
* call.
|
|
70
|
+
* - `propagate()`, `withDefaults()`, `resource()`, `mergeDefaults()`
|
|
71
|
+
* (`../composite.ts`) are composite *definition* helpers, not value-position
|
|
72
|
+
* helpers. `propagate()` in particular mutates its argument in place, and
|
|
73
|
+
* the composite spine in fold-import.ts already resolves it live.
|
|
74
|
+
* - `createResource()`/`createProperty()` (`../runtime.ts`) are used at a
|
|
75
|
+
* lexicon module's top level to build classes, never as a value inside a
|
|
76
|
+
* project file.
|
|
77
|
+
* - Lexicon intrinsics in CALL form (`Ref(...)`, `Join(...)`) are lexicon
|
|
78
|
+
* surface, not chant's own, and are #1044's registry (`IntrinsicDef`,
|
|
79
|
+
* `../lexicon.ts`) to admit — not this one. Only their tagged-template
|
|
80
|
+
* form folds today.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One registered helper. `module` and `note` carry no runtime behavior — they
|
|
85
|
+
* are the audit trail for why this entry passed the admission criteria above,
|
|
86
|
+
* kept next to the name it justifies rather than in a comment that can drift
|
|
87
|
+
* away from the list.
|
|
88
|
+
*/
|
|
89
|
+
export interface FoldableHelperDef {
|
|
90
|
+
/** The exported name, matched against the callee identifier's text. */
|
|
91
|
+
readonly name: string;
|
|
92
|
+
/** Where chant defines it (a path under `packages/core/src`, for the audit trail). */
|
|
93
|
+
readonly module: string;
|
|
94
|
+
/** Why it qualifies — what it returns and why calling it at fold time is safe. */
|
|
95
|
+
readonly note: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The allowlist. Adding an entry is a deliberate act: it must satisfy every
|
|
100
|
+
* admission criterion in this module's doc, and it widens what `fold()` and
|
|
101
|
+
* `findSubsetViolation` accept for EVERY project, so it belongs in a PR that
|
|
102
|
+
* says so.
|
|
103
|
+
*
|
|
104
|
+
* Two names below are defined TWICE in chant, by different modules, with
|
|
105
|
+
* different return types (`phase`/`gate` by both the component contract and
|
|
106
|
+
* the Op builders; `stackOutput` by both the component contract and the
|
|
107
|
+
* cross-stack output primitive). That is fine and needs no disambiguation
|
|
108
|
+
* here: registration is by name, but the function actually invoked is the one
|
|
109
|
+
* the folding file itself imported (see step 2 in the module doc), so each
|
|
110
|
+
* file gets its own. Both definitions of each name independently satisfy the
|
|
111
|
+
* criteria, which is what makes registering the shared name safe.
|
|
112
|
+
*/
|
|
113
|
+
export const FOLDABLE_AUTHORING_HELPERS: readonly FoldableHelperDef[] = [
|
|
114
|
+
{
|
|
115
|
+
name: "phase",
|
|
116
|
+
module: "components/component.ts, op/builders.ts",
|
|
117
|
+
note: "Returns a plain `{ phase, steps, parallel? }` / `{ name, steps, parallel? }` object literal built from its arguments. No state, no I/O.",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "gate",
|
|
121
|
+
module: "components/component.ts, op/builders.ts",
|
|
122
|
+
note: "Returns a plain `{ kind: 'gate', signalName, ... }` object literal built from its arguments.",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "activity",
|
|
126
|
+
module: "op/builders.ts",
|
|
127
|
+
note: "Returns a plain `{ kind: 'activity', fn, args?, profile? }` object literal built from its arguments.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "stackOutput",
|
|
131
|
+
module: "components/component.ts, stack-output.ts",
|
|
132
|
+
note: "Component form returns a plain `{ stackOutput: { stack, name } }` literal. Cross-stack form derives a `StackOutput` from a real `AttrRef`/`Intrinsic` and throws on anything else — so a fold that only has a symbolic reference fails loudly into run-fallback rather than producing a wrong output.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "output",
|
|
136
|
+
module: "lexicon-output.ts",
|
|
137
|
+
note: "Constructs a `LexiconOutput` from a real `AttrRef`/`Intrinsic` and a name. Pure, but identity-sensitive: it reads through the ref's `WeakRef` to its parent entity. Only folds when the ref argument revives to a REAL live reference (see fold-import.ts's `requireLiveRefs`); a symbolic `{ __attrRef }` envelope is rejected, not silently wrapped.",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const HELPER_NAMES: ReadonlySet<string> = new Set(FOLDABLE_AUTHORING_HELPERS.map((h) => h.name));
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* True when `name` is a registered foldable authoring helper. Name-only — this
|
|
145
|
+
* is the shape-level half of the check (step 1 in the module doc). It says
|
|
146
|
+
* nothing about where the name is bound; `../discovery/fold-import.ts` decides
|
|
147
|
+
* that before anything is invoked.
|
|
148
|
+
*/
|
|
149
|
+
export function isFoldableHelperName(name: string): boolean {
|
|
150
|
+
return HELPER_NAMES.has(name);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Package specifiers chant itself publishes. A registered helper name only
|
|
155
|
+
* folds when the folding file imported it from one of these — or, for in-repo
|
|
156
|
+
* and test callers that import chant-core by relative/absolute path, from
|
|
157
|
+
* inside chant-core's own tree (checked separately, in fold-import.ts, since
|
|
158
|
+
* only that module knows how to resolve a specifier to a path).
|
|
159
|
+
*
|
|
160
|
+
* Lexicon packages are included because several core helpers are re-exported
|
|
161
|
+
* through them and that is the documented import in real projects — e.g.
|
|
162
|
+
* `import { output } from "@intentius/chant-lexicon-aws"`.
|
|
163
|
+
*/
|
|
164
|
+
const CHANT_PACKAGE_SPECIFIERS: readonly string[] = ["@intentius/chant", "@intentius/chant-lexicon-"];
|
|
165
|
+
|
|
166
|
+
/** True for a bare specifier that names a chant-published package (or one of its subpaths). */
|
|
167
|
+
export function isChantOwnedSpecifier(specifier: string): boolean {
|
|
168
|
+
return CHANT_PACKAGE_SPECIFIERS.some(
|
|
169
|
+
(prefix) => specifier === prefix || specifier.startsWith(prefix.endsWith("-") ? prefix : `${prefix}/`),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -135,12 +135,39 @@ describe("subset-doc-parity — supported patterns in typescript-as-data.mdx cla
|
|
|
135
135
|
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("Registered intrinsic calls", () => {
|
|
139
|
+
// chant #1044 — the one doc snippet that needs the registry to classify:
|
|
140
|
+
// `Ref(...)` is only in the subset because a lexicon opted its call form
|
|
141
|
+
// in, and `findSubsetViolation` answers exactly that question when it is
|
|
142
|
+
// given the registry (subset.ts module doc, point 2c). Without one it
|
|
143
|
+
// would report a violation, which is the pre-#1044 answer EVL still gets.
|
|
144
|
+
const consts = parseConsts(extractFencedBlock("Registered intrinsic calls"));
|
|
145
|
+
const intrinsics = [
|
|
146
|
+
{ name: "Sub", isTag: true },
|
|
147
|
+
{ name: "Ref", isTag: false, foldsAsCall: true },
|
|
148
|
+
];
|
|
149
|
+
expect(findSubsetViolation(resourceArg(consts, "store"), intrinsics)).toBeUndefined();
|
|
150
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
138
153
|
test("Typed property-kind constructors", () => {
|
|
139
154
|
const consts = parseConsts(extractFencedBlock("Typed property-kind constructors"));
|
|
140
155
|
expect(findSubsetViolation(resourceArg(consts, "config"))).toBeUndefined();
|
|
141
156
|
expect(findSubsetViolation(resourceArg(consts, "access"))).toBeUndefined();
|
|
142
157
|
});
|
|
143
158
|
|
|
159
|
+
test("Registered authoring helpers", () => {
|
|
160
|
+
// chant #1082 — not a `new Type({...})` declaration, so classify the
|
|
161
|
+
// exported component object literal directly. `findSubsetViolation`
|
|
162
|
+
// checks the helper NAME only (subset.ts module doc, point 2b); the
|
|
163
|
+
// doc's own snippet imports them from chant, which is what the bridge
|
|
164
|
+
// additionally verifies at fold time.
|
|
165
|
+
const consts = parseConsts(extractFencedBlock("Registered authoring helpers"));
|
|
166
|
+
const web = consts.get("web");
|
|
167
|
+
if (!web) throw new Error(`subset-doc-parity: "web" did not parse as a const declaration`);
|
|
168
|
+
expect(findSubsetViolation(web)).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
144
171
|
test("Nullish coalescing for defaults", () => {
|
|
145
172
|
// Not a standalone statement in the doc (a single object-literal
|
|
146
173
|
// property, deliberately shown as a fragment) — wrapped in an object
|