defmodule Wongi.Engine.Rete do
@moduledoc false
import Wongi.Engine.WME, only: [root?: 1, template?: 1]
alias Wongi.Engine.AlphaIndex
alias Wongi.Engine.Beta
alias Wongi.Engine.Beta.Root
alias Wongi.Engine.Compiler
alias Wongi.Engine.JoinResults
alias Wongi.Engine.Token
alias Wongi.Engine.WME
@alpha_patterns [
[:_, :_, :_],
[:subject, :_, :_],
[:_, :predicate, :_],
[:_, :_, :object],
[:subject, :predicate, :_],
[:subject, :_, :object],
[:_, :predicate, :object],
[:subject, :predicate, :object]
]
@index_patterns [
[:subject],
[:predicate],
[:object],
[:subject, :predicate],
[:subject, :object],
[:predicate, :object]
]
@type t() :: %__MODULE__{}
defstruct [
:wmes,
:indexes,
:manual,
:tokens,
:neg_join_results,
:ncc_tokens,
:ncc_owners,
# ---
:generation_by_wme,
:generation_by_token,
# ---
:alpha_subscriptions,
:beta_root,
:beta_table,
:beta_subscriptions,
:productions
]
def new do
root = Root.new()
%__MODULE__{
wmes: MapSet.new(),
indexes:
for pattern <- @index_patterns, into: %{} do
{pattern, AlphaIndex.new(pattern)}
end,
manual: MapSet.new(),
tokens: %{},
neg_join_results: JoinResults.new(),
ncc_tokens: %{},
ncc_owners: %{},
# ---
generation_by_wme: %{},
generation_by_token: %{},
# ---
alpha_subscriptions: %{},
beta_root: root,
beta_table: %{Beta.ref(root) => root},
beta_subscriptions: %{},
productions: MapSet.new()
}
|> seed()
end
@spec compile(t(), Wongi.Engine.DSL.Rule.t()) :: t()
def compile(rete, rule) do
Compiler.compile(rete, rule)
|> add_production(rule.ref)
end
@spec compile_and_get_ref(t(), Wongi.Engine.DSL.Rule.t()) :: {t(), reference()}
def compile_and_get_ref(rete, rule) do
{compile(rete, rule), rule.ref}
end
def assert(rete, wme_ish, generator \\ nil)
def assert(rete, %WME{} = wme, generator) do
rete
|> handle_operation({:assert, wme, generator})
end
def assert(rete, {s, p, o}, generator),
do: assert(rete, WME.new(s, p, o), generator)
def assert(rete, subject, object, predicate, generator \\ nil)
def assert(rete, s, p, o, generator),
do: assert(rete, WME.new(s, p, o), generator)
def retract(rete, wme_ish, generator \\ nil)
def retract(rete, %WME{} = wme, generator) do
rete
|> handle_operation({:retract, wme, generator})
end
def retract(rete, {s, p, o}, generator),
do: retract(rete, WME.new(s, p, o), generator)
def retract(rete, subject, object, predicate, generator \\ nil)
def retract(rete, s, p, o, generator),
do: retract(rete, WME.new(s, p, o), generator)
def select(%__MODULE__{} = rete, %WME{} = wme) when not template?(wme) do
if has_wme?(rete, wme) do
MapSet.new([wme])
else
MapSet.new()
end
end
def select(%__MODULE__{wmes: wmes}, %WME{} = wme) when root?(wme) do
wmes
end
def select(%__MODULE__{} = rete, %WME{} = wme) do
rete
|> matching(wme)
end
def select(rete, {s, p, o}),
do: select(rete, WME.new(s, p, o))
def select(rete, s, p, o),
do: select(rete, WME.new(s, p, o))
def subscribe_to_alpha(
%__MODULE__{alpha_subscriptions: alpha_subscriptions} = rete,
%WME{} = template,
node
) do
key = [template.subject, template.predicate, template.object]
subscriptions = alpha_subscriptions |> Map.get(key, [])
subscriptions = [Beta.ref(node) | subscriptions]
alpha_subscriptions = Map.put(alpha_subscriptions, key, subscriptions)
%__MODULE__{rete | alpha_subscriptions: alpha_subscriptions}
end
def subscribe_to_alpha(rete, [s, p, o], node),
do: subscribe_to_alpha(rete, WME.new(s, p, o), node)
def add_beta(%__MODULE__{beta_table: beta_table} = rete, node) do
ref = Beta.ref(node)
rete = put_beta_table(rete, Map.put(beta_table, ref, node))
Enum.reduce(Beta.parent_refs(node), rete, &subscribe_to_beta(&2, &1, ref))
end
def subscribe_to_beta(
%__MODULE__{beta_subscriptions: beta_subscriptions} = rete,
parent,
child
)
when is_reference(parent) and is_reference(child) do
rete
|> put_beta_subscriptions(subscribe_to_beta(beta_subscriptions, parent, child))
end
def subscribe_to_beta(%__MODULE__{} = rete, parent, child)
when is_reference(parent) do
subscribe_to_beta(rete, parent, Beta.ref(child))
end
def subscribe_to_beta(subscriptions, parent, child) do
parent_subs = Map.get(subscriptions, parent, [])
Map.put(subscriptions, parent, [child | parent_subs])
end
def get_beta(%__MODULE__{beta_table: beta_table}, ref) when is_reference(ref) do
Map.get(beta_table, ref)
end
def get_beta(%__MODULE__{beta_table: beta_table}, beta_node) do
Map.get(beta_table, Beta.ref(beta_node))
end
defp seed(%__MODULE__{beta_root: beta_root} = rete) do
Root.seed(beta_root, rete)
end
defp handle_operation(rete, {:assert, wme, generator}) do
rete
|> do_assert(wme, generator)
end
defp handle_operation(rete, {:retract, wme, generator}) do
rete
|> do_retract(wme, generator)
end
defp do_assert(%__MODULE__{} = rete, wme, generator) do
rete
|> add_wme(wme, generator)
|> activate(wme, has_wme?(rete, wme))
end
defp do_retract(%__MODULE__{} = rete, wme, generator) do
rete
|> remove_wme(wme, generator)
|> deactivate(wme, has_wme?(rete, wme))
end
defp add_wme(rete, wme, generator)
defp add_wme(rete, wme, nil) do
rete
|> put_wme(wme)
|> set_manual(wme)
end
defp add_wme(rete, wme, generating_token) do
rete
|> put_wme(wme)
|> track_generation(wme, generating_token)
end
defp put_wme(%__MODULE__{wmes: wmes} = rete, wme) do
if has_wme?(rete, wme) do
rete
else
%__MODULE__{rete | wmes: MapSet.put(wmes, wme)}
|> index(wme)
end
end
defp remove_wme(rete, wme, generator)
defp remove_wme(rete, wme, nil) do
rete
|> clear_manual(wme)
|> delete_wme(wme)
end
defp remove_wme(rete, wme, _generator) do
rete
|> delete_wme(wme)
end
defp can_delete_wme?(%__MODULE__{} = rete, wme) do
!has_manual?(rete, wme) && not generated?(rete, wme)
end
defp delete_wme(%__MODULE__{wmes: wmes} = rete, wme) do
if can_delete_wme?(rete, wme) do
%__MODULE__{rete | wmes: MapSet.delete(wmes, wme)}
|> unindex(wme)
else
rete
end
end
defp activate(rete, wme, existing)
defp activate(rete, _, true), do: rete
defp activate(rete, wme, false) do
subscriptions =
rete
|> alpha_subscriptions(wme)
subscriptions |> Enum.reduce(rete, &Beta.alpha_activate(&1, wme, &2))
end
defp deactivate(rete, wme, existing)
defp deactivate(rete, _, false), do: rete
defp deactivate(rete, wme, true) do
subscriptions =
rete
|> alpha_subscriptions(wme)
subscriptions |> Enum.reduce(rete, &Beta.alpha_deactivate(&1, wme, &2))
end
defp alpha_subscriptions(rete, wme) do
@alpha_patterns
|> Enum.flat_map(fn pattern ->
key =
Enum.map(pattern, fn
:_ -> :_
field -> Map.get(wme, field)
end)
Map.get(rete.alpha_subscriptions, key, [])
end)
end
def beta_subscriptions(%__MODULE__{beta_subscriptions: beta_subscriptions}, node) do
Map.get(beta_subscriptions, Beta.ref(node), [])
end
def add_token(%__MODULE__{tokens: tokens} = rete, %Wongi.Engine.Token{} = token) do
node_tokens =
tokens
|> Map.get_lazy(token.node_ref, &MapSet.new/0)
|> MapSet.put(token)
rete
|> put_tokens(token.node_ref, node_tokens)
end
def remove_token(%__MODULE__{tokens: tokens} = rete, token) do
wmes = generated_wmes(rete, token)
rete =
case Map.fetch(tokens, token.node_ref) do
{:ok, node_tokens} ->
rete
|> put_tokens(token.node_ref, MapSet.delete(node_tokens, token))
|> remove_neg_join_result(token)
|> remove_generator(token)
|> remove_ncc(token)
:error ->
rete
end
case wmes do
nil ->
rete
_ ->
wmes |> Enum.reduce(rete, &retract(&2, &1, token))
end
end
def tokens(%__MODULE__{tokens: tokens}, node) do
Map.get_lazy(tokens, Beta.ref(node), &MapSet.new/0)
end
def add_neg_join_result(%__MODULE__{neg_join_results: jrs} = rete, token, wme) do
rete
|> put_neg_join_results(JoinResults.put(jrs, token, wme))
end
def remove_neg_join_result(%__MODULE__{neg_join_results: jrs} = rete, token, wme) do
rete
|> put_neg_join_results(JoinResults.delete(jrs, {token, wme}))
end
defp remove_neg_join_result(%__MODULE__{neg_join_results: jrs} = rete, token) do
rete
|> put_neg_join_results(JoinResults.delete(jrs, token))
end
def neg_join_results(%__MODULE__{neg_join_results: jrs}, token_or_wme) do
JoinResults.get(jrs, token_or_wme)
end
def add_ncc_token(
%__MODULE__{ncc_tokens: ncc_tokens, ncc_owners: ncc_owners} = rete,
%Token{ref: ref} = token,
ncc_token
) do
tokens =
case ncc_tokens do
%{^ref => tokens} -> MapSet.put(tokens, ncc_token)
_ -> MapSet.new([ncc_token])
end
ncc_tokens = Map.put(ncc_tokens, token.ref, tokens)
ncc_owners = Map.put(ncc_owners, ncc_token.ref, token)
%__MODULE__{rete | ncc_tokens: ncc_tokens, ncc_owners: ncc_owners}
end
def ncc_owner(%__MODULE__{ncc_owners: ncc_owners}, %Token{ref: ref}) do
Map.get(ncc_owners, ref)
end
def has_ncc_tokens?(%__MODULE__{ncc_tokens: ncc_tokens}, %Token{ref: ref}) do
case ncc_tokens do
%{^ref => tokens} -> !Enum.empty?(tokens)
_ -> false
end
end
defp remove_ncc(
%__MODULE__{ncc_tokens: ncc_tokens, ncc_owners: ncc_owners} = rete,
token
) do
{ncc_tokens, ncc_owners} =
{ncc_tokens, ncc_owners}
|> ncc_partner_token(token)
|> ncc_owner_token(token)
%__MODULE__{rete | ncc_tokens: ncc_tokens, ncc_owners: ncc_owners}
end
defp ncc_partner_token({ncc_tokens, ncc_owners}, %Token{ref: ref} = token) do
case ncc_owners do
%{^ref => %Token{ref: owner_ref}} ->
ncc_owners = Map.delete(ncc_owners, ref)
ncc_tokens_of_owner =
Map.get(ncc_tokens, owner_ref)
|> MapSet.delete(token)
ncc_tokens =
if Enum.empty?(ncc_tokens_of_owner) do
ncc_tokens |> Map.delete(owner_ref)
else
ncc_tokens |> Map.put(owner_ref, ncc_tokens_of_owner)
end
{ncc_tokens, ncc_owners}
_ ->
{ncc_tokens, ncc_owners}
end
end
defp ncc_owner_token({ncc_tokens, ncc_owners}, %Token{ref: ref}) do
case ncc_tokens do
%{^ref => tokens} ->
ncc_owners =
Enum.reduce(tokens, ncc_owners, fn %Token{ref: partner_ref}, ncc_owners ->
Map.delete(ncc_owners, partner_ref)
end)
ncc_tokens = Map.delete(ncc_tokens, ref)
{ncc_tokens, ncc_owners}
_ ->
{ncc_tokens, ncc_owners}
end
end
defp add_production(%__MODULE__{productions: productions} = rete, ref) do
rete
|> put_productions(MapSet.put(productions, ref))
end
@spec productions(Wongi.Engine.Rete.t()) :: any()
def productions(%__MODULE__{productions: productions}), do: productions
defp put_beta_table(%__MODULE__{} = rete, beta_table) do
%__MODULE__{rete | beta_table: beta_table}
end
defp put_beta_subscriptions(%__MODULE__{} = rete, beta_subscriptions) do
%__MODULE__{rete | beta_subscriptions: beta_subscriptions}
end
defp put_productions(%__MODULE__{} = rete, productions) do
%__MODULE__{rete | productions: productions}
end
defp has_wme?(%__MODULE__{wmes: wmes}, wme) do
MapSet.member?(wmes, wme)
end
defp matching(rete, %WME{} = template) do
matching(rete, WME.index_pattern(template))
end
defp matching(%__MODULE__{indexes: indexes}, {fields, values}) do
indexes[fields] |> AlphaIndex.get(values)
end
defp index(%__MODULE__{indexes: indexes} = rete, wme) do
indexes =
for {pattern, index} <- indexes, into: %{} do
{pattern, AlphaIndex.put(index, wme)}
end
%__MODULE__{rete | indexes: indexes}
end
defp unindex(%__MODULE__{indexes: indexes} = rete, wme) do
indexes =
for {pattern, index} <- indexes, into: %{} do
{pattern, AlphaIndex.delete(index, wme)}
end
%__MODULE__{rete | indexes: indexes}
end
defp track_generation(%__MODULE__{} = rete, wme, token) do
rete
|> generation_add(wme, token)
end
defp remove_generator(%__MODULE__{} = rete, token) do
rete
|> generation_remove(token)
end
defp set_manual(%__MODULE__{manual: manual} = rete, wme) do
%__MODULE__{rete | manual: MapSet.put(manual, wme)}
end
defp clear_manual(%__MODULE__{manual: manual} = rete, wme) do
%__MODULE__{rete | manual: MapSet.delete(manual, wme)}
end
defp has_manual?(%__MODULE__{manual: manual}, wme) do
MapSet.member?(manual, wme)
end
defp put_tokens(%__MODULE__{tokens: tokens} = rete, node_ref, node_tokens) do
tokens =
if Enum.empty?(node_tokens) do
Map.delete(tokens, node_ref)
else
Map.put(tokens, node_ref, node_tokens)
end
%__MODULE__{rete | tokens: tokens}
end
defp put_neg_join_results(rete, jrs) do
%__MODULE__{rete | neg_join_results: jrs}
end
defp generation_add(
%__MODULE__{generation_by_wme: by_wme, generation_by_token: by_token} = rete,
wme,
token
) do
by_wme =
by_wme
|> Map.put_new_lazy(wme, &MapSet.new/0)
|> Map.update!(wme, &MapSet.put(&1, token))
by_token =
by_token
|> Map.put_new_lazy(token, &MapSet.new/0)
|> Map.update!(token, &MapSet.put(&1, wme))
%__MODULE__{
rete
| generation_by_wme: by_wme,
generation_by_token: by_token
}
end
defp generated_wmes(%__MODULE__{generation_by_token: by_token}, %Token{} = token) do
Map.get(by_token, token)
end
defp generated?(%__MODULE__{generation_by_wme: by_wme}, %WME{} = wme) do
Map.has_key?(by_wme, wme)
end
defp generation_remove(
%__MODULE__{generation_by_wme: by_wme, generation_by_token: by_token} = rete,
token
) do
by_token = Map.delete(by_token, token)
by_wme =
case generated_wmes(rete, token) do
nil ->
by_wme
wmes ->
Enum.reduce(wmes, by_wme, fn wme, by_wme ->
by_wme
|> Map.update!(wme, &MapSet.delete(&1, token))
|> delete_if(wme, &Enum.empty?/1)
end)
end
%__MODULE__{
rete
| generation_by_wme: by_wme,
generation_by_token: by_token
}
end
defp delete_if(%{} = map, key, predicate) do
if predicate.(Map.get(map, key)) do
Map.delete(map, key)
else
map
end
end
end