@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/module.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { foldExpr, collectConsts, collectLocalFunctions, FoldRejection } from "./fold.js";
|
|
3
|
+
import { findShapeViolation, type ShapeViolation } from "./subset.js";
|
|
4
|
+
import { registerHelpers, registerHostSpecifiers } from "./foldable-helpers.js";
|
|
5
|
+
import { EMPTY_HOST, type Host } from "./host.js";
|
|
6
|
+
|
|
7
|
+
export function installHost(host: Host): void { registerHelpers(host.helpers); registerHostSpecifiers(host.ownedSpecifierPrefixes); }
|
|
8
|
+
export function parse(source: string, fileName = "fixture.ts"): ts.SourceFile { return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); }
|
|
9
|
+
export function exportInitializer(sf: ts.SourceFile, name: string, admitDefault = false): ts.Expression | undefined {
|
|
10
|
+
for (const st of sf.statements) {
|
|
11
|
+
// S-ExportDefault: `export default ⟨Expr⟩` is the export named `default`, in data-host.
|
|
12
|
+
if (name === "default" && admitDefault && ts.isExportAssignment(st) && !st.isExportEquals) return st.expression;
|
|
13
|
+
if (!ts.isVariableStatement(st) || !st.modifiers?.some((x) => x.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
14
|
+
for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name) && d.name.text === name) return d.initializer;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/** S-CallLocal's callee set: names this file binds by S-LocalFunction or imports from a project specifier. Syntax only. */
|
|
19
|
+
export function localCallees(sf: ts.SourceFile): Set<string> {
|
|
20
|
+
const names = new Set<string>();
|
|
21
|
+
for (const st of sf.statements) {
|
|
22
|
+
if (ts.isFunctionDeclaration(st) && st.name && st.body) names.add(st.name.text);
|
|
23
|
+
if (ts.isVariableStatement(st) && (st.declarationList.flags & ts.NodeFlags.Const) !== 0) {
|
|
24
|
+
for (const d of st.declarationList.declarations) {
|
|
25
|
+
if (ts.isIdentifier(d.name) && d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) names.add(d.name.text);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (ts.isImportDeclaration(st) && ts.isStringLiteral(st.moduleSpecifier) && /^\.{1,2}\//.test(st.moduleSpecifier.text) && st.importClause && !st.importClause.isTypeOnly) {
|
|
29
|
+
const c = st.importClause;
|
|
30
|
+
if (c.name) names.add(c.name.text);
|
|
31
|
+
if (c.namedBindings && ts.isNamedImports(c.namedBindings)) for (const el of c.namedBindings.elements) if (!el.isTypeOnly) names.add(el.name.text);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function shapeOfExport(source: string, name: string, host: Host = EMPTY_HOST): ShapeViolation | undefined | "no-such-export" {
|
|
38
|
+
installHost(host); const sf = parse(source); const init = exportInitializer(sf, name, host.profile === "data-host");
|
|
39
|
+
if (!init) return "no-such-export";
|
|
40
|
+
return findShapeViolation(init, host.intrinsics, localCallees(sf));
|
|
41
|
+
}
|
|
42
|
+
export function foldExport(source: string, name: string, host: Host = EMPTY_HOST, externals: ReadonlyMap<string, unknown> = new Map(), fileName = "fixture.ts"): { ok: true; value: unknown } | { ok: false; rule: string; line: number; column: number; message: string } {
|
|
43
|
+
installHost(host); const sf = parse(source, fileName); const init = exportInitializer(sf, name, host.profile === "data-host");
|
|
44
|
+
if (!init) return { ok: false, rule: "S-Module", line: 1, column: 1, message: `no export named ${name}` };
|
|
45
|
+
// F-Bind at the expression level: the file's own functions are bound so a
|
|
46
|
+
// same-file call folds here the way it does in a build.
|
|
47
|
+
const consts = collectConsts(sf);
|
|
48
|
+
const bound = new Map<string, unknown>(externals);
|
|
49
|
+
for (const fn of collectLocalFunctions(sf, fileName, consts, bound)) bound.set(fn.name, fn);
|
|
50
|
+
try { return { ok: true, value: foldExpr(init, { consts, externals: bound, depth: 0 }, { intrinsics: host.intrinsics }) }; }
|
|
51
|
+
catch (e) { if (e instanceof FoldRejection) return { ok: false, rule: e.rule, line: e.line, column: e.column, message: e.message }; throw e; }
|
|
52
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F-NoOwnExecution (J4). The reference parses project source and reduces it;
|
|
3
|
+
* it never runs a statement of it. Revival (#61) is the one place real code is
|
|
4
|
+
* invoked, and what it invokes is the *host's* constructors and functions,
|
|
5
|
+
* never anything the folded file wrote.
|
|
6
|
+
*
|
|
7
|
+
* Asserted by folding files whose top-level statements would be observable if
|
|
8
|
+
* they ran.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { requireHost } from "@intentius/tsad-conformance";
|
|
12
|
+
import { foldProject } from "./project";
|
|
13
|
+
import type { Host } from "./host";
|
|
14
|
+
|
|
15
|
+
const shapes = requireHost("shapes");
|
|
16
|
+
const host: Host = {
|
|
17
|
+
intrinsics: shapes.intrinsics.map((i) => ({ name: i.name, isTag: i.isTag })),
|
|
18
|
+
helpers: shapes.helpers,
|
|
19
|
+
ownedSpecifierPrefixes: shapes.ownedSpecifierPrefixes,
|
|
20
|
+
values: shapes.values,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
// eslint-disable-next-line no-var
|
|
25
|
+
var __tsadRan: string[] | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("F-NoOwnExecution", () => {
|
|
29
|
+
test("a top-level side effect never runs, and the file folds anyway", () => {
|
|
30
|
+
globalThis.__tsadRan = [];
|
|
31
|
+
const files = new Map([
|
|
32
|
+
[
|
|
33
|
+
"sideEffects.ts",
|
|
34
|
+
`import { Bucket } from "@tsad/shapes";\n` +
|
|
35
|
+
`globalThis.__tsadRan.push("top level");\n` +
|
|
36
|
+
`export const bucket = new Bucket({ name: "b" });\n`,
|
|
37
|
+
],
|
|
38
|
+
]);
|
|
39
|
+
const v = foldProject(files, host).verdicts.get("sideEffects.ts");
|
|
40
|
+
// The resource folds and revives into a real instance, which means the host
|
|
41
|
+
// constructor ran...
|
|
42
|
+
expect(v?.kind).toBe("fold");
|
|
43
|
+
if (v?.kind === "fold") expect((v.exports.get("bucket") as { entityType: string }).entityType).toBe("Bucket");
|
|
44
|
+
// ...and the file's own statement did not. Non-exported statements are
|
|
45
|
+
// invisible to the gate precisely because they never execute.
|
|
46
|
+
expect(globalThis.__tsadRan).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a side effect in a called body is rejected, not executed", () => {
|
|
50
|
+
globalThis.__tsadRan = [];
|
|
51
|
+
const files = new Map([
|
|
52
|
+
["lib.ts", `export function helper() {\n globalThis.__tsadRan.push("body");\n return 1;\n}\nexport const marker = 1;\n`],
|
|
53
|
+
["caller.ts", `import { helper } from "./lib";\nexport const n = helper();\n`],
|
|
54
|
+
]);
|
|
55
|
+
const r = foldProject(files, host);
|
|
56
|
+
const v = r.verdicts.get("caller.ts");
|
|
57
|
+
expect(v?.kind).toBe("run");
|
|
58
|
+
// S-FnBody refuses the body rather than running it, and the forward edge
|
|
59
|
+
// then takes lib.ts with it.
|
|
60
|
+
expect(r.verdicts.get("lib.ts")?.kind).toBe("run");
|
|
61
|
+
expect(globalThis.__tsadRan).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a host constructor is the only thing revival calls", () => {
|
|
65
|
+
const files = new Map([["a.ts", `import { Bucket } from "@tsad/shapes";\nexport const b = new Bucket({ n: 1 });\n`]]);
|
|
66
|
+
const v = foldProject(files, host).verdicts.get("a.ts");
|
|
67
|
+
expect(v?.kind).toBe("fold");
|
|
68
|
+
if (v?.kind === "fold") {
|
|
69
|
+
const b = v.exports.get("b") as { lexicon: string; props: unknown };
|
|
70
|
+
expect(b.lexicon).toBe("shapes");
|
|
71
|
+
expect(b.props).toEqual({ n: 1 });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("with no host, a resource cannot be revived and the file falls back", () => {
|
|
76
|
+
// Correct rather than silent: a namespace holding an unrevived envelope is
|
|
77
|
+
// not a folded file. F-Val-Fate has no arm that leaves one in place.
|
|
78
|
+
const files = new Map([["a.ts", `import { Bucket } from "@tsad/shapes";\nexport const b = new Bucket({ n: 1 });\n`]]);
|
|
79
|
+
const v = foldProject(files).verdicts.get("a.ts");
|
|
80
|
+
expect(v?.kind).toBe("run");
|
|
81
|
+
if (v?.kind === "run") {
|
|
82
|
+
expect(v.rule).toBe("F-Val-Fate");
|
|
83
|
+
expect(v.reason).toContain("Bucket");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** #68 — F-Prebuild and F-Count: one instance per same-file construction, shared by every reference. */
|
|
2
|
+
import { describe, test, expect } from "vitest";
|
|
3
|
+
import { foldProject } from "./project";
|
|
4
|
+
import { requireHost } from "@intentius/tsad-conformance";
|
|
5
|
+
import { referenceAdapter } from "./adapter";
|
|
6
|
+
|
|
7
|
+
const hostOf = (name: string) => {
|
|
8
|
+
const h = requireHost(name);
|
|
9
|
+
return { intrinsics: h.intrinsics, helpers: h.helpers, ownedSpecifierPrefixes: h.ownedSpecifierPrefixes, values: h.values };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("F-Prebuild (#68)", () => {
|
|
13
|
+
test("every reference to a same-file construction is the one instance, exported or not", () => {
|
|
14
|
+
const files = new Map([
|
|
15
|
+
[
|
|
16
|
+
"a.ts",
|
|
17
|
+
`import { Bucket } from "@tsad/shapes";
|
|
18
|
+
const base = new Bucket({ name: "base" });
|
|
19
|
+
export const wrapper = { inner: base, again: base };
|
|
20
|
+
export { base };`,
|
|
21
|
+
],
|
|
22
|
+
]);
|
|
23
|
+
const r = foldProject(files, hostOf("shapes"));
|
|
24
|
+
const v = r.verdicts.get("a.ts");
|
|
25
|
+
expect(v?.kind).toBe("fold");
|
|
26
|
+
if (v?.kind !== "fold") return;
|
|
27
|
+
const wrapper = v.exports.get("wrapper") as { inner: unknown; again: unknown };
|
|
28
|
+
expect(wrapper.inner).toBe(wrapper.again);
|
|
29
|
+
expect(v.exports.get("base")).toBe(wrapper.inner);
|
|
30
|
+
});
|
|
31
|
+
test("an exported construction re-exported under another name is still one instance", () => {
|
|
32
|
+
const files = new Map([
|
|
33
|
+
["a.ts", `import { Bucket } from "@tsad/shapes";\nexport const a = new Bucket({ name: "a" });\nexport { a as b };`],
|
|
34
|
+
]);
|
|
35
|
+
const v = foldProject(files, hostOf("shapes")).verdicts.get("a.ts");
|
|
36
|
+
expect(v?.kind).toBe("fold");
|
|
37
|
+
if (v?.kind === "fold") expect(v.exports.get("a")).toBe(v.exports.get("b"));
|
|
38
|
+
});
|
|
39
|
+
test("a later construction sees an earlier one, in source order", () => {
|
|
40
|
+
const files = new Map([
|
|
41
|
+
[
|
|
42
|
+
"a.ts",
|
|
43
|
+
`import { Bucket, Pair } from "@tsad/shapes";
|
|
44
|
+
const left = new Bucket({ name: "l" });
|
|
45
|
+
const pair = new Pair(left, "r");
|
|
46
|
+
export { pair };`,
|
|
47
|
+
],
|
|
48
|
+
]);
|
|
49
|
+
const v = foldProject(files, hostOf("shapes")).verdicts.get("a.ts");
|
|
50
|
+
expect(v?.kind).toBe("fold");
|
|
51
|
+
if (v?.kind === "fold") {
|
|
52
|
+
const pair = v.exports.get("pair") as { left: unknown };
|
|
53
|
+
expect((pair.left as { props: { name: string } }).props.name).toBe("l");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
test("a construction that fails is skipped, and the reference to it rejects under step 1", () => {
|
|
57
|
+
// `Missing` is not a class the host supplies, so revival cannot construct it.
|
|
58
|
+
const files = new Map([
|
|
59
|
+
["a.ts", `import { Missing } from "@tsad/shapes";\nconst m = new Missing({});\nexport const wrapper = { m };`],
|
|
60
|
+
]);
|
|
61
|
+
const v = foldProject(files, hostOf("shapes")).verdicts.get("a.ts");
|
|
62
|
+
expect(v?.kind).toBe("run");
|
|
63
|
+
if (v?.kind === "run") expect(v.rule).toBe("F-Eval-Ident");
|
|
64
|
+
});
|
|
65
|
+
test("the adapter reports the fixture shape", () => {
|
|
66
|
+
expect(typeof referenceAdapter.foldProject).toBe("function");
|
|
67
|
+
});
|
|
68
|
+
});
|