@adobe/ecs 0.2.3 → 0.2.4
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/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +4 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/activate.js +14 -2
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +40 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +3 -2
- package/dist/implementation/world/tables.js +83 -37
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +28 -1
- package/dist/interface/registry/schedule.js +182 -21
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +17 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +26 -5
- package/dist/interface/system/create-resolved.js +1 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/06-sync-points.md +12 -8
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +9 -5
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +3 -150
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/world/activate.ts +11 -2
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +36 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +106 -4
- package/src/implementation/world/tables.ts +71 -33
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/schedule.ts +196 -23
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +15 -2
- package/src/interface/system/commands.ts +23 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +8 -0
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +18 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
Error: System "explode" (@adobe/ecs@0.2.
|
|
1
|
+
Error: System "explode" (@adobe/ecs@0.2.4/src/guide/18-identity.ts#explode) failed: boom
|
package/src/guide/guide.test.ts
CHANGED
|
@@ -1,70 +1,5 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
-
import {
|
|
3
|
-
import { formatWithOptions } from "node:util";
|
|
4
|
-
import ts from "typescript";
|
|
5
|
-
import { afterAll, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Each chapter here is its prose, `NN-name.md`, and a program, which its `main` runs. Run, it prints what the `.out`
|
|
9
|
-
* file beside it says: a line per console call, as node writes its values, on one line each. Ids name the package's
|
|
10
|
-
* version, which is left out of the comparison, so a new version changes no example. The folder is read as the test
|
|
11
|
-
* runs, not through `import.meta.glob`, which vitest's cache of transformed modules would keep as of the test's first
|
|
12
|
-
* run, missing a chapter added since.
|
|
13
|
-
*/
|
|
14
|
-
const folder = new URL(".", import.meta.url);
|
|
15
|
-
const files = readdirSync(folder);
|
|
16
|
-
const read = (file: string): string => readFileSync(new URL(file, folder), "utf8");
|
|
17
|
-
const names = files.filter(file => /^\d+-[\w-]+\.ts$/.test(file) && !file.endsWith(".type-test.ts")).map(file => file.slice(0, -".ts".length)).sort();
|
|
18
|
-
const proseFiles = files.filter(file => /^\d+-[\w-]+\.md$/.test(file)).sort();
|
|
19
|
-
/** Where the mutation tests write each program changed: `.mutants` in the package. */
|
|
20
|
-
const mutants = new URL("../../.mutants/", import.meta.url);
|
|
21
|
-
afterAll(() => rmSync(mutants, { recursive: true, force: true }));
|
|
22
|
-
|
|
23
|
-
const levels = ["log", "info", "warn", "error", "debug"] as const;
|
|
24
|
-
const unversioned = (text: string): string => text.replace(/@adobe\/ecs@[^/\s]+\//g, "@adobe/ecs/");
|
|
25
|
-
const chapterOf = (file: string): number => Number(/^(\d+)-/.exec(file)?.[1]);
|
|
26
|
-
|
|
27
|
-
/** What `main` prints, a line per console call, and how it failed, if it did. */
|
|
28
|
-
const printed = async (main: () => Promise<void>): Promise<string> => {
|
|
29
|
-
const lines: string[] = [];
|
|
30
|
-
const spies = levels.map(level => vi.spyOn(console, level).mockImplementation((...values: unknown[]) => {
|
|
31
|
-
lines.push(formatWithOptions({ breakLength: Infinity, compact: true }, ...values));
|
|
32
|
-
}));
|
|
33
|
-
try {
|
|
34
|
-
await main();
|
|
35
|
-
} catch (error) {
|
|
36
|
-
lines.push(`main failed: ${String(error)}`);
|
|
37
|
-
} finally {
|
|
38
|
-
for (const spy of spies) spy.mockRestore();
|
|
39
|
-
}
|
|
40
|
-
return lines.map(line => `${line}\n`).join("");
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** What a program prints, run from its file, or how it failed, as a declaration the world rejects fails its import. */
|
|
44
|
-
const run = (file: URL): Promise<string> => printed(async () => {
|
|
45
|
-
const program: unknown = await import(/* @vite-ignore */ file.href);
|
|
46
|
-
const main: unknown = Reflect.get(Object(program), "main");
|
|
47
|
-
if (typeof main !== "function") throw new Error(`${file.pathname} exports no main`);
|
|
48
|
-
await Reflect.apply(main, undefined, []);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
/** Code without its comments, as TypeScript prints it. */
|
|
52
|
-
const withoutComments = (source: string): string =>
|
|
53
|
-
ts.createPrinter({ removeComments: true }).printFile(ts.createSourceFile("program.ts", source, ts.ScriptTarget.ESNext, false));
|
|
54
|
-
|
|
55
|
-
/** Each chapter's code: its files', without comments. */
|
|
56
|
-
const codeOf = (chapter: number): string =>
|
|
57
|
-
files.filter(file => file.endsWith(".ts") && !file.endsWith(".test.ts") && chapterOf(file) === chapter).map(file => withoutComments(read(file))).join("\n");
|
|
58
|
-
|
|
59
|
-
/** What prose shows as code: its inline code and its blocks. */
|
|
60
|
-
const shownIn = (prose: string): readonly string[] => [...prose.matchAll(/```\w*\n([\s\S]*?)```|`([^`\n]+)`/g)].map(([, block, inline]) => block ?? inline ?? "");
|
|
61
|
-
|
|
62
|
-
/** Each link of the prose to lines of a program, which the interactive guide shows in its editor: `NN-name.ts#L4-L9`. */
|
|
63
|
-
const editorLinks = proseFiles.flatMap(prose => [...read(prose).matchAll(/\]\(([^)#\s]*)#L(\d+)(?:-L(\d+))?\)/g)].map(([, file = "", from = "", to]) =>
|
|
64
|
-
({ prose, file, from: Number(from), to: Number(to ?? from) })));
|
|
65
|
-
|
|
66
|
-
/** The chapters the guide's index lists, in order: each its number, title and prose. */
|
|
67
|
-
const listed = [...read("README.md").matchAll(/^(?:- |(\d+)\. )\[([^\]]+)\]\(([^)]+)\)$/gm)].map(([, number = "0", title = "", file = ""]) => ({ number: Number(number), title, file }));
|
|
2
|
+
import { checkChapters } from "ecs-guide-checks";
|
|
68
3
|
|
|
69
4
|
/** What each chapter teaches, which its prose names as code and its program does; 0 is the unnumbered keys chapter's. */
|
|
70
5
|
const teaches: { readonly [chapter: number]: readonly string[] } = {
|
|
@@ -111,7 +46,7 @@ const mutations: { readonly [chapter: number]: readonly (readonly [string, strin
|
|
|
111
46
|
],
|
|
112
47
|
4: [["world.remove(advance);", ""]],
|
|
113
48
|
5: [["after: [hit] ", ""], ["during: [update] ", ""]],
|
|
114
|
-
6: [["commands.add(hero, health, 5);", ""], ["commands.despawn(commands.spawn([health])({ [health]: 1 }));", "commands.spawn([health])({ [health]: 1 });"], ["watched.add(watch);", ""], ["const watch = System.create({ readonly: true, ", "const watch = System.create({ "]],
|
|
49
|
+
6: [["commands.add(hero, health, 5);", ""], ["after: [spawn] ", ""], ["commands.despawn(commands.spawn([health])({ [health]: 1 }));", "commands.spawn([health])({ [health]: 1 });"], ["watched.add(watch);", ""], ["const watch = System.create({ readonly: true, ", "const watch = System.create({ "]],
|
|
115
50
|
7: [["resources: { changed: [level] } ", ""], [", changed: [score]", ""], [", not: [paused]", ""], ["is: [map], ", ""]],
|
|
116
51
|
8: [["target: \"entity\",", ""]],
|
|
117
52
|
9: [["with: [mesh], not: [hidden]", "with: [mesh]"], ["Rule.create({ components: { with: [sprite], not: [hidden] }, derive: [drawn] });", ""], ["Rule.create({ components: { with: [drawn] }, derive: [layer] });", ""]],
|
|
@@ -132,86 +67,4 @@ const mutations: { readonly [chapter: number]: readonly (readonly [string, strin
|
|
|
132
67
|
22: [["context.fill();", ""]],
|
|
133
68
|
};
|
|
134
69
|
|
|
135
|
-
|
|
136
|
-
expect(files.filter(file => file.endsWith(".out")).map(file => file.slice(0, -".out".length)).sort()).toEqual(names);
|
|
137
|
-
expect(proseFiles).toEqual(["00-setup.md", ...names.map(name => `${name}.md`)].sort());
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("lists every chapter in its index, the unnumbered first, numbered as its files are and by the title its prose gives", () => {
|
|
141
|
-
expect(listed.map(chapter => chapter.file).sort()).toEqual(proseFiles);
|
|
142
|
-
expect(listed.map(chapter => chapter.number)).toEqual(listed.map(chapter => chapter.number).sort((a, b) => a - b));
|
|
143
|
-
for (const { number, title, file } of listed) {
|
|
144
|
-
expect(number).toBe(chapterOf(file));
|
|
145
|
-
const [heading, ...rest] = read(file).split("\n");
|
|
146
|
-
expect(heading).toBe(`# ${title}`);
|
|
147
|
-
expect(rest.join("\n").trim()).not.toBe("");
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it.each(names)("%s prints what its .out says", async name => {
|
|
152
|
-
const expected = read(`${name}.out`);
|
|
153
|
-
expect(expected.trim()).not.toBe("");
|
|
154
|
-
expect(unversioned(await run(new URL(`${name}.ts`, folder)))).toBe(unversioned(expected));
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it.each(Object.entries(teaches))("chapter %s's prose names what it teaches, as code, and its program does it", (chapter, taught) => {
|
|
158
|
-
const prose = `${names.find(name => chapterOf(name) === Number(chapter)) ?? ""}.md`;
|
|
159
|
-
const shown = shownIn(read(prose)).join("\n");
|
|
160
|
-
const code = codeOf(Number(chapter));
|
|
161
|
-
expect(taught.filter(name => !shown.includes(name))).toEqual([]);
|
|
162
|
-
expect(taught.filter(name => !code.includes(name))).toEqual([]);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("teaches something in every chapter", () => {
|
|
166
|
-
expect(Object.keys(teaches).map(Number)).toEqual(names.map(chapterOf));
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("keeps code in the prose to a line or two, the program in the editor showing the rest", () => {
|
|
170
|
-
const long = proseFiles.flatMap(file => [...read(file).matchAll(/```\w*\n([\s\S]*?)```/g)].filter(([, block = ""]) => block.split("\n").length > 3).map(() => file));
|
|
171
|
-
expect(long).toEqual([]);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("links chapters only to chapters there are", () => {
|
|
175
|
-
const chapterLinks = proseFiles.flatMap(file => [...read(file).matchAll(/\]\(([^)#\s]+\.md)\)/g)].map(([, target = ""]) => target));
|
|
176
|
-
expect(chapterLinks.length).toBeGreaterThan(0);
|
|
177
|
-
expect(chapterLinks.filter(target => !proseFiles.includes(target))).toEqual([]);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
/** The messages of TypeScript's syntax errors in code. */
|
|
181
|
-
const syntaxErrors = (code: string): readonly string[] => {
|
|
182
|
-
const { diagnostics = [] } = ts.transpileModule(code, { reportDiagnostics: true, compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } });
|
|
183
|
-
return diagnostics.map(diagnostic => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
it.each(editorLinks.map(link => [`${link.prose} to ${link.file}#L${link.from}-L${link.to}`, link] as const))("links %s, lines of its own program that are whole code", (_place, { prose, file, from, to }) => {
|
|
187
|
-
expect(file.startsWith(prose.replace(/\.md$/, "."))).toBe(true);
|
|
188
|
-
const lines = read(file).split("\n");
|
|
189
|
-
expect(from).toBeGreaterThanOrEqual(1);
|
|
190
|
-
expect(to).toBeGreaterThanOrEqual(from);
|
|
191
|
-
expect(to).toBeLessThanOrEqual(lines.length);
|
|
192
|
-
const code = lines.slice(from - 1, to).join("\n");
|
|
193
|
-
expect(withoutComments(code).trim()).not.toBe("");
|
|
194
|
-
expect(lines[from - 1]?.trim()).not.toBe("");
|
|
195
|
-
expect(lines[to - 1]?.trim()).not.toBe("");
|
|
196
|
-
// Lines cut short leave a brace or a call open, which parses neither as statements nor as a declaration's members.
|
|
197
|
-
expect(syntaxErrors(code).length === 0 || syntaxErrors(`interface Members {\n${code}\n}`).length === 0 ? [] : syntaxErrors(code)).toEqual([]);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it.each(Object.entries(mutations).flatMap(([chapter, changes]) => changes.map(([from, to, failure], index) => [Number(chapter), from, to, failure, index] as const)))(
|
|
201
|
-
"catches chapter %s's point taken away, %j, in its output", async (chapter, from, to, failure, index) => {
|
|
202
|
-
const name = names.find(item => chapterOf(item) === chapter) ?? "";
|
|
203
|
-
const source = read(`${name}.ts`);
|
|
204
|
-
expect(source.split(from).length).toBe(2);
|
|
205
|
-
// A file of the package's own, compiled as the program is, outside its sources, which other tests read.
|
|
206
|
-
const mutant = `${name}-${index}.ts`;
|
|
207
|
-
mkdirSync(mutants, { recursive: true });
|
|
208
|
-
const file = new URL(mutant, mutants);
|
|
209
|
-
writeFileSync(file, source.replace(from, to).replace(` from "../index.js";`, ` from "../src/index.js";`));
|
|
210
|
-
try {
|
|
211
|
-
const output = (await run(file)).replaceAll(`.mutants/${mutant}`, `src/guide/${name}.ts`);
|
|
212
|
-
expect(unversioned(output)).not.toBe(unversioned(read(`${name}.out`)));
|
|
213
|
-
if (failure !== undefined) expect(output).toContain(failure);
|
|
214
|
-
} finally {
|
|
215
|
-
rmSync(file, { force: true });
|
|
216
|
-
}
|
|
217
|
-
});
|
|
70
|
+
checkChapters({ folder: new URL(".", import.meta.url), teaches, mutations });
|
|
@@ -9,14 +9,23 @@ type Access = { readonly read: ReadonlySet<string>; readonly write: ReadonlySet<
|
|
|
9
9
|
/** The keys a body reads, writes and contributes to, by module-scope binding name; null where the body doesn't show every use. */
|
|
10
10
|
export type Used = {
|
|
11
11
|
readonly query: Access | null;
|
|
12
|
+
/** Where and why the build lost track of the row, unless the declaration takes it whole on purpose. */
|
|
13
|
+
readonly rowLost?: { readonly at: Node; readonly why: string };
|
|
12
14
|
readonly resources: (Access & { readonly reduce: ReadonlySet<string> }) | null;
|
|
13
15
|
/** The components read from each lookup's results, by lookup name: null for one whose results aren't followed. */
|
|
14
16
|
readonly lookups: ReadonlyMap<string, ReadonlySet<string> | null> | null;
|
|
15
17
|
/** The components read from `up`. */
|
|
16
18
|
readonly up: ReadonlySet<string> | null;
|
|
19
|
+
/**
|
|
20
|
+
* What its commands name, by kind: keys and lists of keys, as their module-scope bindings; whether it despawns.
|
|
21
|
+
* Null where the body passes its commands on, aliases them, or names what they take otherwise.
|
|
22
|
+
*/
|
|
23
|
+
readonly commands: Commanded | null;
|
|
17
24
|
};
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
type Commanded = { readonly spawn: ReadonlySet<string>; readonly add: ReadonlySet<string>; readonly remove: ReadonlySet<string>; readonly despawn: boolean };
|
|
27
|
+
|
|
28
|
+
const unknownAt = (at: Node, why: string): Used => ({ query: null, rowLost: { at, why }, resources: null, lookups: null, up: null, commands: null });
|
|
20
29
|
|
|
21
30
|
/** Whether `node`, a child of `parent`, names a property or label, or declares a binding, rather than referring to one. */
|
|
22
31
|
const isName = (node: Node, parent: Node | undefined): boolean => {
|
|
@@ -115,6 +124,13 @@ const indexLookupNames = (declaration: ObjectExpression): ReadonlySet<string> =>
|
|
|
115
124
|
|
|
116
125
|
type Group = "resources" | "lookup" | "reduce" | "up";
|
|
117
126
|
|
|
127
|
+
/** Whether a declaration's `components` says `whole: true`: its body uses its row whole, on purpose. */
|
|
128
|
+
const takesRowWhole = (declaration: ObjectExpression): boolean => declaration.properties.some(property =>
|
|
129
|
+
property.type === "Property" && !property.computed && property.key.type === "Identifier" && property.key.name === "components"
|
|
130
|
+
&& property.value.type === "ObjectExpression" && property.value.properties.some(member =>
|
|
131
|
+
member.type === "Property" && !member.computed && member.key.type === "Identifier" && member.key.name === "whole"
|
|
132
|
+
&& member.value.type === "Literal" && member.value.value === true));
|
|
133
|
+
|
|
118
134
|
const isGroup = (name: string): name is Group => name === "resources" || name === "lookup" || name === "reduce" || name === "up";
|
|
119
135
|
|
|
120
136
|
/**
|
|
@@ -123,15 +139,27 @@ const isGroup = (name: string): name is Group => name === "resources" || name ==
|
|
|
123
139
|
* context's `resources`, `lookup` and `reduce` are indexed only by module-scope keys the body doesn't shadow, or
|
|
124
140
|
* destructured by them, and never passed on, aliased or spread. A lookup's results are followed where each is indexed
|
|
125
141
|
* by such keys or tested, directly or through a `const` declared once; a lookup through an index's lists where they're
|
|
126
|
-
* read for `length` and `changed`, and their rows from `at` as a lookup's results are.
|
|
142
|
+
* read for `length` and `changed`, and their rows from `at` as a lookup's results are. A row whose declaration takes it
|
|
143
|
+
* `whole` isn't followed.
|
|
127
144
|
*/
|
|
128
145
|
export const usedBy = (declaration: ObjectExpression, body: Node, module: ModuleScope, positions: { readonly row: number; readonly context: number }): Used => {
|
|
129
|
-
|
|
130
|
-
if (
|
|
146
|
+
const used = followedBy(declaration, body, module, positions);
|
|
147
|
+
if (takesRowWhole(declaration)) return { query: null, resources: used.resources, lookups: used.lookups, up: used.up, commands: used.commands };
|
|
148
|
+
// A body without a row loses none.
|
|
149
|
+
return positions.row < 0 && used.rowLost !== undefined ? { query: used.query, resources: used.resources, lookups: used.lookups, up: used.up, commands: used.commands } : used;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const followedBy = (declaration: ObjectExpression, body: Node, module: ModuleScope, positions: { readonly row: number; readonly context: number }): Used => {
|
|
153
|
+
if (!isFunctionBody(body)) return unknownAt(body, "its body isn't a function written in place");
|
|
154
|
+
if (body.type === "FunctionExpression" && mentions(body.body, "arguments")) return unknownAt(body, "it reads `arguments`");
|
|
131
155
|
// Code `eval` runs can use anything.
|
|
132
|
-
if (mentions(body, "eval")) return
|
|
156
|
+
if (mentions(body, "eval")) return unknownAt(body, "it calls `eval`");
|
|
133
157
|
const counts = new Map<string, number>();
|
|
134
|
-
|
|
158
|
+
const declaredAt = new Map<string, Node>();
|
|
159
|
+
declaredIn(body.body, (name, at) => {
|
|
160
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
161
|
+
if (!declaredAt.has(name)) declaredAt.set(name, at);
|
|
162
|
+
});
|
|
135
163
|
const parameters = new Set(body.params.flatMap(parameter => patternNames(parameter)));
|
|
136
164
|
const isKey = (name: string): boolean => module.has(name) && !counts.has(name) && !parameters.has(name);
|
|
137
165
|
/** The key `node` names: a module-scope binding, or a namespace's member (`Shapes.circle`), as its source; null for any other. */
|
|
@@ -150,9 +178,15 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
150
178
|
const up = new Set<string>();
|
|
151
179
|
let upKnown = true;
|
|
152
180
|
const lookupReads = new Map<string, Set<string> | null>((lookups ?? []).map(name => [name, new Set<string>()]));
|
|
153
|
-
let
|
|
181
|
+
let rowLost: { readonly at: Node; readonly why: string } | undefined;
|
|
182
|
+
/** Stops following the row, at the first use it can't. */
|
|
183
|
+
const loseRow = (at: Node, why: string): void => { rowLost ??= { at, why }; };
|
|
154
184
|
let resourcesKnown = true;
|
|
155
185
|
let lookupsKnown = lookups !== null;
|
|
186
|
+
const commands = { spawn: new Set<string>(), add: new Set<string>(), remove: new Set<string>(), despawn: false };
|
|
187
|
+
let commandsKnown = true;
|
|
188
|
+
/** Names bound to the context's commands. */
|
|
189
|
+
const commandNames = new Set<string>();
|
|
156
190
|
const unknownLookup = (lookup: string): void => { lookupReads.set(lookup, null); };
|
|
157
191
|
const unknownGroup = (group: Group): void => {
|
|
158
192
|
if (group === "lookup") lookupsKnown = false;
|
|
@@ -206,6 +240,11 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
206
240
|
};
|
|
207
241
|
/** Binds the context's groups from a destructuring of it; `declaredOnce` says whether a bound name is safe to follow. */
|
|
208
242
|
const contextNamed = (declaredOnce: (name: string) => boolean) => (property: string, value: Pattern): boolean => {
|
|
243
|
+
if (property === "commands") {
|
|
244
|
+
if (value.type === "Identifier" && declaredOnce(value.name)) commandNames.add(value.name);
|
|
245
|
+
else commandsKnown = false;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
209
248
|
if (!isGroup(property)) return true;
|
|
210
249
|
if (value.type === "Identifier" && declaredOnce(value.name)) groups.set(value.name, property);
|
|
211
250
|
else if (value.type === "ObjectPattern") destructureGroup(property, value);
|
|
@@ -216,13 +255,15 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
216
255
|
resourcesKnown = false;
|
|
217
256
|
lookupsKnown = false;
|
|
218
257
|
upKnown = false;
|
|
258
|
+
commandsKnown = false;
|
|
219
259
|
};
|
|
220
260
|
|
|
221
261
|
const row = positions.row < 0 ? undefined : body.params[positions.row];
|
|
222
262
|
const context = positions.context < 0 ? undefined : body.params[positions.context];
|
|
223
263
|
const rowName = row?.type === "Identifier" && !counts.has(row.name) ? row.name : null;
|
|
224
264
|
const contextName = context?.type === "Identifier" && !counts.has(context.name) ? context.name : null;
|
|
225
|
-
if (row
|
|
265
|
+
if (row?.type === "Identifier" && rowName === null) loseRow(declaredAt.get(row.name) ?? row, `it declares another \`${row.name}\` here, which hides its row: rename one`);
|
|
266
|
+
else if (row !== undefined && rowName === null && !destructure(row, rowNamed, key => query.read.add(key))) loseRow(row, "it destructures its row by something other than its keys and `entity`");
|
|
226
267
|
if (context !== undefined && contextName === null && !destructure(context, contextNamed(name => !counts.has(name)), () => unknownContext())) unknownContext();
|
|
227
268
|
|
|
228
269
|
/** A lookup's row `value`, a child of `ancestors[at]`: indexed by a key, read for `entity`, bound to a `const`, or tested. */
|
|
@@ -284,6 +325,44 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
284
325
|
const lookup = list.type === "Identifier" ? results.get(list.name) : calledLookup(list);
|
|
285
326
|
return lookup !== undefined && indexed.has(lookup) ? lookup : undefined;
|
|
286
327
|
};
|
|
328
|
+
/** Whether `node` is `context.commands`. */
|
|
329
|
+
const isContextCommands = (node: Node): boolean => node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier"
|
|
330
|
+
&& node.object.name === contextName && node.property.type === "Identifier" && node.property.name === "commands";
|
|
331
|
+
/** The keys and lists of keys a command's argument names: a key or list by binding, or an array literal of them. */
|
|
332
|
+
const listOf = (node: Node | undefined): readonly string[] | null => {
|
|
333
|
+
if (node === undefined) return null;
|
|
334
|
+
const key = keyOf(node);
|
|
335
|
+
if (key !== null) return [key];
|
|
336
|
+
if (node.type !== "ArrayExpression") return null;
|
|
337
|
+
const names: string[] = [];
|
|
338
|
+
for (const element of node.elements) {
|
|
339
|
+
const named = element === null ? null : keyOf(element.type === "SpreadElement" ? element.argument : element);
|
|
340
|
+
if (named === null) return null;
|
|
341
|
+
names.push(named);
|
|
342
|
+
}
|
|
343
|
+
return names;
|
|
344
|
+
};
|
|
345
|
+
/** A use of the commands at `node`, a child of `ancestors[at]`: a call of one of them, whose keys are listed. */
|
|
346
|
+
const useCommands = (node: Node, ancestors: readonly Node[], at: number): void => {
|
|
347
|
+
const parent = ancestors[at];
|
|
348
|
+
const call = ancestors[at - 1];
|
|
349
|
+
if (parent?.type !== "MemberExpression" || parent.object !== node || parent.computed || parent.property.type !== "Identifier"
|
|
350
|
+
|| call?.type !== "CallExpression" || call.callee !== parent) {
|
|
351
|
+
commandsKnown = false;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const command = parent.property.name;
|
|
355
|
+
if (command === "despawn" || command === "despawnWhere") {
|
|
356
|
+
commands.despawn = true;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const listed = command === "spawn" ? listOf(call.arguments[0]) : command === "add" || command === "remove" ? listOf(call.arguments[1]) : null;
|
|
360
|
+
if (listed === null || (command !== "spawn" && command !== "add" && command !== "remove")) {
|
|
361
|
+
commandsKnown = false;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const name of listed) commands[command].add(name);
|
|
365
|
+
};
|
|
287
366
|
/** Finds groups and lookups bound by `const` declarations declared once, and `const`s holding a lookup's result, before any reference is classified. */
|
|
288
367
|
const collect = (node: Node, kind: string | undefined): void => {
|
|
289
368
|
if (node.type === "VariableDeclarator" && kind === "const" && node.init) {
|
|
@@ -298,6 +377,10 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
298
377
|
groups.set(node.id.name, group);
|
|
299
378
|
followed.add(node.init);
|
|
300
379
|
}
|
|
380
|
+
if (isContextCommands(node.init)) {
|
|
381
|
+
commandNames.add(node.id.name);
|
|
382
|
+
followed.add(node.init);
|
|
383
|
+
}
|
|
301
384
|
}
|
|
302
385
|
else if (node.init.type === "Identifier" && node.init.name === contextName) {
|
|
303
386
|
if (destructure(node.id, contextNamed(once), () => unknownContext())) followed.add(node.init);
|
|
@@ -354,31 +437,36 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
354
437
|
if (parent?.type === "MemberExpression" && parent.object === node) {
|
|
355
438
|
const { property } = parent;
|
|
356
439
|
if (!parent.computed) {
|
|
357
|
-
if (property.type !== "Identifier" || !rowNamed(property.name))
|
|
440
|
+
if (property.type !== "Identifier" || !rowNamed(property.name)) loseRow(parent, "it reads a member of its row by name, and a row names only `entity`");
|
|
358
441
|
return;
|
|
359
442
|
}
|
|
360
443
|
const key = keyOf(property);
|
|
361
444
|
// A part of a value written, `row[key][0] = …`, writes it, which the compiler rejects as it must.
|
|
362
445
|
const use = key === null ? null : deepUseOf(parent, ancestors, ancestors.length - 2);
|
|
363
|
-
if (
|
|
446
|
+
if (key === null) loseRow(parent, "it indexes its row by something other than a module-scope key");
|
|
447
|
+
else if (use === null) loseRow(parent, "it deletes a member of its row");
|
|
364
448
|
else {
|
|
365
449
|
if (use.read) query.read.add(key);
|
|
366
450
|
if (use.write) query.write.add(key);
|
|
367
451
|
if (use.mutate) query.mutate.add(key);
|
|
368
452
|
}
|
|
369
453
|
}
|
|
370
|
-
else if (!(parent?.type === "VariableDeclarator" && parent.init === node && destructure(parent.id, rowNamed, key => query.read.add(key))))
|
|
454
|
+
else if (!(parent?.type === "VariableDeclarator" && parent.init === node && destructure(parent.id, rowNamed, key => query.read.add(key)))) {
|
|
455
|
+
loseRow(parent ?? node, "it passes its row on, aliases or spreads it");
|
|
456
|
+
}
|
|
371
457
|
}
|
|
372
458
|
else if (name === contextName) {
|
|
373
459
|
if (parent?.type === "MemberExpression" && parent.object === node && !parent.computed && parent.property.type === "Identifier") {
|
|
374
460
|
const property = parent.property.name;
|
|
375
461
|
if (isGroup(property)) useGroup(property, parent, ancestors, ancestors.length - 2);
|
|
462
|
+
else if (property === "commands" && !followed.has(parent)) useCommands(parent, ancestors, ancestors.length - 2);
|
|
376
463
|
return;
|
|
377
464
|
}
|
|
378
465
|
if (!followed.has(node)) unknownContext();
|
|
379
466
|
}
|
|
380
467
|
else {
|
|
381
468
|
const at = ancestors.length - 1;
|
|
469
|
+
if (commandNames.has(name)) return useCommands(node, ancestors, at);
|
|
382
470
|
const group = groups.get(name);
|
|
383
471
|
if (group !== undefined) return useGroup(group, node, ancestors, at);
|
|
384
472
|
const caller = callers.get(name);
|
|
@@ -426,9 +514,11 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
426
514
|
for (const parameter of body.params) visitPattern(parameter);
|
|
427
515
|
visit(body.body);
|
|
428
516
|
return {
|
|
429
|
-
query:
|
|
517
|
+
query: rowLost === undefined ? query : null,
|
|
518
|
+
...rowLost === undefined ? {} : { rowLost },
|
|
430
519
|
resources: resourcesKnown ? resources : null,
|
|
431
520
|
lookups: lookupsKnown ? lookupReads : null,
|
|
432
521
|
up: upKnown ? up : null,
|
|
522
|
+
commands: commandsKnown ? commands : null,
|
|
433
523
|
};
|
|
434
524
|
};
|
|
@@ -47,18 +47,21 @@ export const patternNames = (pattern: Pattern): readonly string[] => {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Calls `declare` for each name declared anywhere inside `node`, once per declaration, with the pattern or name that
|
|
52
|
+
* declares it: variables, parameters, functions, classes and catch parameters.
|
|
53
|
+
*/
|
|
54
|
+
export const declaredIn = (node: Node, declare: (name: string, at: Node) => void): void => {
|
|
52
55
|
switch (node.type) {
|
|
53
|
-
case "VariableDeclarator": for (const name of patternNames(node.id)) declare(name); break;
|
|
56
|
+
case "VariableDeclarator": for (const name of patternNames(node.id)) declare(name, node.id); break;
|
|
54
57
|
case "FunctionDeclaration": case "FunctionExpression": case "ClassDeclaration": case "ClassExpression":
|
|
55
|
-
if (node.id) declare(node.id.name);
|
|
58
|
+
if (node.id) declare(node.id.name, node.id);
|
|
56
59
|
break;
|
|
57
|
-
case "CatchClause": if (node.param) for (const name of patternNames(node.param)) declare(name); break;
|
|
60
|
+
case "CatchClause": if (node.param) for (const name of patternNames(node.param)) declare(name, node.param); break;
|
|
58
61
|
default: break;
|
|
59
62
|
}
|
|
60
63
|
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") {
|
|
61
|
-
for (const param of node.params) for (const name of patternNames(param)) declare(name);
|
|
64
|
+
for (const param of node.params) for (const name of patternNames(param)) declare(name, param);
|
|
62
65
|
}
|
|
63
66
|
for (const child of children(node)) declaredIn(child, declare);
|
|
64
67
|
};
|
|
@@ -26,7 +26,7 @@ const readOf = (declaration: ObjectExpression, name: string): readonly string[]
|
|
|
26
26
|
describe("resolvedDeclaration", () => {
|
|
27
27
|
test("takes a lookup by entity whose rows the build didn't follow as reading all it requires", () => {
|
|
28
28
|
const declaration = declarationOf("{ lookup: { targets: { with: [mass, position] } } }");
|
|
29
|
-
const resolved = resolvedDeclaration(declaration, { query: null, resources: null, lookups: new Map([["targets", null]]), up: null });
|
|
29
|
+
const resolved = resolvedDeclaration(declaration, { query: null, resources: null, lookups: new Map([["targets", null]]), up: null, commands: null });
|
|
30
30
|
expect(readOf(resolved, "targets")).toEqual(["mass", "position"]);
|
|
31
31
|
});
|
|
32
32
|
});
|
|
@@ -77,7 +77,7 @@ const list = (names: Iterable<string>): string => `[${[...names].join(", ")}]`;
|
|
|
77
77
|
* module-scope bindings the body indexes by, null for a group it couldn't follow, and `at` where the body is.
|
|
78
78
|
*/
|
|
79
79
|
export const accessText = (used: Used, at: string, where?: ReadonlySet<string> | null): string => {
|
|
80
|
-
const { query, resources, lookups, up } = used;
|
|
80
|
+
const { query, resources, lookups, up, commands } = used;
|
|
81
81
|
// Keys changed inside their values are listed only when there are some, for registration to judge by kind.
|
|
82
82
|
const mutate = (keys: ReadonlySet<string>): string => keys.size === 0 ? "" : `, mutate: ${list(keys)}`;
|
|
83
83
|
const components = query === null ? "null" : `{ read: ${list([...query.read].filter(key => !query.write.has(key)))}, write: ${list(query.write)}${mutate(query.mutate)} }`;
|
|
@@ -85,7 +85,9 @@ export const accessText = (used: Used, at: string, where?: ReadonlySet<string> |
|
|
|
85
85
|
const lookupText = lookups === null ? "null" : `{ ${[...lookups].map(([name, read]) => `${JSON.stringify(name)}: ${read === null ? "null" : list(read)}`).join(", ")} }`;
|
|
86
86
|
// The resources `where`'s predicates read, only with predicates.
|
|
87
87
|
const whereText = where === undefined ? "" : `, where: ${where === null ? "null" : list(where)}`;
|
|
88
|
-
|
|
88
|
+
// Lists of keys stay as their bindings name them, so a list imported from another module is read where it's made.
|
|
89
|
+
const commandText = commands === null ? "null" : `{ spawn: ${list(commands.spawn)}, add: ${list(commands.add)}, remove: ${list(commands.remove)}, despawn: ${commands.despawn} }`;
|
|
90
|
+
return `{ components: ${components}, resources: ${resourceText}, lookup: ${lookupText}, up: ${up === null ? "null" : list(up)}${whereText}, commands: ${commandText}, at: ${JSON.stringify(at)} }`;
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
/** A declaration's parts the build makes, located at `at`, so a failure in them points at the declaration. */
|
|
@@ -924,10 +924,10 @@ describe("terminal bodies", () => {
|
|
|
924
924
|
return code.includes("terminal$((") && code.includes("function terminal$(body)");
|
|
925
925
|
};
|
|
926
926
|
test.each([
|
|
927
|
-
["no context", "(row)", "console.log(row);", `{ components: { with: [mass] } }`],
|
|
927
|
+
["no context", "(row)", "console.log(row);", `{ components: { with: [mass], whole: true } }`],
|
|
928
928
|
["a context destructured without commands", "({ resources })", "console.log(resources);", `{}`],
|
|
929
929
|
["a context used only through its other properties", "(context)", "console.log(context.resources);", `{ resources: {} }`],
|
|
930
|
-
["a context reaching resources and lookups", "(row, context)", "console.log(context.resources, context.lookup, row);", `{ components: { with: [mass] } }`],
|
|
930
|
+
["a context reaching resources and lookups", "(row, context)", "console.log(context.resources, context.lookup, row);", `{ components: { with: [mass], whole: true } }`],
|
|
931
931
|
["nested destructuring", "({ resources: { [delta]: d } })", "console.log(d);", `{ resources: {} }`],
|
|
932
932
|
])("mark a body with %s", async (_, parameters, body, declaration) => {
|
|
933
933
|
expect(await marked(parameters, body, declaration)).toBe(true);
|
|
@@ -1412,34 +1412,54 @@ describe("inferred access", () => {
|
|
|
1412
1412
|
.toContain("components: { read: [position], write: [] }");
|
|
1413
1413
|
});
|
|
1414
1414
|
|
|
1415
|
-
test("a row it can't follow
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1415
|
+
test("fails at the use of a system's row it can't follow, saying why, unless its declaration takes the row whole", async () => {
|
|
1416
|
+
const failure = async (source: string) => {
|
|
1417
|
+
const result = await transform(source);
|
|
1418
|
+
return result !== null && "failure" in result ? result.failure : null;
|
|
1419
|
+
};
|
|
1420
|
+
const passed = await failure(system("components: { with: [position] }", "(row) => { helper(row); }"));
|
|
1421
|
+
expect(passed?.message).toMatch(/^the build can't follow how this body uses its row, since it passes its row on, aliases or spreads it, so it would take every component in with, added and changed as written: .*components: \{ whole: true \}/);
|
|
1422
|
+
expect(passed?.position).toBe(header.length + "const system = System.create({ components: { with: [position] } }, (row) => { ".length);
|
|
1423
|
+
const shadowed = await failure(system("components: { with: [position] }", "(row, { commands }) => { for (const row of list) commands.despawn(row); }"));
|
|
1424
|
+
expect(shadowed?.message).toContain("it declares another `row` here, which hides its row: rename one");
|
|
1425
|
+
expect(shadowed?.position).toBe(header.length + "const system = System.create({ components: { with: [position] } }, (row, { commands }) => { for (const ".length);
|
|
1426
|
+
expect((await failure(system("components: { with: [position] }", "(row) => { console.log(row.size); }")))?.message).toContain("it reads a member of its row by name");
|
|
1427
|
+
expect((await failure(system("components: { with: [position] }", "(row) => { delete row[position]; }")))?.message).toContain("it deletes a member of its row");
|
|
1428
|
+
for (const readonly of ["readonly: false, ", "readonly: watching, "]) {
|
|
1429
|
+
expect(await failure(`const watching = true;\n${system(`${readonly}components: { with: [position] }`, "(row) => { helper(row); }")}`)).not.toBe(null);
|
|
1430
|
+
}
|
|
1431
|
+
const whole = await transform(system("components: { with: [position], whole: true }", "(row) => { helper(row); }"));
|
|
1432
|
+
expect(whole !== null && "code" in whole ? [whole.code.includes("components: null"), whole.warnings] : null).toEqual([true, []]);
|
|
1433
|
+
// Taken whole on purpose, a row the build could follow is still taken whole.
|
|
1434
|
+
expect(await inferred(system("components: { with: [position], whole: true }", "(row) => { console.log(row[position]); }"))).toContain("components: null");
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
test("commands by kind, keys and lists as their bindings name them, however the context reaches them", async () => {
|
|
1438
|
+
const commanded = async (body: string, declaration = "components: { with: [position] }"): Promise<string> =>
|
|
1439
|
+
/commands: (null|\{[^}]*\})/.exec(await inferred(`const Shapes = [mass];\nconst Listed = [velocity];\n${system(declaration, body)}`))?.[1] ?? "none";
|
|
1440
|
+
expect(await commanded("(row, { commands }) => { commands.spawn([mass, ...Shapes])({}); commands.add(row.entity, velocity, 1); commands.remove(row.entity, [mass]); }"))
|
|
1441
|
+
.toBe("{ spawn: [mass, Shapes], add: [velocity], remove: [mass], despawn: false }");
|
|
1442
|
+
expect(await commanded("(row, context) => { const c = context.commands; c.despawn(row.entity); context.commands.add(row.entity, Listed, {}); }"))
|
|
1443
|
+
.toBe("{ spawn: [], add: [Listed], remove: [], despawn: true }");
|
|
1444
|
+
expect(await commanded("(row, context) => { const { commands } = context; commands.despawnWhere({ with: [mass] }); }")).toContain("despawn: true");
|
|
1445
|
+
expect(await commanded("(row) => { row[position] = 1; }")).toBe("{ spawn: [], add: [], remove: [], despawn: false }");
|
|
1446
|
+
for (const body of ["(row, { commands }) => { helper(commands); }", "(row, { commands }) => { commands.add(row.entity, keyOf(1), 1); }",
|
|
1447
|
+
"(row, { commands: { spawn } }) => { spawn([mass]); }", "(row, context) => { helper(context); }",
|
|
1448
|
+
"(row, { commands }) => { commands.spawnTables(tables); }"]) {
|
|
1449
|
+
expect(await commanded(body)).toBe("null");
|
|
1450
|
+
}
|
|
1418
1451
|
});
|
|
1419
1452
|
|
|
1420
|
-
test("
|
|
1453
|
+
test("a readonly system's row it can't follow as unknown without a failure, since the world takes a readonly system's as read", async () => {
|
|
1421
1454
|
const result = await transform(system("readonly: true, components: { with: [position] }", "(row) => { helper(row); }"));
|
|
1422
1455
|
expect(result !== null && "code" in result ? [result.code.includes("components: null"), result.warnings] : null).toEqual([true, []]);
|
|
1423
1456
|
});
|
|
1424
1457
|
|
|
1425
|
-
test("a
|
|
1426
|
-
for (const observer of ["observer: false, ", "observer: watching, "]) {
|
|
1427
|
-
const result = await transform(`const watching = true;\n${system(`${observer}components: { with: [position] }`, "(row) => { helper(row); }")}`);
|
|
1428
|
-
expect(result !== null && "code" in result ? result.warnings.length : null).toBe(1);
|
|
1429
|
-
}
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
test("a lookup's rows it can't follow without a warning, since what they hold is what the declaration says", async () => {
|
|
1458
|
+
test("a lookup's rows it can't follow without a failure, since what they hold is what the declaration says", async () => {
|
|
1433
1459
|
const result = await transform(system("components: { with: [target] }, lookup: { targets: { with: [mass] } }", "(row, { lookup }) => { helper(lookup.targets(row[target])); }"));
|
|
1434
1460
|
expect(result !== null && "code" in result ? [result.code.includes(`"targets": null`), result.warnings] : null).toEqual([true, []]);
|
|
1435
1461
|
});
|
|
1436
1462
|
|
|
1437
|
-
test("warnings name the module by the label given, with line and column", async () => {
|
|
1438
|
-
const result = await transformKernels(`${header}${system("components: { with: [position] }", "(row) => { helper(row); }")}`, "/repo/app/src/features/a/model.ts", parse,
|
|
1439
|
-
async specifier => specifier === "ecs" ? ownCopy : null, () => "", createLinker(host({}, reductions), parse), true, "src/features/a/model.ts");
|
|
1440
|
-
expect(result !== null && "code" in result ? result.warnings.map(warning => /^[^ ]*:/.exec(warning)?.[0]) : null).toEqual([`src/features/a/model.ts:${header.split("\n").length}:${"const system = System.create({ components: { with: [position] } }, ".length + 1}:`]);
|
|
1441
|
-
});
|
|
1442
|
-
|
|
1443
1463
|
test("resources read, written and reduced, however its context reaches them", async () => {
|
|
1444
1464
|
expect(await inferred(system("", "(context) => { context.resources[score] = context.resources[delta]; }")))
|
|
1445
1465
|
.toContain("resources: { read: [delta], write: [score], reduce: [] }");
|
|
@@ -1519,7 +1539,7 @@ describe("inferred access", () => {
|
|
|
1519
1539
|
});
|
|
1520
1540
|
|
|
1521
1541
|
test("a body mentioning eval as one it can't follow", async () => {
|
|
1522
|
-
expect(await inferred(system("components: { with: [position] }", "(row) => { eval(\"row[position] = 1\"); }"))).toContain("
|
|
1542
|
+
expect(await inferred(system("components: { with: [position] }", "(row) => { eval(\"row[position] = 1\"); }"))).toContain("since it calls `eval`");
|
|
1523
1543
|
});
|
|
1524
1544
|
|
|
1525
1545
|
test("fail the build for an async or generator body", async () => {
|
|
@@ -313,9 +313,11 @@ export const transformKernels = async (
|
|
|
313
313
|
const used = kernel?.used ?? inOrder(usedBy(declared, body, module.bindings, positions), declared);
|
|
314
314
|
requireRequired(used, declared, body);
|
|
315
315
|
const where = at(body);
|
|
316
|
-
// A readonly system's row, and a lookup's rows
|
|
317
|
-
// declarations already say;
|
|
318
|
-
if (used.
|
|
316
|
+
// A readonly system's row, a finalizer's, and a lookup's rows it can't follow are taken as read whole, which is
|
|
317
|
+
// what their declarations already say; a system's row taken as written whole would change its schedule unseen.
|
|
318
|
+
if (created === "system" && used.rowLost !== undefined && !declaresTrue(declared, "readonly")) {
|
|
319
|
+
fail(used.rowLost.at, `the build can't follow how this body uses its row, since ${used.rowLost.why}, so it would take every component in with, added and changed as written: index the row by key directly, or destructure it by key; or declare \`components: { whole: true }\` for a body that uses its row whole`);
|
|
320
|
+
}
|
|
319
321
|
accessing = true;
|
|
320
322
|
const access = accessText(used, where, whereReads.has(node) ? whereReads.get(node) ?? null : undefined);
|
|
321
323
|
if (kernel !== undefined) {
|
|
@@ -3,13 +3,14 @@ import type { Finalizer } from '../../interface/finalizer/created.js';
|
|
|
3
3
|
import type { Rule } from '../../interface/rule/created.js';
|
|
4
4
|
import type { RuleFacts } from '../../interface/registry/strata.js';
|
|
5
5
|
import { createFinalizerRunner, placeFinalizer, type FinalizerRunner } from './finalizers.js';
|
|
6
|
+
import { watchResource } from './resource-changes.js';
|
|
6
7
|
import { finalizerKeysOf, type TaskChannel } from './remote-systems.js';
|
|
7
8
|
import { programFinalizers, programRules, programVersion } from '../../interface/registry/program.js';
|
|
8
9
|
import { triggersOf } from './program.js';
|
|
9
10
|
import { ensureResources, findResource } from './resources.js';
|
|
10
11
|
import { idOf, metIdOf, type Key, type State } from './state.js';
|
|
11
12
|
import { restratify } from './structure.js';
|
|
12
|
-
import { idOfItem, idOfKey, labelOf, threadOf } from '../../interface/registry/index.js';
|
|
13
|
+
import { idOfItem, idOfKey, kindOf, labelOf, threadOf } from '../../interface/registry/index.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Why a joined thread that imported the items of ids `imported` can't claim `threads`: a program finalizer on one that
|
|
@@ -67,7 +68,11 @@ export const placeFinalizers = (state: State): void => {
|
|
|
67
68
|
state.finalizers.splice(0, state.finalizers.length, ...kept);
|
|
68
69
|
};
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
/** The resources a finalizer reads, writes or, for a changes finalizer, watches. */
|
|
72
|
+
const resourcesOf = (finalizer: Finalizer): readonly Key[] => [
|
|
73
|
+
...finalizer.declaration.resources?.read ?? [], ...finalizer.declaration.resources?.write ?? [],
|
|
74
|
+
...finalizer.kind === "changes" ? (finalizer.declaration.watch ?? []).filter(key => kindOf(key) === "resource") : [],
|
|
75
|
+
];
|
|
71
76
|
|
|
72
77
|
/** Between runs: adds the resources of every finalizer in the program, so one can activate at any sync point. */
|
|
73
78
|
export const ensureProgramResources = (state: State, ensured: { version: number }): void => {
|
|
@@ -75,6 +80,10 @@ export const ensureProgramResources = (state: State, ensured: { version: number
|
|
|
75
80
|
if (ensured.version === version) return;
|
|
76
81
|
ensured.version = version;
|
|
77
82
|
ensureResources(state, new Set(programFinalizers().flatMap(resourcesOf)));
|
|
83
|
+
// A watched resource is followed from now, before its finalizer activates, so writes before then are seen.
|
|
84
|
+
for (const finalizer of programFinalizers()) {
|
|
85
|
+
if (finalizer.kind === "changes") for (const key of finalizer.declaration.watch ?? []) if (kindOf(key) === "resource") watchResource(state, idOf(state, key));
|
|
86
|
+
}
|
|
78
87
|
// Finalizers waiting for these can activate at the next sync point.
|
|
79
88
|
state.activation.met = true;
|
|
80
89
|
};
|