The propagation loop and the fire cycle.
Internal. Not part of the public API. Call it through Rete.Session.
Propagation drains a queue of pending work. A node consumes one unit, and returns the work it produced. Firing pops the most salient activation, runs its right hand side, and inserts what it returned. Propagation drains to completion before the next activation fires, so a rule always sees a settled network.
fire_rules/2 is the only entry point that drains. new/1, insert/3 and retract/3
queue work and return.
fire_rules/2 returns at quiescence. Every rule whose left hand side holds has fired,
and nothing whose support has gone is still asserting anything.
See docs/design/engine.md §2 for the loops, §8 for truth maintenance, and
docs/design/observability.md §3 for the loop guard.
Summary
Functions
Every fact the session holds, inserted or concluded.
Fires until the agenda is empty.
Records facts and queues their propagation.
The state a listener has accumulated, or nil if it is not attached.
A state over a network, with nothing inserted.
Runs a query: one result per match, computed by the query's body.
Which index filters would use at a query, or :scan.
Removes facts and queues the retraction.
Whether the state has no propagation queued.
Attaches a listener with its initial state.
Functions
@spec facts(Rete.Engine.State.t()) :: [term()]
Every fact the session holds, inserted or concluded.
This excludes the marker facts an extracted compound negation inserts. They express a negated conjunction to the network, and no rule of the user's concluded them. Everywhere else, they are ordinary facts.
@spec fire_rules( Rete.Engine.State.t(), keyword() ) :: Rete.Engine.State.t()
Fires until the agenda is empty.
Options:
:max_cycles— how many cycles one call may fire. A cycle is one pass of the fire loop: one activation at the default concurrency, one whole activation group above it.:infinityby default, so an oscillating ruleset spins rather than raising. Firing that many and still having work pending raises with the rules that fired most. Firing that many and settling is fine. Seedocs/design/observability.md§3.:concurrency— how many rule bodies of one activation group run at once.1by default, which is the sequential path. Above1, the bodies of a group run on tasks and their conclusions are applied in group order. Worth raising only when a body is expensive: a body that just builds a tuple is about 1.5% of firing, and a task costs more than that. Seedocs/design/engine.md§11.:timeout— milliseconds a single body may take, or:infinity, the default. Only applies when:concurrencyis above1.
@spec insert(Rete.Engine.State.t(), [term()], Rete.Listener.origin()) :: Rete.Engine.State.t()
Records facts and queues their propagation.
A fact equal to one already present bumps its count and queues nothing. The matches it would make already exist.
This does not propagate. Rete.Memory holds the fact at once, so facts/1 sees it.
The alpha work waits in the queue until fire_rules/2 drains it. See
docs/design/engine.md §2.
@spec listener_state(Rete.Engine.State.t(), module()) :: term()
The state a listener has accumulated, or nil if it is not attached.
@spec new(Rete.Network.t()) :: Rete.Engine.State.t()
A state over a network, with nothing inserted.
Queues the root token rather than propagating it. A rule whose whole left hand side is an
absence or an empty collection is true of the empty session. So the token must exist
before a fact arrives. fire_rules/2 is what propagates it. A state nobody has fired
holds no matches and no activations. See docs/design/engine.md §6.
@spec query( Rete.Engine.State.t(), {module(), atom()}, keyword() | %{required(atom()) => term()} ) :: [ term() ]
Runs a query: one result per match, computed by the query's body.
A query is named by the {module, name} pair it was defined under. defquery summary(...) also defines summary/2 in its own module. MyRuleset.summary(session, filters) is the readable form of this call.
filters narrows the matches by equality on the bindings, before the body runs. It
may name any variable the left hand side binds.
Row order is unspecified. Rows follow the order the facts arrived in.
This used to sort every result, so that one fact set always answered the same way. The contract never promised that, and the sort cost O(n log n) on every call. The rows are the same without it. Only their sequence moves.
@spec query_plan( Rete.Engine.State.t(), {module(), atom()}, keyword() | %{required(atom()) => term()} ) :: {:index, [atom()]} | :scan
Which index filters would use at a query, or :scan.
A declared index that no call ever uses is silently no faster, which is the one thing
that cannot be seen from the outside. This says so. See Rete.Inspect.query_plan/3.
@spec retract(Rete.Engine.State.t(), [term()], Rete.Listener.origin()) :: Rete.Engine.State.t()
Removes facts and queues the retraction.
Only the last occurrence of a fact queues anything. The engine retracts anything
concluded from it in turn, once fire_rules/2 drains the queue and the network settles.
This does not propagate, for the reason insert/3 gives. Queuing an insert and then
a retract of the same fact drains to a net no-op. The queued work for one node keeps the
order it arrived in, whatever a caller put between the two calls.
@spec settled?(Rete.Engine.State.t()) :: boolean()
Whether the state has no propagation queued.
false means a fact reached working memory and no node has seen it yet. A state fresh
from new/1 is unsettled, because the root token is queued too. See
docs/design/engine.md §2.
This reads the queue alone, and that is enough. On every state a caller can hold, the
agenda is empty whenever the queue is: fire_loop/4 returns only on :empty, and every
other way out of a fire raises, which discards the state. So there is no state with work
queued nowhere and an activation still waiting. Keep it that way, or this answers true
about a state that has yet to fire something.
@spec with_listener(Rete.Engine.State.t(), module(), term()) :: Rete.Engine.State.t()
Attaches a listener with its initial state.