@intentius/chant 0.19.1 → 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/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +71 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.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/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +239 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- 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 +134 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
- package/src/runtime.ts +11 -2
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { isFoldableHelperName } from "./foldable-helpers";
|
|
3
|
+
import { intrinsicCallFolds, type IntrinsicDef } from "../lexicon";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* subset — the single canonical definition of chant's statically-foldable
|
|
7
|
+
* expression subset (chant #1024, epic #1019).
|
|
8
|
+
*
|
|
9
|
+
* `fold()` ({@link "./fold"}, the enforcement layer — a construct outside
|
|
10
|
+
* this subset simply has no case there) and EVL001/EVL003
|
|
11
|
+
* ({@link "../lint/rules/evl001-non-literal-expression"},
|
|
12
|
+
* {@link "../lint/rules/evl003-dynamic-property-access"} — the pre-flight
|
|
13
|
+
* diagnostic layer) both import this module so the two can never drift
|
|
14
|
+
* apart on *which node kinds, operators, and key shapes* are foldable.
|
|
15
|
+
* Before this module existed, `fold()` and EVL001 each hand-rolled their
|
|
16
|
+
* own recursive classifier; they agreed almost everywhere but had several
|
|
17
|
+
* real, silent gaps (see git history of #1024 for the enumerated list —
|
|
18
|
+
* dynamic object-literal keys, template/tagged-template interiors, and
|
|
19
|
+
* unrestricted binary/unary operators were all accepted by EVL001 but
|
|
20
|
+
* rejected by `fold()`). This module is the fix: one classifier, two
|
|
21
|
+
* importers.
|
|
22
|
+
*
|
|
23
|
+
* Scope — this module classifies *shape* only: the syntactic kind of an
|
|
24
|
+
* expression, its operator, and (for keys) its literal-ness. It
|
|
25
|
+
* deliberately does NOT resolve bindings or perform any evaluation, because
|
|
26
|
+
* three things a full fold needs are inherently environment-dependent and
|
|
27
|
+
* cannot be recovered from shape alone:
|
|
28
|
+
*
|
|
29
|
+
* 1. Identifier *resolution* — is a bare name actually a local `const`,
|
|
30
|
+
* vs. an unbound name? That needs the file's `consts` map. Both
|
|
31
|
+
* `fold()` and this module treat a bare identifier as shape-valid;
|
|
32
|
+
* `fold()` alone resolves it (and rejects if unresolved) once it has
|
|
33
|
+
* that map. A lint rule has no equivalent binding-resolution pass
|
|
34
|
+
* today, so EVL (via this module) stays permissive here — a known,
|
|
35
|
+
* intentional asymmetry, not a bug: it can only ever be a *false
|
|
36
|
+
* negative* on EVL's part (EVL passes something `fold()` might later
|
|
37
|
+
* reject for being unresolved), never the reverse.
|
|
38
|
+
* 2. Tagged-template *tag registration* — needs a lexicon's intrinsics
|
|
39
|
+
* manifest, which isn't available to a syntax-only lint rule. `fold()`
|
|
40
|
+
* alone checks it; this module treats any tag name as shape-valid and
|
|
41
|
+
* only classifies the interpolated values.
|
|
42
|
+
* 2b. Authoring-helper *provenance* (chant #1082) — a call to a registered
|
|
43
|
+
* chant helper (`phase(...)`, `output(...)`; ./foldable-helpers.ts)
|
|
44
|
+
* folds, but only when the name is genuinely bound to an import of
|
|
45
|
+
* chant's own. That needs the module graph, which a syntax-only lint
|
|
46
|
+
* rule doesn't have. This module checks the NAME only and stays
|
|
47
|
+
* permissive; `fold()`'s bridge does the provenance check and falls the
|
|
48
|
+
* file back to run when it fails. Same direction as every other item
|
|
49
|
+
* here — a false negative for EVL, never a false positive.
|
|
50
|
+
* 2c. Intrinsic *call-form* registration (chant #1044) — a plain call to a
|
|
51
|
+
* lexicon intrinsic the lexicon opted in (`Ref(bucket)`,
|
|
52
|
+
* `Concat(a, b)`; `IntrinsicDef.foldsAsCall`, ../lexicon.ts) folds.
|
|
53
|
+
* Whether a given name is such an intrinsic is not knowable from shape,
|
|
54
|
+
* so {@link findSubsetViolation} takes the registry as an OPTIONAL
|
|
55
|
+
* parameter instead of guessing: supply it and the answer for a call is
|
|
56
|
+
* exact (fold()'s own), omit it and every call is a violation, the
|
|
57
|
+
* pre-#1044 answer.
|
|
58
|
+
*
|
|
59
|
+
* That parameter is the whole reason the call case lives here rather
|
|
60
|
+
* than only in `fold()`. This module is a shared predicate, and the
|
|
61
|
+
* point of a shared predicate is that a consumer can ask "will this
|
|
62
|
+
* fold?" without running fold — a control plane deciding whether a
|
|
63
|
+
* repository needs a sandboxed child process at all, for instance.
|
|
64
|
+
* Keeping the case out of here would make the predicate answer "no" for
|
|
65
|
+
* idiomatic `Ref(...)` source that `fold()` reduces cleanly: not a
|
|
66
|
+
* permissive gap but a systematically WRONG answer in the expensive
|
|
67
|
+
* direction, and the one direction this module is not allowed to be
|
|
68
|
+
* wrong in (see the false-negative/false-positive rule in point 1, and
|
|
69
|
+
* the flow-sensitivity note below — the single divergence in the other
|
|
70
|
+
* direction, and the one this module treats as a wart). A caller with
|
|
71
|
+
* no registry degrades to "assume it runs", which is safe and cheap to
|
|
72
|
+
* reason about; EVL is exactly such a caller and its behavior on calls
|
|
73
|
+
* is unchanged by #1044.
|
|
74
|
+
* 3. Runtime *type* of a folded value — e.g. spreading `const n = 5`
|
|
75
|
+
* (`{...n}`) is shape-valid (`n` is a plain identifier) but `fold()`
|
|
76
|
+
* rejects it once it discovers `n` folds to a number, not an object.
|
|
77
|
+
* Only real evaluation catches this; EVL004 independently narrows
|
|
78
|
+
* spread sources to a stricter "traceable to a const" shape, which
|
|
79
|
+
* catches most real-world misuse without evaluating, but is not a
|
|
80
|
+
* full substitute.
|
|
81
|
+
*
|
|
82
|
+
* One more inherent gap, on the *value-flow* side rather than shape:
|
|
83
|
+
* `fold()` evaluates `&&`/`||`/`??` and `? :` lazily — it only folds the
|
|
84
|
+
* side/branch actually taken, exactly like the JS runtime — so an
|
|
85
|
+
* otherwise-unfoldable *untaken* branch does not reject
|
|
86
|
+
* (`false && sideEffect()` folds cleanly to `false`). This module (and so
|
|
87
|
+
* EVL) is flow-insensitive: it has no notion of "taken", so it requires
|
|
88
|
+
* every operand/branch to be shape-valid. This can only make EVL *stricter*
|
|
89
|
+
* than `fold()` (a false positive relative to fold, flagging code the
|
|
90
|
+
* folder would actually accept) — never the reverse. Making EVL
|
|
91
|
+
* flow-sensitive would mean re-implementing an evaluator inside a lint
|
|
92
|
+
* rule; out of scope here. See #1024.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
96
|
+
export type SubsetRuleId = "EVL001" | "EVL003";
|
|
97
|
+
|
|
98
|
+
/** A located, shape-level rejection: the offending node, the EVL rule id it maps to, and a message. */
|
|
99
|
+
export interface SubsetViolation {
|
|
100
|
+
node: ts.Node;
|
|
101
|
+
ruleId: SubsetRuleId;
|
|
102
|
+
message: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function violation(node: ts.Node, message: string, ruleId: SubsetRuleId = "EVL001"): SubsetViolation {
|
|
106
|
+
return { node, ruleId, message };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Binary operators `fold()` implements — see `fold()`'s operator switch in ./fold.ts. */
|
|
110
|
+
export const SUPPORTED_BINARY_OPERATORS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
111
|
+
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
112
|
+
ts.SyntaxKind.BarBarToken,
|
|
113
|
+
ts.SyntaxKind.QuestionQuestionToken,
|
|
114
|
+
ts.SyntaxKind.PlusToken,
|
|
115
|
+
ts.SyntaxKind.MinusToken,
|
|
116
|
+
ts.SyntaxKind.AsteriskToken,
|
|
117
|
+
ts.SyntaxKind.SlashToken,
|
|
118
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
119
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
120
|
+
ts.SyntaxKind.GreaterThanToken,
|
|
121
|
+
ts.SyntaxKind.LessThanToken,
|
|
122
|
+
ts.SyntaxKind.GreaterThanEqualsToken,
|
|
123
|
+
ts.SyntaxKind.LessThanEqualsToken,
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
/** Prefix unary operators `fold()` implements: logical-not and numeric negation. */
|
|
127
|
+
export const SUPPORTED_UNARY_OPERATORS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
128
|
+
ts.SyntaxKind.ExclamationToken,
|
|
129
|
+
ts.SyntaxKind.MinusToken,
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
/** A property name foldable without execution: identifier, string, or numeric literal (not a computed name). */
|
|
133
|
+
export function isLiteralPropertyName(
|
|
134
|
+
node: ts.PropertyName,
|
|
135
|
+
): node is ts.Identifier | ts.StringLiteral | ts.NumericLiteral {
|
|
136
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** An element-access key foldable without execution: a string or numeric LITERAL only (EVL003 semantics). */
|
|
140
|
+
export function isLiteralElementKey(node: ts.Expression): node is ts.StringLiteral | ts.NumericLiteral {
|
|
141
|
+
return ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Shared message builders — `fold()` and `findSubsetViolation` both call
|
|
146
|
+
// these so the diagnostic text for the same violation kind is the same
|
|
147
|
+
// string, not two hand-written copies that can drift.
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export function computedPropertyNameMessage(node: ts.PropertyName): string {
|
|
151
|
+
return `computed/dynamic property name not foldable: ${node.getText()}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function dynamicElementAccessMessage(keyNode: ts.Expression): string {
|
|
155
|
+
return `dynamic property access — computed key must be a string or numeric literal: ${keyNode.getText()}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const UNSUPPORTED_OBJECT_MEMBER_MESSAGE = "unsupported object member";
|
|
159
|
+
|
|
160
|
+
export const UNSUPPORTED_UNARY_MESSAGE = "unsupported unary";
|
|
161
|
+
|
|
162
|
+
export function unsupportedBinaryMessage(opKind: ts.SyntaxKind): string {
|
|
163
|
+
return `unsupported binary operator: ${ts.SyntaxKind[opKind]}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function callExpressionMessage(node: ts.CallExpression): string {
|
|
167
|
+
return `function call as a value is not foldable: ${node.expression.getText()}(...)`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function unsupportedExpressionMessage(node: ts.Node): string {
|
|
171
|
+
return `unsupported expression: ${ts.SyntaxKind[node.kind]}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Classify one object-literal member (`{ a: 1 }`'s `a: 1`, `{ ...x }`'s
|
|
176
|
+
* `...x`, or a shorthand `{ a }`). Checks the key's shape before the
|
|
177
|
+
* value's, mirroring `propName()` in fold.ts, which runs before folding
|
|
178
|
+
* the value. Returns the first violation within this member, or
|
|
179
|
+
* `undefined` when it's fully in the subset.
|
|
180
|
+
*/
|
|
181
|
+
export function checkObjectMember(
|
|
182
|
+
prop: ts.ObjectLiteralElementLike,
|
|
183
|
+
intrinsics?: readonly IntrinsicDef[],
|
|
184
|
+
): SubsetViolation | undefined {
|
|
185
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
186
|
+
if (!isLiteralPropertyName(prop.name)) {
|
|
187
|
+
return violation(prop.name, computedPropertyNameMessage(prop.name));
|
|
188
|
+
}
|
|
189
|
+
return findSubsetViolation(prop.initializer, intrinsics);
|
|
190
|
+
}
|
|
191
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
if (ts.isSpreadAssignment(prop)) {
|
|
195
|
+
return findSubsetViolation(prop.expression, intrinsics);
|
|
196
|
+
}
|
|
197
|
+
return violation(prop, UNSUPPORTED_OBJECT_MEMBER_MESSAGE);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Classify one array-literal element: a value, or a `...spread`. */
|
|
201
|
+
function checkArrayElement(
|
|
202
|
+
el: ts.Expression,
|
|
203
|
+
intrinsics?: readonly IntrinsicDef[],
|
|
204
|
+
): SubsetViolation | undefined {
|
|
205
|
+
if (ts.isSpreadElement(el)) return findSubsetViolation(el.expression, intrinsics);
|
|
206
|
+
return findSubsetViolation(el, intrinsics);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The canonical recursive shape classifier: is `node`'s expression *shape*
|
|
211
|
+
* within the subset `fold()` can reduce? Mirrors `fold()`'s own dispatch
|
|
212
|
+
* node-kind for node-kind (see ./fold.ts) — every branch here has a
|
|
213
|
+
* matching branch there, and vice versa — but performs no
|
|
214
|
+
* resolution/evaluation (see the module doc comment for the three
|
|
215
|
+
* environment-dependent exceptions). Returns the first (deepest,
|
|
216
|
+
* `fold()`-evaluation-order) unsupported node, or `undefined` when `node`'s
|
|
217
|
+
* whole shape is foldable.
|
|
218
|
+
*/
|
|
219
|
+
export function findSubsetViolation(
|
|
220
|
+
node: ts.Node,
|
|
221
|
+
intrinsics?: readonly IntrinsicDef[],
|
|
222
|
+
): SubsetViolation | undefined {
|
|
223
|
+
if (
|
|
224
|
+
ts.isParenthesizedExpression(node) ||
|
|
225
|
+
ts.isAsExpression(node) ||
|
|
226
|
+
ts.isSatisfiesExpression(node) ||
|
|
227
|
+
ts.isNonNullExpression(node)
|
|
228
|
+
) {
|
|
229
|
+
return findSubsetViolation(node.expression, intrinsics);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
ts.isStringLiteral(node) ||
|
|
234
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
235
|
+
ts.isNumericLiteral(node) ||
|
|
236
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
237
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
238
|
+
node.kind === ts.SyntaxKind.NullKeyword
|
|
239
|
+
) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Identifiers (including a bare `undefined` reference, which the parser
|
|
244
|
+
// represents as an Identifier, not a keyword token) — resolution is
|
|
245
|
+
// environment-dependent, see module doc.
|
|
246
|
+
if (ts.isIdentifier(node)) return undefined;
|
|
247
|
+
|
|
248
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
249
|
+
// Tagged-template interiors are OPAQUE to the shape classifier. A tagged
|
|
250
|
+
// template may be a registered lexicon intrinsic (e.g. Sub`...`) whose
|
|
251
|
+
// interpolations legitimately contain deploy-time intrinsic references
|
|
252
|
+
// (Ref(env), AWS.StackName, ...) — valid, but not statically foldable. EVL
|
|
253
|
+
// has no intrinsic registry at lint time (see module doc), so it cannot tell
|
|
254
|
+
// an intrinsic call from a plain one; recursing here would false-flag Ref()
|
|
255
|
+
// inside Sub`...` and break every intrinsic-using example. fold() DOES have
|
|
256
|
+
// the registry: it recurses into a *registered* tag's interior itself
|
|
257
|
+
// (foldIntrinsicValue) and rejects an unfoldable one there. So an unfoldable
|
|
258
|
+
// tagged-template interior is a documented fold/EVL divergence, not a hole.
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (ts.isTemplateExpression(node)) {
|
|
263
|
+
for (const span of node.templateSpans) {
|
|
264
|
+
const v = findSubsetViolation(span.expression, intrinsics);
|
|
265
|
+
if (v) return v;
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
271
|
+
for (const prop of node.properties) {
|
|
272
|
+
const v = checkObjectMember(prop, intrinsics);
|
|
273
|
+
if (v) return v;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
279
|
+
for (const el of node.elements) {
|
|
280
|
+
const v = checkArrayElement(el, intrinsics);
|
|
281
|
+
if (v) return v;
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
287
|
+
return findSubsetViolation(node.expression, intrinsics);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (ts.isElementAccessExpression(node)) {
|
|
291
|
+
if (!isLiteralElementKey(node.argumentExpression)) {
|
|
292
|
+
return violation(node.argumentExpression, dynamicElementAccessMessage(node.argumentExpression), "EVL003");
|
|
293
|
+
}
|
|
294
|
+
return findSubsetViolation(node.expression, intrinsics);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
298
|
+
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) {
|
|
299
|
+
return violation(node, UNSUPPORTED_UNARY_MESSAGE);
|
|
300
|
+
}
|
|
301
|
+
return findSubsetViolation(node.operand, intrinsics);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (ts.isBinaryExpression(node)) {
|
|
305
|
+
const opKind = node.operatorToken.kind;
|
|
306
|
+
if (!SUPPORTED_BINARY_OPERATORS.has(opKind)) {
|
|
307
|
+
return violation(node, unsupportedBinaryMessage(opKind));
|
|
308
|
+
}
|
|
309
|
+
// Flow-insensitive — see module doc: fold() short-circuits &&/||/?? and
|
|
310
|
+
// only evaluates the taken side; EVL requires both sides shape-valid.
|
|
311
|
+
return findSubsetViolation(node.left, intrinsics) ?? findSubsetViolation(node.right, intrinsics);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (ts.isConditionalExpression(node)) {
|
|
315
|
+
// Flow-insensitive — see module doc: fold() only folds the taken branch.
|
|
316
|
+
return (
|
|
317
|
+
findSubsetViolation(node.condition, intrinsics) ??
|
|
318
|
+
findSubsetViolation(node.whenTrue, intrinsics) ??
|
|
319
|
+
findSubsetViolation(node.whenFalse, intrinsics)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (ts.isNewExpression(node)) {
|
|
324
|
+
// chant #1082 — no positional assumption about which argument is the
|
|
325
|
+
// props object. `foldResource` folds every argument in source order (the
|
|
326
|
+
// props object is second in `new Parameter("String", {...})`), so every
|
|
327
|
+
// argument is classified on its own terms and nothing is rejected merely
|
|
328
|
+
// for being in the "wrong" position.
|
|
329
|
+
for (const arg of node.arguments ?? []) {
|
|
330
|
+
const v = findSubsetViolation(arg, intrinsics);
|
|
331
|
+
if (v) return v;
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (ts.isSpreadElement(node)) {
|
|
337
|
+
return findSubsetViolation(node.expression, intrinsics);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (ts.isCallExpression(node)) {
|
|
341
|
+
// chant #1082 — a call to a REGISTERED chant authoring helper folds
|
|
342
|
+
// (`phase(...)`, `output(...)`, …; see ./foldable-helpers.ts), so this
|
|
343
|
+
// classifier must accept it too or EVL001 would flag source `fold()`
|
|
344
|
+
// reduces cleanly. Name-only here, deliberately: this module classifies
|
|
345
|
+
// shape and never resolves bindings (module doc, point 1), and the
|
|
346
|
+
// provenance half of the check — is this name actually bound to an import
|
|
347
|
+
// of chant's own? — needs the module graph, which only
|
|
348
|
+
// ../discovery/fold-import.ts has. Same asymmetry as intrinsic tag
|
|
349
|
+
// registration (point 2) and in the same direction: this module can only
|
|
350
|
+
// ever be MORE permissive than `fold()`, never stricter.
|
|
351
|
+
if (ts.isIdentifier(node.expression) && isFoldableHelperName(node.expression.text)) {
|
|
352
|
+
for (const arg of node.arguments) {
|
|
353
|
+
const v = findSubsetViolation(arg, intrinsics);
|
|
354
|
+
if (v) return v;
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// chant #1044 — a plain call to a lexicon intrinsic whose lexicon opted
|
|
360
|
+
// its call form in folds too (`Ref(bucket)`, `Concat("a", b)`). Unlike
|
|
361
|
+
// the helper case above, this one is only answerable with the registry
|
|
362
|
+
// in hand, which is exactly why it is a parameter: a caller that passes
|
|
363
|
+
// `intrinsics` gets fold()'s own answer, and a caller that can't supply
|
|
364
|
+
// one (EVL, any syntax-only tool) keeps the pre-#1044 answer — every
|
|
365
|
+
// call is a violation. See the module doc, point 2c, for why the
|
|
366
|
+
// registry-less answer is the safe one to leave in place.
|
|
367
|
+
if (
|
|
368
|
+
intrinsics &&
|
|
369
|
+
ts.isIdentifier(node.expression) &&
|
|
370
|
+
intrinsics.some((i) => i.name === (node.expression as ts.Identifier).text && intrinsicCallFolds(i))
|
|
371
|
+
) {
|
|
372
|
+
for (const arg of node.arguments) {
|
|
373
|
+
const v = findSubsetViolation(arg, intrinsics);
|
|
374
|
+
if (v) return v;
|
|
375
|
+
}
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return violation(node, callExpressionMessage(node));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return violation(node, unsupportedExpressionMessage(node));
|
|
383
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export * from "./declarable";
|
|
8
8
|
export * from "./composite";
|
|
9
9
|
export * from "./provenance";
|
|
10
|
+
export * from "./build-params";
|
|
10
11
|
export * from "./intrinsic";
|
|
11
12
|
export * from "./types";
|
|
12
13
|
export * from "./errors";
|
|
@@ -33,6 +34,7 @@ export * from "./graph-dot";
|
|
|
33
34
|
export * from "./graph-layout";
|
|
34
35
|
export * from "./graph-lens";
|
|
35
36
|
export * from "./detectLexicon";
|
|
37
|
+
export * from "./fold/fold";
|
|
36
38
|
export * from "./lint/parser";
|
|
37
39
|
export * from "./lint/rule";
|
|
38
40
|
export * from "./lint/rules";
|
package/src/lexicon-output.ts
CHANGED
|
@@ -45,8 +45,13 @@ export class LexiconOutput implements Intrinsic {
|
|
|
45
45
|
readonly outputName: string;
|
|
46
46
|
/** @internal WeakRef to the source entity object for identity-based matching */
|
|
47
47
|
readonly _sourceParent: WeakRef<object> | null;
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @internal Intrinsic value when constructed from an Intrinsic rather than AttrRef.
|
|
50
|
+
* Readable outside the class (like `_sourceParent` above) so the entity-wire
|
|
51
|
+
* encoder can reach it without an `as unknown as` cast that would erase type
|
|
52
|
+
* checking on every field it reads (#1047).
|
|
53
|
+
*/
|
|
54
|
+
readonly _intrinsic: Intrinsic | null;
|
|
50
55
|
|
|
51
56
|
constructor(ref: AttrRef | Intrinsic | string, name: string) {
|
|
52
57
|
if (ref instanceof AttrRef) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IntrinsicDefSchema,
|
|
7
7
|
LexiconEntrySchema,
|
|
8
8
|
} from "./lexicon-schema";
|
|
9
|
+
import { intrinsicFolds, intrinsicTagFolds, intrinsicCallFolds } from "./lexicon";
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// validateManifest
|
|
@@ -131,6 +132,62 @@ describe("IntrinsicDefSchema", () => {
|
|
|
131
132
|
const result = IntrinsicDefSchema.safeParse({ description: "no name" });
|
|
132
133
|
expect(result.success).toBe(false);
|
|
133
134
|
});
|
|
135
|
+
|
|
136
|
+
// chant #1067 — isTag went from optional to required: a registration
|
|
137
|
+
// silently defaulting to "not a tag" is exactly how #1039 shipped
|
|
138
|
+
// (aws's Sub had no isTag at all and never folded in production).
|
|
139
|
+
test("rejects a registration with isTag omitted", () => {
|
|
140
|
+
const result = IntrinsicDefSchema.safeParse({ name: "Sub", description: "no isTag" });
|
|
141
|
+
expect(result.success).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("accepts isTag: true and isTag: false, rejects non-boolean isTag", () => {
|
|
145
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Sub", isTag: true }).success).toBe(true);
|
|
146
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Ref", isTag: false }).success).toBe(true);
|
|
147
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Ref", isTag: "false" }).success).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("foldsAsCall is optional and boolean — omitted means not opted in (chant #1044)", () => {
|
|
151
|
+
const omitted = IntrinsicDefSchema.safeParse({ name: "Ref", isTag: false });
|
|
152
|
+
expect(omitted.success).toBe(true);
|
|
153
|
+
if (omitted.success) expect(omitted.data.foldsAsCall).toBeUndefined();
|
|
154
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Ref", isTag: false, foldsAsCall: true }).success).toBe(true);
|
|
155
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Ref", isTag: false, foldsAsCall: "yes" }).success).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Foldability predicates (chant #1044)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
describe("intrinsic foldability predicates", () => {
|
|
164
|
+
test("a tagged template folds as a tag, never as a call", () => {
|
|
165
|
+
const sub = { isTag: true };
|
|
166
|
+
expect(intrinsicTagFolds(sub)).toBe(true);
|
|
167
|
+
expect(intrinsicCallFolds(sub)).toBe(false);
|
|
168
|
+
expect(intrinsicFolds(sub)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a plain call folds only once its lexicon opts it in — never by inference", () => {
|
|
172
|
+
const optedIn = { isTag: false, foldsAsCall: true };
|
|
173
|
+
expect(intrinsicCallFolds({ isTag: false })).toBe(false);
|
|
174
|
+
expect(intrinsicFolds({ isTag: false })).toBe(false);
|
|
175
|
+
expect(intrinsicCallFolds(optedIn)).toBe(true);
|
|
176
|
+
expect(intrinsicFolds(optedIn)).toBe(true);
|
|
177
|
+
expect(intrinsicTagFolds(optedIn)).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a registration claiming BOTH forms does not fold as a call — isTag wins, and check-lexicon fails it", () => {
|
|
181
|
+
const both = { isTag: true, foldsAsCall: true };
|
|
182
|
+
expect(intrinsicCallFolds(both)).toBe(false);
|
|
183
|
+
expect(intrinsicTagFolds(both)).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("an older manifest with neither field folds in neither form", () => {
|
|
187
|
+
expect(intrinsicFolds({})).toBe(false);
|
|
188
|
+
expect(intrinsicTagFolds({})).toBe(false);
|
|
189
|
+
expect(intrinsicCallFolds({})).toBe(false);
|
|
190
|
+
});
|
|
134
191
|
});
|
|
135
192
|
|
|
136
193
|
// ---------------------------------------------------------------------------
|
package/src/lexicon-schema.ts
CHANGED
|
@@ -13,7 +13,14 @@ export const IntrinsicDefSchema = z.object({
|
|
|
13
13
|
name: z.string().min(1, "intrinsic name must not be empty"),
|
|
14
14
|
description: z.string().optional(),
|
|
15
15
|
outputKey: z.string().optional(),
|
|
16
|
-
|
|
16
|
+
// Required (chant #1067) — no silent default for whether an intrinsic
|
|
17
|
+
// folds. See IntrinsicDef.isTag in ../lexicon.ts for the history.
|
|
18
|
+
isTag: z.boolean(),
|
|
19
|
+
// chant #1044 — optional and default-off on purpose: absent means "this
|
|
20
|
+
// intrinsic's plain-call form does not fold", which is what every
|
|
21
|
+
// registration written before #1044 means. Only an explicit `true` opts a
|
|
22
|
+
// call into folding. See IntrinsicDef.foldsAsCall in ../lexicon.ts.
|
|
23
|
+
foldsAsCall: z.boolean().optional(),
|
|
17
24
|
});
|
|
18
25
|
|
|
19
26
|
// ---------------------------------------------------------------------------
|
package/src/lexicon.ts
CHANGED
|
@@ -18,6 +18,41 @@ export type { ReferenceCatalog, IdentityRule, RefRule } from "./graph-refs";
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Manifest for a packaged lexicon — metadata embedded in the tarball.
|
|
21
|
+
*
|
|
22
|
+
* chant #1067 decided each optional field below explicitly rather than
|
|
23
|
+
* leaving them all equally unchecked:
|
|
24
|
+
*
|
|
25
|
+
* - `chantVersion` — now validated for presence/shape by `chant dev
|
|
26
|
+
* check-lexicon` (a lexicon's `dist/manifest.json` must declare one).
|
|
27
|
+
* NOT validated for compatibility against the core version actually
|
|
28
|
+
* running — that needs a live check at plugin-load time (a different
|
|
29
|
+
* surface: `loadPlugin`/`loadPlugins` in `./cli/plugins.ts`), which stays
|
|
30
|
+
* a deliberate non-goal here.
|
|
31
|
+
* - `namespace` — deliberately NOT validated for cross-lexicon uniqueness in
|
|
32
|
+
* #1067. `chant dev check-lexicon <dir>` only ever inspects one lexicon at
|
|
33
|
+
* a time, so it structurally can't catch a collision between two
|
|
34
|
+
* *different* lexicons. The natural home is `checkConflicts` (./cli/
|
|
35
|
+
* conflict-check.ts), which already detects cross-lexicon rule-id/skill/
|
|
36
|
+
* MCP-tool/MCP-resource collisions when multiple plugins load together —
|
|
37
|
+
* but `namespace` isn't on the runtime `LexiconPlugin` surface at all
|
|
38
|
+
* (manifest-only, baked in at package time), so extending that check would
|
|
39
|
+
* mean adding a new field to `LexiconPlugin` itself. Deferred rather than
|
|
40
|
+
* done under time pressure alongside a concurrent, unrelated change to
|
|
41
|
+
* this same interface (chant #1064).
|
|
42
|
+
* - `pseudoParameters` — deliberately NOT validated against what the
|
|
43
|
+
* lexicon actually exports in #1067, unlike `intrinsics` (see
|
|
44
|
+
* `IntrinsicDef.isTag` below and `./cli/commands/check-lexicon-
|
|
45
|
+
* intrinsics.ts`). The same static-analysis approach would apply — check
|
|
46
|
+
* each declared pseudo-parameter's short name against the real exported
|
|
47
|
+
* namespace object's properties (`AWS.StackName`, `Azure.ResourceGroupName`,
|
|
48
|
+
* ...) — but unlike `intrinsics()`, `pseudoParameters(): string[]` is a
|
|
49
|
+
* flat list of dotted strings with no structural link back to a specific
|
|
50
|
+
* export/property declaration, making the same trick more involved to
|
|
51
|
+
* generalize correctly. Relevant to #1063 (folding cross-file references
|
|
52
|
+
* into lexicon pseudo-parameter namespaces): once that lands, a wrong or
|
|
53
|
+
* missing pseudo-parameter property would silently break folding exactly
|
|
54
|
+
* the way #1039 did for intrinsics, which would be the moment this
|
|
55
|
+
* deferral needs revisiting.
|
|
21
56
|
*/
|
|
22
57
|
export interface LexiconManifest {
|
|
23
58
|
name: string;
|
|
@@ -102,7 +137,103 @@ export interface IntrinsicDef {
|
|
|
102
137
|
readonly name: string;
|
|
103
138
|
readonly description?: string;
|
|
104
139
|
readonly outputKey?: string;
|
|
105
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Whether this intrinsic is authored as a JS tagged template (`` Sub`...` ``)
|
|
142
|
+
* rather than a plain function call (`Ref(...)`). Required — chant #1067 —
|
|
143
|
+
* because an omitted value silently defaulted to "not a tag" with no
|
|
144
|
+
* signal that the registration had never been decided. That produced #1039
|
|
145
|
+
* in both directions at once: aws's `Sub` (a genuine tagged template, the
|
|
146
|
+
* most-used intrinsic in the ecosystem) shipped with no `isTag` at all and
|
|
147
|
+
* silently never folded, while gitlab's `reference()` (a plain call)
|
|
148
|
+
* shipped with `isTag: true`. Both were wrong, and both shipped, because
|
|
149
|
+
* nothing forced the declaration or checked it against how the intrinsic is
|
|
150
|
+
* actually authored. `chant dev check-lexicon` now validates every
|
|
151
|
+
* registration here against its real declaration (tagged-template
|
|
152
|
+
* signature vs plain call) and against the package's own exports — see
|
|
153
|
+
* `../cli/commands/check-lexicon-intrinsics.ts`.
|
|
154
|
+
*/
|
|
155
|
+
readonly isTag: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* chant #1044 — opt this intrinsic's PLAIN-CALL form into folding
|
|
158
|
+
* (`Ref(bucket)`, `Concat(a, b)` reduce to their intrinsic node instead of
|
|
159
|
+
* falling the whole file back to the run path).
|
|
160
|
+
*
|
|
161
|
+
* Optional, and OFF unless a lexicon writes `true`. That default is the
|
|
162
|
+
* point: `fold()` has no general `CallExpression` case by construction
|
|
163
|
+
* (epic #1019), and this field is the only thing that admits one. It is a
|
|
164
|
+
* closed, lexicon-declared allowlist, decided one intrinsic at a time —
|
|
165
|
+
* never inferred from `isTag`, from the name, or from the call's shape. An
|
|
166
|
+
* intrinsic with no `foldsAsCall` behaves exactly as it did before #1044.
|
|
167
|
+
*
|
|
168
|
+
* Only set it when calling the intrinsic is a pure function of its
|
|
169
|
+
* arguments that builds a deterministic data envelope — the whole
|
|
170
|
+
* correctness argument is that invoking it while folding is
|
|
171
|
+
* indistinguishable from invoking it during a real run of the file. An
|
|
172
|
+
* intrinsic that reads the environment, mutates state, or depends on
|
|
173
|
+
* anything but its arguments does not qualify, and neither does a tagged
|
|
174
|
+
* template (see {@link isTag}: the two forms are mutually exclusive, and
|
|
175
|
+
* `chant dev check-lexicon` rejects `isTag: true` + `foldsAsCall: true`).
|
|
176
|
+
*
|
|
177
|
+
* Registration is by name. It is not permission to invoke whatever that
|
|
178
|
+
* name happens to be bound to: `fold()` reduces the call to a symbolic
|
|
179
|
+
* envelope executing nothing, and `../discovery/fold-import.ts` resolves
|
|
180
|
+
* the name through the folding FILE'S OWN imports before invoking the real
|
|
181
|
+
* function — so the function that runs while folding is the same one the
|
|
182
|
+
* run path would have called, from the module the source itself named.
|
|
183
|
+
*/
|
|
184
|
+
readonly foldsAsCall?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whether `chant build --fold` can ever fold a use of this intrinsic
|
|
189
|
+
* (chant #1062, epic #1019) — in EITHER authored form.
|
|
190
|
+
*
|
|
191
|
+
* Two disjoint ways to qualify, one per form:
|
|
192
|
+
*
|
|
193
|
+
* - a registered tagged-template intrinsic (`Sub\`...\``) folds because
|
|
194
|
+
* `foldTaggedTemplate` recognizes its tag and recurses into the interior
|
|
195
|
+
* ({@link intrinsicTagFolds});
|
|
196
|
+
* - a registered plain-call intrinsic (`Ref(...)`, `Concat(...)`) folds
|
|
197
|
+
* only when its lexicon opted it in with `foldsAsCall`
|
|
198
|
+
* ({@link intrinsicCallFolds}, chant #1044). Before #1044 no plain call
|
|
199
|
+
* folded at all, whatever it was named or registered as.
|
|
200
|
+
*
|
|
201
|
+
* This function is the single predicate the generated per-lexicon intrinsics
|
|
202
|
+
* page (`../codegen/docs-sections.ts`'s "Folds?" column) calls — never a
|
|
203
|
+
* restated copy that could silently drift from the code. `fold()` itself
|
|
204
|
+
* calls the two form-specific predicates below rather than this one, because
|
|
205
|
+
* it always knows which form it is looking at, and a tag must not fold as a
|
|
206
|
+
* call (or vice versa) merely because the other form was opted in.
|
|
207
|
+
*
|
|
208
|
+
* Takes a structural `{ isTag?, foldsAsCall? }` rather than
|
|
209
|
+
* `Pick<IntrinsicDef, ...>` deliberately: `IntrinsicDef.isTag` is required
|
|
210
|
+
* for new registrations (chant #1067), but these predicates also read off
|
|
211
|
+
* untrusted, possibly-older parsed JSON (`ManifestJSON` in
|
|
212
|
+
* `./codegen/docs-types.ts`, on disk as a published lexicon's
|
|
213
|
+
* `dist/manifest.json`) that may predate either field. `undefined` there
|
|
214
|
+
* means what it always did — not a tag, not opted in.
|
|
215
|
+
*/
|
|
216
|
+
export function intrinsicFolds(def: { isTag?: boolean; foldsAsCall?: boolean }): boolean {
|
|
217
|
+
return intrinsicTagFolds(def) || intrinsicCallFolds(def);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** True when this intrinsic's TAGGED-TEMPLATE form folds (`Sub\`...\``) — see {@link intrinsicFolds}. */
|
|
221
|
+
export function intrinsicTagFolds(def: { isTag?: boolean }): boolean {
|
|
222
|
+
return def.isTag === true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* True when this intrinsic's PLAIN-CALL form folds (`Ref(...)`) — i.e. the
|
|
227
|
+
* lexicon opted it in via {@link IntrinsicDef.foldsAsCall} (chant #1044).
|
|
228
|
+
*
|
|
229
|
+
* `isTag: true` disqualifies regardless: a tagged template is invoked as
|
|
230
|
+
* `` Name`...` ``, so a call to it isn't the registered authoring form at
|
|
231
|
+
* all. Keeping that here rather than trusting registrations means a lexicon
|
|
232
|
+
* that declares both flags cannot quietly widen `fold()`'s call case — and
|
|
233
|
+
* `chant dev check-lexicon` fails the registration outright.
|
|
234
|
+
*/
|
|
235
|
+
export function intrinsicCallFolds(def: { isTag?: boolean; foldsAsCall?: boolean }): boolean {
|
|
236
|
+
return def.isTag !== true && def.foldsAsCall === true;
|
|
106
237
|
}
|
|
107
238
|
|
|
108
239
|
/**
|