ISO/IEC JTC1 SC22 WG17 post-N215
Draft proposal for setup_call_cleanup/3

Ulrich Neumerkel, 2011-09-21 (Version history)
This draft proposal is based on 8.15.5 of N208 2008-11-17. Earlier versions: N211 2009-07-02. N215 2009-10-29. It is intended to be added as 7.8.11. References refer to 13211-1:1995. Parts needing improvements are underlined.

Contributors

Bart Demoen (Belgium). Paulo Moura (Portugal). Jonathan Hodgson (USA). Richard O'Keefe (New Zealand). Feliks Kluzniak (USA). Jeffrey Rosenwald (USA). Markus Triska (Austria). Jan Wielemaker (The Netherlands). Vítor Santos Costa (Portugal). Jan Burse (Switzerland).

7.8.11 setup_call_cleanup/3

This control construct allows managing resources related to the execution of a goal. It provides protected setup and performs cleanup as soon as the goal has completed execution.
NOTE — A built-in predicate call_cleanup/2 with similar functionality is implemented in many existing processors. It is often used to free temporary resources. In the presence of interrupts (7.12 note c), call_cleanup/2 can cause leakage of resources: A processor receiving interrupts between resource allocation and call_cleanup/2 is unable to free the resource in a timely manner. To overcome this problem, setup_call_cleanup/3 protects resource allocation from interrupts.

7.8.11.1 Description

setup_call_cleanup(S, G, C) is true iff once(S), call(G) is true.

Procedurally, the control construct shall be executed as follows:

a) once(S) is executed while being protected from interrupts:
  1. either once(S) is executed entirely and the cleanup handler C is installed upon success;
  2. or once(S) is interrupted by an implementation dependent interrupt (7.12 note c). In this case, once(S) does not leave an observable effect.
b) After the cleanup handler is installed, call(G) is called.
c) The cleanup handler is called exactly once; no later than upon failure of G. Earlier moments are:
  1. If G is true or false, C is called at an implementation dependent moment after the last solution and after the last observable effect of G.
  2. When G or the continuation of G is interrupted by a call of throw/1 (7.8.9 a, b), whose corresponding call of catch/3 is above G.
    After executing the cleanup C, throw/1 continues to search the corresponding call of catch/3, regardless of the outcome of C. Thus, if C is interrupted by another call of throw/1, that throw/1 is lost.
  3. When the continuation of G executes a cut explicitly or implicitly that is associated with the cutparent of G (7.7.2, 7.8.4.1 Note 1b).
    After the cut suceeds, C is called in the place of the cut. A cut is performed implicitly for (->)/2 - if-then (7.8.7), (;)/2 - if-then-else (7.8.8), (\+)/1 (8.15.1), once/1 (8.15.2).
d) C is called as once(C). Failure of C is ignored. An explicit or implicit throw/1 is ignored in case 7.8.11.1 c2; otherwise (case 7.8.11.1 c1 or 7.8.11.1 c3), it is passed towards the corresponding call of catch/3.
Several cleanup goals triggered simultaneously are executed in reversed order of installation.

C shares bindings and variables with S, with G and with the continuation. The bindings up to and including S are always present. No further bindings are present when C is executed at 7.8.11.1 c2 and at the latest moment of 7.8.11.1 c1. At 7.8.11.1 c2, and at the earliest moment of 7.8.11.1 c1, all bindings are present at the time of calling the cut. All other cases of 7.8.11.1 c1 are implementation dependent.

A processor may restrict execution of C in an implementation defined manner. For example, a processor may restrict the handling of interrupts within C or limit resource consumption.

NOTES
1 Existing processors locate c1 either when the last solution is found, or upon failure. The precise moment is implementation dependent due to the varying ability of Prolog processors to detect determinism. For example, a processor may execute call((call(G);fail)) as call(G) and vice versa.
2 For nonterminating goals like repeat (8.15.3) only c2 and c3 can trigger the cleanup. And if a nonterminating goal does not find a solution, only c2 can trigger.
3 A throw/1 of G has priority over one of C (7.8.11.1 c2). This is an advantage when recovering errors, like resource errors (7.12.2 h), where the error of C is often a consequence of the error of G.

7.8.11.2 Template and modes

setup_call_cleanup(goal, goal, goal)
NOTE — The arguments should eventually be +callable_term. However, neither type nor mode is checked when this control construct is executed (8.1.2.2).

7.8.11.3 Errors

a) S is a callable term that is not permitted due to an implementation defined restriction
representation_error(setup_goal).
b) S finds a solution, but C is a variable
instantiation_error. Goal G is not executed.
c) S finds a solution, but C is neither a variable nor a callable term
type_error(callable, C). Goal G is not executed.
NOTE — Further errors in S and G may happen due to calling once/1 or call/1 (see 7.8.11.1).

7.8.11.4 Examples

In the following, write/1 uses the standard operator table (6.3.4.4, table 7). Unique variables are written as _ which is one possible way to realize 7.10.5 a. The different outcomes (labeled either/or) are due to the implementation dependence in c1.
setup_call_cleanup(fail, _, _).
   Fails.  Neither the goal nor the cleanup is executed.

setup_call_cleanup(throw(ex), _, _).
   System error due to uncaught ex.

setup_call_cleanup(true, throw(unthrown),_).
   Instantiation error.

setup_call_cleanup(true, true, ( true ; throw(x) )).
   Succeeds. No system error.

setup_call_cleanup(true, X = 1, X = 2).
   Succeeds, unifying X = 1.

setup_call_cleanup(true, true, X = 2).
   Either: Succeeds, unifying X = 2.
   Or: Succeeds.

setup_call_cleanup(true, X=true,X).
   Instantiation error.

setup_call_cleanup(X=throw(ex), true, X).
   Either: System error due to uncaught ex.
   Or: Succeeds. System error on backtracking.

setup_call_cleanup(true, true, fail).
   Succeeds.

setup_call_cleanup(S=1, G=2, C=3).
   Either: Succeeds, unifying S = 1, G = 2, C = 3.
   Or:  Succeeds, unifying S = 1, G = 2.

setup_call_cleanup((S=1;S=2), G=3, C=4).
   Either: Succeeds, unifying
           S = 1, G = 3, C = 4.
   Or: Succeeds, unifying S = 1, G = 3.

setup_call_cleanup(S=1,G=2,write(S+G)).
   Succeeds, unifying S = 1, G = 2.
   Either: outputs '1+2'
   Or: outputs on backtracking '1+_' prior to failure.
   Or (?): outputs on backtracking '1+2' prior to failure.

setup_call_cleanup(S=1,(G=2;G=3),write(S+G)).
   Succeeds, unifying S = 1, G = 2.
   On backtracking, succeeds unifying S = 1 and G = 3.
   Either: outputs '1+3'
   Or: on backtracking outputs '1+_' prior to failing
   Or (?): on backtracking outputs '1+3' prior to failing.

setup_call_cleanup(S=1,G=2,write(S+G>A+B)), A=3, B=4.
   Succeeds, unifying S=1,G=2,A=3,B=4.
   Either:  Outputs one of the following before succeeding
   1+2>_+_.   1+2>3+_.   1+2>3+4.   Disputable: 1+2>_+4.
   Or: outputs one of the above outputs on backtracking
   prior to failing.

setup_call_cleanup(S=1,(G=2;G=3,throw(x)),write(S+G)).
   Succeeds, unifying S = 1, G = 2.
   On backtracking, outputs '1+_' prior
   to system error due to uncaught x.

setup_call_cleanup(open(f,read,S),read(S,X),close(S)).
   Opens file f for reading, reads a term and closes the file.
   Succeeds, unifying S with an implementation dependent stream
   term, unifying X with the term read.  The file is closed, either
   immediately, or on backtracking, and even if there is an error in
   read.

Events in the continuation

setup_call_cleanup(S=1,(G=2;G=3),write(S+G>B)), B=4, !.
   Outputs '1+2>4'. Succeeds, unifying S = 1, G = 2, B = 4.

setup_call_cleanup(S=1,G=2,write(S+G>B)),B=3,!.
   Either: Outputs  '1+2>_'. Succeeds, unifying
      S = 1, G = 2, B = 3.
   Or:  Outputs '1+2>3'. Succeeds, unifying
      S = 1, G = 2, B = 3.

setup_call_cleanup(S=1,(G=2;fail),write(S+G>B)), B=3, !.
   Same as above.

setup_call_cleanup(S=1,(G=2;S=2),write(S+G>B)), B=3, !.
   Same as above.

setup_call_cleanup(S=1,(G=2;G=3), write(S+G>B)), B=4, throw(x).
   Outputs '1+_>_'. system_error due to uncaught x.

setup_call_cleanup(S=1,(G=2;G=3), write(S+G>B)), B=4, !, throw(x).
   Outputs '1+2>4'. system_error due to uncaught x.

setup_call_cleanup(true, (X=1;X=2), write(a)),
      setup_call_cleanup(true,(Y=1;Y=2),write(b)), throw(x).
   Outputs 'ba'. system_error due to uncaught x.

setup_call_cleanup(true, (X=1;X=2), write(a)),
      setup_call_cleanup(true,(Y=1;Y=2),write(b)), !.
   Outputs 'ba'.  Succeeds, unifying X = 1, Y = 1.

Multiple exceptions

catch(setup_call_cleanup(true,throw(goal),throw(cl)), Pat, true).
   Succeeds unifying Pat = goal.

catch(( setup_call_cleanup(true,(G=1;G=2),throw(cl)), throw(cont)), Pat, true).
   Succeeds unifying Pat = cont.

catch(
   setup_call_cleanup(true, throw(a),
      setup_call_cleanup(true,fail,throw(b))
      ),
   Pat, true).
   Succeeds unifying Pat with a.

Implementation status

Systems with setup_call_cleanup/3

SWI: fully compilant since 2009-11-13. Almost fully since Autumn 2007.
YAP-6: only difference: c3 and order
Qu-Prolog: almost fully compliant. Open: error b, c, bindings for c2.
B-Prolog: very close. Open: bindings (in d) for c2 different. Different priorities in c2.
ECLiPSe: As implementation specific library. No bindings between Cleanup and Goal. throw/1 from Cleanup not visible.

Systems with call_cleanup/2

The following implementations are compared using
setup_call_cleanup(S, G, C) :-
   once(S),
   call_cleanup(G, C).
SICStus: original implementation 1997. c2 different. d is not called as once(C). fixed in 4.8.0 2022-12.
CxProlog: No bindings between Cleanup and Goal
XSB: sometimes the cleanup is not executed for c2 and c3 (as of 2009-07)
untested: K-Prolog

Validated HTML