ecs: std/ecs data layer: entities, columns, stores, queries, hierarchy, events (step 1 of #89) #453
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Step 1 of 3 of #89 (ECS slice 1). The specification is ECS.md sections 3, 4, 5, 7 (data layer, queries, structural change, hierarchy) and section 6, "Resources and events"; the owner's decisions are section 15 (
Store/Columns,pairs/triples/quadruples,Events<Event>in the package, noWorldinstd/ecs). The code form exists inexamples/ecs-probe-2/src/ecs.trbandspace.trb(depthOrder); this issue moves it into a new std package, completes it and pins it with tests.System/Stage/Schedule(step 2) andstd/transform(step 3) are not part of it.Goal
A new package
std/ecs(std/ecs/project.trb,name = "std/ecs", no dependency beyondstd/coreandstd/collections; noshared type, nothing that knows a position) with the data layer of a world:Entity,Entities,Column<Component>,Store<Component>,ParentanddepthOrder, the four query arities,Events<Event>andComponentKey.Depends on
Nothing unpublished. Verified on
main(bc504c24): compiler gaps 1, 3, 4, 5 and 11 of ECS.md section 12 are closed, package imports resolve from a conformance program (use ... from "std/collections"is used bytests/conformance), and std packages need no registration in the compiler (a directory understd/with aproject.trb, asstd/linear). No seed impact: no syntax, no native, no driver flag, no std name the compiler looks up by string.Decisions (refinement)
std/ecs/src/lib.trbre-exports (public use ... from "./x") fromentity.trb(Entity,Entities),column.trb,store.trb(Store,ComponentKey),query.trb,hierarchy.trb(Parent,depthOrder),events.trb. Tests instd/ecs/tests/*.test.trb.Entityexactly as ECS.md section 3 (index,generation = 0,Equals, Hash, Show).ColumnandStoreas in the probe, pluseach(body: (var value: Component) => Void)on the column (a closure bindsvar, closures.md rule 9); a row visited byeachcounts as written (stamped with one clock tick), as Bevy marks mutable access.Entitiesis the allocator and the only place a generation changes:var fn spawn(): Entity(a freed index is reused first, last freed first, withgeneration + 1; otherwise the next new index, starting at 0),var fn despawn(entity: Entity): Bool(a stale or unknown handle changes nothing and answersfalse),fn isAlive(entity: Entity): Bool,fn length(): Int(live entities). Deterministic given the same operations, which is the lockstep rule of section 3 "Determinism". It does not touch columns: removing components on despawn is the program's world.query,pairs,triples,quadrupleswalk the column of the first argument in insertion order and probe the others (never iterate an index). The user chooses the driving column by argument order; the order is a function of the operations, not of sizes. A query holds a copy of the groups for its duration (ECS.md section 5):for ... in pairs(...)may spawn, attach and detach on the live groups. Signatures follow the call sites of the document, one group argument per column (pairs(space, motion)), not the singleSpacethe document prints in section 4; correct that signature block in the document in the same PR.moving(world); its generic system names the component in the bound). Writequery<Space, Component>(space: Space): Iterate<(Entity, Component)> where Space: Store<Component>and the three others first. If the checker cannot inferComponentfrom the bound, ship the form with explicit type arguments at the call site (query<Motions, Lifetime>(motion)), state this in the PR and in ECS.md section 4, and do not touch the checker in this PR; the inference is then a note on #89, not new work here.depthOrder: a free functiondepthOrder(parents: Column<Parent>, entities: Iterate<Entity>): List<Entity>(the group that owns the tree passes its own column and the entities it wants ordered; the probe'splacerecursion, made iterative or kept recursive with a depth that a test pins at 1 000 links). Every parent before its children, an entity listed once, order of the input otherwise. A cycle panics with the entity in the message (ECS.md section 7).Events<Event>: two buffers, Bevy's shape.type EventCursor with Equals, Show { var seen: Int = 0 }is held by the consumer.var fn send(event: Event),fn read(var cursor: EventCursor): List<Event>(the events sent since the cursor, oldest first; moves the cursor to now; does not mutate the events, so a read-only system may takeEventsby value),var fn advance()(the frame boundary: the older buffer is dropped, the newer becomes the older),fn length(): Int. An event survives exactly twoadvancecalls; a consumer that reads at least once per frame sees every event exactly once; one that skips frames loses the oldest and gets the rest.ComponentKey:public type ComponentKey with Equals, Hash, Show { name: String }, written by hand by the package (ComponentKey("acme/game/Position")). NocomponentKey<Component>(); that is #90 and needs a compiler intrinsic.# Examplesblock (the probe's are the model); no semicolons, command calls where the canon says so.Acceptance criteria
std/ecsexists as above;torb check std/ecsandtorb format --checkare clean;tools/check-commits.shclean.Entities: spawn/despawn/isAlive/length as decided; a despawned handle is not alive, its index comes back withgeneration + 1, the old handle misses inColumn.getandhas, and despawning it again answersfalse.Column:put,remove(swap with the last row,rowsfixed up, including removing the last row and removing a missing entity),get,has,length,ownerAt,valueAt,version,changedSince,each; insertion order is kept and a removal moves exactly one row. Tests coverchangedSinceafterput, aftereach, afterremove.Storeand the four query arities: tests with two hand-written column groups (aPlacesand aMotions, as the probe) and a generic system bounded by twoStorebounds;query,pairs,triplesandquadrupleseach answer the right tuples in the first column's order; a missing component drops the row; the with/without filter forms of ECS.md section 4 (pairs(...).filter { ... }) run.const snapshot = world, a rollback is an assignment (test on a program-shapedWorldvalue).ParentanddepthOrder: parent-first on a three-level tree given in child-first order, a forest, and an entity without a parent. A cycle panics naming the entity, pinned bytests/conformance/ecs-parent-cycle.trbwith.exitand.stderr.Events<Event>as decided, including the skipped-frame case and two independent cursors.tests/conformance/ecs-data.trb(a world with two groups: spawn, attach, a system over twoStorebounds,pairs, structural change in a query,depthOrder, an event round, snapshot and rollback) prints its result; tier B runs it in the VM and natively and both must giveecs-data.expectedbyte for byte. The.allocationsbudget is taken from the first forge CI run's report; a leak run is clean.docs/standard-library/ecs.mdand its line indocs/standard-library/index.md;skills/regenerated (torb docs skill docs skills,--checkis a tier A gate). ECS.md: thepairssignature block corrected, and section 13 slice 1 marked "data layer landed" with this issue.Seed impact
None (see "Depends on").
Size
One PR, two Conventional Commits:
feat(std/ecs): entities, columns, stores, queries, hierarchy and events(package, tests, the two conformance programs) anddocs(std/ecs): standard-library page, skills and the design record(docs, regeneratedskills/, ECS.md edits).Part of #89.
std/ecswith oneStoreper group type, andstd/transform(ECS slice 1) #89[loop:torbscript] decided by the loop: step 1 of the split of #89. Slice 1 plus
std/transformis a package, a scheduler with a native gate and a second package, far beyond one PR of one or two commits, so the first step is the data layer ofstd/ecsand the rest is the checklist of #89 (issues filed when they start; the backlog gate is off). Technical choices made here, all inside ECS.md and without a question to the owner:Entitiesallocator semantics (reuse withgeneration + 1, stale handle despawns tofalse), query order = the first argument's column in insertion order (deterministic, no size-dependent order),depthOrderas a free function overColumn<Parent>,Eventswith consumer-heldEventCursorand a two-bufferadvance,eachstamps the rows it visits. Finding: the probe's queries are concrete, so inferringComponentfrom aStore<Component>bound in a genericqueryis unproven; the issue gives the explicit-type-argument form as the fallback instead of a checker change. No seed impact (no syntax, native, driver flag or looked-up std name), no collision with a design record, no owner decision needed (every product choice for slice 1 is in ECS.md section 15). Definition of ready met, labelledready. Part of #89.std/ecswith oneStoreper group type, andstd/transform(ECS slice 1) #89[loop:torbscript] claim implement by session:loop-torbscript-20261024 until 2026-10-24T02:25:29Z
[loop:torbscript] implement: std/ecs data layer (76 tests, two conformance programs, docs page, skills) is in #456 (Closes #453); forge CI was still queued at 02:52. Open: tests/conformance/ecs-data.allocations from the first CI report, and the native panic-frame format of ecs-parent-cycle.stderr. next: waiting (CI green and the allocation budget)
[loop:torbscript] claim fix by session:loop-torbscript-20261024 until 2026-10-24T04:46:21Z
[loop:torbscript] fix: tier A of
b26ebea2had two red gates, both docs:docs check(docs/standard-library/ecs.md summary 242 > 240 characters, andsum += ...in a check block, which the parser rejects; 3e3119fa/a2a3c192 shorten the summary and writesum = sum + position.x, generated skills/index copies synced) anddocs source --panics(depthOrder had no# Panicssection;3e3119fa). Everything else in tier A was green. Still open, only visible once tier B runs (it was skipped behind tier A): theecs-data.allocationscount and theecs-parent-cycle.stderrpanic frames. next: waiting (forge CI ofa2a3c192)[loop:torbscript] claim fix by session:loop-torbscript-20261024 until 2026-10-24T07:26:45Z
[loop:torbscript] fix: Argus's REQUEST_CHANGES on #456 (review 384) has one blocker:
tests/conformance/ecs-data.allocationsis missing andecs-data/ecs-parent-cyclehave never run natively (tier B skipped by the path filter). This box has no C compiler, so the count comes from forge CI: the throwaway probe PR #462 (heade9c6269a, on top ofa2a3c192, widens the tier B trigger) is queued. Its runs 1616/1617 are stillwaitingbehind runs 1611-1615 (twomainpushes and PRs #460/#457) on the single runner, so tier B has not started. Nothing was changed on #456 this round.Remaining, once probe tier B has run: (1) take the
ecs-dataallocation count and committests/conformance/ecs-data.allocationsonloop/453-std-ecs-data-layer; (2) check theecs-parent-cyclenative and--vmresult and fix.stderrframes if tier B shows a diff; (3) close #462 with a comment; (4) reply to the review and re-request Argus.next: waiting (probe CI #462 queued behind the single runner)