@intentius/tsad-reference 1.4.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/CAVEATS.md +95 -0
- package/README.md +42 -0
- package/dist/adapter.d.ts +5 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +50 -0
- package/dist/adapter.js.map +1 -0
- package/dist/fnbody.d.ts +11 -0
- package/dist/fnbody.d.ts.map +1 -0
- package/dist/fnbody.js +68 -0
- package/dist/fnbody.js.map +1 -0
- package/dist/fold.d.ts +86 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +558 -0
- package/dist/fold.js.map +1 -0
- package/dist/foldable-helpers.d.ts +23 -0
- package/dist/foldable-helpers.d.ts.map +1 -0
- package/dist/foldable-helpers.js +25 -0
- package/dist/foldable-helpers.js.map +1 -0
- package/dist/host.d.ts +45 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +5 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +20 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +74 -0
- package/dist/module.js.map +1 -0
- package/dist/project.d.ts +22 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +481 -0
- package/dist/project.js.map +1 -0
- package/dist/revive.d.ts +16 -0
- package/dist/revive.d.ts.map +1 -0
- package/dist/revive.js +109 -0
- package/dist/revive.js.map +1 -0
- package/dist/subset.d.ts +42 -0
- package/dist/subset.d.ts.map +1 -0
- package/dist/subset.js +214 -0
- package/dist/subset.js.map +1 -0
- package/package.json +37 -0
- package/src/adapter.ts +48 -0
- package/src/fnbody.ts +61 -0
- package/src/fold.ts +581 -0
- package/src/foldable-helpers.ts +38 -0
- package/src/host.ts +38 -0
- package/src/index.ts +8 -0
- package/src/module.ts +52 -0
- package/src/no-own-execution.test.ts +86 -0
- package/src/prebuild.test.ts +68 -0
- package/src/project.ts +495 -0
- package/src/revive.ts +120 -0
- package/src/subset.ts +224 -0
package/src/fold.ts
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expression evaluation: judgments.md J1, one branch per F-Eval-* rule.
|
|
3
|
+
*
|
|
4
|
+
* Written from the specification text, not derived from any implementation
|
|
5
|
+
* (#50). Executes nothing of the source under fold. Its only executions are
|
|
6
|
+
* F-Eval-CallEager and F-Eval-CallMethod on a real receiver, both of which
|
|
7
|
+
* run code the file imported rather than code it wrote.
|
|
8
|
+
*/
|
|
9
|
+
import * as ts from "typescript";
|
|
10
|
+
import {
|
|
11
|
+
intrinsicCallFolds,
|
|
12
|
+
intrinsicCallFoldsEagerly,
|
|
13
|
+
intrinsicTagFolds,
|
|
14
|
+
type IntrinsicDef,
|
|
15
|
+
} from "./host.js";
|
|
16
|
+
import { isFoldableHelperName } from "./foldable-helpers.js";
|
|
17
|
+
import { isLiteralKey, isLiteralElementKey, isUnclaimedCallee } from "./subset.js";
|
|
18
|
+
import { findFnBodyViolation, plainBindingKey, type FnDecl } from "./fnbody.js";
|
|
19
|
+
|
|
20
|
+
/** A located rejection, per R9.3: the node and the rule, wording unconstrained. */
|
|
21
|
+
export class FoldRejection extends Error {
|
|
22
|
+
constructor(
|
|
23
|
+
readonly rule: string,
|
|
24
|
+
readonly line: number,
|
|
25
|
+
readonly column: number,
|
|
26
|
+
message: string,
|
|
27
|
+
) {
|
|
28
|
+
super(`${line}:${column} - ${message}`);
|
|
29
|
+
this.name = "FoldRejection";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Γ = (consts, externals, depth), per R6.6. */
|
|
34
|
+
export interface Scope {
|
|
35
|
+
readonly consts: Map<string, ts.Expression>;
|
|
36
|
+
readonly externals: ReadonlyMap<string, unknown>;
|
|
37
|
+
readonly depth: number;
|
|
38
|
+
/**
|
|
39
|
+
* F-Capture's sink for the file being folded, when there is one. A call that
|
|
40
|
+
* leaks the defining module's identity (F-CallLeak) records that module here,
|
|
41
|
+
* which is the same edge an import capture records.
|
|
42
|
+
*/
|
|
43
|
+
readonly captures?: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
/** H = (ρ, helpers). The helper allowlist is consulted through foldable-helpers. */
|
|
46
|
+
export interface EvalHost {
|
|
47
|
+
readonly intrinsics: readonly IntrinsicDef[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* F-Val-Callable's marker. J1 may *call* one; it is never a value. Only the
|
|
52
|
+
* module layer populates `externals` with these, so nothing produces one in
|
|
53
|
+
* this implementation yet (#21, #22).
|
|
54
|
+
*/
|
|
55
|
+
export class FoldableFunction {
|
|
56
|
+
/** Set when a call returned a live object the body produced (F-CallLeak, J3). */
|
|
57
|
+
leakedIdentity = false;
|
|
58
|
+
constructor(
|
|
59
|
+
readonly name: string,
|
|
60
|
+
readonly fn: FnDecl,
|
|
61
|
+
/** The defining module's path, for the re-anchored reason of F-Eval-CallLocal step 7. */
|
|
62
|
+
readonly file: string,
|
|
63
|
+
/** The DEFINING module's scope: a body folds there, not in the caller's (R6.6). */
|
|
64
|
+
readonly consts: Map<string, ts.Expression>,
|
|
65
|
+
readonly externals: ReadonlyMap<string, unknown>,
|
|
66
|
+
) {}
|
|
67
|
+
}
|
|
68
|
+
export const isFoldableFunction = (v: unknown): v is FoldableFunction => v instanceof FoldableFunction;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The chain-short-circuit value of F-Eval-Member steps 3 and 4. Not a member
|
|
72
|
+
* of the value domain: it never escapes an optional chain.
|
|
73
|
+
*/
|
|
74
|
+
const CHAIN = Symbol("chain-short-circuit");
|
|
75
|
+
const isChain = (v: unknown): boolean => v === CHAIN;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* F-Val-Live: carries a live object when it, or anything reachable through
|
|
79
|
+
* plain objects and arrays, has a prototype other than the plain ones, or is
|
|
80
|
+
* a function. This is F-Identity's entity test, the normative one, and the
|
|
81
|
+
* test F-CallLeak uses. F-Import uses the broader reference test instead, on
|
|
82
|
+
* purpose; F-Identity in J3 says why.
|
|
83
|
+
*/
|
|
84
|
+
export function carriesLiveObject(v: unknown, seen = new Set<unknown>()): boolean {
|
|
85
|
+
if (v === null || typeof v !== "object") return typeof v === "function";
|
|
86
|
+
if (seen.has(v)) return false;
|
|
87
|
+
seen.add(v);
|
|
88
|
+
if (isLiveObject(v)) return true;
|
|
89
|
+
return Object.values(v).some((inner) => carriesLiveObject(inner, seen));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The non-recursive half of F-Val-Live: this value *is* a live object, rather
|
|
94
|
+
* than a plain structure that may hold one. Revival passes these through
|
|
95
|
+
* unchanged (L6.1); rebuilding one would destroy the identity J3 preserves.
|
|
96
|
+
*/
|
|
97
|
+
export function isLiveObject(v: unknown): boolean {
|
|
98
|
+
if (typeof v === "function") return true;
|
|
99
|
+
if (v === null || typeof v !== "object") return false;
|
|
100
|
+
const proto = Object.getPrototypeOf(v);
|
|
101
|
+
return proto !== Object.prototype && proto !== Array.prototype && proto !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** F-Val-Envelope: a non-array object carrying one of the six keys. */
|
|
105
|
+
const ENVELOPE_KEYS = ["__attrRef", "__intrinsic", "__helper", "__resource", "__compositeStep", "__symbol"] as const;
|
|
106
|
+
export function isEnvelope(v: unknown): boolean {
|
|
107
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
|
|
108
|
+
return ENVELOPE_KEYS.some((k) => k in (v as object));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function locate(n: ts.Node): { line: number; column: number } {
|
|
112
|
+
const { line, character } = n.getSourceFile().getLineAndCharacterOfPosition(n.getStart());
|
|
113
|
+
return { line: line + 1, column: character + 1 };
|
|
114
|
+
}
|
|
115
|
+
function reject(rule: string, n: ts.Node, message: string): never {
|
|
116
|
+
const { line, column } = locate(n);
|
|
117
|
+
throw new FoldRejection(rule, line, column, message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** True when this access is itself part of an optional chain, so a sentinel propagates. */
|
|
121
|
+
function continuesChain(n: ts.Node): boolean {
|
|
122
|
+
const parent: ts.Node | undefined = n.parent;
|
|
123
|
+
if (parent === undefined) return false;
|
|
124
|
+
if (ts.isNonNullExpression(parent) && ts.isOptionalChain(parent)) return continuesChain(parent);
|
|
125
|
+
return (
|
|
126
|
+
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent) || ts.isCallExpression(parent)) &&
|
|
127
|
+
parent.expression === n &&
|
|
128
|
+
ts.isOptionalChain(parent)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True when `n` is a const whose initializer is a `new` expression (F-Eval-Ident step 1, F-Eval-Member step 1). */
|
|
133
|
+
const boundToNew = (scope: Scope, name: string): boolean => {
|
|
134
|
+
const init = scope.consts.get(name);
|
|
135
|
+
return init !== undefined && ts.isNewExpression(init);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* F-Eval-Interior. Inside an intrinsic's interior an identifier, or a
|
|
140
|
+
* `.`/`[]`/`!` chain rooted at one, that is not `undefined` and resolves in
|
|
141
|
+
* neither consts nor externals folds to `{__symbol}` instead of rejecting.
|
|
142
|
+
*/
|
|
143
|
+
function isUnresolvedChain(n: ts.Expression, scope: Scope): boolean {
|
|
144
|
+
if (ts.isIdentifier(n)) return n.text !== "undefined" && !scope.consts.has(n.text) && !scope.externals.has(n.text);
|
|
145
|
+
if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n) || ts.isNonNullExpression(n)) {
|
|
146
|
+
return isUnresolvedChain(n.expression, scope);
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function foldInterior(n: ts.Expression, scope: Scope, host: EvalHost): unknown {
|
|
151
|
+
if (isUnresolvedChain(n, scope)) return { __symbol: n.getText() };
|
|
152
|
+
return foldExpr(n, scope, host);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** F-Eval-New's arity rules, per F-Val-Arity. */
|
|
156
|
+
function foldNew(node: ts.NewExpression, scope: Scope, host: EvalHost): unknown {
|
|
157
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
158
|
+
reject("F-Eval-New", node, "a constructor reached through anything but a plain identifier is not foldable");
|
|
159
|
+
}
|
|
160
|
+
if (scope.depth > 0) reject("F-Eval-New", node, "`new` inside a folded function body is not foldable");
|
|
161
|
+
const name = node.expression.text;
|
|
162
|
+
const args = node.arguments ?? ([] as unknown as ts.NodeArray<ts.Expression>);
|
|
163
|
+
if (args.length === 0) return { __resource: name, props: {} };
|
|
164
|
+
const folded = args.map((a) => foldExpr(a, scope, host));
|
|
165
|
+
const first = args[0];
|
|
166
|
+
if (ts.isObjectLiteralExpression(first)) {
|
|
167
|
+
if (args.length === 1) return { __resource: name, props: folded[0] };
|
|
168
|
+
if (args.length === 2 && ts.isObjectLiteralExpression(args[1])) {
|
|
169
|
+
return { __resource: name, props: folded[0], attributes: folded[1] };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const propsIndex = args.findIndex((a) => ts.isObjectLiteralExpression(a));
|
|
173
|
+
return { __resource: name, props: propsIndex === -1 ? {} : folded[propsIndex], args: folded };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** F-Eval-Member and F-Eval-Index share every step but how the key is obtained. */
|
|
177
|
+
function foldAccess(
|
|
178
|
+
node: ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
|
179
|
+
key: string,
|
|
180
|
+
optional: boolean,
|
|
181
|
+
scope: Scope,
|
|
182
|
+
host: EvalHost,
|
|
183
|
+
): unknown {
|
|
184
|
+
// 1. an attribute of a same-file construction is a reference by NAME
|
|
185
|
+
if (ts.isIdentifier(node.expression) && boundToNew(scope, node.expression.text)) {
|
|
186
|
+
return { __attrRef: { entity: node.expression.text, attribute: key } };
|
|
187
|
+
}
|
|
188
|
+
// 2. S-CompositeStep, handled by the caller for property access only
|
|
189
|
+
// 3. sentinel propagation
|
|
190
|
+
const object = foldExpr(node.expression, scope, host);
|
|
191
|
+
if (isChain(object)) return continuesChain(node) ? CHAIN : undefined;
|
|
192
|
+
// 4. nullish
|
|
193
|
+
if (object === null || object === undefined) {
|
|
194
|
+
if (optional) return continuesChain(node) ? CHAIN : undefined;
|
|
195
|
+
reject(
|
|
196
|
+
"F-Eval-Member",
|
|
197
|
+
node,
|
|
198
|
+
`property "${key}" read on ${String(object)} is not foldable: running this expression throws. Write \`?.\` if the value is genuinely optional`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
// 5. an attribute read on a construction used as a value
|
|
202
|
+
if (isEnvelope(object) && "__resource" in (object as object)) {
|
|
203
|
+
if (ts.isIdentifier(node.expression)) return { __attrRef: { entity: node.expression.text, attribute: key } };
|
|
204
|
+
reject("F-Eval-Member", node, `attribute "${key}" read on an inline construction has no name to reference it by`);
|
|
205
|
+
}
|
|
206
|
+
// 6. a plain index
|
|
207
|
+
return (object as Record<string, unknown>)[key];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The depth bound of F-Eval-CallLocal step 2, stated per F-Depth. */
|
|
211
|
+
export const MAX_CALL_DEPTH = 32;
|
|
212
|
+
|
|
213
|
+
/** F-Eval-CallLocal: evaluate a call to a project-local function, seven steps. */
|
|
214
|
+
function callLocal(callee: FoldableFunction, node: ts.CallExpression, scope: Scope, host: EvalHost): unknown {
|
|
215
|
+
const label = `call to "${callee.name}" (${callee.file})`;
|
|
216
|
+
// 1. the declaration must satisfy S-FnBody
|
|
217
|
+
const violation = findFnBodyViolation(callee.fn);
|
|
218
|
+
if (violation) reject("F-Eval-CallLocal", node, `${label} is not foldable: ${violation}`);
|
|
219
|
+
// 2. depth bound
|
|
220
|
+
if (scope.depth >= MAX_CALL_DEPTH) reject("F-Eval-CallLocal", node, `${label} is not foldable: call depth exceeded`);
|
|
221
|
+
// 3. arguments fold in the CALLER's scope; a spread argument rejects
|
|
222
|
+
const args: unknown[] = [];
|
|
223
|
+
for (const a of node.arguments) {
|
|
224
|
+
if (ts.isSpreadElement(a)) reject("F-Eval-CallLocal", node, `${label} is not foldable: a spread argument is not foldable`);
|
|
225
|
+
args.push(foldExpr(a, scope, host));
|
|
226
|
+
}
|
|
227
|
+
// 4. the body folds in the DEFINING module's scope, parameters bound on top
|
|
228
|
+
const consts = new Map(callee.consts);
|
|
229
|
+
const externals = new Map(callee.externals);
|
|
230
|
+
const bind = (n: string, v: unknown) => { consts.delete(n); externals.set(n, v); };
|
|
231
|
+
const inner: Scope = { consts, externals, depth: scope.depth + 1 };
|
|
232
|
+
callee.fn.parameters.forEach((param, i) => {
|
|
233
|
+
let value = args[i];
|
|
234
|
+
if (value === undefined && param.initializer) value = foldExpr(param.initializer, inner, host);
|
|
235
|
+
if (ts.isIdentifier(param.name)) { bind(param.name.text, value); return; }
|
|
236
|
+
if (value === null || typeof value !== "object") {
|
|
237
|
+
reject("F-Eval-CallLocal", node, `${label} is not foldable: a destructured parameter's argument is not an object`);
|
|
238
|
+
}
|
|
239
|
+
for (const el of (param.name as ts.ObjectBindingPattern).elements) {
|
|
240
|
+
bind((el.name as ts.Identifier).text, (value as Record<string, unknown>)[plainBindingKey(el)!]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
// 5. a concise body is its expression; a block folds its consts then returns
|
|
244
|
+
let result: unknown;
|
|
245
|
+
try {
|
|
246
|
+
const body = callee.fn.body as ts.ConciseBody;
|
|
247
|
+
if (!ts.isBlock(body)) {
|
|
248
|
+
result = foldExpr(body, inner, host);
|
|
249
|
+
} else {
|
|
250
|
+
result = undefined;
|
|
251
|
+
for (const st of body.statements) {
|
|
252
|
+
if (ts.isReturnStatement(st)) { result = st.expression ? foldExpr(st.expression, inner, host) : undefined; break; }
|
|
253
|
+
for (const d of (st as ts.VariableStatement).declarationList.declarations) {
|
|
254
|
+
const v = foldExpr(d.initializer!, inner, host);
|
|
255
|
+
if (ts.isIdentifier(d.name)) bind(d.name.text, v);
|
|
256
|
+
else {
|
|
257
|
+
if (v === null || typeof v !== "object") reject("F-Eval-CallLocal", node, `${label} is not foldable: a destructured const's source is not an object`);
|
|
258
|
+
for (const el of (d.name as ts.ObjectBindingPattern).elements) {
|
|
259
|
+
bind((el.name as ts.Identifier).text, (v as Record<string, unknown>)[plainBindingKey(el)!]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
// 7. re-anchor a failure inside the body at the call site
|
|
267
|
+
if (e instanceof FoldRejection) {
|
|
268
|
+
reject(e.rule, node, `${label} is not foldable: ${callee.file}:${e.line}:${e.column} - ${e.message.replace(/^\d+:\d+ - /, "")}`);
|
|
269
|
+
}
|
|
270
|
+
throw e;
|
|
271
|
+
}
|
|
272
|
+
// 6. a live object the body produced, that no argument carried, leaks identity
|
|
273
|
+
if (carriesLiveObject(result) && !args.some((a) => carriesLiveObject(a))) {
|
|
274
|
+
callee.leakedIdentity = true;
|
|
275
|
+
scope.captures?.add(callee.file);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Γ, H ⊢ e ⇓ v. */
|
|
281
|
+
export function foldExpr(node: ts.Expression, scope: Scope, host: EvalHost): unknown {
|
|
282
|
+
const F = (n: ts.Expression) => foldExpr(n, scope, host);
|
|
283
|
+
|
|
284
|
+
// F-Eval-Unwrap
|
|
285
|
+
if (
|
|
286
|
+
ts.isParenthesizedExpression(node) ||
|
|
287
|
+
ts.isAsExpression(node) ||
|
|
288
|
+
ts.isSatisfiesExpression(node) ||
|
|
289
|
+
ts.isNonNullExpression(node)
|
|
290
|
+
) {
|
|
291
|
+
const inner = F(node.expression);
|
|
292
|
+
if (isChain(inner)) return continuesChain(node) ? CHAIN : undefined;
|
|
293
|
+
return inner;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// F-Eval-Function
|
|
297
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
298
|
+
reject("F-Eval-Function", node, "a function used as a value is not foldable");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// F-Eval-Literal
|
|
302
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
303
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
304
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
305
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
306
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
307
|
+
|
|
308
|
+
// F-Eval-Ident, in the order the rule gives
|
|
309
|
+
if (ts.isIdentifier(node)) {
|
|
310
|
+
const name = node.text;
|
|
311
|
+
if (name === "undefined") return undefined; // F-Eval-Undefined
|
|
312
|
+
if (scope.consts.has(name)) {
|
|
313
|
+
if (boundToNew(scope, name)) {
|
|
314
|
+
if (scope.externals.has(name)) return scope.externals.get(name);
|
|
315
|
+
reject(
|
|
316
|
+
"F-Eval-Ident",
|
|
317
|
+
node,
|
|
318
|
+
`same-file construction \`${name}\` used as a value is not foldable: folding it again would build a second one`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return F(scope.consts.get(name)!);
|
|
322
|
+
}
|
|
323
|
+
if (scope.externals.has(name)) {
|
|
324
|
+
const value = scope.externals.get(name);
|
|
325
|
+
// F-Eval-Ident step 3, F-Val-Callable (#69): a callable of any kind is
|
|
326
|
+
// reached by the form that invokes it, never as a value.
|
|
327
|
+
if (isFoldableFunction(value)) reject("F-Eval-Ident", node, `function "${name}" used as a value is not foldable`);
|
|
328
|
+
if (typeof value === "function") {
|
|
329
|
+
const eager = host.intrinsics.some((i) => i.name === name && intrinsicCallFoldsEagerly(i));
|
|
330
|
+
reject("F-Eval-Ident", node, `function "${name}" used as a value is not foldable${eager ? ": call it instead" : ""}`);
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
if (name === "process") {
|
|
335
|
+
reject(
|
|
336
|
+
"F-Eval-Ident",
|
|
337
|
+
node,
|
|
338
|
+
'ambient "process" read is not foldable: declare a build-time parameter and reference it instead',
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
reject("F-Eval-Ident", node, `unresolved identifier: ${name}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// F-Eval-Tagged
|
|
345
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
346
|
+
if (scope.depth > 0) reject("F-Eval-Tagged", node, "a tagged template inside a folded function body is not foldable");
|
|
347
|
+
const tag = node.tag.getText();
|
|
348
|
+
if (!host.intrinsics.some((i) => i.name === tag && intrinsicTagFolds(i))) {
|
|
349
|
+
reject("F-Eval-Tagged", node, `unregistered tagged template intrinsic: ${tag}`);
|
|
350
|
+
}
|
|
351
|
+
const t = node.template;
|
|
352
|
+
if (ts.isNoSubstitutionTemplateLiteral(t)) return { __intrinsic: tag, strings: [t.text], values: [] };
|
|
353
|
+
return {
|
|
354
|
+
__intrinsic: tag,
|
|
355
|
+
strings: [t.head.text, ...t.templateSpans.map((s) => s.literal.text)],
|
|
356
|
+
values: t.templateSpans.map((s) => foldInterior(s.expression, scope, host)),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// F-Eval-Template
|
|
361
|
+
if (ts.isTemplateExpression(node)) {
|
|
362
|
+
let out = node.head.text;
|
|
363
|
+
for (const span of node.templateSpans) {
|
|
364
|
+
const value = F(span.expression);
|
|
365
|
+
if (isEnvelope(value)) {
|
|
366
|
+
reject(
|
|
367
|
+
"F-Eval-Template",
|
|
368
|
+
span.expression,
|
|
369
|
+
"a symbolic value interpolated into a plain template literal is not foldable: it has no string form until the build resolves it",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
out += String(value) + span.literal.text;
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// F-Eval-Object
|
|
378
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
379
|
+
const out: Record<string, unknown> = {};
|
|
380
|
+
for (const member of node.properties) {
|
|
381
|
+
if (ts.isPropertyAssignment(member)) {
|
|
382
|
+
if (!isLiteralKey(member.name)) reject("F-Eval-Object", member.name, "computed or dynamic property name");
|
|
383
|
+
out[member.name.text] = F(member.initializer);
|
|
384
|
+
} else if (ts.isShorthandPropertyAssignment(member)) {
|
|
385
|
+
out[member.name.text] = F(member.name);
|
|
386
|
+
} else if (ts.isSpreadAssignment(member)) {
|
|
387
|
+
const source = F(member.expression);
|
|
388
|
+
if (source === null || typeof source !== "object") {
|
|
389
|
+
reject("F-Eval-Object", member, "spread source is not an object");
|
|
390
|
+
}
|
|
391
|
+
Object.assign(out, source);
|
|
392
|
+
} else {
|
|
393
|
+
reject("F-Eval-Object", member, "unsupported object member");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// F-Eval-Array
|
|
400
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
401
|
+
const out: unknown[] = [];
|
|
402
|
+
for (const el of node.elements) {
|
|
403
|
+
if (ts.isSpreadElement(el)) {
|
|
404
|
+
const source = F(el.expression);
|
|
405
|
+
if (!Array.isArray(source)) reject("F-Eval-Array", el, "spread source is not an array");
|
|
406
|
+
out.push(...source);
|
|
407
|
+
} else {
|
|
408
|
+
out.push(F(el));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// F-Eval-Member, with step 2 checked before the object is folded
|
|
415
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
416
|
+
if (node.name.text === "step" && ts.isCallExpression(node.expression) && isUnclaimedCallee(node.expression, host.intrinsics)) {
|
|
417
|
+
if (scope.depth > 0) reject("F-Eval-Member", node, "a composite step inside a folded function body is not foldable");
|
|
418
|
+
const call = node.expression;
|
|
419
|
+
return {
|
|
420
|
+
__compositeStep: (call.expression as ts.Identifier).text,
|
|
421
|
+
args: call.arguments.map((a) => F(a)),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return foldAccess(node, node.name.text, node.questionDotToken !== undefined, scope, host);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// F-Eval-Index
|
|
428
|
+
if (ts.isElementAccessExpression(node)) {
|
|
429
|
+
const key = node.argumentExpression;
|
|
430
|
+
if (!isLiteralElementKey(key)) {
|
|
431
|
+
reject("F-Eval-Index", key, "element-access key must be a string or numeric literal");
|
|
432
|
+
}
|
|
433
|
+
return foldAccess(node, key.text, node.questionDotToken !== undefined, scope, host);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// F-Eval-Unary
|
|
437
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
438
|
+
if (node.operator === ts.SyntaxKind.ExclamationToken) return !F(node.operand);
|
|
439
|
+
if (node.operator === ts.SyntaxKind.MinusToken) return -Number(F(node.operand));
|
|
440
|
+
reject("F-Eval-Unary", node, `unsupported unary operator: ${ts.SyntaxKind[node.operator]}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// F-Eval-Binary
|
|
444
|
+
if (ts.isBinaryExpression(node)) {
|
|
445
|
+
const K = ts.SyntaxKind;
|
|
446
|
+
const op = node.operatorToken.kind;
|
|
447
|
+
if (op === K.AmpersandAmpersandToken) {
|
|
448
|
+
const left = F(node.left);
|
|
449
|
+
return left ? F(node.right) : left;
|
|
450
|
+
}
|
|
451
|
+
if (op === K.BarBarToken) {
|
|
452
|
+
const left = F(node.left);
|
|
453
|
+
return left ? left : F(node.right);
|
|
454
|
+
}
|
|
455
|
+
if (op === K.QuestionQuestionToken) {
|
|
456
|
+
const left = F(node.left);
|
|
457
|
+
return left === null || left === undefined ? F(node.right) : left;
|
|
458
|
+
}
|
|
459
|
+
const l = F(node.left) as never;
|
|
460
|
+
const r = F(node.right) as never;
|
|
461
|
+
switch (op) {
|
|
462
|
+
case K.PlusToken: return (l as unknown as number) + (r as unknown as number);
|
|
463
|
+
case K.MinusToken: return (l as unknown as number) - (r as unknown as number);
|
|
464
|
+
case K.AsteriskToken: return (l as unknown as number) * (r as unknown as number);
|
|
465
|
+
case K.SlashToken: return (l as unknown as number) / (r as unknown as number);
|
|
466
|
+
case K.EqualsEqualsEqualsToken: return l === r;
|
|
467
|
+
case K.ExclamationEqualsEqualsToken: return l !== r;
|
|
468
|
+
case K.GreaterThanToken: return l > r;
|
|
469
|
+
case K.LessThanToken: return l < r;
|
|
470
|
+
case K.GreaterThanEqualsToken: return l >= r;
|
|
471
|
+
case K.LessThanEqualsToken: return l <= r;
|
|
472
|
+
default: reject("F-Eval-Binary", node, `unsupported binary operator: ${K[op]}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// F-Eval-Conditional
|
|
477
|
+
if (ts.isConditionalExpression(node)) return F(node.condition) ? F(node.whenTrue) : F(node.whenFalse);
|
|
478
|
+
|
|
479
|
+
// F-Eval-New
|
|
480
|
+
if (ts.isNewExpression(node)) return foldNew(node, scope, host);
|
|
481
|
+
|
|
482
|
+
// F-Eval-Call*: helper, intrinsic, local, eager, method, in that order
|
|
483
|
+
if (ts.isCallExpression(node)) {
|
|
484
|
+
const callee = node.expression;
|
|
485
|
+
|
|
486
|
+
if (ts.isIdentifier(callee) && !scope.consts.has(callee.text)) {
|
|
487
|
+
const name = callee.text;
|
|
488
|
+
|
|
489
|
+
// F-Eval-CallHelper
|
|
490
|
+
if (isFoldableHelperName(name)) {
|
|
491
|
+
if (scope.depth > 0) reject("F-Eval-CallHelper", node, "an authoring helper call inside a folded function body is not foldable");
|
|
492
|
+
return { __helper: name, args: node.arguments.map((a) => F(a)) };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// F-Eval-CallIntrinsic
|
|
496
|
+
if (host.intrinsics.some((i) => i.name === name && intrinsicCallFolds(i))) {
|
|
497
|
+
if (scope.depth > 0) reject("F-Eval-CallIntrinsic", node, "an intrinsic call inside a folded function body is not foldable");
|
|
498
|
+
return { __intrinsic: name, args: node.arguments.map((a) => foldInterior(a, scope, host)) };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// F-Eval-CallLocal, after the two registered shapes
|
|
502
|
+
const local = scope.externals.get(name);
|
|
503
|
+
if (isFoldableFunction(local)) return callLocal(local, node, scope, host);
|
|
504
|
+
|
|
505
|
+
// F-Eval-CallEager
|
|
506
|
+
if (host.intrinsics.some((i) => i.name === name && intrinsicCallFoldsEagerly(i))) {
|
|
507
|
+
if (typeof local !== "function") reject("F-Eval-CallEager", node, `"${name}" did not resolve to a function`);
|
|
508
|
+
return (local as (...a: unknown[]) => unknown)(...node.arguments.map((a) => F(a)));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// F-Eval-CallMethod
|
|
513
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
514
|
+
const method = callee.name.text;
|
|
515
|
+
const receiver = F(callee.expression);
|
|
516
|
+
if (isChain(receiver)) return continuesChain(node) ? CHAIN : undefined;
|
|
517
|
+
if (receiver === null || receiver === undefined) {
|
|
518
|
+
if (callee.questionDotToken) return continuesChain(node) ? CHAIN : undefined;
|
|
519
|
+
reject("F-Eval-CallMethod", node, `cannot call ".${method}(...)" on ${String(receiver)}`);
|
|
520
|
+
}
|
|
521
|
+
if (isEnvelope(receiver)) {
|
|
522
|
+
reject("F-Eval-CallMethod", node, `a method call on a symbolic value is not foldable`);
|
|
523
|
+
}
|
|
524
|
+
const fn = (receiver as Record<string, unknown>)[method];
|
|
525
|
+
if (typeof fn !== "function") reject("F-Eval-CallMethod", node, `"${method}" is not a callable method on the folded value`);
|
|
526
|
+
return (fn as (...a: unknown[]) => unknown).apply(receiver, node.arguments.map((a) => F(a)));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
reject("F-Eval-Reject", node, "function call as a value is not foldable");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// F-Eval-Reject
|
|
533
|
+
reject("F-Eval-Reject", node, `unsupported expression: ${ts.SyntaxKind[node.kind]}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** F-Bind: a top-level `const` with an identifier name and an initializer. */
|
|
537
|
+
export function collectConsts(sf: ts.SourceFile): Map<string, ts.Expression> {
|
|
538
|
+
const out = new Map<string, ts.Expression>();
|
|
539
|
+
for (const st of sf.statements) {
|
|
540
|
+
if (!ts.isVariableStatement(st)) continue;
|
|
541
|
+
if ((st.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
542
|
+
for (const d of st.declarationList.declarations) {
|
|
543
|
+
// A const bound to a function is a project-local function, not a value
|
|
544
|
+
// (S-LocalFunction, F-Bind): it binds through collectLocalFunctions.
|
|
545
|
+
if (ts.isIdentifier(d.name) && d.initializer && !isFunctionInitializer(d.initializer)) out.set(d.name.text, d.initializer);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const isFunctionInitializer = (e: ts.Expression): e is ts.ArrowFunction | ts.FunctionExpression =>
|
|
552
|
+
ts.isArrowFunction(e) || ts.isFunctionExpression(e);
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* S-LocalFunction, F-Bind: every top-level `function` declaration, exported or
|
|
556
|
+
* not, and every top-level `const` bound to an arrow or function expression,
|
|
557
|
+
* as `FoldableFunction` markers for the file's own scope. A call reaches
|
|
558
|
+
* F-Eval-CallLocal; a use as a value is F-Eval-Ident step 3's rejection. The
|
|
559
|
+
* body is judged by S-FnBody at the call, never here (spec 1.2, #95).
|
|
560
|
+
*/
|
|
561
|
+
export function collectLocalFunctions(
|
|
562
|
+
sf: ts.SourceFile,
|
|
563
|
+
file: string,
|
|
564
|
+
consts: Map<string, ts.Expression>,
|
|
565
|
+
externals: ReadonlyMap<string, unknown>,
|
|
566
|
+
): FoldableFunction[] {
|
|
567
|
+
const out: FoldableFunction[] = [];
|
|
568
|
+
for (const st of sf.statements) {
|
|
569
|
+
if (ts.isFunctionDeclaration(st) && st.name && st.body && !st.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
570
|
+
out.push(new FoldableFunction(st.name.text, st, file, consts, externals));
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (!ts.isVariableStatement(st) || (st.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
574
|
+
for (const d of st.declarationList.declarations) {
|
|
575
|
+
if (ts.isIdentifier(d.name) && d.initializer && isFunctionInitializer(d.initializer)) {
|
|
576
|
+
out.push(new FoldableFunction(d.name.text, d.initializer, file, consts, externals));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authoring-helper allowlist and the owned-specifier prefixes, the two
|
|
3
|
+
* host-installed lists F-Host-Interface items 4 and 5 name. Written from
|
|
4
|
+
* hosts.md (#50); the reference ships neither list.
|
|
5
|
+
*/
|
|
6
|
+
export interface FoldableHelperDef {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** Where the host defines it, for the audit trail F-Host-Admission asks for. */
|
|
9
|
+
readonly module: string;
|
|
10
|
+
/** Why it satisfies F-Host-Admission: pure, deterministic, same at fold time as at run time. */
|
|
11
|
+
readonly note: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const HELPERS: FoldableHelperDef[] = [];
|
|
15
|
+
const HELPER_NAMES = new Set<string>();
|
|
16
|
+
export function registerHelpers(defs: readonly FoldableHelperDef[]): void {
|
|
17
|
+
HELPERS.splice(0, HELPERS.length, ...defs);
|
|
18
|
+
HELPER_NAMES.clear();
|
|
19
|
+
for (const d of defs) HELPER_NAMES.add(d.name);
|
|
20
|
+
}
|
|
21
|
+
export const FOLDABLE_AUTHORING_HELPERS: readonly FoldableHelperDef[] = HELPERS;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The name half of F-Eval-CallHelper. Provenance, that the name is bound by
|
|
25
|
+
* an import from the host, is revival's question (F-Div-Provenance).
|
|
26
|
+
*/
|
|
27
|
+
export function isFoldableHelperName(name: string): boolean {
|
|
28
|
+
return HELPER_NAMES.has(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const HOST_PACKAGE_SPECIFIERS: string[] = [];
|
|
32
|
+
export function registerHostSpecifiers(prefixes: readonly string[]): void {
|
|
33
|
+
HOST_PACKAGE_SPECIFIERS.splice(0, HOST_PACKAGE_SPECIFIERS.length, ...prefixes);
|
|
34
|
+
}
|
|
35
|
+
/** F-Host-Trust arm 1, the text-matched half. */
|
|
36
|
+
export function isHostOwnedSpecifier(specifier: string): boolean {
|
|
37
|
+
return HOST_PACKAGE_SPECIFIERS.some((p) => specifier === p || specifier.startsWith(p.endsWith("-") ? p : `${p}/`));
|
|
38
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The host interface the ported expression layer needs (#20, F-Host-Interface).
|
|
2
|
+
// IntrinsicDef and the three predicates are copied from chant's lexicon.ts at 1b9f5133;
|
|
3
|
+
// registerHelpers / registerHostSpecifiers live in foldable-helpers.ts.
|
|
4
|
+
export interface IntrinsicDef {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly description?: string;
|
|
7
|
+
readonly outputKey?: string;
|
|
8
|
+
/** Required: authored as a tagged template. Mutually exclusive with the two flags below (F-Host-Registry). */
|
|
9
|
+
readonly isTag: boolean;
|
|
10
|
+
/** Opt-in, per intrinsic: the plain-call form folds to an envelope. */
|
|
11
|
+
readonly foldsAsCall?: boolean;
|
|
12
|
+
/** Opt-in, per intrinsic: the plain-call form is evaluated at fold time. */
|
|
13
|
+
readonly foldsEagerly?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export function intrinsicTagFolds(def: { isTag?: boolean }): boolean { return def.isTag === true; }
|
|
16
|
+
export function intrinsicCallFolds(def: { isTag?: boolean; foldsAsCall?: boolean }): boolean { return def.isTag !== true && def.foldsAsCall === true; }
|
|
17
|
+
export function intrinsicCallFoldsEagerly(def: { isTag?: boolean; foldsEagerly?: boolean }): boolean { return def.isTag !== true && def.foldsEagerly === true; }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a host-owned specifier really exports: specifier, then export name, to
|
|
21
|
+
* the live value. F-Host-Interface item 1's entity constructors live here, as
|
|
22
|
+
* do the intrinsic and helper functions items 3 and 4 name, because revival
|
|
23
|
+
* has to *call* them (F-Val-Fate) and a description cannot be called.
|
|
24
|
+
*/
|
|
25
|
+
export type HostValues = ReadonlyMap<string, ReadonlyMap<string, unknown>>;
|
|
26
|
+
|
|
27
|
+
/** F-Profile: which profile the implementation is being judged in. */
|
|
28
|
+
export type Profile = "full" | "data-host";
|
|
29
|
+
|
|
30
|
+
export interface Host {
|
|
31
|
+
/** Defaults to `full`. `data-host` admits S-ExportDefault (spec 1.2) and is what an evaluator with no runtime declares. */
|
|
32
|
+
readonly profile?: Profile;
|
|
33
|
+
readonly intrinsics: readonly IntrinsicDef[];
|
|
34
|
+
readonly helpers: readonly { name: string; module: string; note: string }[];
|
|
35
|
+
readonly ownedSpecifierPrefixes: readonly string[];
|
|
36
|
+
readonly values: HostValues;
|
|
37
|
+
}
|
|
38
|
+
export const EMPTY_HOST: Host = { profile: "full", intrinsics: [], helpers: [], ownedSpecifierPrefixes: [], values: new Map() };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./host.js";
|
|
2
|
+
export * from "./subset.js";
|
|
3
|
+
export * from "./fnbody.js";
|
|
4
|
+
export * from "./fold.js";
|
|
5
|
+
export * from "./foldable-helpers.js";
|
|
6
|
+
export * from "./module.js";
|
|
7
|
+
export * from "./project.js";
|
|
8
|
+
export { referenceAdapter, referenceDataHostAdapter } from "./adapter.js";
|