Skip to content

Commit 5869513

Browse files
committed
Skip if_/3 inlining if Then_0 is unsafe or invalid
As a part of optimization goal expansion in library(reif) inlines Then_0 argument verbatim into predicate body – this avoids unnecessary call/N invocations and dramatically increases performance, but not all goals are safe to be inlined in such a way. Here we are skipping this optimization if !s or invalid goal were detected to prevent undesired side-effects from leaking into outer goals.
1 parent 487794a commit 5869513

File tree

3 files changed

+100
-7
lines changed

3 files changed

+100
-7
lines changed

src/lib/reif.pl

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525

2626
:- use_module(library(dif)).
27+
:- use_module(library(loader), [goal_sanitized/2]).
2728

2829
:- meta_predicate(if_(1, 0, 0)).
2930
:- meta_predicate(cond_t(1, 0, ?)).
@@ -91,19 +92,16 @@
9192
sameargs(0, _, _).
9293

9394

94-
/*
95-
no !s that cut outside.
96-
no variables in place of goals
97-
no malformed goals like integers
98-
*/
9995

10096

10197
/* 2do: unqualified If_1: error
10298
*/
10399

104100
%
105101
user:goal_expansion(if_(If_1, Then_0, Else_0), G_0) :-
106-
ugoal_expansion(if_(If_1, Then_0, Else_0), G_0).
102+
goal_sanitized(Then_0, SanitizedThen_0),
103+
goal_sanitized(Else_0, SanitizedElse_0),
104+
ugoal_expansion(if_(If_1, SanitizedThen_0, SanitizedElse_0), G_0).
107105

108106
%
109107
%

src/loader.pl

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
strip_module/3,
99
use_module/1,
1010
use_module/2,
11-
current_module/1
11+
current_module/1,
12+
goal_sanitized/2
1213
]).
1314

1415
:- use_module(library(error)).
@@ -42,6 +43,67 @@
4243
nl,
4344
'$fail'.
4445

46+
47+
never(_) :- fail.
48+
49+
50+
%% goal_sanitized(?G_0, ?S_0).
51+
%
52+
% Both `G_0` and `S_0` are valid callable terms having the same meaning, but
53+
% additionally `S_0` is safe to be called in combination with surrounding
54+
% goals, without worrying that cut side-effect will escape and contaminate
55+
% outer goals. `S_0 = call(G_0)` when it contains callable !s that cut outside,
56+
% and `S_0 = G_0` otherwise.
57+
%
58+
% For example: given `G_0 = (a,!)` then compound goal `b,a,!` will remove
59+
% choice points generated by `b`, but since `S_0 = call((a,!))` then
60+
% `b, call((a,!))` is safe.
61+
%
62+
% TODO: Should it be marked with meta_predicate(goal_sanitized(:,-))?
63+
%
64+
goal_sanitized(G, S) :-
65+
catch(goal_sanitized_aux(G, S), stop(_), false).
66+
67+
goal_sanitized_aux(G, call(G)) :- cuts_outside(G).
68+
goal_sanitized_aux(G, G ) :- \+ cuts_outside(G).
69+
70+
71+
%% cuts_outside(?G_0).
72+
%
73+
% `G_0` is a goal for which side-effects of a cut may spill out to the
74+
% surrounding goals. Throws `stop(_)` when it doesn't represent a valid goal.
75+
%
76+
% For example it succeeds for terms `a, (!, b)` and `a, b -> !` where cut
77+
% removes choice points generated by `a`, but fails for `a, (! -> b)` and
78+
% `a, \+ \+ !`.
79+
%
80+
cuts_outside(G_0) :- cuts_outside(G_0, =(!)).
81+
82+
%% cuts_outside(?G_0, +StopCondition_1).
83+
%
84+
cuts_outside(G, C_1) :- callable_term(G), cuts_outside_aux(G, C_1).
85+
86+
cuts_outside_aux(G, C_1) :- call(C_1, G).
87+
cuts_outside_aux(M:A, C_1) :- module_name(M), cuts_outside(A, C_1).
88+
cuts_outside_aux((A,B), C_1) :- cuts_outside(B, C_1); cuts_outside(A, C_1).
89+
cuts_outside_aux((A;B), C_1) :- cuts_outside(B, C_1); cuts_outside(A, C_1).
90+
% FIXME: There is an issue with `C, (! -> B)` construct, see #2739
91+
cuts_outside_aux((A->B), C_1) :- cuts_outside(A, loader:never); cuts_outside(B, C_1).
92+
93+
94+
module_name(M) :-
95+
atom(M) -> true; throw(stop(type_error(atom,M))).
96+
97+
98+
callable_term(T) :-
99+
callable(T) ->
100+
( acyclic_term(T) ->
101+
true
102+
; throw(stop(type_error(acyclic_term,T)))
103+
)
104+
; throw(stop(type_error(callable,T))).
105+
106+
45107
expand_term(Term, ExpandedTerm) :-
46108
( '$predicate_defined'(user, term_expansion, 2),
47109
catch(user:term_expansion(Term, ExpandedTerm0),

src/tests/reif.pl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,39 @@
110110
Solutions == [if_(1=1,a=a,2), error(type_error(callable,2),call/1)]
111111
)).
112112

113+
test(W, loader:call(T)) :-
114+
member(T, [
115+
cuts_outside(!),
116+
cuts_outside(foo:!),
117+
cuts_outside((a,!)),
118+
cuts_outside((!;b(_))),
119+
cuts_outside(((a;b(_,_);c),!,d)),
120+
\+ cuts_outside(call((a,!))),
121+
\+ cuts_outside(((a;b;c),\+ !,d)),
122+
\+ cuts_outside((! -> a; b)),
123+
\+ cuts_outside(((x,!;y) -> a; b)),
124+
catch((cuts_outside(_),false), E0, E0 = stop(type_error(callable,_))),
125+
catch((cuts_outside(2),false), E1, E1 == stop(type_error(callable,2))),
126+
catch((cuts_outside(1:!),false), E2, E2 == stop(type_error(atom,1))),
127+
catch((cuts_outside(_:!),false), E3, E3 = stop(type_error(atom,_))),
128+
(G0 = a(G0), catch((cuts_outside(G0),false), E4, E4 = stop(type_error(acyclic_term,_)))),
129+
(G1 = m:G1, catch((cuts_outside(G1),false), E5, E5 = stop(type_error(acyclic_term,_)))),
130+
catch((cuts_outside((6->a)),false), E6, E6 == stop(type_error(callable,6))),
131+
(goal_sanitized(a, X0), X0 == a),
132+
(goal_sanitized(!, X1), X1 == call(!)),
133+
(goal_sanitized((a,b;c,d), X2), X2 == (a,b;c,d)),
134+
(goal_sanitized((\+ \+ a), X3), X3 == (\+ \+ a)),
135+
% Questionable test case, see #2739
136+
(goal_sanitized((!,a->c;d), X4), X4 == (!,a->c;d)),
137+
(goal_sanitized((x,a->!;d), X5), X5 == call((x,a->!;d))),
138+
(goal_sanitized((a,b,c,!), X6), X6 == call((a,b,c,!))),
139+
\+ goal_sanitized(0, _),
140+
\+ goal_sanitized(_, _),
141+
\+ goal_sanitized((a,_), _),
142+
\+ goal_sanitized((a,b;1), _)
143+
]),
144+
phrase(format_("callable cut: ~q", [T]), W).
145+
113146
result_or_exception(Goal, Result) :-
114147
catch((Goal,Result=Goal), Result, true).
115148

0 commit comments

Comments
 (0)