Declarative Scheduling — Auto-DAG (RFC 07)
Systems declare phase and read/write access; the scheduler derives the DAG and rejects unsafe overlaps at Build().
Status: ✅ Implemented · Visibility: Public · Level: 🟣 Advanced · Category: Runtime
🎯 What it solves
Manual dependency wiring (.After() edges on every system) doesn't scale: adding a new writer of a
component means auditing every existing system that touches it, and a missed edge is a silent race —
nondeterministic behavior that only reproduces under load. It also leaves cores idle, because a
hand-written DAG is only as parallel as the developer bothered to make it. Declarative access turns
"who reads/writes what" into an explicit fact the scheduler can act on, so overlapping writes and
ambiguous reads become a Build()-time error instead of a production bug.
⚙️ How it works (in brief)
Every system declares a phase (its position in a DAG-local ordering) and a read/write set on
SystemBuilder — Reads<T>(), Writes<T>(), ReadsFresh<T>(), ReadsSnapshot<T>(), plus
event-queue and named-resource variants. At Build(), the scheduler groups systems by phase, derives
a DAG edge for every access relationship it can prove is safe (e.g. a fresh reader runs after its
writer), and throws — with a copy-paste-ready fix — for relationships it can't (two writers of the
same component, same phase, no declared order). The derived graph is an ordinary dependency DAG; the
any-worker, claim-on-CAS dispatch loop that executes it at runtime is unchanged by this RFC — only how
the DAG gets built changed.
Sub-features
| Sub-feature | Use it when... |
|---|---|
| Track → DAG → Phase Partitioning | Structuring the schedule itself — grouping systems into independent DAGs, ordering coarse execution stages, declaring a DAG's phase sequence |
| Access Declarations & Build-Time Conflict Detection | Declaring what one system reads/writes so the scheduler can order it safely and parallelize it against everything else |
⚠️ Guarantees & limits
W×W(two writers of the same component, same phase) and ambiguousR×W(plainReads<T>()against a same-phase writer) areBuild()-time errors with copy-paste-ready suggestions — never silent races.- Access tracking is component-level, not field-level —
Writes<T>()means "touches any field of T." .After()/.Before()remain as the escape hatch for non-access ordering constraints, and as the required disambiguation tool for an intentional same-phaseW×W.- A DEBUG-only assertion checks every
EntityRef.Write<T>()against the executing system's declaredWrites/SideWritesset;[Conditional("DEBUG")]strips the call entirely in RELEASE — zero production overhead. - This is declaration, not inference — Typhon never inspects a system body to detect its actual reads/writes. An undeclared access is simply invisible to the scheduler (and, in DEBUG, only the write side is cross-checked at runtime).
Build()validation has no suppress switch — it's a one-shot startup cost; a false positive should be fixed by correcting the declaration, not disabled.