
% File: lpe.pl
% Authors: Peter Clark and Rob Holte
% Date: updated 24th Feb 1992
% Purpose: Full machinery for lazy partial evaluation (LPE)
%	See README for more information on using LPE
%	See lpe.doc for more documentation on this particular implementation

/*
	========================================
		DEFINED PREDICATES:
		-------------------
	lpe_setup(Functor/Arity)       % set up for LPE for Goal with F/A
        lpe_call(+Goal)		       % evaluate Goal, doing LPE at same time
	lpe_listing(Functor/Arity)     % list current defn of concept with F/A

	set_tracing_mode(+Mode)	       % Mode = {on, off}
	set_learning_mode(+Mode)       % Mode = {lpe, pe, ebg, none}
	========================================

(That's all you need for LPE!)
*/

:- ensure_loaded([utils,kassert]).

:- dynamic learning_mode/1, tracing/1.
learning_mode(lpe).
tracing(off).

% ======================================================================
%		INITIAL SETTING UP FOR LPE
% ======================================================================

% lpe_setup(Functor/Arity).
% Simply copy clauses from the assert to the kassert database. Clauses are
% asserted in the kassert db ordered according to their `quality' (best first).
lpe_setup(F/A) :-
	kabolish(F/A),
	functor(Head, F, A),
	clause(Head, Body),
	tidy_and_split(Body, Op, Nonop),
	ordered_kassert( (Head:-call(Op),Nonop) ),
	fail.
lpe_setup(_).

% ======================================================================
%		I: THE LPE INTERPRETER
% ======================================================================
% Generates a revised concept definition of GenGoal from the (leaves of a) 
% (successful or failed) proof of Goal. It is used by the concept definition
% ordering/execution algorithm (which implements lpe_call/1) below.

lpe(Goal, GenGoal, GenProof) :-
	lpe(Goal, GenGoal, _, GenProof, _).

% lpe(+Goal, +GenGoal, -Proof, -GenProof, -TFFlag).
% Returns (the leaves of) a Proof and a generalised proof (GenProof) of Goal.
% If the proof was successful for Goal, TFFlag = true, if not TFFlag = false.
% The proof process of repeatedly replacing goals with subgoals ([1]) halts 
% immediately it is known that the proof does not apply to Goal ([2]).

lpe((A,B), (GenA,GenB), (ProofA,ProofB), (GenProofA,GenProofB), TF) :-   % [5]
	!,
	lpe(A, GenA, ProofA, GenProofA, TFA),				 % [6]
	(TFA = true -> 	lpe(B, GenB, ProofB, GenProofB, TF)		 % [7]
		    ;   ProofB = B, GenProofB = GenB, TF = false ).	 % [8]
lpe((A;B), (GenA;GenB), Proof, GenProof, TF) :- 
	!,
	( lpe(A, GenA, Proof, GenProof, TF)
	; lpe(B, GenB, Proof, GenProof, TF) ).
lpe(true, true, true, true, true) :-
	!.
lpe(A, GenA, false, GenA, false) :-
	learning_mode(lpe),					% [4]
	operational(GenA),
	\+ call(A),						% [2]
	!.
lpe(A, GenA, A, GenA, true) :-
	operational(GenA),
	!,
	( learning_mode(pe) -> may_succeed(GenA) ; call(A) ).   % [3]
lpe(Goal, GenGoal, Proof, GenProof, TF) :-
	clause(GenGoal, GenBody),				% [1]
	copy((GenGoal:-GenBody), (SpecGoal:-GoalBody)),
	UnificationGoal = (Goal=SpecGoal),
	lpe((UnificationGoal,GoalBody), (true,GenBody), Proof, GenProof, TF).

operational(F) :- \+ predicate_property(F, (dynamic)), !.

% ======================================================================
%	II: THE CONCEPT DEFINITION EXECUTION/ORDERING ALGORITHM
% ======================================================================

% lpe_call(+Goal).
% Like Prolog's call/1 except it also revises clauses for Goal using the LPE.
% lpe_call/1 uses the LPE interpreter above (lpe/3) to generate revised 
% concept definitions as required.
% The data structure for clauses is    Head :- call(OpGoals), NonopGoals.
% See lpe.doc for more information.

lpe_call(Goal) :- 
	learning_mode(none), !, call(Goal).
lpe_call(Goal) :- 
	kclause(Goal, Body, K),				     		 % [1]
	evaluate_body((Goal:-Body), K).

evaluate_body( (_:-call(true), true), _) :- !.
evaluate_body( (_:-call(  Op), true), _) :- !, call(Op).     % operational [2]
evaluate_body(Clause, K) :-
	learning_mode(ebg), !,
	kclause(GenHead, GenBody, K),		        % get copy of clause K
	Clause = ( _ :- call(Op), Nonop ),
	call(Op),
	\+ \+ call(Nonop),				% check is >= 1 proof 
	GenClause = (GenHead:-GenBody),
	expand(Clause, GenClause, NewClause),		% Find a proof & EBG it
	ordered_kassert(NewClause).			% Store learned op defn
evaluate_body(Clause, K) :-
	Clause = ( _ :- call(Op), _ ),
	call(Op), !,				        % Op bit passes    [3]
	kclause(GenHead, GenBody, K),		        % get copy of clause K
	GenClause = (GenHead:-GenBody),
	all_expands_and_kasserts(Clause, GenClause, K), % find all proofs  [4]
	kretract(_, K),				        % del orig. defn,  [5]
	fail.		    	          % bcktrck to [1] continue search [6]

%     -------------------------------------------------------------
%     | EXPANSION: Generate a revised concept definition based on |
%     | replacing non-operational goals in Body with subgoals.    |
%     -------------------------------------------------------------

% find and store all expansions of Clause (has key K)
all_expands_and_kasserts(Clause, GenClause, K) :- 
	expand(Clause, GenClause, NewClause), 
	ordered_kassert(NewClause, K),
	fail.
all_expands_and_kasserts(_, _, _).

% expand(+SpecClause, +GenClause, -NewClause)
% 	SpecClause (`specialised clause') is a Clause for Goal, evaluated as 
%	far as the first non-operational subgoal in its body. 
% expand/3 finds an LPE proof for the remaining subgoals in SpecClause ([1]), 
%       and returns a revised clause (NewClause) for Goal based on this proof.
% See lpe.doc for more information.

expand(SpeClause, GenClause, NewClause) :-
		( tracing(off) -> true 
		; pretty_write(GenClause), write(' expands to...'), nl ),
	SpeClause = (       _ :- call(    _),    Nonop),
	GenClause = ( GenHead :- call(GenOp), GenNonop),
	lpe(Nonop, GenNonop, GenProof),					% [1]
	tidy_and_split(GenProof, ExtraGenOp, NewGenNonop),	        % [2]
	join(GenOp, ExtraGenOp, NewGenOp),                              % [3]
	NewClause = (GenHead :- call(NewGenOp),NewGenNonop),
		write('*'), ttyflush,				% Tracing...
		( tracing(off) -> true 
		; write('	'), pretty_write(NewClause), write('.'), nl ).

%	   --------------------------------------------------------------
%	   | ordered_kassert/{1,2} : Assert a new clause in the kassert |
%   	   | db in the right place, so that the clauses are ordered by  |
%   	   | (upper bound on) quality (best first).                     |
%   	   --------------------------------------------------------------

% ordered_kassert/{1,2}
% Search for the right place to insert NewClause in kassert db, and kassert it.
% [1] search from clause FromK downwards to find the last clause KInsert 
% which is better than NewClause, [2] insert NewClause immediately after 
% (kassertn0/2 and kassertn/2). See lpe.doc for more info.

ordered_kassert((Head:-Body)) :-
	kstartkey(Head, K0),     % get K0 = key of start flag for clauses with
	ordered_kassert((Head:-Body), K0).	  % same functor/arity as Head

ordered_kassert(NewClause, FromK) :-
	place_to_insert(NewClause, FromK, KInsert), !,		% [1]
	kassertn(NewClause, KInsert).				% [2]
ordered_kassert(_, _).				     % already know NewClause

% place_to_insert(+NewClause, +Key, -KeyToInsert)
% Given a NewClause (constructed by LPEing the clause with Key), find where
% to insert it in the domain theory so that clauses are ordered by quality.
place_to_insert(NewClause, K, KInsert) :-
	next_kclause(K, NextK),
	kclause(Head, Body, NextK),
	\+ lpe_better_than(NewClause, (Head:-Body)),   % ie. worse or equal to
	!,
	not_equivalent((Head:-Body), NewClause),
	place_to_insert(NewClause, NextK, KInsert).
place_to_insert(_, K, K).

% ----------
% not_equivalent(+Clause1, +Clause2).
% This allows LPE to ignore definitions it already knows. See not_equivalent/2
% in lpe.doc for more details.
not_equivalent(A, B) :- A \= B, !.
not_equivalent(A, B) :- \+ ( numbervars(A,0,N), numbervars(B,0,N), A=B ). %[1]

% ----------
lpe_better_than(Defn1, Defn2) :-		   % EBG mode only:
	Defn1 = (_:-call(_), true), 		   % = operational defn
	Defn2 = (_:-call(_),Nonop), Nonop \= true, % = non-operational defn
	learning_mode(ebg),			   % EBG: operational defns
	!.            				   % necc. better than nonop 
lpe_better_than(Defn1, Defn2) :-
	quality(Defn1, Q1),
	quality(Defn2, Q2),
	Q1 @< Q2.	% must define Q s.t. Q1 better than Q2 iff Q1 @< Q2

% ======================================================================
% Default (upper bound on) quality of a concept definition = length of the
% operational part (shorter the better). If there's a tie, prefer fully
% operational definitions to non-operational definitions.

quality(Defn, Q-Q2) :-
	Defn = (_ :- call(Op),Nonop),
	clause_length(Op, Q),
	( Nonop = true -> Q2 = 0 ; Q2 = 1 ).  % 2nd order term in case of tie

clause_length((A,B), L) :- !,
	clause_length(A, LA), 
	clause_length(B, LB),
	L is LA + LB.
clause_length(_, 1).

% ======================================================================
%			UTILITIES
% ======================================================================

% may_succeed(+Goal). 
% It is called in PE mode only, to see if there might be a solution to Goal.
% If not then this line of proof is abandoned.
% See lpe.doc for more comments.

may_succeed(Goal) :- \+ \+ pe_evaluate(Goal).

pe_evaluate(\+ Goal) :- \+ is_ground(Goal), !.
pe_evaluate(A \= B) :- \+ is_ground(A \= B), !.
pe_evaluate(nonvar(_)) :- !.
pe_evaluate(_ is B) :- \+ is_ground(B), !.
pe_evaluate(Goal) :- call(Goal).

% --------------------
% tidy_and_split(+Goals, -OpPart, -NonopPart).
% Splits Goals (eg. (a,b,c,d,e)) into its operational part (eg. (a,b)) and
% its `non-operational' part (eg. (c,d,e)), where the `non-operational' part
% is defined as the first non-operational subgoal (eg. c) and all subsequent
% subgoals.
% See lpe.doc for more comments.

tidy_and_split(Proof, Op, Nonop) :-
	tidy_and_split(Proof, Op, Nonop, _).

tidy_and_split((A,B), Op, Nonop, Flag) :-
	!,
	tidy_and_split(A, OpA, NonopA, FlagA),
	( FlagA = true ->		% (NonopA necessarily = true)    % [2]
		tidy_and_split(B, OpB, NonopB, FlagB),
		join(OpA,OpB,Op),  Nonop =  NonopB,       Flag = FlagB
	; 	Op =  OpA, 	   join(NonopA,B,Nonop),  Flag = FlagA ). %[3]
tidy_and_split(A, true, true, true) :- single_soln(A), !, call(A).	 % [4]
tidy_and_split(A, true, true, true) :- can_filter_out(A), !.		 % [6]
tidy_and_split(A,    A, true, true) :- operational(A), !.
tidy_and_split(A, true,   A, false).					 % [1]

single_soln(A) :- is_ground(A), !.					 % [5]
single_soln(_=_).

can_filter_out(_) :- fail.           % (in general, don't filter anything) [7]

% --------------------
% join(+Goal1, +Goals2, -Goals). 
% concatenates Goals1 and Goals2 together.
join(A, true, A) :- !.
join((A,B), C, (A,D)) :- !, join(B, C, D).
join(true, B, B) :- !.
join(A, B, (A,B)).

% ======================================================================
%		LIST THE LPE CLAUSES IN THE KASSERT DATABASE:
% ======================================================================

% lpe_listing(Functor/Arity).
% List the clauses with Head of functor/arity, stored in the kassert db.
% This is a minor variation of klisting/1 in kassert.pl, and filters out
% LPE's `syntactic sugar' of storing clauses `Head:-call(Op),NonOp', instead
% displaying them as Head:-Op,NonOp.

lpe_listing(F/A) :-
	functor(Head, F, A),
	kclause(Head, (call(Op),NonOp)),
	join(Op, NonOp, Body),
	( Body = true -> pretty_write(Head) ; pretty_write((Head:-Body)) ),
	write('.'), nl,
	fail.
lpe_listing(_).

% ======================================================================
%		ADDITIONAL UTILITIES
% ======================================================================

set_tracing_mode(Mode) :-
	member(Mode, [on,off]), !,
	retractall( tracing(_) ),
	assertz( tracing(Mode) ),
	write('Tracing is now '), write(Mode), write('.'), nl.
set_tracing_mode(_) :-
	write('Hey! You can only switch the tracing mode on or off!'),
	nl, fail.

set_learning_mode(Mode) :-
	member(Mode, [lpe,pe,ebg,none]), !,
	retractall( learning_mode(_) ),
	assertz( learning_mode(Mode) ),
	write('Learning mode is now '), write(Mode), write('.'), nl.
set_learning_mode(_) :-
	write('Invalid learning mode! Must be one of lpe, pe, ebg or none.'),
	nl, fail.

