Skip to content

[M1-ECS-04] Query API + iteration legality - #19

Merged
offdev merged 2 commits into
masterfrom
m1-ecs-04-query-api
Sep 13, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-ecs-04-query-api

Conversation

@offdev

@offdev offdev commented Sep 13, 2026

Copy link
Copy Markdown
Owner

M1-ECS-04 · Query API + iteration legality

Implements roadmap/M1-heartbeat.md M1-ECS-04 (scope only — no
deterministic-iteration contract, no systems):

  • World::each<T1, T2, ...>(fn, Read/Write tags...) over the
    M1-ECS-03 SoA rows: superset match (extra components do not exclude
    an entity); per-component access declared by tag type (Read →
    const T&, Write → T&, decided at compile time; count/type
    checked by static_assert). The access tags follow the callable — a
    pack of parameters must be last to be deducible, so the PRD sketch
    order (access_flags, fn) is settled here as (fn, tags...) and
    documented in query.h. each<>(fn) visits all live entities in
    ascending slot order; an unregistered listed type matches nothing
    (ok Status, zero visits).
  • Iteration legality (FR-12.3): a stack-scoped guard (two
    membership-only 256-bit sets: the matched-archetype set, complete
    before the first callback, plus the Read-declared component set).
    Illegal — an in-place write of a Read-declared queried component, a
    structural add/remove/destroy/clear touching a matched source or
    target archetype, or a nested each() — asserts in debug and in
    release returns ErrorCode::InvalidArgument (no new codes — the
    registry stays additive-only) + one rate-limited warn + skip,
    never applied
    , while the iteration continues over the unmutated
    storage. Legal: create() always, structural moves outside the
    matched set, writes through Write references, in-place overwrites
    of Write-declared components.
  • No hidden allocations: iteration state is stack-scoped
    (ids[N]/cols[N]/matchedIds[256]/two 256-bit sets); the
    10k-entity × 2-pass window measured zero process-wide heap
    allocations
    (test-only operator new counter, non-sanitizer
    trees; sanitizer trees leak-free) and zero reservation delta
    (pool-steady), ≈0.044 µs/visit on the -O0 tree — machine-greppable
    query-iteration <stats> lines per ctest run (CORE-001,
    M1-BENCH-01 baseline input).
  • clear() is now Status (the guard check runs first — a clear
    under a live iteration is rejected whole, never partial; the dtor
    never sees an active iteration); all call sites honor
    [[nodiscard]] (internal M1 API change, pre-gate, no external
    consumers).

Tests — new query CTest entry (ctest -R query), 17 cases:
exact mixed sets (incl. the superset case), empty-query slot order +
a legal concurrent destroy, unregistered no-match, access-tag
reference kinds, pinned archetype-then-slot visit order, guard release
after iteration, the legal-mutation matrix, the release skip matrix
(state unchanged, iteration continues over both entities, the same op
succeeds afterwards), six forked SIGABRT debug-assert children
(one per violation kind), warn-once + rate_limited summary via a
memory sink, and the zero-alloc window.

Docs: new docs/api/query.md (contract + Performance section),
cross-refs in entity.md / archetype.md / component_registry.md /
docs index / sim README; laige-api.json regenerated (451 symbols);
roadmap checkbox, Progress Board 4/25, one-line Change Log.

Verify: canonical g++ tree zero-warning, full ctest 37/37,
ctest -R query green (debug: 15 passed + 2 release-only skips;
release: 11 passed + 6 debug-only skips); sim suites green on
build-asan (ASan+UBSan, leak-free), build-clang, build-release,
build-shared, build-tsan; tools/laige-include-lint OK.

World::each<T1, T2, ...>(fn, Read/Write tags...) over the archetype
SoA rows:

- superset match: iterate every entity having all listed components;
  per-component access declared by the tag type (Read -> const T&,
  Write -> T&, decided at compile time); tags follow the callable
  (a pack of parameters must be last to be deducible) - the PRD
  sketch's argument order is settled in this step
- iteration-legality guard (stack-scoped IdSet256 pairs): in-place
  writes of Read-declared queried components, structural
  add/remove/destroy/clear touching a matched archetype, and nested
  each() assert in debug (forked SIGABRT children) and return
  InvalidArgument + one rate-limited warn + skip-with-log in release
  (FR-12.3); rejected mutations are never applied, the iteration
  continues over unmutated storage
- no hidden allocations: iteration state is stack-scoped (ids/cols
  arrays, 256-bit sets); 10k-entity window proves zero process-wide
  heap allocations + zero reservation delta (test-only operator new
  counter, non-sanitizer trees; sanitizer trees leak-free)
- empty query each<>(fn) visits all live entities ascending slot
  order; unregistered listed types match nothing (ok, zero visits)
- clear() now returns Status (guard check first; dtor never sees an
  active iteration); existing clear() call sites honor nodiscard

New public header laige/sim/query.h (Access/Read/Write tags,
IdSet256, full contract); query.cpp defines the guard helpers;
entity.h carries each() + the compile-time recursion (packs ride as
tuple types - an explicit template argument list cannot partition
between consecutive packs; rowRef returns one conditional reference
type - a decltype(auto) if/constexpr pair of returns would force an
inconsistent deduction).

17 tests in the new ctest query entry (Query*): exact mixed sets,
empty-query order + concurrent destroy, unregistered no-match,
access-tag reference kinds, pinned visit order, guard release, legal
mutation matrix, six forked debug-assert cases, release skip matrix,
warn-once rate-limit summary, zero-alloc window (machine-greppable
stats lines). laige-api.json regenerated (451 symbols; api-real-tree
green). Docs: docs/api/query.md + cross-refs (entity.md,
archetype.md, component_registry.md, docs/README, sim README);
roadboard/heartbeat updated.
@offdev
offdev merged commit cf8ccff into master Sep 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant