Apply reduction limit during evaluation

Caps the amount of virtual CPU time that a single goal
can consume during all searches for solutions. Each
reduction is one predicate invocation or alternate tried.

Change-Id: If3d41c14b9dd5330afe30f5a9ba502e2582c2e83
diff --git a/src/compiler/Compiler.java b/src/compiler/Compiler.java
index 1912022..cfc1a78 100644
--- a/src/compiler/Compiler.java
+++ b/src/compiler/Compiler.java
@@ -101,6 +101,7 @@
       pcl = new BufferingPrologControl();
       pcl.setEnabled(EnumSet.allOf(Prolog.Feature.class), true);
       pcl.setPrologClassLoader(new PrologClassLoader(Compiler.class.getClassLoader()));
+      pcl.setReductionLimit(Long.MAX_VALUE);
       options = EnumSet.noneOf(Option.class);
       enableDefaultOptions();
     }
diff --git a/src/exceptions/ReductionLimitException.java b/src/exceptions/ReductionLimitException.java
new file mode 100644
index 0000000..80ba112
--- /dev/null
+++ b/src/exceptions/ReductionLimitException.java
@@ -0,0 +1,14 @@
+package com.googlecode.prolog_cafe.exceptions;
+
+/**
+ * Thrown if a goal exceeds the configured reduction limit.
+ *
+ * @see com.googlecode.prolog_cafe.lang.PrologControl#setReductionLimit(long)
+ */
+public class ReductionLimitException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public ReductionLimitException(long limit) {
+    super(String.format("exceeded reduction limit of %d", limit));
+  }
+}
diff --git a/src/lang/PrologControl.java b/src/lang/PrologControl.java
index 5882060..02bd6ae 100644
--- a/src/lang/PrologControl.java
+++ b/src/lang/PrologControl.java
@@ -2,6 +2,7 @@
 
 import com.googlecode.prolog_cafe.exceptions.HaltException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
+import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -32,6 +33,10 @@
     /** Holds a Prolog goal to be executed. */
     protected Operation code;
 
+    /** How many operations can be executed before exceeding cost limit. */
+    private long reductionLimit = 1 << 20;
+    private long reductionsUsed;
+
     /** Constructs a new <code>PrologControl</code>. */
     public PrologControl() {
       engine = new Prolog(this);
@@ -172,11 +177,14 @@
     protected void executePredicate() throws PrologException {
       Prolog engine = this.engine;
       Operation code = this.code;
+      long reductionsRemaining = reductionLimit;
       try {
         engine.init();
 
         do {
           if (isEngineStopped()) return;
+          if (--reductionsRemaining <= 0)
+              throw new ReductionLimitException(reductionLimit);
           code = code.exec(engine);
         } while (engine.halt == 0);
 
@@ -184,11 +192,22 @@
             throw new HaltException(engine.halt - 1);
         }
       } finally {
+        this.reductionsUsed = reductionLimit - reductionsRemaining;
         this.code = code;
         SymbolTerm.gc();
       }
     }
 
+    /** @return number of reductions used by execution. */
+    public long getReductions() {
+      return reductionsUsed;
+    }
+
+    /** Applies an upper limit on number of reductions. */
+    public void setReductionLimit(long limit) {
+      reductionLimit = Math.max(0, limit);
+    }
+
     /** @param err stack trace to print (or log). */
     public void printStackTrace(Throwable err) {
       err.printStackTrace();