@adobe/ecs 0.2.1 → 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 (330) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/body-access.d.ts +18 -1
  3. package/dist/implementation/compiler/body-access.js +104 -15
  4. package/dist/implementation/compiler/body-access.js.map +1 -1
  5. package/dist/implementation/compiler/describe.d.ts +1 -1
  6. package/dist/implementation/compiler/describe.js +3 -3
  7. package/dist/implementation/compiler/describe.js.map +1 -1
  8. package/dist/implementation/compiler/estree.d.ts +5 -2
  9. package/dist/implementation/compiler/estree.js +8 -5
  10. package/dist/implementation/compiler/estree.js.map +1 -1
  11. package/dist/implementation/compiler/inferred.js +4 -2
  12. package/dist/implementation/compiler/inferred.js.map +1 -1
  13. package/dist/implementation/compiler/signature.js +1 -1
  14. package/dist/implementation/compiler/transform.js +6 -5
  15. package/dist/implementation/compiler/transform.js.map +1 -1
  16. package/dist/implementation/world/activate.js +14 -2
  17. package/dist/implementation/world/activate.js.map +1 -1
  18. package/dist/implementation/world/buffer.d.ts +7 -0
  19. package/dist/implementation/world/buffer.js +7 -0
  20. package/dist/implementation/world/buffer.js.map +1 -1
  21. package/dist/implementation/world/cascade.d.ts +10 -3
  22. package/dist/implementation/world/cascade.js +48 -23
  23. package/dist/implementation/world/cascade.js.map +1 -1
  24. package/dist/implementation/world/commands.js +86 -2
  25. package/dist/implementation/world/commands.js.map +1 -1
  26. package/dist/implementation/world/create.js +53 -2
  27. package/dist/implementation/world/create.js.map +1 -1
  28. package/dist/implementation/world/entities.d.ts +2 -0
  29. package/dist/implementation/world/entities.js +6 -0
  30. package/dist/implementation/world/entities.js.map +1 -1
  31. package/dist/implementation/world/execute.d.ts +4 -4
  32. package/dist/implementation/world/execute.js +132 -83
  33. package/dist/implementation/world/execute.js.map +1 -1
  34. package/dist/implementation/world/finalizers.d.ts +2 -0
  35. package/dist/implementation/world/finalizers.js +24 -3
  36. package/dist/implementation/world/finalizers.js.map +1 -1
  37. package/dist/implementation/world/join.js +22 -4
  38. package/dist/implementation/world/join.js.map +1 -1
  39. package/dist/implementation/world/register.js +2 -2
  40. package/dist/implementation/world/resource-changes.d.ts +18 -0
  41. package/dist/implementation/world/resource-changes.js +66 -0
  42. package/dist/implementation/world/resource-changes.js.map +1 -0
  43. package/dist/implementation/world/rows.d.ts +1 -1
  44. package/dist/implementation/world/rows.js +23 -13
  45. package/dist/implementation/world/rows.js.map +1 -1
  46. package/dist/implementation/world/run-changes.d.ts +38 -0
  47. package/dist/implementation/world/run-changes.js +138 -0
  48. package/dist/implementation/world/run-changes.js.map +1 -0
  49. package/dist/implementation/world/runner.js +19 -15
  50. package/dist/implementation/world/runner.js.map +1 -1
  51. package/dist/implementation/world/sharing.js +1 -1
  52. package/dist/implementation/world/staging.d.ts +4 -0
  53. package/dist/implementation/world/staging.js +15 -3
  54. package/dist/implementation/world/staging.js.map +1 -1
  55. package/dist/implementation/world/state.d.ts +13 -0
  56. package/dist/implementation/world/state.js +13 -1
  57. package/dist/implementation/world/state.js.map +1 -1
  58. package/dist/implementation/world/synchronize.js +72 -22
  59. package/dist/implementation/world/synchronize.js.map +1 -1
  60. package/dist/implementation/world/tables-read.d.ts +9 -0
  61. package/dist/implementation/world/tables-read.js +72 -0
  62. package/dist/implementation/world/tables-read.js.map +1 -0
  63. package/dist/implementation/world/tables.d.ts +10 -0
  64. package/dist/implementation/world/tables.js +401 -0
  65. package/dist/implementation/world/tables.js.map +1 -0
  66. package/dist/implementation/world/thread-observer.js +1 -1
  67. package/dist/implementation/world/thread-observer.js.map +1 -1
  68. package/dist/implementation/world/thread-systems.js +11 -6
  69. package/dist/implementation/world/thread-systems.js.map +1 -1
  70. package/dist/implementation/world/up.js +7 -1
  71. package/dist/implementation/world/up.js.map +1 -1
  72. package/dist/implementation/world/where.d.ts +8 -0
  73. package/dist/implementation/world/where.js +40 -11
  74. package/dist/implementation/world/where.js.map +1 -1
  75. package/dist/index.d.ts +8 -28
  76. package/dist/index.js +4 -9
  77. package/dist/index.js.map +1 -1
  78. package/dist/interface/component/declarations.d.ts +4 -0
  79. package/dist/interface/component/declarations.js +8 -0
  80. package/dist/interface/component/declarations.js.map +1 -0
  81. package/dist/interface/component/declared.d.ts +12 -0
  82. package/dist/interface/component/declared.js +2 -0
  83. package/dist/interface/component/declared.js.map +1 -0
  84. package/dist/interface/component/index.d.ts +2 -0
  85. package/dist/interface/component/index.js +1 -1
  86. package/dist/interface/component/index.js.map +1 -1
  87. package/dist/interface/finalizer/body.d.ts +18 -3
  88. package/dist/interface/finalizer/create.d.ts +7 -5
  89. package/dist/interface/finalizer/create.js +8 -5
  90. package/dist/interface/finalizer/create.js.map +1 -1
  91. package/dist/interface/finalizer/declaration.d.ts +4 -2
  92. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  93. package/dist/interface/indexes/declaration-of.js +20 -0
  94. package/dist/interface/indexes/declaration-of.js.map +1 -0
  95. package/dist/interface/indexes/index.d.ts +1 -0
  96. package/dist/interface/indexes/index.js +1 -0
  97. package/dist/interface/indexes/index.js.map +1 -1
  98. package/dist/interface/registry/names.d.ts +2 -0
  99. package/dist/interface/registry/names.js +9 -0
  100. package/dist/interface/registry/names.js.map +1 -1
  101. package/dist/interface/registry/schedule.d.ts +31 -4
  102. package/dist/interface/registry/schedule.js +185 -24
  103. package/dist/interface/registry/schedule.js.map +1 -1
  104. package/dist/interface/resource/declarations.d.ts +4 -0
  105. package/dist/interface/resource/declarations.js +8 -0
  106. package/dist/interface/resource/declarations.js.map +1 -0
  107. package/dist/interface/resource/index.d.ts +2 -0
  108. package/dist/interface/resource/index.js +1 -1
  109. package/dist/interface/resource/index.js.map +1 -1
  110. package/dist/interface/system/access.d.ts +15 -0
  111. package/dist/interface/system/body.d.ts +11 -6
  112. package/dist/interface/system/check.d.ts +8 -1
  113. package/dist/interface/system/check.js +42 -12
  114. package/dist/interface/system/check.js.map +1 -1
  115. package/dist/interface/system/commands.d.ts +37 -0
  116. package/dist/interface/system/create-resolved.js +1 -1
  117. package/dist/interface/system/create-resolved.js.map +1 -1
  118. package/dist/interface/system/create.d.ts +31 -15
  119. package/dist/interface/system/create.js +8 -7
  120. package/dist/interface/system/create.js.map +1 -1
  121. package/dist/interface/system/declaration.d.ts +9 -2
  122. package/dist/interface/system/describe.d.ts +120 -0
  123. package/dist/interface/system/describe.js +116 -0
  124. package/dist/interface/system/describe.js.map +1 -0
  125. package/dist/interface/system/index.d.ts +1 -0
  126. package/dist/interface/system/index.js +1 -0
  127. package/dist/interface/system/index.js.map +1 -1
  128. package/dist/interface/system/leaf.d.ts +6 -0
  129. package/dist/interface/system/render.js +1 -0
  130. package/dist/interface/system/render.js.map +1 -1
  131. package/dist/interface/system/resolve.d.ts +5 -1
  132. package/dist/interface/system/resolve.js +19 -1
  133. package/dist/interface/system/resolve.js.map +1 -1
  134. package/dist/interface/system/shared-only.d.ts +7 -0
  135. package/dist/interface/system/shared-only.js +2 -0
  136. package/dist/interface/system/shared-only.js.map +1 -0
  137. package/dist/interface/system/tables.d.ts +28 -0
  138. package/dist/interface/system/tables.js +2 -0
  139. package/dist/interface/system/tables.js.map +1 -0
  140. package/dist/interface/world/index.d.ts +1 -1
  141. package/dist/interface/world/table/index.d.ts +27 -0
  142. package/dist/interface/world/table/index.js +3 -0
  143. package/dist/interface/world/table/index.js.map +1 -0
  144. package/dist/interface/world/table/table.d.ts +11 -0
  145. package/dist/interface/world/table/table.js +2 -0
  146. package/dist/interface/world/table/table.js.map +1 -0
  147. package/dist/interface/world/world.d.ts +25 -10
  148. package/dist/world.d.ts +27 -0
  149. package/dist/world.js +27 -0
  150. package/dist/world.js.map +1 -0
  151. package/package.json +11 -2
  152. package/src/exports.test.ts +1 -1
  153. package/src/guide/00-keys.ts +1 -1
  154. package/src/guide/01-resources.md +1 -1
  155. package/src/guide/01-resources.ts +1 -1
  156. package/src/guide/02-entities.ts +2 -2
  157. package/src/guide/03-systems.md +2 -2
  158. package/src/guide/03-systems.out +1 -1
  159. package/src/guide/03-systems.ts +3 -3
  160. package/src/guide/04-ticks.md +1 -1
  161. package/src/guide/04-ticks.ts +3 -3
  162. package/src/guide/05-scheduling.ts +1 -1
  163. package/src/guide/06-sync-points.md +18 -7
  164. package/src/guide/06-sync-points.out +1 -2
  165. package/src/guide/06-sync-points.ts +10 -6
  166. package/src/guide/07-change-filters.ts +2 -2
  167. package/src/guide/08-input.ts +1 -1
  168. package/src/guide/09-rules.ts +1 -1
  169. package/src/guide/11-traits.ts +3 -3
  170. package/src/guide/12-lookups.ts +2 -2
  171. package/src/guide/13-indexes.ts +4 -4
  172. package/src/guide/14-kernels.ts +1 -1
  173. package/src/guide/15-reductions.ts +1 -1
  174. package/src/guide/16-gpu.ts +1 -1
  175. package/src/guide/17-hierarchies.ts +1 -1
  176. package/src/guide/18-identity.out +1 -1
  177. package/src/guide/19-threads.md +4 -2
  178. package/src/guide/19-threads.ts +2 -2
  179. package/src/guide/20-shared.ts +3 -3
  180. package/src/guide/21-dom.md +2 -2
  181. package/src/guide/21-dom.ts +1 -1
  182. package/src/guide/22-canvas.md +1 -1
  183. package/src/guide/22-canvas.ts +1 -1
  184. package/src/guide/CLAUDE.md +1 -1
  185. package/src/guide/guide.test.ts +9 -154
  186. package/src/implementation/compiler/body-access.ts +102 -12
  187. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  188. package/src/implementation/compiler/describe.test.ts +1 -1
  189. package/src/implementation/compiler/describe.ts +3 -3
  190. package/src/implementation/compiler/estree.ts +9 -6
  191. package/src/implementation/compiler/hygiene.test.ts +1 -1
  192. package/src/implementation/compiler/inferred.test.ts +1 -1
  193. package/src/implementation/compiler/inferred.ts +4 -2
  194. package/src/implementation/compiler/plugin.test.ts +2 -2
  195. package/src/implementation/compiler/signature.ts +1 -1
  196. package/src/implementation/compiler/transform.test.ts +43 -23
  197. package/src/implementation/compiler/transform.ts +6 -4
  198. package/src/implementation/world/activate.ts +11 -2
  199. package/src/implementation/world/batches.test.ts +3 -3
  200. package/src/implementation/world/buffer.ts +7 -0
  201. package/src/implementation/world/cascade.ts +46 -22
  202. package/src/implementation/world/change-blocks.test.ts +1 -1
  203. package/src/implementation/world/commands.ts +76 -2
  204. package/src/implementation/world/create.test.ts +8 -8
  205. package/src/implementation/world/create.ts +57 -2
  206. package/src/implementation/world/defaults.test.ts +1 -1
  207. package/src/implementation/world/despawn-where.test.ts +287 -0
  208. package/src/implementation/world/entities.ts +6 -0
  209. package/src/implementation/world/execute.ts +125 -78
  210. package/src/implementation/world/finalizers.test.ts +1 -1
  211. package/src/implementation/world/finalizers.ts +27 -4
  212. package/src/implementation/world/ids.test.ts +1 -1
  213. package/src/implementation/world/index-changed.test.ts +2 -2
  214. package/src/implementation/world/index-joined.test.ts +5 -5
  215. package/src/implementation/world/index-order.test.ts +1 -1
  216. package/src/implementation/world/indexes.test.ts +2 -1
  217. package/src/implementation/world/join.test.ts +23 -4
  218. package/src/implementation/world/join.ts +26 -4
  219. package/src/implementation/world/joined-observer.test.ts +1 -1
  220. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  221. package/src/implementation/world/joining.fixture.ts +1 -1
  222. package/src/implementation/world/joining.test-helper.ts +1 -1
  223. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  224. package/src/implementation/world/names.test.ts +1 -2
  225. package/src/implementation/world/observe-factories.test.ts +2 -2
  226. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  227. package/src/implementation/world/observe.test-helper.ts +1 -1
  228. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  229. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  230. package/src/implementation/world/observer-contained.test.ts +2 -2
  231. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  232. package/src/implementation/world/observer-joined.test.ts +23 -15
  233. package/src/implementation/world/observer.browser.test.ts +2 -2
  234. package/src/implementation/world/observer.test.ts +8 -8
  235. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  236. package/src/implementation/world/optional.test.ts +7 -7
  237. package/src/implementation/world/perf.md +3 -1
  238. package/src/implementation/world/reclaim.test.ts +3 -3
  239. package/src/implementation/world/register.ts +2 -2
  240. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  241. package/src/implementation/world/resource-changes.test.ts +61 -0
  242. package/src/implementation/world/resource-changes.ts +67 -0
  243. package/src/implementation/world/rows.ts +24 -13
  244. package/src/implementation/world/rules.test.ts +37 -0
  245. package/src/implementation/world/run-changes.test.ts +88 -0
  246. package/src/implementation/world/run-changes.ts +160 -0
  247. package/src/implementation/world/run-results.test.ts +10 -10
  248. package/src/implementation/world/runner.ts +20 -16
  249. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  250. package/src/implementation/world/shared-kernels.test.ts +2 -2
  251. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  252. package/src/implementation/world/shared.test.ts +10 -10
  253. package/src/implementation/world/sharing.ts +1 -1
  254. package/src/implementation/world/staging.ts +16 -3
  255. package/src/implementation/world/state.ts +19 -0
  256. package/src/implementation/world/sync-points.test.ts +135 -0
  257. package/src/implementation/world/synchronize.ts +65 -18
  258. package/src/implementation/world/tables-read.ts +70 -0
  259. package/src/implementation/world/tables.test.ts +369 -0
  260. package/src/implementation/world/tables.ts +368 -0
  261. package/src/implementation/world/tails.test.ts +1 -1
  262. package/src/implementation/world/target.test.ts +2 -2
  263. package/src/implementation/world/thread-observer.ts +1 -1
  264. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  265. package/src/implementation/world/thread-systems.ts +11 -6
  266. package/src/implementation/world/up.test.ts +13 -1
  267. package/src/implementation/world/up.ts +6 -1
  268. package/src/implementation/world/where.test.ts +4 -4
  269. package/src/implementation/world/where.ts +38 -10
  270. package/src/implementation/world/workers.fixture.ts +2 -2
  271. package/src/index.ts +4 -29
  272. package/src/interface/component/declarations.ts +11 -0
  273. package/src/interface/component/declared.ts +9 -0
  274. package/src/interface/component/index.ts +2 -0
  275. package/src/interface/finalizer/body.ts +13 -3
  276. package/src/interface/finalizer/create.test.ts +3 -1
  277. package/src/interface/finalizer/create.ts +12 -7
  278. package/src/interface/finalizer/declaration.ts +4 -2
  279. package/src/interface/index.type-test.ts +2 -2
  280. package/src/interface/indexes/create.test.ts +10 -0
  281. package/src/interface/indexes/declaration-of.ts +34 -0
  282. package/src/interface/indexes/index.ts +1 -0
  283. package/src/interface/indexes/index.type-test.ts +1 -1
  284. package/src/interface/registry/names.ts +8 -0
  285. package/src/interface/registry/schedule.ts +200 -27
  286. package/src/interface/resource/declarations.ts +11 -0
  287. package/src/interface/resource/index.ts +2 -0
  288. package/src/interface/system/access.ts +16 -0
  289. package/src/interface/system/body.ts +7 -5
  290. package/src/interface/system/check.ts +37 -10
  291. package/src/interface/system/commands.ts +33 -0
  292. package/src/interface/system/commands.type-test.ts +31 -0
  293. package/src/interface/system/create-resolved.ts +1 -1
  294. package/src/interface/system/create.test.ts +21 -13
  295. package/src/interface/system/create.ts +24 -15
  296. package/src/interface/system/declaration.ts +9 -2
  297. package/src/interface/system/describe.test.ts +79 -0
  298. package/src/interface/system/describe.ts +122 -0
  299. package/src/interface/system/index.ts +1 -0
  300. package/src/interface/system/leaf.ts +6 -0
  301. package/src/interface/system/lists.type-test.ts +1 -1
  302. package/src/interface/system/optional.type-test.ts +1 -1
  303. package/src/interface/system/render.ts +1 -0
  304. package/src/interface/system/resolve.ts +25 -2
  305. package/src/interface/system/shared-only.ts +5 -0
  306. package/src/interface/system/shared.type-test.ts +1 -1
  307. package/src/interface/system/system.type-test.ts +26 -20
  308. package/src/interface/system/tables.ts +27 -0
  309. package/src/interface/system/up.type-test.ts +3 -3
  310. package/src/interface/world/index.ts +2 -2
  311. package/src/interface/world/run.type-test.ts +14 -4
  312. package/src/interface/world/table/index.ts +20 -0
  313. package/src/interface/world/table/table.ts +12 -0
  314. package/src/interface/world/world.ts +23 -10
  315. package/src/observe/entity.ts +2 -2
  316. package/src/observe/resource.ts +2 -2
  317. package/src/observe/select.ts +4 -4
  318. package/src/observe/world-state.ts +3 -3
  319. package/src/perftest/benchmarks.ts +19 -1
  320. package/src/perftest/indexes.ts +3 -3
  321. package/src/perftest/loop.ts +1 -1
  322. package/src/perftest/observer.ts +4 -4
  323. package/src/perftest/reads.ts +2 -2
  324. package/src/perftest/remote.ts +5 -5
  325. package/src/perftest/strings.ts +1 -1
  326. package/src/perftest/structural.ts +20 -0
  327. package/src/perftest/suite.ts +1 -1
  328. package/src/perftest/tables.ts +107 -0
  329. package/src/world.test.ts +2 -2
  330. package/src/world.ts +32 -0
@@ -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
- const unknown: Used = { query: null, resources: null, lookups: null, up: null };
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
- if (!isFunctionBody(body)) return unknown;
130
- if (body.type === "FunctionExpression" && mentions(body.body, "arguments")) return unknown;
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 unknown;
156
+ if (mentions(body, "eval")) return unknownAt(body, "it calls `eval`");
133
157
  const counts = new Map<string, number>();
134
- declaredIn(body.body, name => counts.set(name, (counts.get(name) ?? 0) + 1));
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 queryKnown = true;
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 !== undefined && rowName === null && !destructure(row, rowNamed, key => query.read.add(key))) queryKnown = false;
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)) queryKnown = false;
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 (use === null || key === null) queryKnown = false;
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)))) queryKnown = false;
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: queryKnown ? query : null,
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
  };
@@ -51,7 +51,7 @@ export const spawn = System.create(({ commands }) => {
51
51
  commands.spawn([position, velocity])({ [position]: [0, 0, 0], [velocity]: [1, 0, 0] });
52
52
  });
53
53
 
54
- export const observe = System.create({ observer: true, components: { with: [position] } }, row => {
54
+ export const observe = System.create({ readonly: true, components: { with: [position] } }, row => {
55
55
  console.log(row[position]);
56
56
  });
57
57
 
@@ -109,7 +109,7 @@ describe("a module's systems described", () => {
109
109
  it("says why systems that run as written have no code: per-run systems, observers, render systems, kernel: false and one declared in a function", () => {
110
110
  expect(described("spawn")).toMatchObject({ kind: "per-run", runs: `Its body as written, on the "world" thread` });
111
111
  for (const backend of ["js", "wasm", "wgsl"] as const) expect(reasonOf(described("spawn")[backend])).toBe("per-run systems run as written; only row systems compile");
112
- expect(described("observe")).toMatchObject({ kind: "observer", js: { kind: "none", reason: expect.stringMatching(/^an observer runs as written/) } });
112
+ expect(described("observe")).toMatchObject({ kind: "readonly", js: { kind: "none", reason: expect.stringMatching(/^a readonly system runs as written/) } });
113
113
  expect(described("draw")).toMatchObject({ kind: "render", runs: expect.stringContaining("while the world has a GPU device"), wgsl: { kind: "none", reason: expect.stringMatching(/^a render system runs as written/) } });
114
114
  const unflagged = described("unflagged");
115
115
  expect(unflagged).toMatchObject({ kernel: false, js: { kind: "none", reason: "as written, because `kernel: false`" }, wasm: { kind: "none", reason: "as written, because `kernel: false`" }, wgsl: { kind: "eligible" } });
@@ -30,7 +30,7 @@ export type Backend =
30
30
  * What runs a system: over every matched row, once per run, as an observer, on one entity its input names (`target`),
31
31
  * or as a render system.
32
32
  */
33
- export type SystemKind = "row" | "per-run" | "observer" | "target" | "render";
33
+ export type SystemKind = "row" | "per-run" | "readonly" | "target" | "render";
34
34
 
35
35
  /**
36
36
  * A system of a module as the build compiles it: its call's offsets in the code transformed, the binding it's
@@ -427,7 +427,7 @@ export const describeSystems = async (sites: TransformedSites, format: (code: st
427
427
  const kernel: boolean | "gpu" | null = literal === true || literal === false ? literal : literal === "gpu" ? "gpu" : null;
428
428
  const kind: SystemKind = created === "render" ? "render"
429
429
  : declaration === undefined ? "per-run"
430
- : propertyOf(declaration, "observer") !== null ? "observer"
430
+ : propertyOf(declaration, "readonly") !== null ? "readonly"
431
431
  : propertyOf(declaration, "target") !== null ? "target"
432
432
  : propertyOf(declaration, "components") !== null ? "row" : "per-run";
433
433
  const base = { start: node.start, end: node.end, binding: bindings.get(node) ?? null, kind, kernel };
@@ -442,7 +442,7 @@ export const describeSystems = async (sites: TransformedSites, format: (code: st
442
442
  ? threadOf(declaration, body, kind === "render" ? positionsOf("render", declaration) : positionsOf("system", declaration)) : "world";
443
443
  if (kind === "render") return asWritten("a render system runs as written, recording GPU work with shaders of your own; `Gpu.wgsl` gives them their accessors", "world");
444
444
  if (kind === "per-run") return asWritten("per-run systems run as written; only row systems compile", written());
445
- if (kind === "observer") return asWritten("an observer runs as written, reading the settled state after the run's last sync point; only row systems that aren't observers compile", written());
445
+ if (kind === "readonly") return asWritten("a readonly system runs as written, reading the settled state after the run's last sync point; only row systems that aren't readonly compile", written());
446
446
  if (kind === "target") return asWritten("a system with `target` runs as written, once, on the entity its input names; only systems over every matched row compile", written());
447
447
  if (declaration === undefined || body === undefined || body.type === "SpreadElement" || !isFunction(body)) {
448
448
  return asWritten(declaration === undefined ? "its declaration isn't written in its call, so the build can't read whether it's a kernel" : "its body isn't a function written in place or bound at module scope, so the build can't read it", "world");
@@ -47,18 +47,21 @@ export const patternNames = (pattern: Pattern): readonly string[] => {
47
47
  }
48
48
  };
49
49
 
50
- /** Calls `declare` for each name declared anywhere inside `node`, once per declaration: variables, parameters, functions, classes and catch parameters. */
51
- export const declaredIn = (node: Node, declare: (name: string) => void): void => {
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
  };
@@ -34,7 +34,7 @@ const spawn = System.create(({ commands }) => {
34
34
  });
35
35
 
36
36
  const read: number[][] = [];
37
- const readAll = System.create({ observer: true, components: { with: [p, c, l, s, e, v0, v1] } }, row => {
37
+ const readAll = System.create({ readonly: true, components: { with: [p, c, l, s, e, v0, v1] } }, row => {
38
38
  read.push([row[p], row[c], row[l], row[s], row[e], row[v0], row[v1]]);
39
39
  });
40
40
 
@@ -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
- return `{ components: ${components}, resources: ${resourceText}, lookup: ${lookupText}, up: ${up === null ? "null" : list(up)}${whereText}, at: ${JSON.stringify(at)} }`;
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. */
@@ -645,7 +645,7 @@ export { World as OtherWorld };
645
645
  export const runOther = async (key) => {
646
646
  const spawn = System.create(({ commands }) => { commands.spawn([key].slice())({ [key]: 10 }); });
647
647
  let total = 0;
648
- const count = System.create({ observer: true, components: { with: [key] } }, (row) => { total += row[key]; });
648
+ const count = System.create({ readonly: true, components: { with: [key] } }, (row) => { total += row[key]; });
649
649
  const world = World.create();
650
650
  await world.run(spawn);
651
651
  await world.run(spawn);
@@ -662,7 +662,7 @@ declare$(score, "component", { type: { type: "number" } });
662
662
  ${recorder}
663
663
  const spawn = System.create(({ commands }) => { commands.spawn([score].slice())({ [score]: 1 }); });
664
664
  let counted = 0;
665
- const count = System.create({ observer: true, components: { with: [score] } }, (row) => { counted += row[score]; });
665
+ const count = System.create({ readonly: true, components: { with: [score] } }, (row) => { counted += row[score]; });
666
666
  export const copies = World === OtherWorld ? 1 : 2;
667
667
  export const run = async () => {
668
668
  const world = World.create();
@@ -120,7 +120,7 @@ export const kernelOf = (declaration: ObjectExpression): KernelRequest | null =>
120
120
  }
121
121
  }
122
122
  if (declaration.properties.some(property => property.type === "SpreadElement")) return null;
123
- if (!declares(declaration, "components") || declares(declaration, "observer") || declares(declaration, "target")) return null;
123
+ if (!declares(declaration, "components") || declares(declaration, "readonly") || declares(declaration, "target")) return null;
124
124
  return { target: "cpu", required: false };
125
125
  };
126
126
 
@@ -335,7 +335,7 @@ describe("kernels by default", () => {
335
335
  test("runs as written, without failing, a body that leaves the subset or a declaration a kernel can't have", async () => {
336
336
  expect(await attached(`const log = []; const kernel = System.create({ components: { with: [mass] } }, row => { log.push(row[mass]); });`)).toBe(false);
337
337
  expect(await attached(`const kernel = System.create({ components: { with: [mass] } }, (row, { resources }) => { resources[delta] = row[mass]; });`)).toBe(false);
338
- expect(await attached(`const kernel = System.create({ observer: true, components: { with: [mass] } }, row => { void row[mass]; });`)).toBe(false);
338
+ expect(await attached(`const kernel = System.create({ readonly: true, components: { with: [mass] } }, row => { void row[mass]; });`)).toBe(false);
339
339
  expect(await attached(`const kernel = System.create({ target: "entity", components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
340
340
  expect(await attached(`const make = () => System.create({ components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
341
341
  });
@@ -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 as unknown, which the world takes as every member written, with a warning", async () => {
1416
- const result = await transform(system("components: { with: [position] }", "(row) => { helper(row); }"));
1417
- expect(result !== null && "code" in result ? [result.code.includes("components: null"), result.warnings.length] : null).toEqual([true, 1]);
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("an observer's row it can't follow as unknown without a warning, since the world takes an observer's as read", async () => {
1421
- const result = await transform(system("observer: true, components: { with: [position] }", "(row) => { helper(row); }"));
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 () => {
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 row it can't follow with a warning unless the declaration says observer: true, as the world reads it", async () => {
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("components: null");
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 () => {
@@ -141,7 +141,7 @@ export const functionOf = (binding: ModuleBinding | undefined): Node | undefined
141
141
  const empty: ObjectExpression = { type: "ObjectExpression", properties: [], start: 0, end: 0 };
142
142
 
143
143
  /** Whether a declaration has property `name`, written out. */
144
- /** Whether a declaration gives `name` the literal `true`, as the world takes `observer`. */
144
+ /** Whether a declaration gives `name` the literal `true`, as the world takes `readonly`. */
145
145
  const declaresTrue = (declaration: ObjectExpression, name: string): boolean => declaration.properties.some(property =>
146
146
  property.type === "Property" && !property.computed && property.key.type === "Identifier" && property.key.name === name
147
147
  && property.value.type === "Literal" && property.value.value === true);
@@ -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
- // An observer's row, and a lookup's rows, it can't follow are taken as read whole, which is what their
317
- // declarations already say; only a row taken as written whole costs what the author should know of.
318
- if (used.query === null && !declaresTrue(declared, "observer")) warnings.push(`${where}: the build can't follow how this body uses its row, so it takes every component in with, added and changed as written: index the row by key directly, or destructure it by key, to let it tell what the body reads and writes.`);
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
- const resourcesOf = (finalizer: Finalizer): readonly Key[] => [...finalizer.declaration.resources?.read ?? [], ...finalizer.declaration.resources?.write ?? []];
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
  };
@@ -29,7 +29,7 @@ declare module "@adobe/ecs" {
29
29
  }
30
30
 
31
31
  describe("staged spawns", () => {
32
- it("are seen once by `added` observers in earlier, the same and later tiers, and not before the sync point", async () => {
32
+ it("are seen once by `added` observers in earlier, the same and later tiers, from the sync point after them", async () => {
33
33
  const seen = { early: 0, same: 0, late: 0 };
34
34
  const spawned: Entity[] = [];
35
35
  const early = System.create({ components: { added: [score] } }, () => { seen.early++; });
@@ -42,8 +42,8 @@ describe("staged spawns", () => {
42
42
  const frame = System.create({ systems: [early, spawn, same, late] });
43
43
  const world = createWorld({ [workers]: 0 });
44
44
  await world.run(frame);
45
- // Nothing saw the spawns during their own run.
46
- expect(seen).toEqual({ early: 0, same: 0, late: 0 });
45
+ // Only what follows the spawn saw them in their own run, after the sync point it needs.
46
+ expect(seen).toEqual({ early: 0, same: 0, late: 100 });
47
47
  await world.run(frame);
48
48
  await world.run(frame);
49
49
  expect(seen).toEqual({ early: 100, same: 100, late: 100 });
@@ -7,6 +7,13 @@ export const spawnCommand = 0;
7
7
  export const despawnCommand = 1;
8
8
  export const addCommand = 2;
9
9
  export const removeCommand = 3;
10
+ /**
11
+ * Names the ids of its query's `with`, then of its `not`, and with a `where`, last, `despawnWhereHolds`, whose value is
12
+ * whether an archetype holds the `where`'s values; its entity is how many are of `with`.
13
+ */
14
+ export const despawnWhereCommand = 4;
15
+ /** The id a `despawnWhere` names its `where` by, which no component has. */
16
+ export const despawnWhereHolds = -1;
10
17
 
11
18
  /** Rows `start` to `end` of a kernel's spawn batch, committed in its buffer's place in merge order. */
12
19
  export type Staged = { batch: SpawnBatch; start: number; end: number };