/*
* Kodkod -- Copyright (c) 2005-2007, Emina Torlak
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package kodkod.engine.fol2sat;
import static kodkod.ast.operator.FormulaOperator.AND;
import static kodkod.ast.operator.FormulaOperator.IFF;
import static kodkod.ast.operator.FormulaOperator.IMPLIES;
import static kodkod.ast.operator.FormulaOperator.OR;
import static kodkod.ast.operator.Quantifier.ALL;
import static kodkod.ast.operator.Quantifier.SOME;
import static kodkod.util.nodes.AnnotatedNode.annotate;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import kodkod.ast.BinaryFormula;
import kodkod.ast.ComparisonFormula;
import kodkod.ast.Comprehension;
import kodkod.ast.Decl;
import kodkod.ast.Decls;
import kodkod.ast.Expression;
import kodkod.ast.Formula;
import kodkod.ast.IntComparisonFormula;
import kodkod.ast.IntExpression;
import kodkod.ast.MultiplicityFormula;
import kodkod.ast.NaryFormula;
import kodkod.ast.Node;
import kodkod.ast.NotFormula;
import kodkod.ast.QuantifiedFormula;
import kodkod.ast.Relation;
import kodkod.ast.RelationPredicate;
import kodkod.ast.SumExpression;
import kodkod.ast.Variable;
import kodkod.ast.operator.FormulaOperator;
import kodkod.ast.operator.Multiplicity;
import kodkod.ast.operator.Quantifier;
import kodkod.ast.visitor.AbstractDetector;
import kodkod.ast.visitor.AbstractReplacer;
import kodkod.engine.bool.BooleanMatrix;
import kodkod.engine.config.Options;
import kodkod.engine.config.Reporter;
import kodkod.instance.Bounds;
import kodkod.instance.TupleSet;
import kodkod.util.nodes.AnnotatedNode;
/**
* Skolemizes existential quantifiers, up to a given
* number of nestings (within universal quantifiers).
* @author Emina Torlak
*/
abstract class Skolemizer extends AbstractReplacer {
/**
* Skolemizes the given annotated formula using the given bounds and options. If
* Options.trackFormulas is set and the formula is skolemizable, the resulting annotated
* formula will contain transitive source information for each of its subformulas.
* Specifically, let f be the returned annotated formula, t be a descendeant of f.node, and
* s a descendant of annotated.node from which t was derived. Then,
* f.source[t] = annotated.source[s]. If options.trackFormulas is false, no source
* information will be recorded (i.e. f.source[t] = t for all descendants t of f).
* @effects upper bound mappings for skolem constants, if any, are added to the bounds
* @return the skolemized version of the given formula
* @throws NullPointerException - any of the arguments are null
* @throws IllegalArgumentException - some Relation & annotated.node.^children - bounds.relations
* @throws UnsupportedOperationException - bounds is unmodifiable
*/
@SuppressWarnings("unchecked")
static AnnotatedNode<Formula> skolemize(final AnnotatedNode<Formula> annotated, Bounds bounds, Options options) {
if (options.logTranslation()>0) {
final Map<Node,Node> source = new IdentityHashMap<Node,Node>();
final Skolemizer r = new Skolemizer(annotated, bounds, options) {
protected Formula source(Formula f, Node n) {
//System.out.println("logging " + f + " <-- " + n);
final Node nsource = annotated.sourceOf(n);
if (f!=nsource) source.put(f, nsource);
return f;
}
};
final Formula f = annotated.node().accept(r);
return f==annotated.node() ? annotated : annotate(f, source);
} else {
final Skolemizer r = new Skolemizer(annotated, bounds, options) {};
final Formula f = annotated.node().accept(r);
return f==annotated.node() ? annotated : annotate(f);
}
}
/**
* Contains info about an approximate bound for a
* non-skolemizable decl.
* @specfield decl: Decl
* @specfield upperBound: lone BooleanMatrix
* @invariant decl.expression in upperBound
* @author Emina Torlak
*/
private static final class DeclInfo {
final Decl decl;
BooleanMatrix upperBound;
/**
* Constructs a DeclInfo for the given decl.
* @effects this.decl' = decl && this.upperBound' = null
*/
DeclInfo(Decl decl) {
this.decl = decl;
this.upperBound = null;
}
}
/* replacement environment; maps skolemized variables to their skolem expressions,
* and non-skolemized variables to themselves */
private Environment<Expression> repEnv;
/* the interpreter used to determine the upper bounds for skolem constants;
* the upper bounds for skolem constants will be added to interpreter.bounds */
private final LeafInterpreter interpreter;
/* bounds on which the interpreter is based */
private final Bounds bounds;
/* reporter */
private final Reporter reporter;
/* non-skolemizable quantified declarations in the current scope, in the order of declaration
* (most recent decl is last in the list) */
private final List<DeclInfo> nonSkolems;
/* a Decl-only view of the nonSkolems list */
private final List<Decl> nonSkolemsView;
private final List<Formula> topSkolemConstraints;
/* true if the polarity of the currently visited node is negative, otherwise false */
private boolean negated;
/* depth to which to skolemize; negative depth indicates that no skolemization can be done at that point */
private int skolemDepth;
/**
* Constructs a skolem replacer from the given arguments.
*/
private Skolemizer(AnnotatedNode<Formula> annotated, Bounds bounds, Options options) {
super(annotated.sharedNodes());
// only cache intermediate computations for expressions with no free variables
// and formulas with no free variables and no quantified descendents
final AbstractDetector fvdetect = annotated.freeVariableDetector();
final AbstractDetector qdetect = annotated.quantifiedFormulaDetector();
for(Node n: annotated.sharedNodes()) {
if (!(Boolean)n.accept(fvdetect)) {
if (!(n instanceof Formula) || !((Boolean)n.accept(qdetect)))
this.cache.put(n, null);
}
}
this.reporter = options.reporter();
this.bounds = bounds;
this.interpreter = LeafInterpreter.overapproximating(bounds, options);
this.repEnv = Environment.empty();
this.nonSkolems = new ArrayList<DeclInfo>();
this.nonSkolemsView = new AbstractList<Decl>() {
public Decl get(int index) { return nonSkolems.get(index).decl; }
public int size() { return nonSkolems.size(); }
};
this.topSkolemConstraints = new ArrayList<Formula>();
this.negated = false;
this.skolemDepth = options.skolemDepth();
}
/**
* Caches the given replacement for the specified node, if
* the node is a syntactically shared expression, int expression or declaration with
* no free variables. Otherwise does nothing. The method returns
* the replacement node.
* @return replacement
*/
@Override
protected final <N extends Node> N cache(N node, N replacement) {
if (cache.containsKey(node)) {
cache.put(node, replacement);
}
return replacement;
}
/**
* Records that the given node is the source of the
* specified formula, if this is a tracking skolemizer. Otherwise does nothing.
* This method is always called when the result of visiting a node n will result
* in the creation of a formula f such that f != n.
* @return f
* @effects Records that the given node is the source of the
* specified formula, if this is a tracking skolemizer. Otherwise does nothing.
*/
protected Formula source(Formula f, Node n) {
return f;
}
/*-------declarations---------*/
/**
* Visits the given decl's expression. Note that we must not visit variables
* in case they are re-used. For example, consider the formula
* some x: X | all x: Y | F(x). Since x bound by the existential quantifier
* is going to be skolemized, if we visited the variable in the enclosed
* declaration, we would get the skolem constant as a return value and
* a ClassCastException would be thrown.
*
* @return { d: Declaration | d.variable = decl.variable && d.multiplicity = decl.multiplicity &&
* d.expression = decl.expression.accept(this) }
*/
@Override
public final Decl visit(Decl decl) {
Decl ret = lookup(decl);
if (ret!=null) return ret;
final int oldDepth = skolemDepth;
skolemDepth = -1; // can't skolemize inside a decl
final Expression expression = decl.expression().accept(this);
skolemDepth = oldDepth;
ret = (expression==decl.expression()) ? decl : decl.variable().declare(decl.multiplicity(), expression);
return cache(decl,ret);
}
/**
* This method should be accessed only from the context of a non-skolemizable
* node, because it extends the replacement environment
* with identity mappings for the variables declared in the given decls. To ensure
* that the environment is always extended, the method should be called using the
* visit((Decls) node.declarations()) syntax, since the accept syntax may dynamically
* dispatch the call to the {@link #visit(Decl)} method, producing UnboundLeafExceptions.
* @effects this.repEnv in this.repEnv'.^parent &&
* #(this.repEnv'.*parent - this.repEnv.*parent) = decls.size() &&
* all v: decls.variable | this.repEnv'.lookup(v) = v
* @requires this.skolemDepth < 0
* @return { d: Decls | d.size = decls.size &&
* all i: [0..d.size) | d.declarations[i] = decls.declarations[i].accept(this) }
*/
public final Decls visit(Decls decls) {
Decls ret = lookup(decls);
if (ret==null) {
Decls visitedDecls = null;
boolean allSame = true;
for(Decl decl : decls) {
Decls newDecl = visit(decl);
if (newDecl != decl)
allSame = false;
visitedDecls = (visitedDecls==null) ? newDecl : visitedDecls.and(newDecl);
repEnv = repEnv.extend(decl.variable(), decl.variable());
}
ret = allSame ? decls : visitedDecls;
return cache(decls, ret);
} else { // just extend the replacement environment
for(Decl decl: decls) {
repEnv = repEnv.extend(decl.variable(), decl.variable());
}
return ret;
}
}
/*-------expressions and intexpressions---------*/
/* INVARIANT: whenever an expression or intexpression is visited, skolemDepth < 0 */
/**
* Returns the binding for the given variable in the current replacement environment.
* @return the binding for the given variable in the current replacement environment.
* @throws UnboundLeafException - variable not bound in teh replacement environment.
*/
@Override
public final Expression visit(Variable variable) {
final Expression ret = repEnv.lookup(variable);
if (ret==null)
throw new UnboundLeafException("Unbound variable", variable);
return ret;
}
/**
* @see kodkod.ast.visitor.AbstractReplacer#visit(kodkod.ast.Comprehension)
*/
@Override
public final Expression visit(Comprehension expr) {
Expression ret = lookup(expr);
if (ret!=null) return ret;
final Environment<Expression> oldRepEnv = repEnv; // skolemDepth < 0 at this point
final Decls decls = visit((Decls)expr.decls());
final Formula formula = expr.formula().accept(this);
ret = (decls==expr.decls() && formula==expr.formula()) ? expr : formula.comprehension(decls);
repEnv = oldRepEnv;
return cache(expr,ret);
}
/**
* @see kodkod.ast.visitor.AbstractReplacer#visit(kodkod.ast.SumExpression)
*/
@Override
public final IntExpression visit(SumExpression intExpr) {
IntExpression ret = lookup(intExpr);
if (ret!=null) return ret;
final Environment<Expression> oldRepEnv = repEnv; // skolemDepth < 0 at this point
final Decls decls = visit((Decls)intExpr.decls());
final IntExpression expr = intExpr.intExpr().accept(this);
ret = (decls==intExpr.decls() && expr==intExpr.intExpr()) ? intExpr : expr.sum(decls);
repEnv = oldRepEnv;
return cache(intExpr,ret);
}
/*-------formulas---------*/
/**
* Returns the least sound upper bound on the value of expr
* @return the least sound upper bound on the value of expr
*/
private final BooleanMatrix upperBound(Expression expr, Environment<BooleanMatrix> env) {
return FOL2BoolTranslator.approximate(annotate(expr), interpreter, env);
}
/**
* Adds a bound for the given skolem relation to
* this.bounds, and returns the expression that should replace skolemDecl.variable in the final formula.
* @requires skolem !in this.bounds.relations
* @requires skolem.arity = nonSkolems.size() + skolemDecl.variable().arity()
* @effects adds a sound upper bound for the given skolem relation to this.bounds
* @return the expression that should replace skolemDecl.variable in the final formula
*/
private Expression skolemExpr(Decl skolemDecl, Relation skolem) {
final int depth = nonSkolems.size();
final int arity = depth + skolemDecl.variable().arity();
Expression skolemExpr = skolem;
Environment<BooleanMatrix> skolemEnv = Environment.empty();
for(DeclInfo info : nonSkolems) {
if (info.upperBound==null) {
info.upperBound = upperBound(info.decl.expression(), skolemEnv);
}
skolemEnv = skolemEnv.extend(info.decl.variable(), info.upperBound);
skolemExpr = info.decl.variable().join(skolemExpr);
}
BooleanMatrix matrixBound = upperBound(skolemDecl.expression(), skolemEnv);
for(int i = depth-1; i >= 0; i--) {
matrixBound = nonSkolems.get(i).upperBound.cross(matrixBound);
}
final TupleSet skolemBound = bounds.universe().factory().setOf(arity, matrixBound.denseIndices());
bounds.bound(skolem, skolemBound);
return skolemExpr;
}
/**
* Returns a formula that properly constrains the given skolem's domain.
* @requires !nonSkolems.isEmpty()
* @return a formula that properly constrains the given skolem's domain.
*/
private Formula domainConstraint(Decl skolemDecl, Relation skolem) {
final Iterator<DeclInfo> itr = nonSkolems.iterator();
Decls rangeDecls = itr.next().decl;
while(itr.hasNext()) {
rangeDecls = rangeDecls.and(itr.next().decl);
}
// System.out.println(skolemDecl.expression());
Expression skolemDomain = skolem;
for(int i = 0, max = skolemDecl.variable().arity(); i < max; i++) {
skolemDomain = skolemDomain.join(Expression.UNIV);
}
return skolemDomain.in(Formula.TRUE.comprehension(rangeDecls));
}
/**
* Skolemizes the given formula, if possible, otherwise returns the result
* of replacing its free variables according to the current replacement environment.
* @see kodkod.ast.visitor.AbstractReplacer#visit(kodkod.ast.QuantifiedFormula)
*/
public final Formula visit(QuantifiedFormula qf) {
Formula ret = lookup(qf);
if (ret!=null) return ret;
final Environment<Expression> oldRepEnv = repEnv;
final Quantifier quant = qf.quantifier();
final Decls decls = qf.decls();
if (skolemDepth>=0 && (negated && quant==ALL || !negated && quant==SOME)) { // skolemizable formula
final List<Formula> rangeConstraints = new LinkedList<Formula>();
final List<Formula> domConstraints = new LinkedList<Formula>();
for(Decl decl : decls) {
final Decl skolemDecl = visit(decl);
final Relation skolem = Relation.nary("$"+ skolemDecl.variable().name(), nonSkolems.size() + skolemDecl.variable().arity());
reporter.skolemizing(decl, skolem, nonSkolemsView);
final Expression skolemExpr = skolemExpr(skolemDecl, skolem);
final Multiplicity mult = decl.multiplicity();
rangeConstraints.add(source(skolemExpr.in(skolemDecl.expression()), decl));
if (mult!=Multiplicity.SET) {
rangeConstraints.add(source(skolemExpr.apply(mult), decl));
}
if (!nonSkolems.isEmpty())
domConstraints.add(source(domainConstraint(skolemDecl, skolem), decl));
repEnv = repEnv.extend(decl.variable(), skolemExpr);
}
ret = source(Formula.and(rangeConstraints), decls).compose(negated ? IMPLIES : AND, qf.formula().accept(this));
if (!domConstraints.isEmpty())
topSkolemConstraints.add(source(Formula.and(domConstraints), decls));
} else { // non-skolemizable formula
final Decls newDecls = visit((Decls)qf.decls());
if (skolemDepth>=nonSkolems.size()+newDecls.size()) { // could skolemize below
for(Decl d: newDecls) { nonSkolems.add(new DeclInfo(d)); }
final Formula formula = qf.formula().accept(this);
ret = ((newDecls==decls && formula==qf.formula()) ? qf : formula.quantify(quant, newDecls));
for(int i = newDecls.size(); i > 0; i--) { nonSkolems.remove(nonSkolems.size()-1); }
} else { // can't skolemize below
final int oldDepth = skolemDepth;
skolemDepth = -1;
final Formula formula = qf.formula().accept(this);
ret = ((newDecls==decls && formula==qf.formula()) ? qf : formula.quantify(quant, newDecls));
skolemDepth = oldDepth;
}
}
repEnv = oldRepEnv;
if (repEnv.isEmpty() && !topSkolemConstraints.isEmpty()) {
ret = source(Formula.and(topSkolemConstraints), qf).compose(negated ? IMPLIES : AND, ret);
}
return source(cache(qf,ret), qf);
}
/**
* Calls not.formula.accept(this) after flipping the negation flag and returns the result.
* @see kodkod.ast.visitor.AbstractReplacer#visit(kodkod.ast.NotFormula)
**/
public final Formula visit(NotFormula not) {
Formula ret = lookup(not);
if (ret!=null) return ret;
negated = !negated; // flip the negation flag
final Formula retChild = not.formula().accept(this);
negated = !negated;
return retChild==not.formula() ? cache(not,not) : source(cache(not, retChild.not()), not);
}
/**
* If not cached, visits the formula's children with appropriate settings
* for the negated flag and the skolemDepth parameter.
* @see kodkod.ast.visitor.AbstractReplacer#visit(kodkod.ast.BinaryFormula)
*/
public final Formula visit(BinaryFormula bf) {
Formula ret = lookup(bf);
if (ret!=null) return ret;
final FormulaOperator op = bf.op();
final int oldDepth = skolemDepth;
if (op==IFF || (negated && op==AND) || (!negated && (op==OR || op==IMPLIES))) { // cannot skolemize in these cases
skolemDepth = -1;
}
final Formula left, right;
if (negated && op==IMPLIES) { // !(a => b) = !(!a || b) = a && !b
negated = !negated;
left = bf.left().accept(this);
negated = !negated;
right = bf.right().accept(this);
} else {
left = bf.left().accept(this);
right = bf.right().accept(this);
}
skolemDepth = oldDepth;
ret = (left==bf.left()&&right==bf.right()) ? bf : left.compose(op, right);
return source(cache(bf,ret),bf);
}
/**
* If not cached, visits the formula's children with appropriate settings
* for the negated flag and the skolemDepth parameter.
* @see kodkod.ast.visitor.AbstractReplacer#visit(kodkod.ast.NaryFormula)
*/
public final Formula visit(NaryFormula bf) {
Formula ret = lookup(bf);
if (ret!=null) return ret;
final int oldDepth = skolemDepth;
final FormulaOperator op = bf.op();
switch(op) {
case AND : if (negated) skolemDepth = -1; break;
case OR : if (!negated) skolemDepth = -1; break;
default : throw new IllegalArgumentException("Unknown nary operator: " + op);
}
final Formula[] visited = new Formula[bf.size()];
boolean allSame = true;
for(int i = 0; i < visited.length; i++) {
final Formula child = bf.child(i);
visited[i] = child.accept(this);
allSame = allSame && (child==visited[i]);
}
ret = allSame ? bf : Formula.compose(op, visited);
skolemDepth = oldDepth;
return source(cache(bf,ret),bf);
}
/**
* Calls super.visit(icf) after disabling skolemization and returns the result.
* @return super.visit(icf)
**/
public final Formula visit(IntComparisonFormula icf) {
final int oldDepth = skolemDepth;
skolemDepth = -1; // cannot skolemize inside an int comparison formula
final Formula ret = super.visit(icf);
skolemDepth = oldDepth;
return source(ret,icf);
}
/**
* Calls super.visit(cf) after disabling skolemization and returns the result.
* @return super.visit(cf)
**/
public final Formula visit(ComparisonFormula cf) {
final int oldDepth = skolemDepth;
skolemDepth = -1; // cannot skolemize inside a comparison formula
final Formula ret = super.visit(cf);
skolemDepth = oldDepth;
return source(ret,cf);
}
/**
* Calls super.visit(mf) after disabling skolemization and returns the result.
* @return super.visit(mf)
**/
public final Formula visit(MultiplicityFormula mf) {
final int oldDepth = skolemDepth;
skolemDepth = -1; // cannot skolemize inside a multiplicity formula
final Formula ret = super.visit(mf);
skolemDepth = oldDepth;
return source(ret,mf);
}
/**
* Calls super.visit(pred) after disabling skolemization and returns the result.
* @return super.visit(pred)
**/
public final Formula visit(RelationPredicate pred) {
final int oldDepth = skolemDepth;
skolemDepth = -1; // cannot skolemize inside a relation predicate
final Formula ret = super.visit(pred);
skolemDepth = oldDepth;
return source(ret,pred);
}
}