Prolog for Verification, Analysis and Transformation Tools
LL. Fribourg and M. Heizmann (Eds.): VPT/HCVS 2020EPTCS 320, 2020, pp. 80–94, doi:10.4204/EPTCS.320.6 c (cid:13)
Michael LeuschelThis work is licensed under theCreative Commons Attribution License.
Prolog for Verification, Analysis and Transformation Tools
Michael Leuschel
Lehrstuhl Softwaretechnik und ProgrammiersprachenInstitut f¨ur InformatikHeinrich-Heine-Universit¨at D¨usseldorfUniversit¨atsstr. 1D-40225 D¨usseldorf, Germany [email protected]
This article examines the use of the Prolog language for writing verification, analysis and transfor-mation tools. Guided by experience in teaching and the development of verification tools like P RO Bor specialisation tools like
ECCE and
LOGEN , the article presents an assessment of various aspects ofProlog and provides guidelines for using them. The article shows the usefulness of a few key Prologfeatures. In particular, it discusses how to deal with negation at the level of the object programs beingverified or analysed.
Over the years I have written a variety of tools for verification and transformation, mainly using the Pro-log programming language. Indeed, over the years, I found out that Prolog is both a convenient languageto express the semantics of various programming and specification languages as well as transformationand verification rules and algorithms.My first intense engagement with Prolog was initiated in my Master’s thesis at the KU Leuven,with the goal of writing a partial evaluator for Prolog. Initially I was actually inclined to write thepartial evaluator in a functional programming language. Indeed, my initial contacts with Prolog in the AIcourse at the University of Brussels were not all that compelling to me: Prolog seemed like a theoreticallyappealing language, but practically leading to programs that often either loop or say “no”. While I haveobviously revised my opinion since then, I encounter this initial disappointment and confusion every yearin the eyes of some of the students attending a logic programming course. I have also encountered severalstudents who in their bachelor’s or master’s thesis wanted to implement program analysis techniques forProlog, but were also afraid to write the tools and algorithms themselves in Prolog. I can understand theiranxiety, but try to convince them (not always with success), to use Prolog to write their Prolog analysisalgorithms. Indeed, once the initial hurdles are overcome, Prolog is a very nice language for programverification and analysis, both in research and teaching. For example, in my experience, the manipulationand transformation of abstract syntax trees can often be done much more compactly, reliably and alsoefficiently (both memory and time wise) in Prolog than in more mainstream languages such as Java. Idon’t believe that I would have been able to develop and maintain the core of the P RO B validation tool[23, 24] in an imperative or object-oriented language (without re-inventing a Prolog-inspired library).In the rest of this article I will mention some noteworthy aspects of Prolog for verification and programanalysis tools. This is a follow-on from the article [21] from 2008, also providing arguments for usingdeclarative programming languages for verification. I will look at three issues in more detail in Section 2: • non-determinism, in particular for deterministic languages • unification ichael Leuschel • how to handle negationI will also re-examine some statements from [21] in Section 3. It is particularly easy to write an interpreter for Prolog in Prolog; the plain vanilla interpreter consists justof three small clauses (see, e.g., [16, 5]). Prolog is also a convenient language to express the semanticsof other programming or specification languages. In my lectures, I encode the operational semantics of awide range of imperative languages in Prolog, ranging from three-address code [1] and Java byte code tomore complex languages. In research, I found Prolog useful for the semantics of Petri nets [26, 12], theCSP process algebra in Prolog [25] or the B specification language [23, 24]. A lot of other researchershave encoded various languages in Prolog: Verilog [7], Erlang [6], Java Bytecode [14, 2, 3, 4], processalgebras [34], to name just a few. More recently, constrained Horn clause programs have become verypopular to encode imperative programs [15] and have led to new techniques such as [19].
Prolog’s non-determinism is of course very convenient when modelling non-deterministic specificationlanguages. Operational semantic rules can often be translated to Prolog clauses. Take, for example, thesetwo inference rules for a prefix operator → and an interleaving operator (cid:107) in a process algebra inspired byCSP, where X a (cid:32) X (cid:48) means that the process X can execute the action a and then behave like the process X (cid:48) : a → Y a (cid:32) Y X a (cid:32) X (cid:48) X (cid:107) Y a (cid:32) X (cid:48) (cid:107) Y Y a (cid:32) Y (cid:48) X (cid:107) Y a (cid:32) X (cid:107) Y (cid:48) These rules can be encoded in Prolog as follows, where trans/3 encodes the ternary semanticsrelation (cid:32) : trans(’->’(A,Y),A,Y).trans(’||’(X,Y),A,’||’(X2,Y)) :- trans(X,A,X2).trans(’||’(X,Y),A,’||’(X,Y2)) :- trans(Y,A,Y2). We can then determine that the process a → stop (cid:107) b → stop can perform two possible actions: | ?- trans(’||’(’->’(a,stop),’->’(b,stop)),A,R).A = a,R = ’||’(stop,(b->stop)) ? ;A = b,R = ’||’((a->stop),stop) ? ;no We can also compute the two possible traces of length two: | ?- trans(’||’(’->’(a,stop),’->’(b,stop)),A1,_R1), trans(_R1,A2,R2).A1 = a,A2 = b,R2 = ’||’(stop,stop) ? ;A1 = b,A2 = a,R2 = ’||’(stop,stop) ?yes Prolog for Verification, Analysis and Transformation
It is quite straightforward to perform exhaustive model checking for such specifications in Prolog,in particular if we have access to tabling (aka memoization) to detect repeated reachable states (or pro-cesses), see, e.g., [34, 27].However, Prolog’s non-determinism also comes in handy for deterministic imperative languages,when moving from interpretation to analysis. Here is an excerpt of an interpreter for a subset of the JavaBytecode, which I use in my lectures. Every instruction in the Java Bytecode consists of an opcode (onebyte), followed by its arguments. Java Bytecode uses an operand stack to store arguments to operatorsand to push results of operators. For example, the imul opcode removes the topmost (integer) valuesfrom the stack an pushes the result of the multiplication back onto the stack. The code fragment below shows the code for the iconst opcode to push a constant onto the operatorstack, iop to perform a binary arithmetic operation on the two topmost stack elements, dup to duplicatethe topmost value on the stack, return to stop a method (and return void), and a conditional if1 whichjumps to a given label if an operator (applied to the topmost stack element and a provided constant)returns true.Every instruction in the Java Bytecdoe consists of an opcode (one byte), followed by its arguments.As mentioned above, imul and iadd take no arguments. However, these opcodes are converted forour interpreter into the generic iop instruction (which obviously does not exist as such in the Java vir-tual machine) with the operator as argument. A similar grouping of opcodes has been performed forthe conditional instruction, e.g., the bytecode instruction ifle 25 gets translated into the Prolog term if1(<=,0,25) for our interpreter. Similarly, opcodes like iconst_2 take no arguments, but in theProlog representation below this is represented for simplicity as the Prolog term iconst(2) . The JavaBytecode object program is represented by instr(PC,Opcode,Size) facts, where PC is the position (inbytes) of the opcode, Opcode the Prolog term describing the opcode, and
Size the size in bytes of theopcode (which is needed to determine the position in bytes of the next opcode). This is a small artificialprogram, which computes 2*2 and then decrements the value until it reaches 0: instr(0,iconst(2),1).instr(1,iconst(2),1).instr(2,iop(*),1).instr(3,iconst(-1),1).instr(4,iop(+),1).instr(5,dup,1).instr(6,if1(’>’,0,3),3).instr(9,return,0).
The core of the interpreter contains the following clauses. interpreter_loop(PC,In,Out) :-instr(PC,Opcode,Size),NextPC is PC+Size,format(’> ~w ~w --> ~w~n’,[PC,In,Opcode]),ex_opcode(Opcode,NextPC,In,Out)....ex_opcode(iconst(Const),NextPC,In,Out) :-push(In,Const,Out2),interpreter_loop(NextPC,Out2,Out).ex_opcode(dup,NextPC,In,Out) :-top(In,Top),push(In,Top,Out2), Java Bytecode is thus also zero-address-code, as such instructions do not take arguments: they implicitly know where theoperands are and where the result should stored. ichael Leuschel interpreter_loop(NextPC,Out2,Out).ex_opcode(if1(OP,Cst,Label),NextPC,In,Out) :-pop(In,RHSVAL1,In2),if_then_else(OP,RHSVAL1,Cst,Label,NextPC,In2,Out).ex_opcode(iop(OP),NextPC,In,Out) :-pop(In,RHSVAL1,In1),pop(In1,RHSVAL2,In2),ex_op(OP,RHSVAL1,RHSVAL2,Res),push(In2,Res,Out2),interpreter_loop(NextPC,Out2,Out).ex_opcode(return,_,Env,Env)....if_then_else(OP,Arg1,Arg2,_TrueLabel,FalseLabel,In,Out) :-false_op(OP,Arg1,Arg2),interpreter_loop(FalseLabel,In,Out).if_then_else(OP,Arg1,Arg2,TrueLabel,_FalseLabel,In,Out) :-true_op(OP,Arg1,Arg2),interpreter_loop(TrueLabel,In,Out).ex_op(*,A1,A2,R) :- R is A1 * A2.ex_op(+,A1,A2,R) :- R is A1 + A2.ex_op(-,A1,A2,R) :- R is A1 - A2.true_op(<=,A1,A2) :- A1 =< A2.true_op(>,A1,A2) :- A1 > A2.false_op(<=,A1,A2) :- A1 > A2.false_op(>,A1,A2) :- A1 =< A2.pop(env([X|S],Vars),Top,R) :- !, Top=X,R=env(S,Vars).pop(E,_,_) :- print(’*** Could not pop from stack: ’),print(E),nl,fail.top(env([X|_],_),X).push(env(S,Vars),X,env([X|S],Vars)). We can execute the interpreter for the above bytecode and an initial empty environment (the environ-ment contains as first argument the stack and as second argument values for local variables, which we donot use here): | ?- interpreter_loop(0,env([],[]),Out).> 0 env([],[]) --> iconst(2)> 1 env([2],[]) --> iconst(2)> 2 env([2,2],[]) --> iop(*)> 3 env([4],[]) --> iconst(-1)> 4 env([-1,4],[]) --> iop(+)> 5 env([3],[]) --> dup> 6 env([3,3],[]) --> if1(>,0,3)> 3 env([3],[]) --> iconst(-1)> 4 env([-1,3],[]) --> iop(+)> 5 env([2],[]) --> dup> 6 env([2,2],[]) --> if1(>,0,3)> 3 env([2],[]) --> iconst(-1)> 4 env([-1,2],[]) --> iop(+)> 5 env([1],[]) --> dup> 6 env([1,1],[]) --> if1(>,0,3)> 3 env([1],[]) --> iconst(-1) Prolog for Verification, Analysis and Transformation > 4 env([-1,1],[]) --> iop(+)> 5 env([0],[]) --> dup> 6 env([0,0],[]) --> if1(>,0,3)> 9 env([0],[]) --> returnOut = env([0],[]) ?yes
As you can see, the above interpreter is deterministic : when given a state and an opcode it will com-pute just one solution for the successor state after execution of the opcode. In Prolog one can easilytransform such an interpreter into an analysis tool, for either data flow analysis [1] or abstract interpre-tation [11]. In that case the Prolog program becomes non-deterministic . For example, to transform theabove interpreter into an abstract interpreter, one has to define abstract operations, such as an abstractmultiplication or an abstract “less or equal” test, over some abstract domain. The abstract domain herecontains the following abstract values: • pos to stand for the positive integers • neg to denote the negative integers • to denote the single value 0 • top to denote all integersThe bottom value is not needed here in the Prolog interpreter; it is represented implicitly by Prolog failureof the interpreter. ex_op(*,0,_,0).ex_op(*,pos,X,X).ex_op(*,neg,0,0).ex_op(*,neg,pos,neg).ex_op(*,neg,neg,pos).ex_op(*,neg,top,top).ex_op(*,top,0,0).ex_op(*,top,X,top) :- X\=0.ex_op(+,0,X,X).ex_op(+,pos,0,pos).ex_op(+,pos,pos,pos).ex_op(+,pos,neg,top).ex_op(+,pos,top,top).ex_op(+,neg,0,neg).ex_op(+,neg,pos,top).ex_op(+,neg,neg,neg).ex_op(+,neg,top,top).ex_op(+,top,_,top).true_op(<=,X,X).true_op(<=,top,X) :- X \= top.true_op(<=,neg,X) :- X \= neg.true_op(<=,0,pos).true_op(<=,0,top).true_op(<=,pos,top).true_op(>,_,top).true_op(>,_,neg).true_op(>,pos,0).true_op(>,top,0). ichael Leuschel true_op(>,pos,pos).true_op(>,top,pos).false_op(<=,A1,A2) :- true_op(>,A1,A2).false_op(>,A1,A2) :- true_op(<=,A1,A2). For example, both the call test_op(<=,pos,pos) and false_op(<=,pos,pos) succeeds, and the if_then_else predicate becomes non-deterministic. This is illustrated in Figures 1 and 2 for an opcode ifle 5000 , which would be encoded if1(<=,0,5000) in our Prolog interpreter.To run our interpreter, we first need to use an abstract version of our bytecode program: instr(0,iconst(pos),1).instr(1,iconst(pos),1).instr(2,iop(*),1).instr(3,iconst(neg),1).instr(4,iop(+),1).instr(5,dup,1).instr(6,if1(’>’,0,3),3).instr(9,return,0).
We can now run the same query as above. This time there are infinitely many solutions (paths)through our program. We show the first three: | ?- interpreter_loop(0,env([],[]),R).> 0 env([],[]) --> iconst(pos)> 1 env([pos],[]) --> iconst(pos)> 2 env([pos,pos],[]) --> iop(*)> 3 env([pos],[]) --> iconst(neg)> 4 env([neg,pos],[]) --> iop(+)> 5 env([top],[]) --> dup> 6 env([top,top],[]) --> if1(>,0,3)> 9 env([top],[]) --> returnR = env([top],[]) ? ;> 3 env([top],[]) --> iconst(neg)> 4 env([neg,top],[]) --> iop(+)> 5 env([top],[]) --> dup> 6 env([top,top],[]) --> if1(>,0,3)> 9 env([top],[]) --> returnR = env([top],[]) ? ;> 3 env([top],[]) --> iconst(neg)> 4 env([neg,top],[]) --> iop(+)> 5 env([top],[]) --> dup> 6 env([top,top],[]) --> if1(>,0,3)> 9 env([top],[]) --> returnR = env([top],[]) ?
To transform the interpreter into a terminating abstract interpreter one would still need to store vis-ited program points and corresponding abstract environments and perform the least upper bound of allabstract environments for any given program point (this could be done in the interpreter_loop pred-icate).
You may have noticed that in the above concrete interpreter, the if_then_else predicate was not usingProlog negation \+ or the Prolog if-then-else ( Tst -> Thn ; Els) , but used a predicate true_op for6 Prolog for Verification, Analysis and Transformation Current Adress A 5532-2 ...LocalVariables ...OperandStack
New Adress (A +1 )ConcreteInterpreterExecution Stepifle 5000 Figure 1: Deterministic Concrete Interpreter Step posposneg ...LocalVariables ...OperandStack ⊤ Current Adress A posposneg ...LocalVariables ...OperandStack
New Adress (A +1 )ifle 5000 posposneg ...LocalVariables ...OperandStack New Adress ( )AbstractInterpreterExecution Steps
Figure 2: Non-Deterministic Abstract Interpreter Step ichael Leuschel
87a successful comparison operator and false_op for a failed comparison. Indeed, the use of the Prolognegation would have prevented the transition from the concrete to the abstract interpreter.In fact, is rarely a good idea to use Prolog negation to represent negation of the language beinganalysed. The reason is that Prolog’s built-in negation is not logical negation but so-called “negation-as-failure”. This negation can be given a logical description only when its arguments contain no logicalvariables at the moment it is called. To understand this issue let us examine a simpler program: int(0).int(s(X)) :- int(X).
Below are three queries, to this program: ?- \+ int(a). /* succeeds */?- \+ int(X), X=a. /* fails */?- X=a, \+ int(X). /* succeeds */
As you can see in the last two queries, conjunction is not commutative here and the Prolog negationis not declarative, i.e., it cannot be described within logic (where conjunction is commutative). Moreimportantly, you can see that the query int(X) fails: we cannot use Prolog’s negation to find valueswhich make a predicate false. This is what we required in the abstract interpreter above: find valueswhich lead to a comparison operator to fail and lead to alternate paths through the bytecode program.To safely use negation (inside an interpreter) there are basically four solutions. The first is to alwaysensure that there are no variables when we call the Prolog negation. This may be difficult to achieve insome circumstances; and generally means we can use the interpreter in only one specific way. For ourabstract interpreter above, this means that we cannot use the interpreter to find values and computationpaths which lead to a comparison operator to fail.The second solution is to delay negated goals until they become ground. This can be achieved usingthe built-in predicate when/2 of Prolog. The call when(Cond,Call) waits until the condition
Cond becomes true, at which point
Call is executed. While
Call can be any Prolog goal,
Cond can only use: nonvar(X) , ground(X) , ?=(X,Y) , as well as combinations thereof combined with conjunction (,) anddisjunction (;). With this built-in we can implement a safe version of negation, which will ensure that theProlog negation is only called when no variables are left inside the negated call: safe_not(P) :- when(ground(P), \+(P)). A disadvantage of this approach are refutations which lead to a so-called floundering goal, whereall goals suspend. In that case, one does not know whether the query is a logical consequence of theprogram or not. The G¨odel programming language [17] supported such a safe version of negation. Forprogram analysis tools, however, we again have the problem that we cannot use this kind of negation tofind values for variables.A third solution is to move to another negation, e.g., constructive negation, or well-founded or stablemodel semantics and the associated negation. This is available, e.g., in answer-set programming [29] butnot in the mainstream Prolog systems.Finally, the best solution is to circumvent this problem all together, and use no negation at all. Herewe do this by explicitly writing a predicate for negated formulas. This is what we have done above, inthe form of the predicates true_op and false_op . Below is an illustration of this approach for a smallinterpreter for propositional logic: int(const(true)).int(and(X,Y)) :- int(X), int(Y).int(or(X,Y)) :- int(X) ; int(Y). Prolog for Verification, Analysis and Transformation int(not(X)) :- neg_int(X).neg_int(const(false)).neg_int(and(X,Y)) :- neg_int(X) ; neg_int(Y).neg_int(or(X,Y)) :- neg_int(X),neg_int(Y).neg_int(not(X)) :- int(X).
This interpreter now works as expected for negation and partially instantiated queries: | ?- int(not(const(X))).X = false ? ;no
This interpreter thus actively searches for solutions to the negated formulas. This technique is usedwithin the P RO B system to handle negation in the B specification language. Observe, that had we usedProlog’s negation to define the negation in our object programs as int(not(X)) :- \+ int(X). the answer to the above query would have been no . Unification is often useful for looking up information in a program database and to model semanticsrules. For example, in our Java bytecode interpreter above, we can look for conditional jumps to acertain position by unifying with the instruction database: | ?- instr(FromPC,if1(_,_,ToPC),_).FromPC = 6,ToPC = 3 ?yes
Sometimes unification is not just useful but essential, e.g., when implementing type inference. Hereis a small demo of Hindley-Milner style [30], written in Prolog with DCGs (Definite Clause Grammars[32]). DCGs were initially developed for parsing but are also useful for threading environments ininterpreters and in this case type checkers. Note that these two DCG clauses t(a) --> [].t(b(A,B)) --> t(A),t(B). denote this Prolog fact and rule respectively: t(a,E,E).t(b(A,B),In,Out) :- t(A,In,Env),t(B,Env,Out).
We now encode type inference for a small language containing operations ( union , intersect )and predicates( in_set ) on sets , arithmetic operations ( plus ) and predicates ( gt ), as well as logicalconjunction ( and ) and generic equality ( eq ).The operators are polymorphic. For example, eq can be applied to integers and sets of values.Similarly, union can be applied to sets of values, but in any given set all values must have the sametype. The predicate type(V,Type,In,Out holds if the value V has type Type given the initial typeenvironment In . The output environment may contain additional variables which are henceforth defined. ichael Leuschel type([],set(_)) --> !, [].type(union(A,B),set(R)) --> !,type(A,set(R)), type(B,set(R)).type(intersect(A,B),set(R)) --> !,type(A,set(R)), type(B,set(R)).type(plus(A,B),integer) --> !,type(A,integer), type(B,integer).type(in_set(A,B),predicate) --> !,type(A,TA), type(B,set(TA)).type(gt(A,B),predicate) --> !,type(A,integer), type(B,integer).type(and(A,B),predicate) --> !,type(A,predicate),type(B,predicate).type(eq(A,B),predicate) --> !,type(A,TA),type(B,TA).type(Nr,integer) --> {number(Nr)},!.type([H|T],set(TH)) --> !,type(H,TH), type(T,set(TH)).type(ID,TID) --> {identifier(ID)},\+ defined(id(ID,_)),!,add((id(ID,TID))). % creates fresh variabletype(ID,TID) --> {identifier(ID)},defined(id(ID,TID)),!.type(Expr,T,Env,_) :-format(’Type error for ~w (expected: ~w, Env: ~w)~n’,[Expr,T,Env]),fail.defined(X,Env,Env) :- member(X,Env).add(X,Env,[X|Env]).identifier(ID) :- atom(ID), ID \= [].type(Expr,Result) :- type(Expr,Result,[],Env), format(’Typing env: ~w~n’,[Env]). Note that the identifier predicate uses Prolog’s negation implicitly in the form of the \= operator.This is, however, not an issue as the arguments are all ground.Observe how, e.g., the rule for the union operator requires that the result and each argument ( A and B ) is of a set type, but it uses unification of the shared variable R to ensure that all elements in all setshave the same type (namely R ).We can use this small program to perform type inference on the following formula { z } ∪ { x , y } = y ∧ z > v We can correctly determine the types of all variables in a single pass: | ?- type(and(eq(union([z],[x,y]),u),gt(z,v)),R).Typing env: [id(v,integer),id(u,set(integer)),id(y,integer),id(x,integer),id(z,integer)]R = predicate ?yes
In some cases, the type inference algorithm will not return a ground type. E.g., here we compute thetype of all variables in the formula x = /0 ∪ /0: | ?- type(eq(x,union([],[])),R).Typing env: [id(x,set(_1631))]R = predicate ?yes We see that x is a set, but we do not know what type its elements are. The program can also be usedto generate type error messages: | ?- type(and(eq(x,1),eq([],x)),R).Type error for x (expected: set(_2167), Env: [id(x,integer)])no A more complex version of this interpreter is used successfully for type inference for the B specifi-cation language within the P RO B validation tool.0
Prolog for Verification, Analysis and Transformation
While the logical foundations of Prolog — Horn clauses — are very elegant the full Prolog languagescontains “darker” areas and features which can only be understood and given meaning when taking theoperational semantics of Prolog into account. If you look closely at the example in Section 2.3 above,we used the cut (written as ! ), combined with a catch-all error clause at the end which always matches,to be able to detect typing errors. The cut here is used to prevent generating type error messages uponbacktracking (as the catch-all clause on its own would always be applicable).In [21] I wrote:“Sometimes it is good to view Prolog as a dynamic language, and not feel guilty aboutusing the non-ground representation or dynamically asserting or retracting facts. In manycircumstances taking these shortcuts will lead in much shorter and faster code, and it is notclear whether the effort in attempting to write a declarative version would be worthwhile.”When writing verification or analysis tools in Prolog, it is often a good idea to have a declarativecore, where all predicates p of arity n satisfy for all terms a , . . . , a n and bindings θ : • ∀ θ . p ( a , ..., a n ) , θ ≡ θ , p ( a , ..., a n ) (binding-insensitive) • p ( a , ..., a n ) , fail ≡ fail , p ( a , ..., a n ) (side-effect free)Here G ≡ H means that G and H have the same meaning. Usually, this means the same sets of computedanswers usFor the infrastructure code (e.g., command-line interface, input-output), it is fine or even mandatoryto use impure features of Prolog. For the core of a tool it is also sometimes advisable to use impurefeatures, albeit in a limited fashion. We should strive to keep the predicates declarative in the absenceof error messages. E.g., as shown in the type-inference program, we can use the non-declarative cutcombined with a catch-all to generate error messages, but the cut did not affect regular type inference.Co-routines are a mechanism to influence the selection rule of Prolog: via when or block annotationsone can suspend predicate calls until a certain condition is met. Co-routines can often help to makepredicates more declarative, ensuring that they can be used in multiple directions Sometimes it is evenpossible to use non-declarative features to write a declarative predicate. This is not really surprising, thedeclarative predicates of the finite domain constraint logic programming library CLP(FD) [9] of SICStusProlog [35] are partially implemented in the low-level C-language.Below is the implementation of a declarative addition predicate. We use both co-routines and non-declarative features to ensure that the predicate can be used in multiple directions, is commutative (butstill less powerful than addition in CLP(FD)). :- block plus(-,?,-), plus(?,-,-), plus(-,-,?).plus(X,Y,R) :- ( var(X) -> X is R-Y; var(Y) -> Y is R-X; otherwise -> R is X+Y).
We have that plus(1,1,X) yields
X=2 , while, e.g., plus(X,1,4) yields
X=3 as solution. The blockdeclarations ensure that the predicate plus delays until at least two of its arguments are known. We canthus even solve the equations x + y = z ∧ z + = x ∧ x + = A suspended goal is a co-routine; the concept for logic programming dates back to MU Prolog [31]. ichael Leuschel | ?- plus(X,Y,Z), plus(Z,1,X), plus(X,10,20).X = 10,Y = -1,Z = 9 ? ;no The first two calls to plus will initially be suspended, while the third call plus(X,10,20) will beexecuted, instantiating X to 10. This will unblock the second call plus(Z,1,X) , instantiating Z to 9,which in turn will unblock the first call to plus . Non-determinism and unification of Prolog is useful and as shown sometimes essential, e.g., for typeinference. I find co-routines (when and block) to be absolutely essential for Prolog applications in :custom constraint solver, writing declarative reversible predicates, or implementing the Andorra principle(see also [21]).Similarly, constraint logic programming (CLP) is an important feature of modern Prolog systemsfor many applications. I found the finite domain library CLP(FD) [9] to be the most useful. I have notreally used the boolean constraint solver CLP(B) of SICStus Prolog. For my particular use cases withinP RO B its encoding using binary decision diagrams (BDDs [8]) was too slow and I resorted to writingmy own boolean solver using attributed variables.
Attributed variables allow one to attach attributes to logical variables. One then provides hookswhich are called by Prolog when variables with attributes are unified. Attributed variables are a goodfallback solution for writing custom constraint solvers, but they are very low-level and hence tricky towrite and debug.Constraint Handling Rules (
CHR ) [13] can take the pain out of dealing with attributed variables.CHR provides a higher-level way of writing constraint solving rules. However, I also found CHR codequite difficult to debug and found it difficult to write larger solvers which perform well (and do not loop).Currently CHR is used optionally in P RO B for some integer arithmetic propagation rules, but it is notused heavily.
Tabling [10] can be very useful in the context of verification and program analysis. It is, however,tricky in the context of constraints and co-routines and is not provided, e.g., by SICStus Prolog. Tablingwas used in the XTL [28] and XMC [33, 34] model checkers. In my case, the combination of tabling andobtaining computed answers inside negation (cf. Section 2.2) was tricky to achieve and prevented usingthis approach for more complex specification language. Within the P RO B tool, tabling is implementedin an ad-hoc manner in various instances; in the end the comfort of SICStus and its CLP(FD) library andco-routines turned out to be more important than efficient tabling. But I would still wish to have accessto a Prolog system which combines all of these features.
Prolog: The Missing Bits
The absence of a full-fledged Prolog type checker is an issue. A solutionis to make systematic use of catch-all clauses, as shown in Section 2.3. Generally, this should be com-plemented with extensive unit tests, runtime tests, and integration tests with continuous integration. Forthe latter a command-line interface is very useful. I also found the implementation of a REPL (Read-Eval-Print-Loop) for the object language to be a useful way to generate new unit tests. When using See, e.g., https://en.wikipedia.org/wiki/Read-eval-print_loop . Prolog for Verification, Analysis and Transformation co-routines, it can be useful to programmatically generate unit tests, varying the order in which argu-ments are instantiated from the outside.There are still a few aspects of verification and analysis which are difficult to implement efficiently inProlog. One is loop checking in a graph: as already discussed in [21], the LTL model checker of P RO Bwritten in C for this reason.Similarly, SICStus Prolog unfortunately does not yet provide built-in hash-maps or similar data struc-tures. This was relevant for implementing directed model checking, where one uses a heuristic functionto prioritise the unprocessed states which should be checked next. Within P RO B [22] we resorted to(partially) storing this queue of unprocessed states in a C++ multimap, which enabled us to quickly addnew states and obtain the state with highest priority.The support for parallel execution in Prolog is rather patchy and often limited. For implementing par-allel and distributed model checking in P RO B [20], multiple Prolog instances were used, communicatingvia ZMQ [18] provided to SICStus Prolog via its C interface.Finally, compared to languages like Java or JavaScript, Prolog systems only have access to relativelyfew standard libraries. This meant for example that we used the C++ regular expression library in P RO Bto provide a regular expression library for B specifications.In summary, despite its shortcomings, Prolog is still an excellent language to implement verificationand program analysis and transformation techniques and tools. In particular when it comes to traversaland manipulation of abstract syntax trees, Prolog programs are in my experience more compact, fasterand more memory efficient than many programs written in more mainstream languages like Java.
Acknowledgements
I would like to thank Laurent Fribourg for his useful feedback on an earlier version of the article.
References [1] Alfred V. Aho, Monica S. Lam, Ravi Sethi & Jeffrey D. Ullman (2007):
Compilers. Principles, Techniques,and Tools (Second Edition) . Addison Wesley.[2] Elvira Albert, Puri Arenas, Samir Genaim, Germ´an Puebla & Damiano Zanardini (2007):
Cost Analysis ofJava Bytecode . In Rocco De Nicola, editor:
ESOP , LNCS 4421, Springer-Verlag, pp. 157–172. Available at http://dx.doi.org/10.1007/978-3-540-71316-6_12 .[3] Elvira Albert, Samir Genaim & Miguel G´omez-Zamalloa (2007):
Heap space analysis for java bytecode . InGreg Morrisett & Mooly Sagiv, editors:
ISMM , ACM, pp. 105–116. Available at http://doi.acm.org/10.1145/1296907.1296922 .[4] Elvira Albert, Miguel G´omez-Zamalloa, Laurent Hubert & Germ´an Puebla (2007):
Verification of JavaBytecode Using Analysis and Transformation of Logic Programs . In Michael Hanus, editor:
ProceedingsPADL 2007 , LNCS 4354, Springer-Verlag, pp. 124–139. Available at http://dx.doi.org/10.1007/978-3-540-69611-7_8 .[5] K. R. Apt & F. Turini (1995):
Meta-logics and Logic Programming . MIT Press.[6] Joe Armstrong (2007):
A history of Erlang . In Barbara G. Ryder & Brent Hailpern, editors:
HOPL , ACM,pp. 1–26. Available at http://doi.acm.org/10.1145/1238844.1238850 .[7] Jonathan Bowen (1999):
Animating the Semantics of VERILOG using Prolog . Technical Report UNU/IISTTechnical Report no. 176, United Nations University, Macau.[8] Randy Bryant (1992):
Symbolic Boolean Manipulation with Ordered Binary-Decision Diagrams . ACMComputing Surveys ichael Leuschel [9] Mats Carlsson, Greger Ottosson & Bj¨orn Carlson (1997): An Open-Ended Finite Domain Constraint Solver .In Hugh Glaser Glaser, Pieter H. Hartel & Herbert Kuchen, editors:
Proceedings PLILP’97 , LNCS 1292,Springer-Verlag, pp. 191–206. Available at https://doi.org/10.1007/BFb0033845 .[10] W. Chen & D. S. Warren (1996):
Tabled Evaluation with Delaying for General Logic Programs . Journal ofthe ACM
Abstract Interpretation and Application to Logic Programs . TheJournal of Logic Programming
Model checking object Petri nets in Prolog . In:
ProceedingsPPDP ’04 , ACM Press, New York, NY, USA, pp. 20–31, doi:10.1145/1013963.1013970.[13] Thom Fr¨uhwirth (2009):
Constraint Handling Rules . Cambridge University Press,doi:10.1017/CBO9780511609886.[14] Miguel G´omez-Zamalloa, Elvira Albert & Germ´an Puebla (2007):
Improving the Decompilation of JavaBytecode to Prolog by Partial Evaluation . Electr. Notes Theor. Comput. Sci. http://dx.doi.org/10.1016/j.entcs.2007.02.062 .[15] Sergey Grebenshchikov, Nuno P. Lopes, Corneliu Popeea & Andrey Rybalchenko (2012):
Synthesizing soft-ware verifiers from proof rules . In Jan Vitek, Haibo Lin & Frank Tip, editors:
ACM SIGPLAN Conferenceon Programming Language Design and Implementation, PLDI ’12, Beijing, China - June 11 - 16, 2012 ,ACM, pp. 405–416, doi:10.1145/2254064.2254112. Available at http://dl.acm.org/citation.cfm?id=2254064 .[16] Patricia Hill & John Gallagher (1998):
Meta-programming in logic programming . In D. M. Gabbay, C. J.Hogger & J. A. Robinson, editors:
Handbook of Logic in Artificial Intelligence and Logic Programming , 5,Oxford Science Publications, Oxford University Press, pp. 421–497.[17] Patricia Hill & John W. Lloyd (1994):
The G¨odel Programming Language . MIT Press.[18] Pieter Hintjens (2013):
ZeroMQ: Messaging for Many Applications . O’Reilly Media, Inc.[19] Bishoksan Kafle, John P. Gallagher & Pierre Ganty (2018):
Tree dimension in verification of constrainedHorn clauses . Theory Pract. Log. Program.
Distributed Model Checking Using ProB . In Aaron Dutle,C´esar A. Mu˜noz & Anthony Narkawicz, editors:
NASA Formal Methods - 10th International Symposium,NFM 2018, Newport News, VA, USA, April 17-19, 2018, Proceedings , Lecture Notes in Computer Science
Declarative Programming for Verification: Lessons and Outlook . In:
ProceedingsPPDP’2008 , ACM Press, pp. 1–7, doi:10.1145/1389449.1389450.[22] Michael Leuschel & Jens Bendisposto (2010):
Directed Model Checking for B: An Evaluation and NewTechniques . In Jim Davies, Leila Silva & Adenilso da Silva Sim˜ao, editors:
Formal Methods: Foundationsand Applications - 13th Brazilian Symposium on Formal Methods, SBMF 2010, Natal, Brazil, Novem-ber 8-11, 2010, Revised Selected Papers , Lecture Notes in Computer Science
ProB: A Model Checker for B . In Keijiro Araki, StefaniaGnesi & Dino Mandrioli, editors:
FME 2003: Formal Methods , LNCS 2805, Springer-Verlag, pp. 855–874,doi:10.1007/978-3-540-45236-2 46.[24] Michael Leuschel & Michael J. Butler (2008):
ProB: an automated analysis toolset for the B method . STTT http://dx.doi.org/10.1007/s10009-007-0063-9 .[25] Michael Leuschel & Marc Fontaine (2008):
Probing the Depths of CSP-M: A new FDR-compliant ValidationTool . In:
Proceedings ICFEM 2008 , LNCS, Springer-Verlag, pp. 278–297, doi:10.1007/978-3-540-88194-0 18.[26] Michael Leuschel & Helko Lehmann (2000):
Coverability of Reset Petri Nets and other Well-StructuredTransition Systems by Partial Deduction . In John Lloyd, editor:
Proceedings of the International Conference Prolog for Verification, Analysis and Transformation on Computational Logic (CL’2000) , LNAI 1861, Springer-Verlag, London, UK, pp. 101–115, doi:10.1007/3-540-44957-4 7.[27] Michael Leuschel & Thierry Massart (2000):
Infinite State Model Checking by Abstract Interpretation andProgram Specialisation . In Annalisa Bossi, editor:
Proceedings LOPSTR’99 , LNCS 1817, Venice, Italy, pp.63–82, doi:10.1007/10720327 5.[28] Michael Leuschel, Thierry Massart & Andrew Currie (2001):
How to Make FDR Spin: LTL Model Checkingof CSP by Refinement . In J. N. Oliviera & P. Zave, editors:
FME’2001 , LNCS 2021, Springer-Verlag, Berlin,Germany, pp. 99–118, doi:10.1007/3-540-45251-6 6.[29] Victor W. Marek & Miroslaw Truszczynski (1999):
Stable Models and an Alternative Logic ProgrammingParadigm . In Krzysztof R. Apt, Victor W. Marek, Mirek Truszczynski & David Scott Warren, editors:
The Logic Programming Paradigm - A 25-Year Perspective , Artificial Intelligence, Springer, pp. 375–398,doi:10.1007/978-3-642-60085-2 17.[30] Robin Milner (1978):
A Theory of Type Polymorphism in Programming . Journal of Computer and SystemSciences
17, pp. 348–375, doi:10.1016/0022-0000(78)90014-4.[31] Lee Naish (March 1982 (Revised July 1983)):
An introduction to MU-Prolog . Technical Report 82/2, De-partment of Computer Science, University of Melbourne, Melbourne, Australia.[32] Fernando C. N. Pereira & David H. D. Warren (1980):
Definite Clause Grammars for Language Analysis -A Survey of the Formalism and a Comparison with Augmented Transition Networks . Artif. Intell.
Efficient Model Checking Using Tabled Resolution . In O. Grumberg, editor:
ProceedingsCAV’97 , LNCS 1254, Springer-Verlag, pp. 143–154, doi:10.1007/3-540-63166-6 16.[34] C. R. Ramakrishnan, I. V. Ramakrishnan, Scott A. Smolka, Yifei Dong, Xiaoqun Du, Abhik Roychoudhury& V. N. Venkatakrishnan (2000):
XMC: A Logic-Programming-Based Verification Toolset . In:
Proceedingsof CAV 2000 , pp. 576–580, doi:10.1007/10722167 48.[35] Sweden SICS, Kista:
SICStus Prolog User’s Manual . Available at