Detects cycles in the query dependency graph at runtime.
When query A calls query B which calls query A, the framework must
detect this and respond rather than deadlocking or stack-overflowing.
Current behavior: abort with a clear Roux.Cycle.Error.
Detection is O(n) in the query stack depth, which is bounded by the query DAG depth. The query stack is process-local (in the Context struct), so no synchronization is needed.
Designing for fixed-point iteration
The data structures support future fixed-point iteration without
structural changes. When implemented, check!/2 would return a
provisional value instead of raising, and the runtime would re-execute
the cycle until the result stabilizes. See D7 for rationale.
Summary
Functions
@spec check(Roux.Runtime.Context.t(), Roux.Memo.query_key()) :: :ok | {:cycle, [Roux.Memo.query_key()]}
Non-raising variant of check!/2.
Returns :ok if no cycle, or {:cycle, path} where path is the
list of query keys forming the cycle (starting and ending with the
repeated key).
@spec check!(Roux.Runtime.Context.t(), Roux.Memo.query_key()) :: :ok
Checks if the given query is already on the active stack.
Returns :ok if no cycle exists. Raises Roux.Cycle.Error with the
cycle path if one is detected.
Called by Runtime before pushing a query onto the stack.