Current section
Files
Jump to
Current section
Files
src/backward_chaining_engine.erl
-module(backward_chaining_engine).
-behaviour(gen_statem).
-export([start_link/0, start_link/1]).
-export([prove_goal/2, add_rule/2, add_fact/2, remove_rule/2, remove_fact/2,
reset_knowledge_base/1, get_proof_tree/1, query/2]).
-export([init/1, callback_mode/0, terminate/3, code_change/4]).
-export([idle/3, goal_decomposition/3, rule_matching/3, fact_checking/3,
subgoal_generation/3, recursive_proving/3, unification/3,
proof_construction/3, optimization/3, completed/3]).
-record(proof_node, {
goal :: term(),
rule = undefined :: undefined | term(),
subgoals = [] :: [term()],
bindings = #{} :: #{atom() => term()},
proof_status = unknown :: unknown | proven | failed,
depth = 0 :: non_neg_integer(),
parent = undefined :: undefined | pid(),
children = [] :: [pid()]
}).
-record(chaining_data, {
query_id :: term(),
main_goal :: term(),
knowledge_base = #{
facts => [],
rules => []
} :: #{atom() => [term()]},
proof_tree = #{} :: #{term() => #proof_node{}},
goal_stack = [] :: [term()],
current_goal = undefined :: undefined | term(),
variable_bindings = #{} :: #{atom() => term()},
unification_stack = [] :: [#{atom() => term()}],
resolution_strategy = depth_first :: depth_first | breadth_first | best_first,
max_depth = 100 :: pos_integer(),
current_depth = 0 :: non_neg_integer(),
proof_found = false :: boolean(),
proof_path = [] :: [term()],
alternative_proofs = [] :: [[term()]],
statistics = #{} :: #{atom() => term()},
optimization_enabled = true :: boolean(),
memoization_table = #{} :: #{term() => {proven | failed, [term()]}},
timeout = infinity :: timeout(),
start_time :: erlang:timestamp()
}).
start_link() ->
gen_statem:start_link(?MODULE, [], []).
start_link(Options) ->
gen_statem:start_link(?MODULE, Options, []).
prove_goal(Pid, Goal) ->
gen_statem:call(Pid, {prove_goal, Goal}).
add_rule(Pid, Rule) ->
gen_statem:call(Pid, {add_rule, Rule}).
add_fact(Pid, Fact) ->
gen_statem:call(Pid, {add_fact, Fact}).
remove_rule(Pid, Rule) ->
gen_statem:call(Pid, {remove_rule, Rule}).
remove_fact(Pid, Fact) ->
gen_statem:call(Pid, {remove_fact, Fact}).
reset_knowledge_base(Pid) ->
gen_statem:call(Pid, reset_knowledge_base).
get_proof_tree(Pid) ->
gen_statem:call(Pid, get_proof_tree).
query(Pid, Query) ->
gen_statem:call(Pid, {query, Query}).
init(Options) ->
Data = #chaining_data{
resolution_strategy = proplists:get_value(strategy, Options, depth_first),
max_depth = proplists:get_value(max_depth, Options, 100),
optimization_enabled = proplists:get_value(optimization, Options, true),
timeout = proplists:get_value(timeout, Options, infinity),
start_time = erlang:timestamp(),
statistics = #{
goals_processed => 0,
rules_applied => 0,
unifications => 0,
backtracks => 0,
cache_hits => 0,
cache_misses => 0
}
},
{ok, idle, Data}.
callback_mode() -> [state_functions, state_enter].
idle(enter, _OldState, Data) ->
{keep_state, Data};
idle({call, From}, {prove_goal, Goal}, Data) ->
QueryId = make_ref(),
NewData = initialize_proof_session(Goal, QueryId, Data),
{next_state, goal_decomposition, NewData, [{reply, From, {ok, QueryId}}]};
idle({call, From}, {query, Query}, Data) ->
QueryId = make_ref(),
NewData = initialize_proof_session(Query, QueryId, Data),
{next_state, goal_decomposition, NewData, [{reply, From, {ok, QueryId}}]};
idle({call, From}, {add_rule, Rule}, Data) ->
KB = Data#chaining_data.knowledge_base,
Rules = maps:get(rules, KB, []),
NewKB = maps:put(rules, [Rule | Rules], KB),
{keep_state, Data#chaining_data{knowledge_base = NewKB}, [{reply, From, ok}]};
idle({call, From}, {add_fact, Fact}, Data) ->
KB = Data#chaining_data.knowledge_base,
Facts = maps:get(facts, KB, []),
NewKB = maps:put(facts, [Fact | Facts], KB),
{keep_state, Data#chaining_data{knowledge_base = NewKB}, [{reply, From, ok}]};
idle({call, From}, reset_knowledge_base, Data) ->
NewData = Data#chaining_data{
knowledge_base = #{facts => [], rules => []},
memoization_table = #{}
},
{keep_state, NewData, [{reply, From, ok}]};
idle({call, From}, get_proof_tree, Data) ->
{keep_state, Data, [{reply, From, {ok, Data#chaining_data.proof_tree}}]};
idle(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
goal_decomposition(enter, _OldState, Data) ->
case Data#chaining_data.current_goal of
undefined ->
case Data#chaining_data.goal_stack of
[] -> {next_state, completed, Data};
[Goal | Rest] ->
UpdatedData = Data#chaining_data{
current_goal = Goal,
goal_stack = Rest,
current_depth = Data#chaining_data.current_depth + 1
},
{keep_state, UpdatedData, [{state_timeout, 0, decompose_goal}]}
end;
Goal ->
{keep_state, Data, [{state_timeout, 0, decompose_goal}]}
end;
goal_decomposition(state_timeout, decompose_goal, Data) ->
Goal = Data#chaining_data.current_goal,
case check_memoization(Goal, Data) of
{hit, Result} ->
handle_memoized_result(Result, Data);
miss ->
case check_depth_limit(Data) of
within_limit ->
UpdatedStats = increment_stat(goals_processed, Data#chaining_data.statistics),
{next_state, fact_checking, Data#chaining_data{statistics = UpdatedStats}};
exceeded ->
{next_state, completed, mark_goal_failed(Goal, depth_exceeded, Data)}
end
end;
goal_decomposition(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
fact_checking(enter, _OldState, Data) ->
Goal = Data#chaining_data.current_goal,
Facts = maps:get(facts, Data#chaining_data.knowledge_base, []),
case check_goal_against_facts(Goal, Facts, Data#chaining_data.variable_bindings) of
{proven, NewBindings} ->
ProvenData = mark_goal_proven(Goal, NewBindings, Data),
{next_state, proof_construction, ProvenData};
not_proven ->
{next_state, rule_matching, Data}
end;
fact_checking(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
rule_matching(enter, _OldState, Data) ->
Goal = Data#chaining_data.current_goal,
Rules = maps:get(rules, Data#chaining_data.knowledge_base, []),
case find_matching_rules(Goal, Rules, Data) of
[] ->
FailedData = mark_goal_failed(Goal, no_rules, Data),
{next_state, goal_decomposition, FailedData};
MatchingRules ->
{next_state, subgoal_generation,
Data#chaining_data{proof_tree = store_matching_rules(Goal, MatchingRules, Data#chaining_data.proof_tree)},
[{state_timeout, 0, {process_rules, MatchingRules}}]}
end;
rule_matching(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
subgoal_generation(enter, _OldState, Data) ->
{keep_state, Data};
subgoal_generation(state_timeout, {process_rules, []}, Data) ->
Goal = Data#chaining_data.current_goal,
FailedData = mark_goal_failed(Goal, all_rules_failed, Data),
{next_state, goal_decomposition, FailedData};
subgoal_generation(state_timeout, {process_rules, [Rule | RestRules]}, Data) ->
Goal = Data#chaining_data.current_goal,
case generate_subgoals_from_rule(Goal, Rule, Data) of
{ok, Subgoals, Bindings} ->
NewGoalStack = Subgoals ++ Data#chaining_data.goal_stack,
UpdatedData = Data#chaining_data{
goal_stack = NewGoalStack,
current_goal = undefined,
variable_bindings = merge_bindings(Data#chaining_data.variable_bindings, Bindings),
unification_stack = [Bindings | Data#chaining_data.unification_stack]
},
{next_state, recursive_proving, UpdatedData};
{unification_failed, _Reason} ->
{keep_state, Data, [{state_timeout, 0, {process_rules, RestRules}}]}
end;
subgoal_generation(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
recursive_proving(enter, _OldState, Data) ->
case Data#chaining_data.goal_stack of
[] ->
MainGoal = Data#chaining_data.main_goal,
ProvenData = mark_goal_proven(MainGoal, Data#chaining_data.variable_bindings, Data),
{next_state, proof_construction, ProvenData};
_ ->
{next_state, goal_decomposition, Data}
end;
recursive_proving(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
unification(enter, _OldState, Data) ->
Goal = Data#chaining_data.current_goal,
case perform_unification_step(Goal, Data) of
{success, NewBindings} ->
UpdatedStats = increment_stat(unifications, Data#chaining_data.statistics),
UpdatedData = Data#chaining_data{
variable_bindings = merge_bindings(Data#chaining_data.variable_bindings, NewBindings),
statistics = UpdatedStats
},
{next_state, goal_decomposition, UpdatedData};
{failure, Reason} ->
FailedData = mark_goal_failed(Goal, {unification_failed, Reason}, Data),
{next_state, goal_decomposition, FailedData}
end;
unification(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
proof_construction(enter, _OldState, Data) ->
ProofPath = construct_proof_path(Data),
FinalData = Data#chaining_data{
proof_found = true,
proof_path = ProofPath,
alternative_proofs = [ProofPath | Data#chaining_data.alternative_proofs]
},
case Data#chaining_data.optimization_enabled of
true ->
{next_state, optimization, FinalData};
false ->
{next_state, completed, FinalData}
end;
proof_construction(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
optimization(enter, _OldState, Data) ->
OptimizedData = optimize_proof(Data),
{next_state, completed, OptimizedData};
optimization(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
completed(enter, _OldState, Data) ->
store_in_memoization_table(Data),
{keep_state, Data};
completed({call, From}, get_proof_tree, Data) ->
{keep_state, Data, [{reply, From, {ok, Data#chaining_data.proof_tree}}]};
completed({call, From}, {prove_goal, Goal}, Data) ->
case Data#chaining_data.proof_found of
true ->
{keep_state, Data, [{reply, From, {proven, Data#chaining_data.proof_path}}]};
false ->
{keep_state, Data, [{reply, From, {not_proven, no_proof_found}}]}
end;
completed(EventType, Event, Data) ->
handle_common_events(EventType, Event, Data).
terminate(_Reason, _State, _Data) ->
ok.
code_change(_Vsn, State, Data, _Extra) ->
{ok, State, Data}.
initialize_proof_session(Goal, QueryId, Data) ->
ProofNode = #proof_node{
goal = Goal,
depth = 0,
proof_status = unknown
},
Data#chaining_data{
query_id = QueryId,
main_goal = Goal,
current_goal = Goal,
goal_stack = [],
proof_tree = #{Goal => ProofNode},
variable_bindings = #{},
unification_stack = [],
current_depth = 0,
proof_found = false,
proof_path = [],
start_time = erlang:timestamp()
}.
check_memoization(Goal, Data) ->
case maps:get(Goal, Data#chaining_data.memoization_table, not_found) of
not_found ->
miss;
{Status, ProofPath} ->
{hit, {Status, ProofPath}}
end.
handle_memoized_result({proven, ProofPath}, Data) ->
UpdatedStats = increment_stat(cache_hits, Data#chaining_data.statistics),
ProvenData = Data#chaining_data{
proof_found = true,
proof_path = ProofPath,
statistics = UpdatedStats
},
{next_state, completed, ProvenData};
handle_memoized_result({failed, _Reason}, Data) ->
UpdatedStats = increment_stat(cache_hits, Data#chaining_data.statistics),
FailedData = Data#chaining_data{
proof_found = false,
statistics = UpdatedStats
},
{next_state, completed, FailedData}.
check_depth_limit(Data) ->
case Data#chaining_data.current_depth >= Data#chaining_data.max_depth of
true -> exceeded;
false -> within_limit
end.
check_goal_against_facts(Goal, Facts, Bindings) ->
check_facts_recursive(Goal, Facts, Bindings).
check_facts_recursive(_Goal, [], _Bindings) ->
not_proven;
check_facts_recursive(Goal, [Fact | Rest], Bindings) ->
case unify_terms(Goal, Fact, Bindings) of
{success, NewBindings} ->
{proven, NewBindings};
{failure, _} ->
check_facts_recursive(Goal, Rest, Bindings)
end.
find_matching_rules(Goal, Rules, _Data) ->
lists:filter(fun(Rule) -> rule_matches_goal(Goal, Rule) end, Rules).
rule_matches_goal(_Goal, _Rule) ->
true.
generate_subgoals_from_rule(_Goal, Rule, _Data) ->
case Rule of
{Head, Body} when is_list(Body) ->
{ok, Body, #{}};
_ ->
{ok, [], #{}}
end.
perform_unification_step(_Goal, _Data) ->
{success, #{}}.
construct_proof_path(Data) ->
[Data#chaining_data.main_goal].
optimize_proof(Data) ->
Data.
mark_goal_proven(Goal, Bindings, Data) ->
ProofTree = Data#chaining_data.proof_tree,
UpdatedNode = case maps:get(Goal, ProofTree, undefined) of
undefined ->
#proof_node{goal = Goal, proof_status = proven, bindings = Bindings};
Node ->
Node#proof_node{proof_status = proven, bindings = Bindings}
end,
Data#chaining_data{proof_tree = maps:put(Goal, UpdatedNode, ProofTree)}.
mark_goal_failed(Goal, Reason, Data) ->
ProofTree = Data#chaining_data.proof_tree,
UpdatedNode = case maps:get(Goal, ProofTree, undefined) of
undefined ->
#proof_node{goal = Goal, proof_status = failed};
Node ->
Node#proof_node{proof_status = failed}
end,
Data#chaining_data{proof_tree = maps:put(Goal, UpdatedNode, ProofTree)}.
store_matching_rules(Goal, Rules, ProofTree) ->
case maps:get(Goal, ProofTree, undefined) of
undefined ->
Node = #proof_node{goal = Goal, rule = hd(Rules)},
maps:put(Goal, Node, ProofTree);
Node ->
maps:put(Goal, Node#proof_node{rule = hd(Rules)}, ProofTree)
end.
store_in_memoization_table(Data) ->
Goal = Data#chaining_data.main_goal,
Result = case Data#chaining_data.proof_found of
true -> {proven, Data#chaining_data.proof_path};
false -> {failed, no_proof}
end,
Data#chaining_data{
memoization_table = maps:put(Goal, Result, Data#chaining_data.memoization_table)
}.
merge_bindings(Bindings1, Bindings2) ->
maps:merge(Bindings1, Bindings2).
unify_terms(_Term1, _Term2, Bindings) ->
{success, Bindings}.
increment_stat(Stat, Stats) ->
maps:update_with(Stat, fun(X) -> X + 1 end, 1, Stats).
handle_common_events({call, From}, {add_rule, Rule}, Data) ->
KB = Data#chaining_data.knowledge_base,
Rules = maps:get(rules, KB, []),
NewKB = maps:put(rules, [Rule | Rules], KB),
{keep_state, Data#chaining_data{knowledge_base = NewKB}, [{reply, From, ok}]};
handle_common_events({call, From}, {add_fact, Fact}, Data) ->
KB = Data#chaining_data.knowledge_base,
Facts = maps:get(facts, KB, []),
NewKB = maps:put(facts, [Fact | Facts], KB),
{keep_state, Data#chaining_data{knowledge_base = NewKB}, [{reply, From, ok}]};
handle_common_events({call, From}, {remove_rule, Rule}, Data) ->
KB = Data#chaining_data.knowledge_base,
Rules = maps:get(rules, KB, []),
NewKB = maps:put(rules, lists:delete(Rule, Rules), KB),
{keep_state, Data#chaining_data{knowledge_base = NewKB}, [{reply, From, ok}]};
handle_common_events({call, From}, {remove_fact, Fact}, Data) ->
KB = Data#chaining_data.knowledge_base,
Facts = maps:get(facts, KB, []),
NewKB = maps:put(facts, lists:delete(Fact, Facts), KB),
{keep_state, Data#chaining_data{knowledge_base = NewKB}, [{reply, From, ok}]};
handle_common_events({call, From}, reset_knowledge_base, Data) ->
NewData = Data#chaining_data{
knowledge_base = #{facts => [], rules => []},
memoization_table = #{}
},
{keep_state, NewData, [{reply, From, ok}]};
handle_common_events(_EventType, _Event, _Data) ->
{keep_state_and_data, [postpone]}.