@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.
Files changed (179) hide show
  1. package/dist/implementation/compiler/body-access.d.ts +18 -1
  2. package/dist/implementation/compiler/body-access.js +104 -15
  3. package/dist/implementation/compiler/body-access.js.map +1 -1
  4. package/dist/implementation/compiler/estree.d.ts +5 -2
  5. package/dist/implementation/compiler/estree.js +8 -5
  6. package/dist/implementation/compiler/estree.js.map +1 -1
  7. package/dist/implementation/compiler/inferred.js +4 -2
  8. package/dist/implementation/compiler/inferred.js.map +1 -1
  9. package/dist/implementation/compiler/transform.js +5 -4
  10. package/dist/implementation/compiler/transform.js.map +1 -1
  11. package/dist/implementation/world/activate.js +14 -2
  12. package/dist/implementation/world/activate.js.map +1 -1
  13. package/dist/implementation/world/buffer.d.ts +7 -0
  14. package/dist/implementation/world/buffer.js +7 -0
  15. package/dist/implementation/world/buffer.js.map +1 -1
  16. package/dist/implementation/world/cascade.d.ts +10 -3
  17. package/dist/implementation/world/cascade.js +48 -23
  18. package/dist/implementation/world/cascade.js.map +1 -1
  19. package/dist/implementation/world/commands.js +40 -5
  20. package/dist/implementation/world/commands.js.map +1 -1
  21. package/dist/implementation/world/create.js +53 -2
  22. package/dist/implementation/world/create.js.map +1 -1
  23. package/dist/implementation/world/execute.d.ts +4 -4
  24. package/dist/implementation/world/execute.js +132 -83
  25. package/dist/implementation/world/execute.js.map +1 -1
  26. package/dist/implementation/world/finalizers.d.ts +2 -0
  27. package/dist/implementation/world/finalizers.js +24 -3
  28. package/dist/implementation/world/finalizers.js.map +1 -1
  29. package/dist/implementation/world/join.js +12 -2
  30. package/dist/implementation/world/join.js.map +1 -1
  31. package/dist/implementation/world/resource-changes.d.ts +18 -0
  32. package/dist/implementation/world/resource-changes.js +66 -0
  33. package/dist/implementation/world/resource-changes.js.map +1 -0
  34. package/dist/implementation/world/rows.js +21 -16
  35. package/dist/implementation/world/rows.js.map +1 -1
  36. package/dist/implementation/world/run-changes.d.ts +38 -0
  37. package/dist/implementation/world/run-changes.js +138 -0
  38. package/dist/implementation/world/run-changes.js.map +1 -0
  39. package/dist/implementation/world/runner.js +16 -12
  40. package/dist/implementation/world/runner.js.map +1 -1
  41. package/dist/implementation/world/state.d.ts +13 -0
  42. package/dist/implementation/world/state.js +13 -1
  43. package/dist/implementation/world/state.js.map +1 -1
  44. package/dist/implementation/world/synchronize.js +72 -22
  45. package/dist/implementation/world/synchronize.js.map +1 -1
  46. package/dist/implementation/world/tables-read.d.ts +5 -3
  47. package/dist/implementation/world/tables-read.js +16 -4
  48. package/dist/implementation/world/tables-read.js.map +1 -1
  49. package/dist/implementation/world/tables.d.ts +3 -2
  50. package/dist/implementation/world/tables.js +83 -37
  51. package/dist/implementation/world/tables.js.map +1 -1
  52. package/dist/implementation/world/thread-observer.js +1 -1
  53. package/dist/implementation/world/thread-observer.js.map +1 -1
  54. package/dist/implementation/world/thread-systems.js +10 -6
  55. package/dist/implementation/world/thread-systems.js.map +1 -1
  56. package/dist/implementation/world/up.js +7 -1
  57. package/dist/implementation/world/up.js.map +1 -1
  58. package/dist/implementation/world/where.d.ts +8 -0
  59. package/dist/implementation/world/where.js +40 -11
  60. package/dist/implementation/world/where.js.map +1 -1
  61. package/dist/interface/finalizer/body.d.ts +18 -3
  62. package/dist/interface/finalizer/create.d.ts +7 -5
  63. package/dist/interface/finalizer/create.js +8 -5
  64. package/dist/interface/finalizer/create.js.map +1 -1
  65. package/dist/interface/finalizer/declaration.d.ts +4 -2
  66. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  67. package/dist/interface/indexes/declaration-of.js +20 -0
  68. package/dist/interface/indexes/declaration-of.js.map +1 -0
  69. package/dist/interface/indexes/index.d.ts +1 -0
  70. package/dist/interface/indexes/index.js +1 -0
  71. package/dist/interface/indexes/index.js.map +1 -1
  72. package/dist/interface/registry/schedule.d.ts +28 -1
  73. package/dist/interface/registry/schedule.js +182 -21
  74. package/dist/interface/registry/schedule.js.map +1 -1
  75. package/dist/interface/system/access.d.ts +15 -0
  76. package/dist/interface/system/check.d.ts +7 -0
  77. package/dist/interface/system/check.js +17 -3
  78. package/dist/interface/system/check.js.map +1 -1
  79. package/dist/interface/system/commands.d.ts +26 -5
  80. package/dist/interface/system/create-resolved.js +1 -1
  81. package/dist/interface/system/create-resolved.js.map +1 -1
  82. package/dist/interface/system/create.d.ts +10 -6
  83. package/dist/interface/system/create.js +5 -4
  84. package/dist/interface/system/create.js.map +1 -1
  85. package/dist/interface/system/describe.d.ts +120 -0
  86. package/dist/interface/system/describe.js +116 -0
  87. package/dist/interface/system/describe.js.map +1 -0
  88. package/dist/interface/system/index.d.ts +1 -0
  89. package/dist/interface/system/index.js +1 -0
  90. package/dist/interface/system/index.js.map +1 -1
  91. package/dist/interface/system/leaf.d.ts +6 -0
  92. package/dist/interface/system/render.js +1 -0
  93. package/dist/interface/system/render.js.map +1 -1
  94. package/dist/interface/system/resolve.d.ts +5 -1
  95. package/dist/interface/system/resolve.js +19 -1
  96. package/dist/interface/system/resolve.js.map +1 -1
  97. package/dist/interface/system/shared-only.d.ts +7 -0
  98. package/dist/interface/system/shared-only.js +2 -0
  99. package/dist/interface/system/shared-only.js.map +1 -0
  100. package/dist/interface/system/tables.d.ts +9 -1
  101. package/dist/interface/world/table/index.d.ts +3 -2
  102. package/dist/interface/world/world.d.ts +15 -0
  103. package/package.json +11 -2
  104. package/src/guide/03-systems.out +1 -1
  105. package/src/guide/06-sync-points.md +12 -8
  106. package/src/guide/06-sync-points.out +1 -2
  107. package/src/guide/06-sync-points.ts +9 -5
  108. package/src/guide/18-identity.out +1 -1
  109. package/src/guide/guide.test.ts +3 -150
  110. package/src/implementation/compiler/body-access.ts +102 -12
  111. package/src/implementation/compiler/estree.ts +9 -6
  112. package/src/implementation/compiler/inferred.test.ts +1 -1
  113. package/src/implementation/compiler/inferred.ts +4 -2
  114. package/src/implementation/compiler/transform.test.ts +41 -21
  115. package/src/implementation/compiler/transform.ts +5 -3
  116. package/src/implementation/world/activate.ts +11 -2
  117. package/src/implementation/world/batches.test.ts +3 -3
  118. package/src/implementation/world/buffer.ts +7 -0
  119. package/src/implementation/world/cascade.ts +46 -22
  120. package/src/implementation/world/commands.ts +36 -5
  121. package/src/implementation/world/create.test.ts +8 -8
  122. package/src/implementation/world/create.ts +57 -2
  123. package/src/implementation/world/despawn-where.test.ts +287 -0
  124. package/src/implementation/world/execute.ts +125 -78
  125. package/src/implementation/world/finalizers.ts +27 -4
  126. package/src/implementation/world/ids.test.ts +1 -1
  127. package/src/implementation/world/indexes.test.ts +2 -1
  128. package/src/implementation/world/join.test.ts +19 -0
  129. package/src/implementation/world/join.ts +16 -2
  130. package/src/implementation/world/names.test.ts +1 -2
  131. package/src/implementation/world/perf.md +3 -1
  132. package/src/implementation/world/resource-changes.test.ts +61 -0
  133. package/src/implementation/world/resource-changes.ts +67 -0
  134. package/src/implementation/world/rows.ts +21 -14
  135. package/src/implementation/world/rules.test.ts +37 -0
  136. package/src/implementation/world/run-changes.test.ts +88 -0
  137. package/src/implementation/world/run-changes.ts +160 -0
  138. package/src/implementation/world/runner.ts +17 -13
  139. package/src/implementation/world/shared.test.ts +2 -2
  140. package/src/implementation/world/state.ts +19 -0
  141. package/src/implementation/world/sync-points.test.ts +135 -0
  142. package/src/implementation/world/synchronize.ts +65 -18
  143. package/src/implementation/world/tables-read.ts +16 -5
  144. package/src/implementation/world/tables.test.ts +106 -4
  145. package/src/implementation/world/tables.ts +71 -33
  146. package/src/implementation/world/thread-observer.ts +1 -1
  147. package/src/implementation/world/thread-systems.ts +10 -6
  148. package/src/implementation/world/up.test.ts +13 -1
  149. package/src/implementation/world/up.ts +6 -1
  150. package/src/implementation/world/where.ts +38 -10
  151. package/src/interface/finalizer/body.ts +13 -3
  152. package/src/interface/finalizer/create.test.ts +3 -1
  153. package/src/interface/finalizer/create.ts +12 -7
  154. package/src/interface/finalizer/declaration.ts +4 -2
  155. package/src/interface/indexes/create.test.ts +10 -0
  156. package/src/interface/indexes/declaration-of.ts +34 -0
  157. package/src/interface/indexes/index.ts +1 -0
  158. package/src/interface/registry/schedule.ts +196 -23
  159. package/src/interface/system/access.ts +16 -0
  160. package/src/interface/system/check.ts +15 -2
  161. package/src/interface/system/commands.ts +23 -4
  162. package/src/interface/system/commands.type-test.ts +31 -0
  163. package/src/interface/system/create-resolved.ts +1 -1
  164. package/src/interface/system/create.test.ts +8 -0
  165. package/src/interface/system/create.ts +9 -5
  166. package/src/interface/system/describe.test.ts +79 -0
  167. package/src/interface/system/describe.ts +122 -0
  168. package/src/interface/system/index.ts +1 -0
  169. package/src/interface/system/leaf.ts +6 -0
  170. package/src/interface/system/render.ts +1 -0
  171. package/src/interface/system/resolve.ts +25 -2
  172. package/src/interface/system/shared-only.ts +5 -0
  173. package/src/interface/system/tables.ts +7 -1
  174. package/src/interface/world/run.type-test.ts +11 -1
  175. package/src/interface/world/table/index.ts +3 -2
  176. package/src/interface/world/world.ts +13 -0
  177. package/src/perftest/benchmarks.ts +18 -0
  178. package/src/perftest/structural.ts +20 -0
  179. package/src/perftest/tables.ts +107 -0
@@ -4,8 +4,8 @@ import type { Definition } from '../system/definition.js';
4
4
  import type { Leaf } from '../system/leaf.js';
5
5
  import { firstItemOf } from './items.js';
6
6
  import { requireDefinition } from './program.js';
7
- import { firstKeyOf, labelOf, sameDeclaration } from './names.js';
8
- import { lookupKeysOf, orderKeysOf } from './indexes.js';
7
+ import { declarationOf, firstKeyOf, labelOf, sameDeclaration } from './names.js';
8
+ import { indexDefinitionOf, lookupKeysOf, orderKeysOf } from './indexes.js';
9
9
 
10
10
  type Key = symbol;
11
11
 
@@ -29,9 +29,15 @@ const checkInputs = (leaves: readonly Leaf[]): void => {
29
29
  /**
30
30
  * Leaves in run order, grouped into tiers. Leaves in one tier have no ordering path between them. `reductions` are the
31
31
  * resources its leaves reduce into, each with how many of them contribute. `observer` are its readonly leaves, in no
32
- * tier: they run after its sync point, ordered with nothing.
32
+ * tier: they run after its sync point, ordered with nothing. `syncs` holds, after each tier, the reasons for a sync
33
+ * point there, which applies the commands so far before the next tier, or null for none.
33
34
  */
34
- export type Schedule = { readonly tiers: readonly (readonly Leaf[])[]; readonly reductions: ReadonlyMap<Key, number>; readonly observer: readonly Leaf[] };
35
+ export type Schedule = {
36
+ readonly tiers: readonly (readonly Leaf[])[];
37
+ readonly reductions: ReadonlyMap<Key, number>;
38
+ readonly observer: readonly Leaf[];
39
+ readonly syncs: readonly (readonly SyncReason[] | null)[];
40
+ };
35
41
 
36
42
  export const isLeaf = (system: Definition): system is Leaf => system.kind !== "composite";
37
43
 
@@ -42,7 +48,14 @@ export const isObserver = (system: Definition): system is Leaf & { readonly decl
42
48
  const start = (unit: number): number => 2 * unit;
43
49
  const end = (unit: number): number => 2 * unit + 1;
44
50
 
45
- type Graph = { readonly units: readonly Definition[]; readonly successors: readonly Set<number>[] };
51
+ type Graph = {
52
+ readonly units: readonly Definition[];
53
+ readonly successors: readonly Set<number>[];
54
+ /** Each unit's members, by unit: none for a leaf. */
55
+ readonly members: readonly (readonly number[])[];
56
+ /** The units a list of systems names, readonly systems left out. */
57
+ readonly resolve: (systems: readonly object[] | undefined) => readonly number[];
58
+ };
46
59
 
47
60
  /**
48
61
  * A leaf runs from its start to its end, and a composite spans its members. `after` and `before` order one system's end
@@ -86,7 +99,7 @@ const graphOf = (root: Definition): Graph => {
86
99
  successors[end(unit)].add(end(other));
87
100
  }
88
101
  });
89
- return { units, successors };
102
+ return { units, successors, members, resolve };
90
103
  };
91
104
 
92
105
  /** Kahn's algorithm over nodes. Throws on a cycle. */
@@ -165,6 +178,74 @@ const conflictOf = (a: Access, b: Access): Key | undefined => {
165
178
  return undefined;
166
179
  };
167
180
 
181
+ /** Every key two accesses conflict on. */
182
+ const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
183
+ ...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
184
+ ...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
185
+ ])];
186
+
187
+ /** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
188
+ const orderedOf = (system: Definition) => {
189
+ const graph = graphOf(system);
190
+ const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
191
+ const accesses = leaves.map(({ leaf }) => accessOf(leaf));
192
+ // Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
193
+ const reductions = new Map<Key, number>();
194
+ accesses.forEach((access, contributor) => {
195
+ for (const key of access.reduces) {
196
+ reductions.set(key, (reductions.get(key) ?? 0) + 1);
197
+ accesses.forEach((other, reader) => {
198
+ if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
199
+ });
200
+ }
201
+ });
202
+ return { graph, leaves, accesses, reductions };
203
+ };
204
+
205
+ /**
206
+ * What put a leaf after another: a constraint of either, or of a composite holding it, naming the other or a composite
207
+ * holding it (`after`, `before`), its reading a fold the other contributes to (`reduction`), or being placed during a
208
+ * span (`during`). `conflicts` are the keys both access, one writing, which need the order.
209
+ */
210
+ export type Placement = { readonly leaf: Leaf; readonly because: "after" | "before" | "during" | "reduction"; readonly conflicts: readonly Key[] };
211
+
212
+ /**
213
+ * For each leaf in a system's tiers, the leaves it follows directly: those with an ordering path to it through no other
214
+ * leaf of its tiers.
215
+ */
216
+ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
217
+ const { graph, leaves, accesses } = orderedOf(system);
218
+ const reach = reachability(topologicalOrder(graph), graph.successors);
219
+ const ancestors = graph.units.map(() => [] as number[]);
220
+ graph.members.forEach((members, unit) => {
221
+ const visit = (member: number): void => {
222
+ ancestors[member].push(unit);
223
+ for (const inner of graph.members[member]) visit(inner);
224
+ };
225
+ for (const member of members) visit(member);
226
+ });
227
+ /** Whether `systems` names `unit` or a composite holding it. */
228
+ const names = (systems: readonly object[] | undefined, unit: number): boolean => {
229
+ const named = graph.resolve(systems);
230
+ return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
231
+ };
232
+ const constrains = (unit: number, list: "after" | "before", other: number): boolean =>
233
+ [unit, ...ancestors[unit]].some(holder => holder !== 0 && names(graph.units[holder].declaration[list], other));
234
+ const placements = new Map<Leaf, readonly Placement[]>();
235
+ leaves.forEach(({ leaf, unit }, b) => {
236
+ const before = leaves.flatMap((other, a) => a !== b && reaches(reach, end(other.unit), start(unit)) ? [a] : []);
237
+ const direct = before.filter(a => !before.some(other => other !== a && reaches(reach, end(leaves[a].unit), start(leaves[other].unit))));
238
+ placements.set(leaf, direct.map(a => {
239
+ const other = leaves[a];
240
+ const because = constrains(unit, "after", other.unit) ? "after"
241
+ : constrains(other.unit, "before", unit) ? "before"
242
+ : [...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during";
243
+ return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
244
+ }));
245
+ });
246
+ return placements;
247
+ };
248
+
168
249
  const schedules = new WeakMap<Definition, Schedule>();
169
250
 
170
251
  /** A system's schedule, created once per system. Throws on a cycle, or on two conflicting leaves with no ordering path between them. */
@@ -179,33 +260,125 @@ export const scheduleOf = (system: Definition): Schedule => {
179
260
  * or on inputs that don't merge.
180
261
  */
181
262
  const createSchedule = (system: Definition): Schedule => {
182
- const graph = graphOf(system);
263
+ const { graph, leaves, accesses, reductions } = orderedOf(system);
183
264
  const observer = graph.units.filter(isObserver);
184
- const leaves = graph.units.flatMap((unit, index) => isLeaf(unit) && !isObserver(unit) ? [{ leaf: unit, unit: index }] : []);
185
265
  checkInputs(leaves.map(({ leaf }) => leaf));
186
- const accesses = leaves.map(({ leaf }) => accessOf(leaf));
187
- // Leaves that read a resource run after every leaf that reduces into it, so they read the finished fold.
188
- const reductions = new Map<Key, number>();
189
- accesses.forEach((access, contributor) => {
190
- for (const key of access.reduces) {
191
- reductions.set(key, (reductions.get(key) ?? 0) + 1);
192
- accesses.forEach((other, reader) => {
193
- if (reader !== contributor && other.reads.has(key)) graph.successors[end(leaves[contributor].unit)].add(start(leaves[reader].unit));
194
- });
195
- }
196
- });
197
266
  const order = topologicalOrder(graph);
198
267
  const reach = reachability(order, graph.successors);
199
268
  for (let a = 0; a < leaves.length; a++) {
200
269
  for (let b = a + 1; b < leaves.length; b++) {
201
270
  const key = conflictOf(accesses[a], accesses[b]);
202
271
  if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
203
- throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.`);
272
+ const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
273
+ .map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
274
+ throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.${whole}`);
204
275
  }
205
276
  }
206
277
  }
207
278
  const depth = depthsOf(graph, order);
208
- const tiers: Leaf[][] = [];
209
- for (const { leaf, unit } of leaves) (tiers[depth[start(unit)]] ??= []).push(leaf);
210
- return { tiers: tiers.filter(tier => tier !== undefined), reductions, observer };
279
+ const byDepth: Leaf[][] = [];
280
+ for (const { leaf, unit } of leaves) (byDepth[depth[start(unit)]] ??= []).push(leaf);
281
+ const tiers = byDepth.filter(tier => tier !== undefined);
282
+ // A sync point comes between tiers, so a schedule of one tier has none.
283
+ if (tiers.length < 2) return { tiers, reductions, observer, syncs: tiers.map(() => null) };
284
+ const tierOf = new Map<Leaf, number>();
285
+ tiers.forEach((tier, t) => { for (const leaf of tier) tierOf.set(leaf, t); });
286
+ const edges: { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[] = [];
287
+ leaves.forEach((from, a) => {
288
+ const changed = structuralOf(from.leaf);
289
+ if (changed === null) return;
290
+ leaves.forEach((to, b) => {
291
+ if (a === b || !reaches(reach, end(from.unit), start(to.unit))) return;
292
+ const keys = affected(changed, touchesOf(to.leaf));
293
+ if (keys !== null) edges.push({ lo: tierOf.get(from.leaf) ?? 0, hi: (tierOf.get(to.leaf) ?? 0) - 1, reason: { from: from.leaf, to: to.leaf, keys } });
294
+ });
295
+ });
296
+ return { tiers, reductions, observer, syncs: syncsOf(tiers.length, edges) };
297
+ };
298
+
299
+ /**
300
+ * Why a sync point follows a tier: a system in it or before it whose commands may change, structurally, what a system
301
+ * after the sync point that follows it reads, matches, looks up or indexes. `keys` are the components its commands name
302
+ * that the later one uses, none where it despawns, or where its commands or a cascade could change anything.
303
+ */
304
+ export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys: readonly Key[] };
305
+
306
+ /** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
307
+ const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
308
+ const { commands } = leaf;
309
+ if (commands === undefined) return null;
310
+ if (commands === null || commands.despawn === true) return "any";
311
+ const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
312
+ // A reference written may despawn its holder, or remove itself, at the sync point.
313
+ for (const key of keys) if (declarationOf(key)?.reference !== undefined) return "any";
314
+ return keys.size === 0 ? null : keys;
315
+ };
316
+
317
+ /**
318
+ * The components a leaf's rows, lookups, indexes and order name, which commands could change its view of; `any` when
319
+ * one of them matches every entity, `cascaded` when it names a component a rule derives or `within` removes.
320
+ */
321
+ const touchesOf = (leaf: Leaf): { readonly keys: ReadonlySet<Key>; readonly any: boolean; readonly cascaded: boolean } => {
322
+ const { components, lookup, up, order } = leaf.declaration;
323
+ const keys = new Set<Key>();
324
+ let any = false;
325
+ const add = (list: readonly Key[] | undefined): void => { for (const key of list ?? []) keys.add(firstKeyOf(key)); };
326
+ /** Names what a query requires; one requiring nothing matches every entity. */
327
+ const requires = (required: readonly (readonly Key[] | undefined)[], others: readonly (readonly Key[] | undefined)[]): void => {
328
+ if (required.every(list => list === undefined || list.length === 0)) any = true;
329
+ for (const list of [...required, ...others]) add(list);
330
+ };
331
+ const indexed = (index: unknown): void => {
332
+ const declaration = indexDefinitionOf(index)?.declaration;
333
+ if (declaration !== undefined) requires([declaration.key, declaration.components?.has], [declaration.components?.not]);
334
+ };
335
+ if (components !== undefined) {
336
+ requires([components.read, components.write, components.has, components.added, components.changed], [components.optional, components.not]);
337
+ add(Object.getOwnPropertySymbols(components.where ?? {}));
338
+ }
339
+ for (const name in lookup) {
340
+ const used = lookup[name];
341
+ if ("index" in used && used.index !== undefined) {
342
+ indexed(used.index);
343
+ add(used.read);
344
+ }
345
+ else if (!("index" in used)) requires([used.read, used.has], [used.optional, used.not, Array.isArray(used.changed) ? used.changed : undefined]);
346
+ }
347
+ if (order !== undefined) indexed(order);
348
+ if (up !== undefined) add([up.through, ...up.read ?? []]);
349
+ const cascaded = [...keys].some(key => { const declared = declarationOf(key); return declared?.derived === true || declared?.within !== undefined; });
350
+ return { keys, any, cascaded };
351
+ };
352
+
353
+ /** The keys through which `changed` may change what `touches` names; null where it can't. */
354
+ const affected = (changed: ReadonlySet<Key> | "any", touches: ReturnType<typeof touchesOf>): readonly Key[] | null => {
355
+ if (touches.keys.size === 0 && !touches.any) return null;
356
+ if (changed === "any") return [];
357
+ if (touches.any || touches.cascaded) return [...changed];
358
+ const keys = [...changed].filter(key => touches.keys.has(key));
359
+ return keys.length === 0 ? null : keys;
360
+ };
361
+
362
+ /**
363
+ * The fewest sync points that put one between the tiers of each edge, after a tier from `lo` to `hi`: each placed as
364
+ * late as the first edge it must cover allows, so it covers every edge open then. An edge's reason goes to the first
365
+ * sync point within its tiers.
366
+ */
367
+ const syncsOf = (count: number, edges: readonly { readonly lo: number; readonly hi: number; readonly reason: SyncReason }[]): readonly (readonly SyncReason[] | null)[] => {
368
+ const syncs: (SyncReason[] | null)[] = new Array<SyncReason[] | null>(count).fill(null);
369
+ let last = -1;
370
+ for (const { lo, hi } of [...edges].sort((a, b) => a.hi - b.hi)) {
371
+ if (last >= lo) continue;
372
+ last = hi;
373
+ syncs[hi] = [];
374
+ }
375
+ for (const { lo, hi, reason } of edges) {
376
+ for (let t = lo; t <= hi; t++) {
377
+ const reasons = syncs[t];
378
+ if (reasons === null) continue;
379
+ reasons.push(reason);
380
+ break;
381
+ }
382
+ }
383
+ return syncs;
211
384
  };
@@ -27,6 +27,12 @@ export type ComponentAccess = {
27
27
  * change filters, an archetype it skipped keeps its rows unseen until it passes again.
28
28
  */
29
29
  readonly where?: { readonly [K in Shared]?: Component<K> | ((value: Component<K>, context: { readonly resources: ReadResources }) => boolean) };
30
+ /**
31
+ * Takes its row whole, as a body passing it on does, where the build can't follow how it's used: every member counts
32
+ * as written, for order, conflicts and placement, but references and shared components, which change only through
33
+ * commands, as read. Without it, a body whose row the build can't follow fails the build.
34
+ */
35
+ readonly whole?: true;
30
36
  };
31
37
 
32
38
  /**
@@ -85,6 +91,8 @@ export type ComponentLists = {
85
91
  readonly where?: { readonly [key: symbol]: unknown };
86
92
  /** The resources its `where`'s predicates read, which the build infers: when one changed, they're evaluated again. */
87
93
  readonly whereReads?: readonly Key[];
94
+ /** Whether it takes its row whole, as its declaration said. */
95
+ readonly whole?: true;
88
96
  };
89
97
 
90
98
  /** A lookup as the world takes it, with what its body reads of its rows in `read`. */
@@ -105,3 +113,11 @@ export type ResourceLists = {
105
113
  readonly is?: readonly Key[];
106
114
  readonly not?: readonly Key[];
107
115
  };
116
+
117
+ /** The components a system's commands name, by kind, and whether it despawns, as the build inferred them. */
118
+ export type CommandLists = {
119
+ readonly spawn?: readonly Key[];
120
+ readonly add?: readonly Key[];
121
+ readonly remove?: readonly Key[];
122
+ readonly despawn?: true;
123
+ };
@@ -138,11 +138,23 @@ const checkWhere = (leaf: Leaf): void => {
138
138
  const where = declaration.components?.where;
139
139
  if (where === undefined) return;
140
140
  if (declaration.up !== undefined || declaration.order !== undefined || declaration.target !== undefined) throw new Error(`System "${name}" uses where with up, order or a target, which run rows across archetypes.`);
141
+ checkWhereValues(`System "${name}"`, "where", "a system", where, true);
142
+ };
143
+
144
+ /**
145
+ * Throws for a `where`, the one `named` says of `subject`, which keeps what `keeps` says to archetypes, naming what
146
+ * isn't a shared component or giving one a value not of its type, or a predicate unless `predicates`.
147
+ */
148
+ export const checkWhereValues = (subject: string, named: string, keeps: string, where: { readonly [key: symbol]: unknown }, predicates: boolean): void => {
141
149
  for (const key of Object.getOwnPropertySymbols(where)) {
142
150
  const found = declarationOf(key);
143
- if (kindOf(key) !== "component" || found?.shared !== true) throw new Error(`System "${name}": where names "${labelOf(key)}", which isn't a shared component: where keeps a system to archetypes by their shared values.`);
151
+ if (kindOf(key) !== "component" || found?.shared !== true) throw new Error(`${subject}: ${named} names "${labelOf(key)}", which isn't a shared component: where keeps ${keeps} to archetypes by their shared values.`);
144
152
  const value: unknown = Reflect.get(where, key);
145
- if (typeof value !== "function" && found.type !== undefined && !matchesSchema(found.type, value)) throw new Error(`System "${name}": where gives "${labelOf(key)}" ${labelOf(value)}, which isn't one of its type's values.`);
153
+ if (typeof value === "function") {
154
+ if (!predicates) throw new Error(`${subject}: ${named} gives "${labelOf(key)}" a predicate, and takes only values.`);
155
+ continue;
156
+ }
157
+ if (found.type !== undefined && !matchesSchema(found.type, value)) throw new Error(`${subject}: ${named} gives "${labelOf(key)}" ${labelOf(value)}, which isn't one of its type's values.`);
146
158
  }
147
159
  };
148
160
 
@@ -176,6 +188,7 @@ const checkTables = (leaf: Leaf): void => {
176
188
  requireRegistered("System", name, "component", tables.with);
177
189
  requireRegistered("System", name, "component", tables.not);
178
190
  requireRegistered("System", name, "resource", tables.resources);
191
+ if (tables.where !== undefined) checkWhereValues(`System "${name}"`, "tables' where", "tables", tables.where, false);
179
192
  };
180
193
 
181
194
  /**
@@ -3,6 +3,7 @@ import type { Component } from '../component/component.js';
3
3
  import type { Entity } from '../entity/entity.js';
4
4
  import type { Essential, Shared } from '../component/value.js';
5
5
  import type { Table } from '../world/table/table.js';
6
+ import type { SharedOnly } from './shared-only.js';
6
7
  import type { ValuesOf } from './values-of.js';
7
8
 
8
9
  /** The value an `add` of one component takes: none for a tag. */
@@ -63,6 +64,18 @@ export type Commands = {
63
64
  ...constant: RequiresPreset<C> extends true ? [constant?: P & SharedPreset<C> & Named<C, P>] : [constant?: P & Named<C, P>]
64
65
  ): (...values: SpawnValues<C, P>) => Entity;
65
66
  despawn(entity: Entity): void;
67
+ /**
68
+ * Despawns, as a `despawn` of each would, every entity that has each component of `with` and none of `not` once the
69
+ * commands issued before it apply, and every entity spawned at the same sync point that does, whenever spawned.
70
+ * Derived components may be named. `where` keeps it to the archetypes holding these values of shared components,
71
+ * each one required as `with` requires it, as a system's `where` of values does. Never the resource entity. Only on
72
+ * the world's thread: a joined thread's throws.
73
+ */
74
+ despawnWhere<const W extends object = {}>(query: {
75
+ readonly with: readonly [Component.Key, ...Component.Key[]];
76
+ readonly not?: readonly Component.Key[];
77
+ readonly where?: W & SharedOnly<NoInfer<W>> & { readonly [K in Shared]?: Component<K> };
78
+ }): void;
66
79
  /** Adds or replaces one component's value, marking it changed. */
67
80
  add<K extends Essential>(entity: Entity, component: K, ...value: ValueArgument<K>): void;
68
81
  /** Adds or replaces the values of `components`, marking them changed. */
@@ -77,9 +90,15 @@ export type Commands = {
77
90
  * outside them isn't spawned, nor anything it owns, through owner references, as despawning its owner would; a
78
91
  * weak reference to one is left out of its row, which is spawned without it; any other value naming one is
79
92
  * `Entity.none`. Returns, at once, each table's new entities, alive at the sync point. A column's stride must be its
80
- * component's in this world, and its length its table's count. `resources` sets each resource it names, by key, at
81
- * the sync point, as commands apply, each value checked against its resource's schema; not one only the world
82
- * writes, nor one living on another thread. On the world's thread only.
93
+ * component's in this world, and its length its table's count. On the world's thread only.
94
+ * - `existing` maps entities to ones already alive in this world, which stand for them: a row whose entity it maps
95
+ * isn't spawned, its entry in the result is the entity it maps to, and every value naming one it maps names that
96
+ * entity, whether or not it's in the tables. Fails for one that isn't alive.
97
+ * - `resources` sets each resource it names, by key, at the sync point, as commands apply, each value checked
98
+ * against its resource's schema; not one only the world writes, nor one living on another thread.
83
99
  */
84
- spawnTables(tables: readonly Table[], resources?: { readonly [key: symbol]: unknown }): Table.Spawned;
100
+ spawnTables(tables: readonly Table[], options?: {
101
+ readonly resources?: { readonly [key: symbol]: unknown };
102
+ readonly existing?: ReadonlyMap<Entity, Entity>;
103
+ }): Table.Spawned;
85
104
  };
@@ -13,6 +13,7 @@ const label = Symbol("commandsTypeTest/label");
13
13
  const player = Symbol("commandsTypeTest/player");
14
14
  const slot = Symbol("commandsTypeTest/slot");
15
15
  const time = Symbol("commandsTypeTest/time");
16
+ const chunk = Symbol("commandsTypeTest/chunk");
16
17
  declare module '@adobe/ecs' {
17
18
  export interface Components {
18
19
  [position]: { type: number };
@@ -20,6 +21,7 @@ declare module '@adobe/ecs' {
20
21
  [label]: { type: string | null };
21
22
  [player]: { type: true };
22
23
  [slot]: { type: number; derived: true };
24
+ [chunk]: { type: number; shared: true };
23
25
  }
24
26
  export interface Resources {
25
27
  [time]: { type: number };
@@ -84,4 +86,33 @@ export const issue = (commands: Commands, entity: Entity): void => {
84
86
  // @ts-expect-error commands never remove a derived component
85
87
  commands.remove(entity, slot);
86
88
  commands.despawn(entity);
89
+
90
+ // `despawnWhere` takes a query of at least one component, derived ones included.
91
+ commands.despawnWhere({ with: [player] });
92
+ commands.despawnWhere({ with: [position, slot], not: [player] });
93
+ commands.despawnWhere({ with: particle });
94
+ // @ts-expect-error `with` names at least one component
95
+ commands.despawnWhere({ with: [] });
96
+ // @ts-expect-error a query names components, not resources
97
+ commands.despawnWhere({ with: [time] });
98
+ // @ts-expect-error in `not` too
99
+ commands.despawnWhere({ with: [player], not: [time] });
100
+ // @ts-expect-error it takes a query, not an entity
101
+ commands.despawnWhere(entity);
102
+ // Its `where` gives values of shared components, as a system's `where` of values does.
103
+ commands.despawnWhere({ with: [position], where: { [chunk]: 3 } });
104
+ const unloaded = { [chunk]: 4 } as const;
105
+ commands.despawnWhere({ with: [position], where: unloaded });
106
+ // @ts-expect-error a value of the component's type
107
+ commands.despawnWhere({ with: [position], where: { [chunk]: "3" } });
108
+ // @ts-expect-error where names only shared components
109
+ commands.despawnWhere({ with: [position], where: { [position]: 3 } });
110
+ // @ts-expect-error where takes values, not predicates
111
+ commands.despawnWhere({ with: [position], where: { [chunk]: (value: number) => value > 3 } });
112
+ const placed = { [position]: 3 } as const;
113
+ // @ts-expect-error where names only shared components, in a constant too
114
+ commands.despawnWhere({ with: [position], where: placed });
115
+ const mixed = { [chunk]: 3, [position]: 3 } as const;
116
+ // @ts-expect-error and in a constant naming a shared one beside it
117
+ commands.despawnWhere({ with: [position], where: mixed });
87
118
  };
@@ -14,7 +14,7 @@ import type { System } from './system.js';
14
14
  export const createResolved = (declaration: Resolved, body: Leaf["body"], input?: unknown): System<unknown> => {
15
15
  const name = nameOf(declaration, undefined, "system");
16
16
  const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
17
- const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
17
+ const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: undefined };
18
18
  checkLeaf(system);
19
19
  claimSystem(system, undefined);
20
20
  return system;
@@ -171,6 +171,14 @@ describe("System.create of a composite", () => {
171
171
  const looped = System.create({ before: [first], after: [second] }, () => {});
172
172
  expect(() => System.create({ systems: [first, second, looped] })).toThrow("Ordering cycle");
173
173
  });
174
+ it("takes a row declared whole as written, every member but references, and says so in a conflict", () => {
175
+ const pass = (row: object): void => { void row; };
176
+ const whole = System.create({ components: { with: [position, parent], whole: true } }, row => { pass(row); });
177
+ const reader = System.create({ components: { with: [position] } }, row => { void row[position]; });
178
+ const definition = definitionOf(whole);
179
+ expect(definition?.kind === "system" ? definition.declaration.components : null).toEqual({ read: [parent], write: [position], whole: true });
180
+ expect(() => System.create([whole, reader])).toThrow('both access "typesSystemTest/position" and one writes it; order them with before or after. "whole" takes its row whole (components.whole), so it writes every member.');
181
+ });
174
182
  it("runs readonly members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
175
183
  const write = System.create({ components: { with: [position] } }, row => { row[position] += 1; });
176
184
  const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
@@ -16,8 +16,9 @@ import type { Listed, Property } from './property.js';
16
16
  import type { System } from './system.js';
17
17
  import type { OneThread, ThreadedBy, ThreadsOf } from './threads.js';
18
18
  import type { HeldBy } from './values.js';
19
+ import type { SharedOnly } from './shared-only.js';
19
20
  import type { Shared } from '../component/value.js';
20
- import { accessOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
21
+ import { accessOf, commandLists, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
21
22
 
22
23
  type Lookups<D> = Property<D, "lookup">;
23
24
 
@@ -68,9 +69,10 @@ type Targeted = {
68
69
  readonly order?: never;
69
70
  };
70
71
 
72
+
71
73
  /** `where` names only shared components, and keeps rows unordered across archetypes, so it's without `up`, `order` and a target. */
72
74
  type Where<D> = D extends { readonly components: { readonly where: infer W } }
73
- ? { readonly components?: { readonly where?: { readonly [K in Exclude<keyof W, Shared>]: { readonly "error: where names only shared components, which each archetype holds one value of": K } } } }
75
+ ? { readonly components?: { readonly where?: SharedOnly<W> } }
74
76
  & (D extends { readonly up: object } | { readonly order: object } | { readonly target: string }
75
77
  ? { readonly components?: { readonly where?: { readonly "error: where can't be used with up, order or a target, which run rows across archetypes": true } } } : {})
76
78
  : {};
@@ -99,6 +101,7 @@ type Validate<D extends Declaration> =
99
101
  ? { readonly kernel?: boolean | { readonly "error: a GPU kernel can't have optional components yet": true } } : {})
100
102
  & (D extends { readonly readonly: true } ? Readonly : {})
101
103
  // Tables are copies a readonly system without components reads.
104
+ & (D extends { readonly tables: { readonly where: infer W } } ? { readonly tables?: { readonly where?: SharedOnly<W> } } : {})
102
105
  & (D extends { readonly tables: object }
103
106
  ? (D extends { readonly readonly: true } ? {} : { readonly tables?: { readonly "error: tables are read by a readonly system": true } })
104
107
  & (Property<D, "components"> extends undefined ? {} : { readonly tables?: { readonly "error: a system reading tables has no components": true } })
@@ -175,9 +178,10 @@ export function create(first: Declaration | Leaf["body"] | readonly System<unkno
175
178
  const body = typeof first === "function" ? first : second;
176
179
  if (body === undefined) throw new Error("A system needs a body, and a composite its systems.");
177
180
  const name = nameOf(first, id, "system");
178
- const declaration = resolve(`System "${name}"`, declared, accessOf(`System "${name}"`, first));
181
+ const access = accessOf(`System "${name}"`, first);
182
+ const declaration = resolve(`System "${name}"`, declared, access);
179
183
  const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
180
- const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body };
184
+ const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: commandLists(access) };
181
185
  checkLeaf(system);
182
186
  claimSystem(system, id);
183
187
  return system;
@@ -194,7 +198,7 @@ const resolve = (what: string, declared: Declaration, access: ReturnType<typeof
194
198
  for (const key of [...whereReads, ...declared.tables?.resources ?? []]) if (!uses.read.includes(key) && !uses.write.includes(key) && !conditions.includes(key)) uses.read.push(key);
195
199
  const resolved: Resolved = {
196
200
  ...rest,
197
- ...components === undefined ? {} : { components: componentLists(components, access.components, reading, whereReads) },
201
+ ...components === undefined ? {} : { components: componentLists(components, components.whole === true ? null : access.components, reading, whereReads) },
198
202
  ...lookup === undefined ? {} : { lookup: lookupsLists(lookup, access) },
199
203
  ...up === undefined ? {} : { up: { through: up.through, read: upReads(what, access) } },
200
204
  };
@@ -0,0 +1,79 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it } from "vitest";
3
+ import { Index } from "../indexes/index-type.js";
4
+ import { System } from "./system.js";
5
+
6
+ const position = Symbol("describeTest/position");
7
+ const cell = Symbol("describeTest/cell");
8
+ const speed = Symbol("describeTest/speed");
9
+ const hover = Symbol("describeTest/hover");
10
+
11
+ declare module "@adobe/ecs" {
12
+ export interface Components {
13
+ [position]: { type: number };
14
+ [cell]: { type: number };
15
+ }
16
+ export interface Resources {
17
+ [speed]: { type: number };
18
+ [hover]: { type: number };
19
+ }
20
+ }
21
+
22
+ const byCell = Index.create({ key: [cell] });
23
+
24
+ const spawnCells = System.create(({ commands }) => { commands.spawn([position])({ [position]: 1 }); });
25
+ const move = System.create({ components: { with: [position] }, after: [spawnCells] }, (row, { resources }) => { row[position] += resources[speed]; });
26
+ const deriveCells = System.create({ components: { with: [position], not: [cell] }, after: [move] }, (row, { commands }) => { commands.add(row.entity, cell, row[position]); });
27
+ const pick = System.create({ lookup: { cells: { index: byCell } }, after: [deriveCells] }, ({ lookup, resources }) => { resources[hover] = lookup.cells(1).length; });
28
+ const nudge = System.create({ components: { with: [position], whole: true } }, row => { Reflect.set(row, position, 0); });
29
+ const watch = System.create({ readonly: true, components: { with: [position] } }, row => { void row[position]; });
30
+ const frame = System.create([spawnCells, move, deriveCells, pick, watch]);
31
+
32
+ describe("System.describe", () => {
33
+ it("gives tiers in run order, readonly systems apart, and what each system uses, as data", () => {
34
+ const described = System.describe(frame);
35
+ expect(described.tiers.map(tier => tier.systems.map(system => system.name))).toEqual([["spawnCells"], ["move"], ["deriveCells"], ["pick"]]);
36
+ expect(described.readonly.map(system => system.system)).toEqual([watch]);
37
+ const [[spawning], [moving], [deriving], [picking]] = described.tiers.map(tier => tier.systems);
38
+ expect(spawning?.commands).toEqual({ spawn: [position], add: [], remove: [], despawn: false });
39
+ expect([moving?.components.write, moving?.resources.read, moving?.commands?.add]).toEqual([[position], [speed], []]);
40
+ expect([deriving?.components.read, deriving?.components.not, deriving?.commands?.add]).toEqual([[position], [cell], [cell]]);
41
+ expect([picking?.lookups, picking?.resources.write]).toEqual([[{ name: "cells", index: "byCell", read: [] }], [hover]]);
42
+ expect(moving?.after).toEqual([{ system: spawnCells, name: "spawnCells", because: "after", conflicts: [] }]);
43
+ expect(deriving?.after).toEqual([{ system: move, name: "move", because: "after", conflicts: [position] }]);
44
+ expect(System.describe(nudge).tiers[0]?.systems[0]?.whole).toBe(true);
45
+ });
46
+ it("gives the fewest sync points between tiers whose commands a later system depends on, and why", () => {
47
+ expect(System.describe(frame).tiers.map(tier => tier.sync)).toEqual([
48
+ [{ from: spawnCells, to: move, keys: [position] }, { from: spawnCells, to: deriveCells, keys: [position] }],
49
+ null,
50
+ [{ from: deriveCells, to: pick, keys: [cell] }],
51
+ null,
52
+ ]);
53
+ // Despawning may change anything a later system matches; writing values needs no sync point.
54
+ const clear = System.create({ components: { with: [cell] } }, (row, { commands }) => { commands.despawn(row.entity); });
55
+ const count = System.create({ components: { with: [position] }, after: [clear] }, row => { void row[position]; });
56
+ const tally = System.create({ after: [count] }, ({ resources }) => { resources[hover] = resources[speed]; });
57
+ expect(System.describe(System.create([clear, count, tally])).tiers.map(tier => tier.sync)).toEqual([[{ from: clear, to: count, keys: [] }], null, null]);
58
+ });
59
+ it("gives a text form to read", () => {
60
+ expect(System.describe(frame).text).toBe([
61
+ "tier 0 spawnCells commands spawn describeTest/position",
62
+ "sync spawnCells's commands of describeTest/position, for move",
63
+ " spawnCells's commands of describeTest/position, for deriveCells",
64
+ "tier 1 move writes describeTest/position",
65
+ " resources reads describeTest/speed",
66
+ " after spawnCells (after)",
67
+ "tier 2 deriveCells reads describeTest/position",
68
+ " requires not describeTest/cell",
69
+ " commands add describeTest/cell",
70
+ " after move (after: describeTest/position)",
71
+ "sync deriveCells's commands of describeTest/cell, for pick",
72
+ "tier 3 pick resources writes describeTest/hover",
73
+ " looks up cells through byCell",
74
+ " after deriveCells (after)",
75
+ "readonly",
76
+ " watch reads describeTest/position",
77
+ ].join("\n"));
78
+ });
79
+ });