/* * Kodkod -- Copyright (c) 2005-present, 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.satlab; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.BitSet; import java.util.List; /** * An implementation of a wrapper for an external SAT solver, * executed in a separate process. * @author Emina Torlak */ final class ExternalSolver implements SATSolver { private final StringBuilder buffer; private final int capacity = 8192; private final boolean deleteTemp; private final String executable, inTemp; private final String[] options; private final RandomAccessFile cnf; private final BitSet solution; private volatile Boolean sat; private volatile int vars, clauses; /** * Constructs an ExternalSolver that will execute the specified binary * with the given options on the {@code inTemp} file. The {@code inTemp} file * will be initialized to contain all clauses added to this solver via the * {@link #addClause(int[])} method. The solver is assumed to write its output * to standard out. The {@code deleteTemp} flag indicates whether the temporary * files should be deleted when they are no longer needed by this solver. */ ExternalSolver(String executable, String inTemp, boolean deleteTemp, String... options) { RandomAccessFile file = null; try { file = new RandomAccessFile(inTemp, "rw"); file.setLength(0); } catch (FileNotFoundException e) { throw new SATAbortedException(e); } catch (IOException e) { close(file); throw new SATAbortedException(e); } this.deleteTemp = deleteTemp; this.cnf = file; // get enough space into the buffer for the cnf header, which will be written last this.buffer = new StringBuilder(); for(int i = headerLength(); i > 0; i--) { buffer.append(" "); } buffer.append("\n"); this.sat = null; this.solution = new BitSet(); this.vars = 0; this.clauses = 0; this.executable = executable; this.inTemp = inTemp; // remove empty strings from the options array final List<String> nonEmpty = new ArrayList<String>(options.length); for(String opt : options) { if (!opt.isEmpty()) nonEmpty.add(opt); } this.options = nonEmpty.toArray(new String[nonEmpty.size()]); } /** * Silently closes the given resource if it is non-null. */ private static void close(Closeable closeable) { try { if (closeable!=null) closeable.close(); } catch (IOException e) { } // ignore } /** * Returns the length, in characters, of the longest possible header * for a cnf file: p cnf Integer.MAX_VALUE Integer.MAX_VALUE * @return the length, in characters, of the longest possible header * for a cnf file: p cnf Integer.MAX_VALUE Integer.MAX_VALUE */ private static final int headerLength() { return String.valueOf(Integer.MAX_VALUE).length()*2 + 8; } /** * Flushes the contents of the string buffer to the cnf file. */ private final void flush(){ try { cnf.writeBytes(buffer.toString()); } catch (IOException e) { close(cnf); throw new SATAbortedException(e); } finally { buffer.setLength(0); } } /** * {@inheritDoc} * @see kodkod.engine.satlab.SATSolver#addClause(int[]) */ public boolean addClause(int[] lits) { clauses++; if (buffer.length()>capacity) flush(); for(int lit: lits) { buffer.append(lit); buffer.append(" "); } buffer.append("0\n"); return true; } /** * @see kodkod.engine.satlab.SATSolver#addVariables(int) */ public void addVariables(int numVars) { if (numVars < 0) throw new IllegalArgumentException("vars < 0: " + numVars); vars += numVars; } /** * @see kodkod.engine.satlab.SATSolver#free() */ public synchronized void free() { close(cnf); if (deleteTemp) { (new File(inTemp)).delete(); } } /** * Releases the resources used by this external solver. */ protected final void finalize() throws Throwable { super.finalize(); free(); } /** * @see kodkod.engine.satlab.SATSolver#numberOfClauses() */ public int numberOfClauses() { return clauses; } /** * @see kodkod.engine.satlab.SATSolver#numberOfVariables() */ public int numberOfVariables() { return vars; } /** * @ensures |lit| <= this.vars && lit != 0 => this.solution'.set(|lit|, lit>0) * @throws SATAbortedException lit=0 || |lit|>this.vars */ private final void updateSolution(int lit) { int abs = StrictMath.abs(lit); if (abs<=vars && abs>0) solution.set(abs-1, lit>0); else throw new SATAbortedException("Invalid variable value: |" + lit + "| !in [1.."+vars+"]"); } /** * @see kodkod.engine.satlab.SATSolver#solve() */ public boolean solve() throws SATAbortedException { if (sat==null) { flush(); Process p = null; BufferedReader out = null; try { cnf.seek(0); cnf.writeBytes("p cnf " + vars + " " + clauses); cnf.close(); final String[] command = new String[options.length+2]; command[0] = executable; System.arraycopy(options, 0, command, 1, options.length); command[command.length-1] = inTemp; p = Runtime.getRuntime().exec(command); new Thread(drain(p.getErrorStream())).start(); out = outputReader(p); String line = null; while((line = out.readLine()) != null) { String[] tokens = line.split("\\s"); int tlength = tokens.length; if (tlength>0) { if (tokens[0].compareToIgnoreCase("s")==0) { if (tlength==2) { if (tokens[1].compareToIgnoreCase("SATISFIABLE")==0) { sat = Boolean.TRUE; continue; } else if (tokens[1].compareToIgnoreCase("UNSATISFIABLE")==0) { sat = Boolean.FALSE; continue; } } throw new SATAbortedException("Invalid " + executable + " output. Line: " + line); } else if (tokens[0].compareToIgnoreCase("v")==0) { int last = tlength-1; for(int i = 1; i < last; i++) { updateSolution(Integer.parseInt(tokens[i])); } int lit = Integer.parseInt(tokens[last]); if (lit!=0) updateSolution(lit); else if (sat!=null) break; } // not a solution line or a variable line, so ignore it. } } if (sat==null) { throw new SATAbortedException("Invalid " + executable + " output: no line specifying the outcome."); } } catch (IOException e) { throw new SATAbortedException(e); } catch (NumberFormatException e) { throw new SATAbortedException("Invalid "+ executable +" output: encountered a non-integer variable token.", e); } finally { close(cnf); close(out); } } return sat; } /** * Returns a runnable that drains the specified input stream. * @return a runnable that drains the specified input stream. */ private static Runnable drain(final InputStream input) { return new Runnable() { public void run() { try { final byte[] buffer=new byte[8192]; while(true) { int n=input.read(buffer); if (n<0) break; } } catch (IOException ex) { } finally { close(input); } } }; } /** * Returns a reader for reading the output of the given process. * @return a reader for reading the output of the given process. * @throws IOException */ private BufferedReader outputReader(Process p) throws IOException { try { return new BufferedReader(new InputStreamReader(p.getInputStream(), "ISO-8859-1")); } catch (IOException e) { close(p.getInputStream()); throw e; } } /** * @see kodkod.engine.satlab.SATSolver#valueOf(int) */ public boolean valueOf(int variable) { if (!Boolean.TRUE.equals(sat)) throw new IllegalStateException(); if (variable < 1 || variable > vars) throw new IllegalArgumentException(variable + " !in [1.." + vars+"]"); return solution.get(variable-1); } /** * @see java.lang.Object#toString() */ public String toString() { return executable + " " + options; } }