@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.
- package/README.md +2 -2
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/describe.d.ts +1 -1
- package/dist/implementation/compiler/describe.js +3 -3
- package/dist/implementation/compiler/describe.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +4 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +6 -5
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/activate.js +14 -2
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +86 -2
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/entities.d.ts +2 -0
- package/dist/implementation/world/entities.js +6 -0
- package/dist/implementation/world/entities.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +22 -4
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.d.ts +1 -1
- package/dist/implementation/world/rows.js +23 -13
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +19 -15
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/sharing.js +1 -1
- package/dist/implementation/world/staging.d.ts +4 -0
- package/dist/implementation/world/staging.js +15 -3
- package/dist/implementation/world/staging.js.map +1 -1
- package/dist/implementation/world/state.d.ts +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +9 -0
- package/dist/implementation/world/tables-read.js +72 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +10 -0
- package/dist/implementation/world/tables.js +401 -0
- package/dist/implementation/world/tables.js.map +1 -0
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +11 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/index.d.ts +8 -28
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/interface/component/declarations.d.ts +4 -0
- package/dist/interface/component/declarations.js +8 -0
- package/dist/interface/component/declarations.js.map +1 -0
- package/dist/interface/component/declared.d.ts +12 -0
- package/dist/interface/component/declared.js +2 -0
- package/dist/interface/component/declared.js.map +1 -0
- package/dist/interface/component/index.d.ts +2 -0
- package/dist/interface/component/index.js +1 -1
- package/dist/interface/component/index.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +2 -0
- package/dist/interface/registry/names.js +9 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +31 -4
- package/dist/interface/registry/schedule.js +185 -24
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/resource/declarations.d.ts +4 -0
- package/dist/interface/resource/declarations.js +8 -0
- package/dist/interface/resource/declarations.js.map +1 -0
- package/dist/interface/resource/index.d.ts +2 -0
- package/dist/interface/resource/index.js +1 -1
- package/dist/interface/resource/index.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/body.d.ts +11 -6
- package/dist/interface/system/check.d.ts +8 -1
- package/dist/interface/system/check.js +42 -12
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +37 -0
- package/dist/interface/system/create-resolved.js +1 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +31 -15
- package/dist/interface/system/create.js +8 -7
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +28 -0
- package/dist/interface/system/tables.js +2 -0
- package/dist/interface/system/tables.js.map +1 -0
- package/dist/interface/world/index.d.ts +1 -1
- package/dist/interface/world/table/index.d.ts +27 -0
- package/dist/interface/world/table/index.js +3 -0
- package/dist/interface/world/table/index.js.map +1 -0
- package/dist/interface/world/table/table.d.ts +11 -0
- package/dist/interface/world/table/table.js +2 -0
- package/dist/interface/world/table/table.js.map +1 -0
- package/dist/interface/world/world.d.ts +25 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +11 -2
- package/src/exports.test.ts +1 -1
- package/src/guide/00-keys.ts +1 -1
- package/src/guide/01-resources.md +1 -1
- package/src/guide/01-resources.ts +1 -1
- package/src/guide/02-entities.ts +2 -2
- package/src/guide/03-systems.md +2 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/03-systems.ts +3 -3
- package/src/guide/04-ticks.md +1 -1
- package/src/guide/04-ticks.ts +3 -3
- package/src/guide/05-scheduling.ts +1 -1
- package/src/guide/06-sync-points.md +18 -7
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +10 -6
- package/src/guide/07-change-filters.ts +2 -2
- package/src/guide/08-input.ts +1 -1
- package/src/guide/09-rules.ts +1 -1
- package/src/guide/11-traits.ts +3 -3
- package/src/guide/12-lookups.ts +2 -2
- package/src/guide/13-indexes.ts +4 -4
- package/src/guide/14-kernels.ts +1 -1
- package/src/guide/15-reductions.ts +1 -1
- package/src/guide/16-gpu.ts +1 -1
- package/src/guide/17-hierarchies.ts +1 -1
- package/src/guide/18-identity.out +1 -1
- package/src/guide/19-threads.md +4 -2
- package/src/guide/19-threads.ts +2 -2
- package/src/guide/20-shared.ts +3 -3
- package/src/guide/21-dom.md +2 -2
- package/src/guide/21-dom.ts +1 -1
- package/src/guide/22-canvas.md +1 -1
- package/src/guide/22-canvas.ts +1 -1
- package/src/guide/CLAUDE.md +1 -1
- package/src/guide/guide.test.ts +9 -154
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/describe.test-helper.ts +1 -1
- package/src/implementation/compiler/describe.test.ts +1 -1
- package/src/implementation/compiler/describe.ts +3 -3
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/hygiene.test.ts +1 -1
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +43 -23
- package/src/implementation/compiler/transform.ts +6 -4
- package/src/implementation/world/activate.ts +11 -2
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +76 -2
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.test.ts +1 -1
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/index-changed.test.ts +2 -2
- package/src/implementation/world/index-joined.test.ts +5 -5
- package/src/implementation/world/index-order.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +23 -4
- package/src/implementation/world/join.ts +26 -4
- package/src/implementation/world/joined-observer.test.ts +1 -1
- package/src/implementation/world/joining-worker.fixture.ts +1 -1
- package/src/implementation/world/joining.fixture.ts +1 -1
- package/src/implementation/world/joining.test-helper.ts +1 -1
- package/src/implementation/world/kernel-spawns.test.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/observe-factories.test.ts +2 -2
- package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
- package/src/implementation/world/observe.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
- package/src/implementation/world/observer-contained.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained.test.ts +2 -2
- package/src/implementation/world/observer-joined.test-helper.ts +4 -4
- package/src/implementation/world/observer-joined.test.ts +23 -15
- package/src/implementation/world/observer.browser.test.ts +2 -2
- package/src/implementation/world/observer.test.ts +8 -8
- package/src/implementation/world/optional-workers.fixture.ts +1 -1
- package/src/implementation/world/optional.test.ts +7 -7
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/reclaim.test.ts +3 -3
- package/src/implementation/world/register.ts +2 -2
- package/src/implementation/world/remote-objects.test-helper.ts +1 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +24 -13
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/run-results.test.ts +10 -10
- package/src/implementation/world/runner.ts +20 -16
- package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
- package/src/implementation/world/shared-kernels.test.ts +2 -2
- package/src/implementation/world/shared-workers.fixture.ts +2 -2
- package/src/implementation/world/shared.test.ts +10 -10
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +70 -0
- package/src/implementation/world/tables.test.ts +369 -0
- package/src/implementation/world/tables.ts +368 -0
- package/src/implementation/world/tails.test.ts +1 -1
- package/src/implementation/world/target.test.ts +2 -2
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.test-helper.ts +3 -3
- package/src/implementation/world/thread-systems.ts +11 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.test.ts +4 -4
- package/src/implementation/world/where.ts +38 -10
- package/src/implementation/world/workers.fixture.ts +2 -2
- package/src/index.ts +4 -29
- package/src/interface/component/declarations.ts +11 -0
- package/src/interface/component/declared.ts +9 -0
- package/src/interface/component/index.ts +2 -0
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- package/src/interface/index.type-test.ts +2 -2
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +200 -27
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -0
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/body.ts +7 -5
- package/src/interface/system/check.ts +37 -10
- package/src/interface/system/commands.ts +33 -0
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +21 -13
- package/src/interface/system/create.ts +24 -15
- package/src/interface/system/declaration.ts +9 -2
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/shared.type-test.ts +1 -1
- package/src/interface/system/system.type-test.ts +26 -20
- package/src/interface/system/tables.ts +27 -0
- package/src/interface/system/up.type-test.ts +3 -3
- package/src/interface/world/index.ts +2 -2
- package/src/interface/world/run.type-test.ts +14 -4
- package/src/interface/world/table/index.ts +20 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +23 -10
- package/src/observe/entity.ts +2 -2
- package/src/observe/resource.ts +2 -2
- package/src/observe/select.ts +4 -4
- package/src/observe/world-state.ts +3 -3
- package/src/perftest/benchmarks.ts +19 -1
- package/src/perftest/indexes.ts +3 -3
- package/src/perftest/loop.ts +1 -1
- package/src/perftest/observer.ts +4 -4
- package/src/perftest/reads.ts +2 -2
- package/src/perftest/remote.ts +5 -5
- package/src/perftest/strings.ts +1 -1
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/suite.ts +1 -1
- package/src/perftest/tables.ts +107 -0
- package/src/world.test.ts +2 -2
- 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
|
-
|
|
26
|
+
type Commanded = { readonly spawn: ReadonlySet<string>; readonly add: ReadonlySet<string>; readonly remove: ReadonlySet<string>; readonly despawn: boolean };
|
|
27
|
+
|
|
28
|
+
const unknownAt = (at: Node, why: string): Used => ({ query: null, rowLost: { at, why }, resources: null, lookups: null, up: null, commands: null });
|
|
20
29
|
|
|
21
30
|
/** Whether `node`, a child of `parent`, names a property or label, or declares a binding, rather than referring to one. */
|
|
22
31
|
const isName = (node: Node, parent: Node | undefined): boolean => {
|
|
@@ -115,6 +124,13 @@ const indexLookupNames = (declaration: ObjectExpression): ReadonlySet<string> =>
|
|
|
115
124
|
|
|
116
125
|
type Group = "resources" | "lookup" | "reduce" | "up";
|
|
117
126
|
|
|
127
|
+
/** Whether a declaration's `components` says `whole: true`: its body uses its row whole, on purpose. */
|
|
128
|
+
const takesRowWhole = (declaration: ObjectExpression): boolean => declaration.properties.some(property =>
|
|
129
|
+
property.type === "Property" && !property.computed && property.key.type === "Identifier" && property.key.name === "components"
|
|
130
|
+
&& property.value.type === "ObjectExpression" && property.value.properties.some(member =>
|
|
131
|
+
member.type === "Property" && !member.computed && member.key.type === "Identifier" && member.key.name === "whole"
|
|
132
|
+
&& member.value.type === "Literal" && member.value.value === true));
|
|
133
|
+
|
|
118
134
|
const isGroup = (name: string): name is Group => name === "resources" || name === "lookup" || name === "reduce" || name === "up";
|
|
119
135
|
|
|
120
136
|
/**
|
|
@@ -123,15 +139,27 @@ const isGroup = (name: string): name is Group => name === "resources" || name ==
|
|
|
123
139
|
* context's `resources`, `lookup` and `reduce` are indexed only by module-scope keys the body doesn't shadow, or
|
|
124
140
|
* destructured by them, and never passed on, aliased or spread. A lookup's results are followed where each is indexed
|
|
125
141
|
* by such keys or tested, directly or through a `const` declared once; a lookup through an index's lists where they're
|
|
126
|
-
* read for `length` and `changed`, and their rows from `at` as a lookup's results are.
|
|
142
|
+
* read for `length` and `changed`, and their rows from `at` as a lookup's results are. A row whose declaration takes it
|
|
143
|
+
* `whole` isn't followed.
|
|
127
144
|
*/
|
|
128
145
|
export const usedBy = (declaration: ObjectExpression, body: Node, module: ModuleScope, positions: { readonly row: number; readonly context: number }): Used => {
|
|
129
|
-
|
|
130
|
-
if (
|
|
146
|
+
const used = followedBy(declaration, body, module, positions);
|
|
147
|
+
if (takesRowWhole(declaration)) return { query: null, resources: used.resources, lookups: used.lookups, up: used.up, commands: used.commands };
|
|
148
|
+
// A body without a row loses none.
|
|
149
|
+
return positions.row < 0 && used.rowLost !== undefined ? { query: used.query, resources: used.resources, lookups: used.lookups, up: used.up, commands: used.commands } : used;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const followedBy = (declaration: ObjectExpression, body: Node, module: ModuleScope, positions: { readonly row: number; readonly context: number }): Used => {
|
|
153
|
+
if (!isFunctionBody(body)) return unknownAt(body, "its body isn't a function written in place");
|
|
154
|
+
if (body.type === "FunctionExpression" && mentions(body.body, "arguments")) return unknownAt(body, "it reads `arguments`");
|
|
131
155
|
// Code `eval` runs can use anything.
|
|
132
|
-
if (mentions(body, "eval")) return
|
|
156
|
+
if (mentions(body, "eval")) return unknownAt(body, "it calls `eval`");
|
|
133
157
|
const counts = new Map<string, number>();
|
|
134
|
-
|
|
158
|
+
const declaredAt = new Map<string, Node>();
|
|
159
|
+
declaredIn(body.body, (name, at) => {
|
|
160
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
161
|
+
if (!declaredAt.has(name)) declaredAt.set(name, at);
|
|
162
|
+
});
|
|
135
163
|
const parameters = new Set(body.params.flatMap(parameter => patternNames(parameter)));
|
|
136
164
|
const isKey = (name: string): boolean => module.has(name) && !counts.has(name) && !parameters.has(name);
|
|
137
165
|
/** The key `node` names: a module-scope binding, or a namespace's member (`Shapes.circle`), as its source; null for any other. */
|
|
@@ -150,9 +178,15 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
150
178
|
const up = new Set<string>();
|
|
151
179
|
let upKnown = true;
|
|
152
180
|
const lookupReads = new Map<string, Set<string> | null>((lookups ?? []).map(name => [name, new Set<string>()]));
|
|
153
|
-
let
|
|
181
|
+
let rowLost: { readonly at: Node; readonly why: string } | undefined;
|
|
182
|
+
/** Stops following the row, at the first use it can't. */
|
|
183
|
+
const loseRow = (at: Node, why: string): void => { rowLost ??= { at, why }; };
|
|
154
184
|
let resourcesKnown = true;
|
|
155
185
|
let lookupsKnown = lookups !== null;
|
|
186
|
+
const commands = { spawn: new Set<string>(), add: new Set<string>(), remove: new Set<string>(), despawn: false };
|
|
187
|
+
let commandsKnown = true;
|
|
188
|
+
/** Names bound to the context's commands. */
|
|
189
|
+
const commandNames = new Set<string>();
|
|
156
190
|
const unknownLookup = (lookup: string): void => { lookupReads.set(lookup, null); };
|
|
157
191
|
const unknownGroup = (group: Group): void => {
|
|
158
192
|
if (group === "lookup") lookupsKnown = false;
|
|
@@ -206,6 +240,11 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
206
240
|
};
|
|
207
241
|
/** Binds the context's groups from a destructuring of it; `declaredOnce` says whether a bound name is safe to follow. */
|
|
208
242
|
const contextNamed = (declaredOnce: (name: string) => boolean) => (property: string, value: Pattern): boolean => {
|
|
243
|
+
if (property === "commands") {
|
|
244
|
+
if (value.type === "Identifier" && declaredOnce(value.name)) commandNames.add(value.name);
|
|
245
|
+
else commandsKnown = false;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
209
248
|
if (!isGroup(property)) return true;
|
|
210
249
|
if (value.type === "Identifier" && declaredOnce(value.name)) groups.set(value.name, property);
|
|
211
250
|
else if (value.type === "ObjectPattern") destructureGroup(property, value);
|
|
@@ -216,13 +255,15 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
216
255
|
resourcesKnown = false;
|
|
217
256
|
lookupsKnown = false;
|
|
218
257
|
upKnown = false;
|
|
258
|
+
commandsKnown = false;
|
|
219
259
|
};
|
|
220
260
|
|
|
221
261
|
const row = positions.row < 0 ? undefined : body.params[positions.row];
|
|
222
262
|
const context = positions.context < 0 ? undefined : body.params[positions.context];
|
|
223
263
|
const rowName = row?.type === "Identifier" && !counts.has(row.name) ? row.name : null;
|
|
224
264
|
const contextName = context?.type === "Identifier" && !counts.has(context.name) ? context.name : null;
|
|
225
|
-
if (row
|
|
265
|
+
if (row?.type === "Identifier" && rowName === null) loseRow(declaredAt.get(row.name) ?? row, `it declares another \`${row.name}\` here, which hides its row: rename one`);
|
|
266
|
+
else if (row !== undefined && rowName === null && !destructure(row, rowNamed, key => query.read.add(key))) loseRow(row, "it destructures its row by something other than its keys and `entity`");
|
|
226
267
|
if (context !== undefined && contextName === null && !destructure(context, contextNamed(name => !counts.has(name)), () => unknownContext())) unknownContext();
|
|
227
268
|
|
|
228
269
|
/** A lookup's row `value`, a child of `ancestors[at]`: indexed by a key, read for `entity`, bound to a `const`, or tested. */
|
|
@@ -284,6 +325,44 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
284
325
|
const lookup = list.type === "Identifier" ? results.get(list.name) : calledLookup(list);
|
|
285
326
|
return lookup !== undefined && indexed.has(lookup) ? lookup : undefined;
|
|
286
327
|
};
|
|
328
|
+
/** Whether `node` is `context.commands`. */
|
|
329
|
+
const isContextCommands = (node: Node): boolean => node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier"
|
|
330
|
+
&& node.object.name === contextName && node.property.type === "Identifier" && node.property.name === "commands";
|
|
331
|
+
/** The keys and lists of keys a command's argument names: a key or list by binding, or an array literal of them. */
|
|
332
|
+
const listOf = (node: Node | undefined): readonly string[] | null => {
|
|
333
|
+
if (node === undefined) return null;
|
|
334
|
+
const key = keyOf(node);
|
|
335
|
+
if (key !== null) return [key];
|
|
336
|
+
if (node.type !== "ArrayExpression") return null;
|
|
337
|
+
const names: string[] = [];
|
|
338
|
+
for (const element of node.elements) {
|
|
339
|
+
const named = element === null ? null : keyOf(element.type === "SpreadElement" ? element.argument : element);
|
|
340
|
+
if (named === null) return null;
|
|
341
|
+
names.push(named);
|
|
342
|
+
}
|
|
343
|
+
return names;
|
|
344
|
+
};
|
|
345
|
+
/** A use of the commands at `node`, a child of `ancestors[at]`: a call of one of them, whose keys are listed. */
|
|
346
|
+
const useCommands = (node: Node, ancestors: readonly Node[], at: number): void => {
|
|
347
|
+
const parent = ancestors[at];
|
|
348
|
+
const call = ancestors[at - 1];
|
|
349
|
+
if (parent?.type !== "MemberExpression" || parent.object !== node || parent.computed || parent.property.type !== "Identifier"
|
|
350
|
+
|| call?.type !== "CallExpression" || call.callee !== parent) {
|
|
351
|
+
commandsKnown = false;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const command = parent.property.name;
|
|
355
|
+
if (command === "despawn" || command === "despawnWhere") {
|
|
356
|
+
commands.despawn = true;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const listed = command === "spawn" ? listOf(call.arguments[0]) : command === "add" || command === "remove" ? listOf(call.arguments[1]) : null;
|
|
360
|
+
if (listed === null || (command !== "spawn" && command !== "add" && command !== "remove")) {
|
|
361
|
+
commandsKnown = false;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
for (const name of listed) commands[command].add(name);
|
|
365
|
+
};
|
|
287
366
|
/** Finds groups and lookups bound by `const` declarations declared once, and `const`s holding a lookup's result, before any reference is classified. */
|
|
288
367
|
const collect = (node: Node, kind: string | undefined): void => {
|
|
289
368
|
if (node.type === "VariableDeclarator" && kind === "const" && node.init) {
|
|
@@ -298,6 +377,10 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
298
377
|
groups.set(node.id.name, group);
|
|
299
378
|
followed.add(node.init);
|
|
300
379
|
}
|
|
380
|
+
if (isContextCommands(node.init)) {
|
|
381
|
+
commandNames.add(node.id.name);
|
|
382
|
+
followed.add(node.init);
|
|
383
|
+
}
|
|
301
384
|
}
|
|
302
385
|
else if (node.init.type === "Identifier" && node.init.name === contextName) {
|
|
303
386
|
if (destructure(node.id, contextNamed(once), () => unknownContext())) followed.add(node.init);
|
|
@@ -354,31 +437,36 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
354
437
|
if (parent?.type === "MemberExpression" && parent.object === node) {
|
|
355
438
|
const { property } = parent;
|
|
356
439
|
if (!parent.computed) {
|
|
357
|
-
if (property.type !== "Identifier" || !rowNamed(property.name))
|
|
440
|
+
if (property.type !== "Identifier" || !rowNamed(property.name)) loseRow(parent, "it reads a member of its row by name, and a row names only `entity`");
|
|
358
441
|
return;
|
|
359
442
|
}
|
|
360
443
|
const key = keyOf(property);
|
|
361
444
|
// A part of a value written, `row[key][0] = …`, writes it, which the compiler rejects as it must.
|
|
362
445
|
const use = key === null ? null : deepUseOf(parent, ancestors, ancestors.length - 2);
|
|
363
|
-
if (
|
|
446
|
+
if (key === null) loseRow(parent, "it indexes its row by something other than a module-scope key");
|
|
447
|
+
else if (use === null) loseRow(parent, "it deletes a member of its row");
|
|
364
448
|
else {
|
|
365
449
|
if (use.read) query.read.add(key);
|
|
366
450
|
if (use.write) query.write.add(key);
|
|
367
451
|
if (use.mutate) query.mutate.add(key);
|
|
368
452
|
}
|
|
369
453
|
}
|
|
370
|
-
else if (!(parent?.type === "VariableDeclarator" && parent.init === node && destructure(parent.id, rowNamed, key => query.read.add(key))))
|
|
454
|
+
else if (!(parent?.type === "VariableDeclarator" && parent.init === node && destructure(parent.id, rowNamed, key => query.read.add(key)))) {
|
|
455
|
+
loseRow(parent ?? node, "it passes its row on, aliases or spreads it");
|
|
456
|
+
}
|
|
371
457
|
}
|
|
372
458
|
else if (name === contextName) {
|
|
373
459
|
if (parent?.type === "MemberExpression" && parent.object === node && !parent.computed && parent.property.type === "Identifier") {
|
|
374
460
|
const property = parent.property.name;
|
|
375
461
|
if (isGroup(property)) useGroup(property, parent, ancestors, ancestors.length - 2);
|
|
462
|
+
else if (property === "commands" && !followed.has(parent)) useCommands(parent, ancestors, ancestors.length - 2);
|
|
376
463
|
return;
|
|
377
464
|
}
|
|
378
465
|
if (!followed.has(node)) unknownContext();
|
|
379
466
|
}
|
|
380
467
|
else {
|
|
381
468
|
const at = ancestors.length - 1;
|
|
469
|
+
if (commandNames.has(name)) return useCommands(node, ancestors, at);
|
|
382
470
|
const group = groups.get(name);
|
|
383
471
|
if (group !== undefined) return useGroup(group, node, ancestors, at);
|
|
384
472
|
const caller = callers.get(name);
|
|
@@ -426,9 +514,11 @@ export const usedBy = (declaration: ObjectExpression, body: Node, module: Module
|
|
|
426
514
|
for (const parameter of body.params) visitPattern(parameter);
|
|
427
515
|
visit(body.body);
|
|
428
516
|
return {
|
|
429
|
-
query:
|
|
517
|
+
query: rowLost === undefined ? query : null,
|
|
518
|
+
...rowLost === undefined ? {} : { rowLost },
|
|
430
519
|
resources: resourcesKnown ? resources : null,
|
|
431
520
|
lookups: lookupsKnown ? lookupReads : null,
|
|
432
521
|
up: upKnown ? up : null,
|
|
522
|
+
commands: commandsKnown ? commands : null,
|
|
433
523
|
};
|
|
434
524
|
};
|
|
@@ -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({
|
|
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: "
|
|
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" | "
|
|
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, "
|
|
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 === "
|
|
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
|
-
/**
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Calls `declare` for each name declared anywhere inside `node`, once per declaration, with the pattern or name that
|
|
52
|
+
* declares it: variables, parameters, functions, classes and catch parameters.
|
|
53
|
+
*/
|
|
54
|
+
export const declaredIn = (node: Node, declare: (name: string, at: Node) => void): void => {
|
|
52
55
|
switch (node.type) {
|
|
53
|
-
case "VariableDeclarator": for (const name of patternNames(node.id)) declare(name); break;
|
|
56
|
+
case "VariableDeclarator": for (const name of patternNames(node.id)) declare(name, node.id); break;
|
|
54
57
|
case "FunctionDeclaration": case "FunctionExpression": case "ClassDeclaration": case "ClassExpression":
|
|
55
|
-
if (node.id) declare(node.id.name);
|
|
58
|
+
if (node.id) declare(node.id.name, node.id);
|
|
56
59
|
break;
|
|
57
|
-
case "CatchClause": if (node.param) for (const name of patternNames(node.param)) declare(name); break;
|
|
60
|
+
case "CatchClause": if (node.param) for (const name of patternNames(node.param)) declare(name, node.param); break;
|
|
58
61
|
default: break;
|
|
59
62
|
}
|
|
60
63
|
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") {
|
|
61
|
-
for (const param of node.params) for (const name of patternNames(param)) declare(name);
|
|
64
|
+
for (const param of node.params) for (const name of patternNames(param)) declare(name, param);
|
|
62
65
|
}
|
|
63
66
|
for (const child of children(node)) declaredIn(child, declare);
|
|
64
67
|
};
|
|
@@ -34,7 +34,7 @@ const spawn = System.create(({ commands }) => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const read: number[][] = [];
|
|
37
|
-
const readAll = System.create({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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, "
|
|
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({
|
|
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
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1415
|
+
test("fails at the use of a system's row it can't follow, saying why, unless its declaration takes the row whole", async () => {
|
|
1416
|
+
const failure = async (source: string) => {
|
|
1417
|
+
const result = await transform(source);
|
|
1418
|
+
return result !== null && "failure" in result ? result.failure : null;
|
|
1419
|
+
};
|
|
1420
|
+
const passed = await failure(system("components: { with: [position] }", "(row) => { helper(row); }"));
|
|
1421
|
+
expect(passed?.message).toMatch(/^the build can't follow how this body uses its row, since it passes its row on, aliases or spreads it, so it would take every component in with, added and changed as written: .*components: \{ whole: true \}/);
|
|
1422
|
+
expect(passed?.position).toBe(header.length + "const system = System.create({ components: { with: [position] } }, (row) => { ".length);
|
|
1423
|
+
const shadowed = await failure(system("components: { with: [position] }", "(row, { commands }) => { for (const row of list) commands.despawn(row); }"));
|
|
1424
|
+
expect(shadowed?.message).toContain("it declares another `row` here, which hides its row: rename one");
|
|
1425
|
+
expect(shadowed?.position).toBe(header.length + "const system = System.create({ components: { with: [position] } }, (row, { commands }) => { for (const ".length);
|
|
1426
|
+
expect((await failure(system("components: { with: [position] }", "(row) => { console.log(row.size); }")))?.message).toContain("it reads a member of its row by name");
|
|
1427
|
+
expect((await failure(system("components: { with: [position] }", "(row) => { delete row[position]; }")))?.message).toContain("it deletes a member of its row");
|
|
1428
|
+
for (const readonly of ["readonly: false, ", "readonly: watching, "]) {
|
|
1429
|
+
expect(await failure(`const watching = true;\n${system(`${readonly}components: { with: [position] }`, "(row) => { helper(row); }")}`)).not.toBe(null);
|
|
1430
|
+
}
|
|
1431
|
+
const whole = await transform(system("components: { with: [position], whole: true }", "(row) => { helper(row); }"));
|
|
1432
|
+
expect(whole !== null && "code" in whole ? [whole.code.includes("components: null"), whole.warnings] : null).toEqual([true, []]);
|
|
1433
|
+
// Taken whole on purpose, a row the build could follow is still taken whole.
|
|
1434
|
+
expect(await inferred(system("components: { with: [position], whole: true }", "(row) => { console.log(row[position]); }"))).toContain("components: null");
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
test("commands by kind, keys and lists as their bindings name them, however the context reaches them", async () => {
|
|
1438
|
+
const commanded = async (body: string, declaration = "components: { with: [position] }"): Promise<string> =>
|
|
1439
|
+
/commands: (null|\{[^}]*\})/.exec(await inferred(`const Shapes = [mass];\nconst Listed = [velocity];\n${system(declaration, body)}`))?.[1] ?? "none";
|
|
1440
|
+
expect(await commanded("(row, { commands }) => { commands.spawn([mass, ...Shapes])({}); commands.add(row.entity, velocity, 1); commands.remove(row.entity, [mass]); }"))
|
|
1441
|
+
.toBe("{ spawn: [mass, Shapes], add: [velocity], remove: [mass], despawn: false }");
|
|
1442
|
+
expect(await commanded("(row, context) => { const c = context.commands; c.despawn(row.entity); context.commands.add(row.entity, Listed, {}); }"))
|
|
1443
|
+
.toBe("{ spawn: [], add: [Listed], remove: [], despawn: true }");
|
|
1444
|
+
expect(await commanded("(row, context) => { const { commands } = context; commands.despawnWhere({ with: [mass] }); }")).toContain("despawn: true");
|
|
1445
|
+
expect(await commanded("(row) => { row[position] = 1; }")).toBe("{ spawn: [], add: [], remove: [], despawn: false }");
|
|
1446
|
+
for (const body of ["(row, { commands }) => { helper(commands); }", "(row, { commands }) => { commands.add(row.entity, keyOf(1), 1); }",
|
|
1447
|
+
"(row, { commands: { spawn } }) => { spawn([mass]); }", "(row, context) => { helper(context); }",
|
|
1448
|
+
"(row, { commands }) => { commands.spawnTables(tables); }"]) {
|
|
1449
|
+
expect(await commanded(body)).toBe("null");
|
|
1450
|
+
}
|
|
1418
1451
|
});
|
|
1419
1452
|
|
|
1420
|
-
test("
|
|
1421
|
-
const result = await transform(system("
|
|
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
|
|
1426
|
-
for (const observer of ["observer: false, ", "observer: watching, "]) {
|
|
1427
|
-
const result = await transform(`const watching = true;\n${system(`${observer}components: { with: [position] }`, "(row) => { helper(row); }")}`);
|
|
1428
|
-
expect(result !== null && "code" in result ? result.warnings.length : null).toBe(1);
|
|
1429
|
-
}
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
test("a lookup's rows it can't follow without a warning, since what they hold is what the declaration says", async () => {
|
|
1458
|
+
test("a lookup's rows it can't follow without a failure, since what they hold is what the declaration says", async () => {
|
|
1433
1459
|
const result = await transform(system("components: { with: [target] }, lookup: { targets: { with: [mass] } }", "(row, { lookup }) => { helper(lookup.targets(row[target])); }"));
|
|
1434
1460
|
expect(result !== null && "code" in result ? [result.code.includes(`"targets": null`), result.warnings] : null).toEqual([true, []]);
|
|
1435
1461
|
});
|
|
1436
1462
|
|
|
1437
|
-
test("warnings name the module by the label given, with line and column", async () => {
|
|
1438
|
-
const result = await transformKernels(`${header}${system("components: { with: [position] }", "(row) => { helper(row); }")}`, "/repo/app/src/features/a/model.ts", parse,
|
|
1439
|
-
async specifier => specifier === "ecs" ? ownCopy : null, () => "", createLinker(host({}, reductions), parse), true, "src/features/a/model.ts");
|
|
1440
|
-
expect(result !== null && "code" in result ? result.warnings.map(warning => /^[^ ]*:/.exec(warning)?.[0]) : null).toEqual([`src/features/a/model.ts:${header.split("\n").length}:${"const system = System.create({ components: { with: [position] } }, ".length + 1}:`]);
|
|
1441
|
-
});
|
|
1442
|
-
|
|
1443
1463
|
test("resources read, written and reduced, however its context reaches them", async () => {
|
|
1444
1464
|
expect(await inferred(system("", "(context) => { context.resources[score] = context.resources[delta]; }")))
|
|
1445
1465
|
.toContain("resources: { read: [delta], write: [score], reduce: [] }");
|
|
@@ -1519,7 +1539,7 @@ describe("inferred access", () => {
|
|
|
1519
1539
|
});
|
|
1520
1540
|
|
|
1521
1541
|
test("a body mentioning eval as one it can't follow", async () => {
|
|
1522
|
-
expect(await inferred(system("components: { with: [position] }", "(row) => { eval(\"row[position] = 1\"); }"))).toContain("
|
|
1542
|
+
expect(await inferred(system("components: { with: [position] }", "(row) => { eval(\"row[position] = 1\"); }"))).toContain("since it calls `eval`");
|
|
1523
1543
|
});
|
|
1524
1544
|
|
|
1525
1545
|
test("fail the build for an async or generator body", async () => {
|
|
@@ -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 `
|
|
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
|
-
//
|
|
317
|
-
// declarations already say;
|
|
318
|
-
if (used.
|
|
316
|
+
// A readonly system's row, a finalizer's, and a lookup's rows it can't follow are taken as read whole, which is
|
|
317
|
+
// what their declarations already say; a system's row taken as written whole would change its schedule unseen.
|
|
318
|
+
if (created === "system" && used.rowLost !== undefined && !declaresTrue(declared, "readonly")) {
|
|
319
|
+
fail(used.rowLost.at, `the build can't follow how this body uses its row, since ${used.rowLost.why}, so it would take every component in with, added and changed as written: index the row by key directly, or destructure it by key; or declare \`components: { whole: true }\` for a body that uses its row whole`);
|
|
320
|
+
}
|
|
319
321
|
accessing = true;
|
|
320
322
|
const access = accessText(used, where, whereReads.has(node) ? whereReads.get(node) ?? null : undefined);
|
|
321
323
|
if (kernel !== undefined) {
|
|
@@ -3,13 +3,14 @@ import type { Finalizer } from '../../interface/finalizer/created.js';
|
|
|
3
3
|
import type { Rule } from '../../interface/rule/created.js';
|
|
4
4
|
import type { RuleFacts } from '../../interface/registry/strata.js';
|
|
5
5
|
import { createFinalizerRunner, placeFinalizer, type FinalizerRunner } from './finalizers.js';
|
|
6
|
+
import { watchResource } from './resource-changes.js';
|
|
6
7
|
import { finalizerKeysOf, type TaskChannel } from './remote-systems.js';
|
|
7
8
|
import { programFinalizers, programRules, programVersion } from '../../interface/registry/program.js';
|
|
8
9
|
import { triggersOf } from './program.js';
|
|
9
10
|
import { ensureResources, findResource } from './resources.js';
|
|
10
11
|
import { idOf, metIdOf, type Key, type State } from './state.js';
|
|
11
12
|
import { restratify } from './structure.js';
|
|
12
|
-
import { idOfItem, idOfKey, labelOf, threadOf } from '../../interface/registry/index.js';
|
|
13
|
+
import { idOfItem, idOfKey, kindOf, labelOf, threadOf } from '../../interface/registry/index.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Why a joined thread that imported the items of ids `imported` can't claim `threads`: a program finalizer on one that
|
|
@@ -67,7 +68,11 @@ export const placeFinalizers = (state: State): void => {
|
|
|
67
68
|
state.finalizers.splice(0, state.finalizers.length, ...kept);
|
|
68
69
|
};
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
/** The resources a finalizer reads, writes or, for a changes finalizer, watches. */
|
|
72
|
+
const resourcesOf = (finalizer: Finalizer): readonly Key[] => [
|
|
73
|
+
...finalizer.declaration.resources?.read ?? [], ...finalizer.declaration.resources?.write ?? [],
|
|
74
|
+
...finalizer.kind === "changes" ? (finalizer.declaration.watch ?? []).filter(key => kindOf(key) === "resource") : [],
|
|
75
|
+
];
|
|
71
76
|
|
|
72
77
|
/** Between runs: adds the resources of every finalizer in the program, so one can activate at any sync point. */
|
|
73
78
|
export const ensureProgramResources = (state: State, ensured: { version: number }): void => {
|
|
@@ -75,6 +80,10 @@ export const ensureProgramResources = (state: State, ensured: { version: number
|
|
|
75
80
|
if (ensured.version === version) return;
|
|
76
81
|
ensured.version = version;
|
|
77
82
|
ensureResources(state, new Set(programFinalizers().flatMap(resourcesOf)));
|
|
83
|
+
// A watched resource is followed from now, before its finalizer activates, so writes before then are seen.
|
|
84
|
+
for (const finalizer of programFinalizers()) {
|
|
85
|
+
if (finalizer.kind === "changes") for (const key of finalizer.declaration.watch ?? []) if (kindOf(key) === "resource") watchResource(state, idOf(state, key));
|
|
86
|
+
}
|
|
78
87
|
// Finalizers waiting for these can activate at the next sync point.
|
|
79
88
|
state.activation.met = true;
|
|
80
89
|
};
|
|
@@ -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,
|
|
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
|
-
//
|
|
46
|
-
expect(seen).toEqual({ early: 0, same: 0, late:
|
|
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 };
|