Extract the Prolog evaluator out of SubmitRuleEvaluator

The new PrologRule class follows the to-be-implemented SubmitRule
interface.
SubmitRuleEvaluator does not depend directly on Prolog anymore, instead,
calls are redirected to the proxy class PrologRule

This gets us one step closer to a working SubmitRule implementation!

Change-Id: I3742dd68dd1ed8f7b8d76791dceb8c7c4cf631f7
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 918ceb2..c03c609 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -14,33 +14,15 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.common.base.Preconditions.checkState;
-
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.rules.PrologEnvironment;
-import com.google.gerrit.server.rules.StoredValues;
+import com.google.gerrit.server.rules.PrologRule;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import com.googlecode.prolog_cafe.exceptions.CompileException;
-import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
-import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.ListTerm;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.StructureTerm;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-import com.googlecode.prolog_cafe.lang.VariableTerm;
-import java.io.StringReader;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import org.slf4j.Logger;
@@ -52,9 +34,26 @@
  */
 public class SubmitRuleEvaluator {
   private static final Logger log = LoggerFactory.getLogger(SubmitRuleEvaluator.class);
-
   private static final String DEFAULT_MSG = "Error evaluating project rules, check server log";
 
+  private final ProjectCache projectCache;
+  private final PrologRule prologRule;
+  private final SubmitRuleOptions opts;
+
+  public interface Factory {
+    /** Returns a new {@link SubmitRuleEvaluator} with the specified options */
+    SubmitRuleEvaluator create(SubmitRuleOptions options);
+  }
+
+  @Inject
+  private SubmitRuleEvaluator(
+      ProjectCache projectCache, PrologRule prologRule, @Assisted SubmitRuleOptions options) {
+    this.projectCache = projectCache;
+    this.prologRule = prologRule;
+
+    this.opts = options;
+  }
+
   public static List<SubmitRecord> defaultRuleError() {
     return createRuleError(DEFAULT_MSG);
   }
@@ -71,46 +70,6 @@
   }
 
   /**
-   * Exception thrown when the label term of a submit record unexpectedly didn't contain a user
-   * term.
-   */
-  private static class UserTermExpected extends Exception {
-    private static final long serialVersionUID = 1L;
-
-    UserTermExpected(SubmitRecord.Label label) {
-      super(String.format("A label with the status %s must contain a user.", label.toString()));
-    }
-  }
-
-  public interface Factory {
-    /** Returns a new {@link SubmitRuleEvaluator} with the specified options */
-    SubmitRuleEvaluator create(SubmitRuleOptions options);
-  }
-
-  private final AccountCache accountCache;
-  private final Accounts accounts;
-  private final Emails emails;
-  private final ProjectCache projectCache;
-  private final SubmitRuleOptions opts;
-
-  private Term submitRule;
-
-  @Inject
-  private SubmitRuleEvaluator(
-      AccountCache accountCache,
-      Accounts accounts,
-      Emails emails,
-      ProjectCache projectCache,
-      @Assisted SubmitRuleOptions options) {
-    this.accountCache = accountCache;
-    this.accounts = accounts;
-    this.emails = emails;
-    this.projectCache = projectCache;
-
-    this.opts = options;
-  }
-
-  /**
    * Evaluate the submit rules.
    *
    * @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
@@ -140,152 +99,7 @@
       return Collections.singletonList(rec);
     }
 
-    List<Term> results;
-    try {
-      results =
-          evaluateImpl(
-              "locate_submit_rule",
-              "can_submit",
-              "locate_submit_filter",
-              "filter_submit_results",
-              cd,
-              projectState);
-    } catch (RuleEvalException e) {
-      return ruleError(e.getMessage(), e);
-    }
-
-    if (results.isEmpty()) {
-      // This should never occur. A well written submit rule will always produce
-      // at least one result informing the caller of the labels that are
-      // required for this change to be submittable. Each label will indicate
-      // whether or not that is actually possible given the permissions.
-      return ruleError(
-          String.format(
-              "Submit rule '%s' for change %s of %s has no solution.",
-              getSubmitRuleName(), cd.getId(), projectState.getName()));
-    }
-
-    return resultsToSubmitRecord(getSubmitRule(), results, cd);
-  }
-
-  /**
-   * Convert the results from Prolog Cafe's format to Gerrit's common format.
-   *
-   * <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
-   * using only that ok(P) record if it exists. This skips partial results that occur early in the
-   * output. Later after the loop the out collection is reversed to restore it to the original
-   * ordering.
-   */
-  public List<SubmitRecord> resultsToSubmitRecord(
-      Term submitRule, List<Term> results, ChangeData cd) {
-    boolean foundOk = false;
-    List<SubmitRecord> out = new ArrayList<>(results.size());
-    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
-      Term submitRecord = results.get(resultIdx);
-      SubmitRecord rec = new SubmitRecord();
-      out.add(rec);
-
-      if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
-        return invalidResult(submitRule, submitRecord, cd);
-      }
-
-      if ("ok".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.OK;
-
-      } else if ("not_ready".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.NOT_READY;
-
-      } else {
-        return invalidResult(submitRule, submitRecord, cd);
-      }
-
-      // Unpack the one argument. This should also be a structure with one
-      // argument per label that needs to be reported on to the caller.
-      //
-      submitRecord = submitRecord.arg(0);
-
-      if (!(submitRecord instanceof StructureTerm)) {
-        return invalidResult(submitRule, submitRecord, cd);
-      }
-
-      rec.labels = new ArrayList<>(submitRecord.arity());
-
-      for (Term state : ((StructureTerm) submitRecord).args()) {
-        if (!(state instanceof StructureTerm)
-            || 2 != state.arity()
-            || !"label".equals(state.name())) {
-          return invalidResult(submitRule, submitRecord, cd);
-        }
-
-        SubmitRecord.Label lbl = new SubmitRecord.Label();
-        rec.labels.add(lbl);
-
-        lbl.label = state.arg(0).name();
-        Term status = state.arg(1);
-
-        try {
-          if ("ok".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.OK;
-            appliedBy(lbl, status);
-
-          } else if ("reject".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.REJECT;
-            appliedBy(lbl, status);
-
-          } else if ("need".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.NEED;
-
-          } else if ("may".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.MAY;
-
-          } else if ("impossible".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
-
-          } else {
-            return invalidResult(submitRule, submitRecord, cd);
-          }
-        } catch (UserTermExpected e) {
-          return invalidResult(submitRule, submitRecord, e.getMessage(), cd);
-        }
-      }
-
-      if (rec.status == SubmitRecord.Status.OK) {
-        foundOk = true;
-        break;
-      }
-    }
-    Collections.reverse(out);
-
-    // This transformation is required to adapt Prolog's behavior to the way Gerrit handles
-    // SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
-    // When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
-    // the change to be submittable when at least one result is OK.
-    if (foundOk) {
-      for (SubmitRecord record : out) {
-        record.status = SubmitRecord.Status.OK;
-      }
-    }
-
-    return out;
-  }
-
-  private List<SubmitRecord> invalidResult(Term rule, Term record, String reason, ChangeData cd) {
-    return ruleError(
-        String.format(
-            "Submit rule %s for change %s of %s output invalid result: %s%s",
-            rule,
-            cd.getId(),
-            cd.project().get(),
-            record,
-            (reason == null ? "" : ". Reason: " + reason)));
-  }
-
-  private List<SubmitRecord> invalidResult(Term rule, Term record, ChangeData cd) {
-    return invalidResult(rule, record, null, cd);
-  }
-
-  private List<SubmitRecord> ruleError(String err) {
-    return ruleError(err, null);
+    return ImmutableList.copyOf(prologRule.evaluate(cd, opts));
   }
 
   private List<SubmitRecord> ruleError(String err, Exception e) {
@@ -317,62 +131,7 @@
       return typeError("Error looking up change " + cd.getId(), e);
     }
 
-    List<Term> results;
-    try {
-      results =
-          evaluateImpl(
-              "locate_submit_type",
-              "get_submit_type",
-              "locate_submit_type_filter",
-              "filter_submit_type_results",
-              cd,
-              projectState);
-    } catch (RuleEvalException e) {
-      return typeError(e.getMessage(), e);
-    }
-
-    if (results.isEmpty()) {
-      // Should never occur for a well written rule
-      return typeError(
-          "Submit rule '"
-              + getSubmitRuleName()
-              + "' for change "
-              + cd.getId()
-              + " of "
-              + projectState.getName()
-              + " has no solution.");
-    }
-
-    Term typeTerm = results.get(0);
-    if (!(typeTerm instanceof SymbolTerm)) {
-      return typeError(
-          "Submit rule '"
-              + getSubmitRuleName()
-              + "' for change "
-              + cd.getId()
-              + " of "
-              + projectState.getName()
-              + " did not return a symbol.");
-    }
-
-    String typeName = ((SymbolTerm) typeTerm).name();
-    try {
-      return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
-    } catch (IllegalArgumentException e) {
-      return typeError(
-          "Submit type rule "
-              + getSubmitRule()
-              + " for change "
-              + cd.getId()
-              + " of "
-              + projectState.getName()
-              + " output invalid result: "
-              + typeName);
-    }
-  }
-
-  private SubmitTypeRecord typeError(String err) {
-    return typeError(err, null);
+    return prologRule.getSubmitType(cd, opts);
   }
 
   private SubmitTypeRecord typeError(String err, Exception e) {
@@ -386,165 +145,4 @@
     }
     return SubmitTypeRecord.error(err);
   }
-
-  private List<Term> evaluateImpl(
-      String userRuleLocatorName,
-      String userRuleWrapperName,
-      String filterRuleLocatorName,
-      String filterRuleWrapperName,
-      ChangeData cd,
-      ProjectState projectState)
-      throws RuleEvalException {
-    PrologEnvironment env = getPrologEnvironment(cd, projectState);
-    try {
-      Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
-      List<Term> results = new ArrayList<>();
-      try {
-        for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
-          results.add(template[1]);
-        }
-      } catch (ReductionLimitException err) {
-        throw new RuleEvalException(
-            String.format(
-                "%s on change %d of %s",
-                err.getMessage(), cd.getId().get(), projectState.getName()));
-      } catch (RuntimeException err) {
-        throw new RuleEvalException(
-            String.format(
-                "Exception calling %s on change %d of %s",
-                sr, cd.getId().get(), projectState.getName()),
-            err);
-      }
-
-      Term resultsTerm = toListTerm(results);
-      if (!opts.skipFilters()) {
-        resultsTerm =
-            runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
-      }
-      List<Term> r;
-      if (resultsTerm instanceof ListTerm) {
-        r = new ArrayList<>();
-        for (Term t = resultsTerm; t instanceof ListTerm; ) {
-          ListTerm l = (ListTerm) t;
-          r.add(l.car().dereference());
-          t = l.cdr().dereference();
-        }
-      } else {
-        r = Collections.emptyList();
-      }
-      submitRule = sr;
-      return r;
-    } finally {
-      env.close();
-    }
-  }
-
-  private PrologEnvironment getPrologEnvironment(ChangeData cd, ProjectState projectState)
-      throws RuleEvalException {
-    PrologEnvironment env;
-    try {
-      if (opts.rule() == null) {
-        env = projectState.newPrologEnvironment();
-      } else {
-        env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
-      }
-    } catch (CompileException err) {
-      String msg;
-      if (opts.rule() == null) {
-        msg =
-            String.format(
-                "Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
-      } else {
-        msg = err.getMessage();
-      }
-      throw new RuleEvalException(msg, err);
-    }
-    env.set(StoredValues.ACCOUNTS, accounts);
-    env.set(StoredValues.ACCOUNT_CACHE, accountCache);
-    env.set(StoredValues.EMAILS, emails);
-    env.set(StoredValues.REVIEW_DB, cd.db());
-    env.set(StoredValues.CHANGE_DATA, cd);
-    env.set(StoredValues.PROJECT_STATE, projectState);
-    return env;
-  }
-
-  private Term runSubmitFilters(
-      Term results,
-      PrologEnvironment env,
-      String filterRuleLocatorName,
-      String filterRuleWrapperName)
-      throws RuleEvalException {
-    PrologEnvironment childEnv = env;
-    ChangeData cd = env.get(StoredValues.CHANGE_DATA);
-    ProjectState projectState = env.get(StoredValues.PROJECT_STATE);
-    for (ProjectState parentState : projectState.parents()) {
-      PrologEnvironment parentEnv;
-      try {
-        parentEnv = parentState.newPrologEnvironment();
-      } catch (CompileException err) {
-        throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
-      }
-
-      parentEnv.copyStoredValues(childEnv);
-      Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
-      try {
-        Term[] template =
-            parentEnv.once(
-                "gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
-        results = template[2];
-      } catch (ReductionLimitException err) {
-        throw new RuleEvalException(
-            String.format(
-                "%s on change %d of %s",
-                err.getMessage(), cd.getId().get(), parentState.getName()));
-      } catch (RuntimeException err) {
-        throw new RuleEvalException(
-            String.format(
-                "Exception calling %s on change %d of %s",
-                filterRule, cd.getId().get(), parentState.getName()),
-            err);
-      }
-      childEnv = parentEnv;
-    }
-    return results;
-  }
-
-  private static Term toListTerm(List<Term> terms) {
-    Term list = Prolog.Nil;
-    for (int i = terms.size() - 1; i >= 0; i--) {
-      list = new ListTerm(terms.get(i), list);
-    }
-    return list;
-  }
-
-  private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
-    if (status instanceof StructureTerm && status.arity() == 1) {
-      Term who = status.arg(0);
-      if (isUser(who)) {
-        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
-      } else {
-        throw new UserTermExpected(label);
-      }
-    }
-  }
-
-  private static boolean isUser(Term who) {
-    return who instanceof StructureTerm
-        && who.arity() == 1
-        && who.name().equals("user")
-        && who.arg(0) instanceof IntegerTerm;
-  }
-
-  public Term getSubmitRule() {
-    checkState(submitRule != null, "getSubmitRule() invalid before evaluation");
-    return submitRule;
-  }
-
-  public String getSubmitRuleName() {
-    return submitRule != null ? submitRule.toString() : "<unknown rule>";
-  }
-
-  private void checkNotStarted() {
-    checkState(opts == null, "cannot set options after starting evaluation");
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
index d7f34e6..c1be1ce 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -78,8 +78,7 @@
     SubmitTypeRecord rec = evaluator.getSubmitType(cd);
 
     if (rec.status != SubmitTypeRecord.Status.OK) {
-      throw new BadRequestException(
-          String.format("rule %s produced invalid result: %s", evaluator.getSubmitRuleName(), rec));
+      throw new BadRequestException(String.format("rule produced invalid result: %s", rec));
     }
 
     return rec.type;
diff --git a/java/com/google/gerrit/server/rules/PrologModule.java b/java/com/google/gerrit/server/rules/PrologModule.java
index 1a8b46c..4e4995d 100644
--- a/java/com/google/gerrit/server/rules/PrologModule.java
+++ b/java/com/google/gerrit/server/rules/PrologModule.java
@@ -22,6 +22,8 @@
   protected void configure() {
     install(new EnvironmentModule());
     bind(PrologEnvironment.Args.class);
+    bind(PrologRule.class);
+    factory(PrologRuleEvaluator.Factory.class);
   }
 
   static class EnvironmentModule extends FactoryModule {
diff --git a/java/com/google/gerrit/server/rules/PrologRule.java b/java/com/google/gerrit/server/rules/PrologRule.java
new file mode 100644
index 0000000..00a65f4
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/PrologRule.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.rules;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologRuleEvaluator.Factory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+
+@Singleton
+public class PrologRule {
+
+  private final Factory factory;
+
+  @Inject
+  private PrologRule(PrologRuleEvaluator.Factory factory) {
+    this.factory = factory;
+  }
+
+  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions opts) {
+    return getEvaluator(cd, opts).evaluate();
+  }
+
+  private PrologRuleEvaluator getEvaluator(ChangeData cd, SubmitRuleOptions opts) {
+    return factory.create(cd, opts);
+  }
+
+  public SubmitTypeRecord getSubmitType(ChangeData cd, SubmitRuleOptions opts) {
+    return getEvaluator(cd, opts).getSubmitType();
+  }
+}
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
new file mode 100644
index 0000000..9d31472
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -0,0 +1,515 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.rules;
+
+import static com.google.gerrit.server.project.SubmitRuleEvaluator.createRuleError;
+import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultRuleError;
+import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultTypeError;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RuleEvalException;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
+import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
+ * the results through rules found in the parent projects, all the way up to All-Projects.
+ */
+public class PrologRuleEvaluator {
+  private static final Logger log = LoggerFactory.getLogger(PrologRuleEvaluator.class);
+
+  public interface Factory {
+    /** Returns a new {@link PrologRuleEvaluator} with the specified options */
+    PrologRuleEvaluator create(ChangeData cd, SubmitRuleOptions options);
+  }
+
+  /**
+   * Exception thrown when the label term of a submit record unexpectedly didn't contain a user
+   * term.
+   */
+  private static class UserTermExpected extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    UserTermExpected(SubmitRecord.Label label) {
+      super(String.format("A label with the status %s must contain a user.", label.toString()));
+    }
+  }
+
+  private final AccountCache accountCache;
+  private final Accounts accounts;
+  private final Emails emails;
+  private final ChangeData cd;
+  private final ProjectState projectState;
+  private final SubmitRuleOptions opts;
+  private Term submitRule;
+
+  @AssistedInject
+  private PrologRuleEvaluator(
+      AccountCache accountCache,
+      Accounts accounts,
+      Emails emails,
+      ProjectCache projectCache,
+      @Assisted ChangeData cd,
+      @Assisted SubmitRuleOptions options) {
+    this.accountCache = accountCache;
+    this.accounts = accounts;
+    this.emails = emails;
+    this.cd = cd;
+    this.opts = options;
+
+    this.projectState = projectCache.get(cd.project());
+  }
+
+  private static Term toListTerm(List<Term> terms) {
+    Term list = Prolog.Nil;
+    for (int i = terms.size() - 1; i >= 0; i--) {
+      list = new ListTerm(terms.get(i), list);
+    }
+    return list;
+  }
+
+  private static boolean isUser(Term who) {
+    return who instanceof StructureTerm
+        && who.arity() == 1
+        && who.name().equals("user")
+        && who.arg(0) instanceof IntegerTerm;
+  }
+
+  private Term getSubmitRule() {
+    return submitRule;
+  }
+
+  /**
+   * Evaluate the submit rules.
+   *
+   * @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
+   *     errors.
+   */
+  public Collection<SubmitRecord> evaluate() {
+    Change change;
+    try {
+      change = cd.change();
+      if (change == null) {
+        throw new OrmException("No change found");
+      }
+
+      if (projectState == null) {
+        throw new NoSuchProjectException(cd.project());
+      }
+    } catch (OrmException | NoSuchProjectException e) {
+      return ruleError("Error looking up change " + cd.getId(), e);
+    }
+
+    if (!opts.allowClosed() && change.getStatus().isClosed()) {
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.CLOSED;
+      return Collections.singletonList(rec);
+    }
+
+    List<Term> results;
+    try {
+      results =
+          evaluateImpl(
+              "locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
+    } catch (RuleEvalException e) {
+      return ruleError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // This should never occur. A well written submit rule will always produce
+      // at least one result informing the caller of the labels that are
+      // required for this change to be submittable. Each label will indicate
+      // whether or not that is actually possible given the permissions.
+      return ruleError(
+          String.format(
+              "Submit rule '%s' for change %s of %s has no solution.",
+              getSubmitRuleName(), cd.getId(), projectState.getName()));
+    }
+
+    return resultsToSubmitRecord(getSubmitRule(), results);
+  }
+
+  private String getSubmitRuleName() {
+    return submitRule == null ? "<unknown>" : submitRule.name();
+  }
+
+  /**
+   * Convert the results from Prolog Cafe's format to Gerrit's common format.
+   *
+   * <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
+   * using only that ok(P) record if it exists. This skips partial results that occur early in the
+   * output. Later after the loop the out collection is reversed to restore it to the original
+   * ordering.
+   */
+  public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
+    boolean foundOk = false;
+    List<SubmitRecord> out = new ArrayList<>(results.size());
+    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
+      Term submitRecord = results.get(resultIdx);
+      SubmitRecord rec = new SubmitRecord();
+      out.add(rec);
+
+      if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      if ("ok".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.OK;
+
+      } else if ("not_ready".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.NOT_READY;
+
+      } else {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      // Unpack the one argument. This should also be a structure with one
+      // argument per label that needs to be reported on to the caller.
+      //
+      submitRecord = submitRecord.arg(0);
+
+      if (!(submitRecord instanceof StructureTerm)) {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      rec.labels = new ArrayList<>(submitRecord.arity());
+
+      for (Term state : ((StructureTerm) submitRecord).args()) {
+        if (!(state instanceof StructureTerm)
+            || 2 != state.arity()
+            || !"label".equals(state.name())) {
+          return invalidResult(submitRule, submitRecord);
+        }
+
+        SubmitRecord.Label lbl = new SubmitRecord.Label();
+        rec.labels.add(lbl);
+
+        lbl.label = state.arg(0).name();
+        Term status = state.arg(1);
+
+        try {
+          if ("ok".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.OK;
+            appliedBy(lbl, status);
+
+          } else if ("reject".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.REJECT;
+            appliedBy(lbl, status);
+
+          } else if ("need".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.NEED;
+
+          } else if ("may".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.MAY;
+
+          } else if ("impossible".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
+
+          } else {
+            return invalidResult(submitRule, submitRecord);
+          }
+        } catch (UserTermExpected e) {
+          return invalidResult(submitRule, submitRecord, e.getMessage());
+        }
+      }
+
+      if (rec.status == SubmitRecord.Status.OK) {
+        foundOk = true;
+        break;
+      }
+    }
+    Collections.reverse(out);
+
+    if (foundOk) {
+      for (SubmitRecord record : out) {
+        record.status = SubmitRecord.Status.OK;
+      }
+    }
+
+    return out;
+  }
+
+  private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
+    return ruleError(
+        String.format(
+            "Submit rule %s for change %s of %s output invalid result: %s%s",
+            rule,
+            cd.getId(),
+            cd.project().get(),
+            record,
+            (reason == null ? "" : ". Reason: " + reason)));
+  }
+
+  private List<SubmitRecord> invalidResult(Term rule, Term record) {
+    return invalidResult(rule, record, null);
+  }
+
+  private List<SubmitRecord> ruleError(String err) {
+    return ruleError(err, null);
+  }
+
+  private List<SubmitRecord> ruleError(String err, Exception e) {
+    if (opts.logErrors()) {
+      if (e == null) {
+        log.error(err);
+      } else {
+        log.error(err, e);
+      }
+      return defaultRuleError();
+    }
+    return createRuleError(err);
+  }
+
+  /**
+   * Evaluate the submit type rules to get the submit type.
+   *
+   * @return record from the evaluated rules.
+   */
+  public SubmitTypeRecord getSubmitType() {
+    try {
+      if (projectState == null) {
+        throw new NoSuchProjectException(cd.project());
+      }
+    } catch (NoSuchProjectException e) {
+      return typeError("Error looking up change " + cd.getId(), e);
+    }
+
+    List<Term> results;
+    try {
+      results =
+          evaluateImpl(
+              "locate_submit_type",
+              "get_submit_type",
+              "locate_submit_type_filter",
+              "filter_submit_type_results");
+    } catch (RuleEvalException e) {
+      return typeError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // Should never occur for a well written rule
+      return typeError(
+          "Submit rule '"
+              + getSubmitRuleName()
+              + "' for change "
+              + cd.getId()
+              + " of "
+              + projectState.getName()
+              + " has no solution.");
+    }
+
+    Term typeTerm = results.get(0);
+    if (!(typeTerm instanceof SymbolTerm)) {
+      return typeError(
+          "Submit rule '"
+              + getSubmitRuleName()
+              + "' for change "
+              + cd.getId()
+              + " of "
+              + projectState.getName()
+              + " did not return a symbol.");
+    }
+
+    String typeName = typeTerm.name();
+    try {
+      return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
+    } catch (IllegalArgumentException e) {
+      return typeError(
+          "Submit type rule "
+              + getSubmitRule()
+              + " for change "
+              + cd.getId()
+              + " of "
+              + projectState.getName()
+              + " output invalid result: "
+              + typeName);
+    }
+  }
+
+  private SubmitTypeRecord typeError(String err) {
+    return typeError(err, null);
+  }
+
+  private SubmitTypeRecord typeError(String err, Exception e) {
+    if (opts.logErrors()) {
+      if (e == null) {
+        log.error(err);
+      } else {
+        log.error(err, e);
+      }
+      return defaultTypeError();
+    }
+    return SubmitTypeRecord.error(err);
+  }
+
+  private List<Term> evaluateImpl(
+      String userRuleLocatorName,
+      String userRuleWrapperName,
+      String filterRuleLocatorName,
+      String filterRuleWrapperName)
+      throws RuleEvalException {
+    PrologEnvironment env = getPrologEnvironment();
+    try {
+      Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
+      List<Term> results = new ArrayList<>();
+      try {
+        for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
+          results.add(template[1]);
+        }
+      } catch (ReductionLimitException err) {
+        throw new RuleEvalException(
+            String.format(
+                "%s on change %d of %s",
+                err.getMessage(), cd.getId().get(), projectState.getName()));
+      } catch (RuntimeException err) {
+        throw new RuleEvalException(
+            String.format(
+                "Exception calling %s on change %d of %s",
+                sr, cd.getId().get(), projectState.getName()),
+            err);
+      }
+
+      Term resultsTerm = toListTerm(results);
+      if (!opts.skipFilters()) {
+        resultsTerm =
+            runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
+      }
+      List<Term> r;
+      if (resultsTerm instanceof ListTerm) {
+        r = new ArrayList<>();
+        for (Term t = resultsTerm; t instanceof ListTerm; ) {
+          ListTerm l = (ListTerm) t;
+          r.add(l.car().dereference());
+          t = l.cdr().dereference();
+        }
+      } else {
+        r = Collections.emptyList();
+      }
+      submitRule = sr;
+      return r;
+    } finally {
+      env.close();
+    }
+  }
+
+  private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
+    PrologEnvironment env;
+    try {
+      if (opts.rule() == null) {
+        env = projectState.newPrologEnvironment();
+      } else {
+        env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
+      }
+    } catch (CompileException err) {
+      String msg;
+      if (opts.rule() == null) {
+        msg =
+            String.format(
+                "Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
+      } else {
+        msg = err.getMessage();
+      }
+      throw new RuleEvalException(msg, err);
+    }
+    env.set(StoredValues.ACCOUNTS, accounts);
+    env.set(StoredValues.ACCOUNT_CACHE, accountCache);
+    env.set(StoredValues.EMAILS, emails);
+    env.set(StoredValues.REVIEW_DB, cd.db());
+    env.set(StoredValues.CHANGE_DATA, cd);
+    env.set(StoredValues.PROJECT_STATE, projectState);
+    return env;
+  }
+
+  private Term runSubmitFilters(
+      Term results,
+      PrologEnvironment env,
+      String filterRuleLocatorName,
+      String filterRuleWrapperName)
+      throws RuleEvalException {
+    PrologEnvironment childEnv = env;
+    ChangeData cd = env.get(StoredValues.CHANGE_DATA);
+    ProjectState projectState = env.get(StoredValues.PROJECT_STATE);
+    for (ProjectState parentState : projectState.parents()) {
+      PrologEnvironment parentEnv;
+      try {
+        parentEnv = parentState.newPrologEnvironment();
+      } catch (CompileException err) {
+        throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
+      }
+
+      parentEnv.copyStoredValues(childEnv);
+      Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
+      try {
+        Term[] template =
+            parentEnv.once(
+                "gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
+        results = template[2];
+      } catch (ReductionLimitException err) {
+        throw new RuleEvalException(
+            String.format(
+                "%s on change %d of %s",
+                err.getMessage(), cd.getId().get(), parentState.getName()));
+      } catch (RuntimeException err) {
+        throw new RuleEvalException(
+            String.format(
+                "Exception calling %s on change %d of %s",
+                filterRule, cd.getId().get(), parentState.getName()),
+            err);
+      }
+      childEnv = parentEnv;
+    }
+    return results;
+  }
+
+  private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
+    if (status instanceof StructureTerm && status.arity() == 1) {
+      Term who = status.arg(0);
+      if (isUser(who)) {
+        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
+      } else {
+        throw new UserTermExpected(label);
+      }
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/BUILD b/javatests/com/google/gerrit/acceptance/server/project/BUILD
index b834f2e..efa1cdb 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/project/BUILD
@@ -4,7 +4,4 @@
     srcs = glob(["*IT.java"]),
     group = "server_project",
     labels = ["server"],
-    deps = [
-        "@prolog_runtime//jar",
-    ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/BUILD b/javatests/com/google/gerrit/acceptance/server/rules/BUILD
new file mode 100644
index 0000000..2e96c0b
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/rules/BUILD
@@ -0,0 +1,10 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_rules",
+    labels = ["server"],
+    deps = [
+        "@prolog_runtime//jar",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRulesEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
similarity index 90%
rename from javatests/com/google/gerrit/acceptance/server/project/SubmitRulesEvaluatorIT.java
rename to javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
index 7941860..8fc32b4 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRulesEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/PrologRuleEvaluatorIT.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.server.project;
+package com.google.gerrit.acceptance.server.rules;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -21,9 +21,9 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologRuleEvaluator;
 import com.google.gerrit.testing.TestChanges;
 import com.google.inject.Inject;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -34,19 +34,18 @@
 import java.util.List;
 import org.junit.Test;
 
-public class SubmitRulesEvaluatorIT extends AbstractDaemonTest {
-  @Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
+public class PrologRuleEvaluatorIT extends AbstractDaemonTest {
+  @Inject private PrologRuleEvaluator.Factory evaluatorFactory;
 
   @Test
   public void convertsPrologToSubmitRecord() {
-    SubmitRuleEvaluator evaluator = makeEvaluator();
-    ChangeData cd = makeChangeData();
+    PrologRuleEvaluator evaluator = makeEvaluator();
 
     StructureTerm verifiedLabel = makeLabel("Verified", "may");
     StructureTerm labels = new StructureTerm("label", verifiedLabel);
 
     List<Term> terms = ImmutableList.of(makeTerm("ok", labels));
-    Collection<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms, cd);
+    Collection<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
 
     assertThat(records).hasSize(1);
   }
@@ -83,8 +82,7 @@
    */
   @Test
   public void abortsEarlyWithOkayRecord() {
-    SubmitRuleEvaluator evaluator = makeEvaluator();
-    ChangeData cd = makeChangeData();
+    PrologRuleEvaluator evaluator = makeEvaluator();
 
     SubmitRecord.Label submitRecordLabel1 = new SubmitRecord.Label();
     submitRecordLabel1.label = "Verified";
@@ -114,7 +112,7 @@
     terms.add(makeTerm("not_ready", makeLabels(label3)));
 
     // When
-    List<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms, cd);
+    List<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
 
     // assert that
     SubmitRecord record1Expected = new SubmitRecord();
@@ -156,7 +154,7 @@
     return cd;
   }
 
-  private SubmitRuleEvaluator makeEvaluator() {
-    return evaluatorFactory.create(SubmitRuleOptions.defaults());
+  private PrologRuleEvaluator makeEvaluator() {
+    return evaluatorFactory.create(makeChangeData(), SubmitRuleOptions.defaults());
   }
 }