001package daikon.simplify;
002
003import static java.nio.charset.StandardCharsets.UTF_8;
004import static java.util.logging.Level.INFO;
005
006import java.io.BufferedReader;
007import java.io.Closeable;
008import java.io.File;
009import java.io.FileOutputStream;
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.InputStreamReader;
013import java.io.PrintStream;
014import java.util.ArrayList;
015import java.util.List;
016import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods;
017import org.checkerframework.checker.initialization.qual.UnknownInitialization;
018import org.checkerframework.checker.lock.qual.GuardSatisfied;
019import org.checkerframework.checker.lock.qual.GuardedBy;
020import org.checkerframework.checker.lock.qual.Holding;
021import org.checkerframework.checker.mustcall.qual.MustCall;
022import org.checkerframework.checker.mustcall.qual.Owning;
023import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
024import org.checkerframework.checker.nullness.qual.NonNull;
025import org.checkerframework.checker.nullness.qual.Nullable;
026
027/**
028 * A session is a channel to the Simplify theorem-proving tool. Once a session is started, commands
029 * may be applied to the session to make queries and manipulate its state.
030 */
031@MustCall("close") public class Session implements Closeable {
032  /**
033   * A non-negative integer, representing the largest number of iterations for which Simplify should
034   * be allowed to run on any single conjecture before giving up. Larger values may cause Simplify
035   * to run longer, but will increase the number of invariants that can be recognized as redundant.
036   * The default value is small enough to keep Simplify from running for more than a few seconds on
037   * any one conjecture, allowing it to verify most simple facts without getting bogged down in long
038   * searches. A value of 0 means not to bound the number of iterations at all, though see also the
039   * {@code simplify_timeout} parameter..
040   */
041  public static int dkconfig_simplify_max_iterations = 1000;
042
043  /**
044   * A non-negative integer, representing the longest time period (in seconds) Simplify should be
045   * allowed to run on any single conjecture before giving up. Larger values may cause Simplify to
046   * run longer, but will increase the number of invariants that can be recognized as redundant.
047   * Roughly speaking, the time spent in Simplify will be bounded by this value, times the number of
048   * invariants generated, though it can be much less. A value of 0 means to not bound Simplify at
049   * all by time, though also see the option {@code simplify_max_iterations}. Beware that using this
050   * option might make Daikon's output depend on the speed of the machine it's run on.
051   */
052  public static int dkconfig_simplify_timeout = 0;
053
054  /**
055   * Positive values mean to print extra indications as each candidate invariant is passed to
056   * Simplify during the {@code --suppress_redundant} check. If the value is 1 or higher, a hyphen
057   * will be printed when each invariant is passed to Simplify, and then replaced by a {@code T} if
058   * the invariant was redundant, {@code F} if it was not found to be, and {@code ?} if Simplify
059   * gave up because of a time limit. If the value is 2 or higher, a {@code <} or {@code >} will
060   * also be printed for each invariant that is pushed onto or popped from from Simplify's
061   * assumption stack. This option is mainly intended for debugging purposes, but can also provide
062   * something to watch when Simplify takes a long time.
063   */
064  public static int dkconfig_verbose_progress = 0;
065
066  /**
067   * Boolean. If true, the input to the Simplify theorem prover will also be directed to a file
068   * named simplifyN.in (where N is a number starting from 0) in the current directory. Simplify's
069   * operation can then be reproduced with a command like {@code Simplify -nosc <simplify0.in}. This
070   * is intended primarily for debugging when Simplify fails.
071   */
072  public static boolean dkconfig_trace_input = false;
073
074  /** non-null if dkconfig_trace_input==true */
075  private final @Owning @MonotonicNonNull PrintStream trace_file;
076
077  /** A unique identifier for creating unique filenames for trace files. */
078  private static int trace_count = 0;
079
080  /* package */ final Process process;
081  private final PrintStream input;
082  private final BufferedReader output;
083
084  /**
085   * Starts a new Simplify process, which runs concurrently; I/O with this process will block.
086   * Initializes the simplify environment for interaction. Use {@code Cmd} objects to interact with
087   * this Session.
088   */
089  public Session() {
090    // Note that this local variable shadows `this.trace_file`.
091    PrintStream trace_file = null;
092    try {
093      List<String> newEnv = new ArrayList<>();
094      if (dkconfig_simplify_max_iterations != 0) {
095        newEnv.add("PROVER_KILL_ITER=" + dkconfig_simplify_max_iterations);
096      }
097      if (dkconfig_simplify_timeout != 0) {
098        newEnv.add("PROVER_KILL_TIME=" + dkconfig_simplify_timeout);
099      }
100      String[] envArray = newEnv.toArray(new String[] {});
101      SessionManager.debugln("Session: exec");
102      // -nosc: don't compute or print invalid context
103      String simplifyPath;
104      if (System.getProperty("simplify.path") == null) {
105        simplifyPath = "Simplify";
106      } else {
107        simplifyPath = System.getProperty("simplify.path");
108      }
109      process = java.lang.Runtime.getRuntime().exec(new String[] {simplifyPath, "-nosc"}, envArray);
110      SessionManager.debugln("Session: exec ok");
111
112      if (dkconfig_trace_input) {
113        File f;
114        while ((f = new File("simplify" + trace_count + ".in")).exists()) {
115          trace_count++;
116        }
117        trace_file = new PrintStream(new FileOutputStream(f));
118      }
119      this.trace_file = trace_file;
120
121      // set up command stream
122      PrintStream tmp_input = new PrintStream(process.getOutputStream());
123      input = tmp_input;
124
125      // set up result stream
126      @NonNull InputStream is = process.getInputStream();
127      output = new BufferedReader(new InputStreamReader(is, UTF_8));
128
129      // turn off prompting
130      SessionManager.debugln("Session: prompt off");
131      sendLine("(PROMPT_OFF)");
132      SessionManager.debugln("Session: eat prompt");
133      // eat first (and only, because we turn it off) prompt
134      String expect = ">\t";
135      byte[] buf = new byte[expect.length()];
136      int pos = is.read(buf);
137      assert pos != -1 : "Prompt exected, stream ended";
138      String actual = new String(buf, 0, pos, UTF_8);
139      assert expect.equals(actual) : "Prompt expected, got '" + actual + "'";
140
141    } catch (Exception | AssertionError e) {
142      if (trace_file != null) {
143        try {
144          trace_file.close();
145        } catch (Exception closeException) {
146          e.addSuppressed(closeException);
147        }
148      }
149      throw new SimplifyError(e);
150    }
151  }
152
153  /* package access */ void sendLine(
154      @UnknownInitialization(Session.class) @GuardSatisfied Session this, String s) {
155    if (dkconfig_trace_input) {
156      assert trace_file != null
157          : "@AssumeAssertion(nullness): dependent: trace_file is non-null (set in constructor) if"
158              + " dkconfig_trace_input is true";
159      trace_file.println(s);
160    }
161    input.println(s);
162    input.flush();
163  }
164
165  @Holding("this")
166  /* package access */
167  @Nullable String readLine(@GuardSatisfied Session this) throws IOException {
168    return output.readLine();
169  }
170
171  /** Releases the resources held by this. */
172  @EnsuresCalledMethods(value = "trace_file", methods = "close")
173  @Override
174  public void close(@GuardSatisfied Session this) {
175    process.destroy();
176    assert dkconfig_trace_input == (trace_file != null)
177        : "@AssumeAssertion(nullness): conditional: trace_file is non-null if"
178            + " dkconfig_trace_input==true";
179    if (trace_file != null) {
180      trace_file.close();
181    }
182  }
183
184  /**
185   * for testing and playing around, not for real use
186   *
187   * @param args command-line arguments
188   */
189  public static void main(String[] args) {
190    daikon.LogHelper.setupLogs(INFO);
191    try (@GuardedBy("<self>") Session s = new Session()) {
192
193      CmdCheck cc;
194
195      cc = new CmdCheck("(EQ 1 1)");
196      cc.apply(s);
197      assert cc.valid;
198
199      cc = new CmdCheck("(EQ 1 2)");
200      cc.apply(s);
201      assert !cc.valid;
202
203      cc = new CmdCheck("(EQ x z)");
204      cc.apply(s);
205      assert !cc.valid;
206
207      CmdAssume a = new CmdAssume("(AND (EQ x y) (EQ y z))");
208      a.apply(s);
209
210      cc.apply(s);
211      assert cc.valid;
212
213      CmdUndoAssume.single.apply(s);
214
215      cc.apply(s);
216      assert !cc.valid;
217    }
218  }
219}