aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/store-query/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/store-query/src/main/java')
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java23
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryBuilder.java3
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryStoreAdapter.java7
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java102
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java30
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java73
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfClause.java9
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java112
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java4
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java15
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java35
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Query.java23
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java13
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/SymbolicParameter.java5
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java5
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DnfEqualityChecker.java4
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralEqualityHelper.java49
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralHashCodeHelper.java17
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralEqualityHelper.java59
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralHashCodeHelper.java42
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java60
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java107
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractLiteral.java34
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java36
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java49
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/BooleanLiteral.java6
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java28
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java4
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java (renamed from subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssumeLiteral.java)50
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Connectivity.java18
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/ConstantLiteral.java38
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CountLiteral.java80
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java61
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literal.java3
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literals.java4
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java119
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AbstractResultSet.java2
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AnyResultSet.java2
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/EmptyResultSet.java2
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/OrderedResultSet.java6
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/ResultSet.java2
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/AbstractRecursiveRewriter.java26
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/ClauseInputParameterResolver.java160
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/CompositeRewriter.java29
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DnfRewriter.java24
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DuplicateDnfRemover.java98
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/InputParameterResolver.java51
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/AbstractTerm.java16
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java18
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyTerm.java3
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java31
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java24
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java25
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java23
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/Parameter.java12
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java4
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java25
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/Variable.java8
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/term/uppercardinality/UpperCardinalitySumAggregator.java2
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/utils/OrderStatisticTree.java2
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/view/AbstractFunctionView.java15
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/view/AnySymbolView.java8
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java3
-rw-r--r--subprojects/store-query/src/main/java/tools/refinery/store/query/view/TuplePreservingView.java16
64 files changed, 1533 insertions, 431 deletions
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java
new file mode 100644
index 00000000..c39277a0
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/InvalidQueryException.java
@@ -0,0 +1,23 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query;
7
8public class InvalidQueryException extends RuntimeException {
9 public InvalidQueryException() {
10 }
11
12 public InvalidQueryException(String message) {
13 super(message);
14 }
15
16 public InvalidQueryException(String message, Throwable cause) {
17 super(message, cause);
18 }
19
20 public InvalidQueryException(Throwable cause) {
21 super(cause);
22 }
23}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryBuilder.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryBuilder.java
index c62a95b5..332e6381 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryBuilder.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryBuilder.java
@@ -8,6 +8,7 @@ package tools.refinery.store.query;
8import tools.refinery.store.adapter.ModelAdapterBuilder; 8import tools.refinery.store.adapter.ModelAdapterBuilder;
9import tools.refinery.store.model.ModelStore; 9import tools.refinery.store.model.ModelStore;
10import tools.refinery.store.query.dnf.AnyQuery; 10import tools.refinery.store.query.dnf.AnyQuery;
11import tools.refinery.store.query.rewriter.DnfRewriter;
11 12
12import java.util.Collection; 13import java.util.Collection;
13import java.util.List; 14import java.util.List;
@@ -25,6 +26,8 @@ public interface ModelQueryBuilder extends ModelAdapterBuilder {
25 26
26 ModelQueryBuilder query(AnyQuery query); 27 ModelQueryBuilder query(AnyQuery query);
27 28
29 ModelQueryBuilder rewriter(DnfRewriter rewriter);
30
28 @Override 31 @Override
29 ModelQueryStoreAdapter build(ModelStore store); 32 ModelQueryStoreAdapter build(ModelStore store);
30} 33}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryStoreAdapter.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryStoreAdapter.java
index f0a950a6..8b67c5c1 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryStoreAdapter.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/ModelQueryStoreAdapter.java
@@ -8,6 +8,7 @@ package tools.refinery.store.query;
8import tools.refinery.store.adapter.ModelStoreAdapter; 8import tools.refinery.store.adapter.ModelStoreAdapter;
9import tools.refinery.store.model.Model; 9import tools.refinery.store.model.Model;
10import tools.refinery.store.query.dnf.AnyQuery; 10import tools.refinery.store.query.dnf.AnyQuery;
11import tools.refinery.store.query.dnf.Query;
11import tools.refinery.store.query.view.AnySymbolView; 12import tools.refinery.store.query.view.AnySymbolView;
12 13
13import java.util.Collection; 14import java.util.Collection;
@@ -17,6 +18,12 @@ public interface ModelQueryStoreAdapter extends ModelStoreAdapter {
17 18
18 Collection<AnyQuery> getQueries(); 19 Collection<AnyQuery> getQueries();
19 20
21 default AnyQuery getCanonicalQuery(AnyQuery query) {
22 return getCanonicalQuery((Query<?>) query);
23 }
24
25 <T> Query<T> getCanonicalQuery(Query<T> query);
26
20 @Override 27 @Override
21 ModelQueryAdapter createModelAdapter(Model model); 28 ModelQueryAdapter createModelAdapter(Model model);
22} 29}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java
index b5e7092b..8800a155 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/ClausePostProcessor.java
@@ -6,13 +6,12 @@
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import org.jetbrains.annotations.NotNull; 8import org.jetbrains.annotations.NotNull;
9import tools.refinery.store.query.literal.BooleanLiteral; 9import tools.refinery.store.query.Constraint;
10import tools.refinery.store.query.literal.EquivalenceLiteral; 10import tools.refinery.store.query.InvalidQueryException;
11import tools.refinery.store.query.literal.Literal; 11import tools.refinery.store.query.literal.*;
12import tools.refinery.store.query.substitution.MapBasedSubstitution; 12import tools.refinery.store.query.substitution.MapBasedSubstitution;
13import tools.refinery.store.query.substitution.StatelessSubstitution; 13import tools.refinery.store.query.substitution.StatelessSubstitution;
14import tools.refinery.store.query.substitution.Substitution; 14import tools.refinery.store.query.substitution.Substitution;
15import tools.refinery.store.query.term.NodeVariable;
16import tools.refinery.store.query.term.ParameterDirection; 15import tools.refinery.store.query.term.ParameterDirection;
17import tools.refinery.store.query.term.Variable; 16import tools.refinery.store.query.term.Variable;
18 17
@@ -22,8 +21,8 @@ import java.util.function.Function;
22class ClausePostProcessor { 21class ClausePostProcessor {
23 private final Map<Variable, ParameterInfo> parameters; 22 private final Map<Variable, ParameterInfo> parameters;
24 private final List<Literal> literals; 23 private final List<Literal> literals;
25 private final Map<NodeVariable, NodeVariable> representatives = new LinkedHashMap<>(); 24 private final Map<Variable, Variable> representatives = new LinkedHashMap<>();
26 private final Map<NodeVariable, Set<NodeVariable>> equivalencePartition = new HashMap<>(); 25 private final Map<Variable, Set<Variable>> equivalencePartition = new HashMap<>();
27 private List<Literal> substitutedLiterals; 26 private List<Literal> substitutedLiterals;
28 private final Set<Variable> existentiallyQuantifiedVariables = new LinkedHashSet<>(); 27 private final Set<Variable> existentiallyQuantifiedVariables = new LinkedHashSet<>();
29 private Set<Variable> positiveVariables; 28 private Set<Variable> positiveVariables;
@@ -58,6 +57,9 @@ class ClausePostProcessor {
58 if (filteredLiterals.isEmpty()) { 57 if (filteredLiterals.isEmpty()) {
59 return ConstantResult.ALWAYS_TRUE; 58 return ConstantResult.ALWAYS_TRUE;
60 } 59 }
60 if (hasContradictoryCall(filteredLiterals)) {
61 return ConstantResult.ALWAYS_FALSE;
62 }
61 var clause = new DnfClause(Collections.unmodifiableSet(positiveVariables), 63 var clause = new DnfClause(Collections.unmodifiableSet(positiveVariables),
62 Collections.unmodifiableList(filteredLiterals)); 64 Collections.unmodifiableList(filteredLiterals));
63 return new ClauseResult(clause); 65 return new ClauseResult(clause);
@@ -67,16 +69,16 @@ class ClausePostProcessor {
67 for (var literal : literals) { 69 for (var literal : literals) {
68 if (isPositiveEquivalence(literal)) { 70 if (isPositiveEquivalence(literal)) {
69 var equivalenceLiteral = (EquivalenceLiteral) literal; 71 var equivalenceLiteral = (EquivalenceLiteral) literal;
70 mergeVariables(equivalenceLiteral.left(), equivalenceLiteral.right()); 72 mergeVariables(equivalenceLiteral.getLeft(), equivalenceLiteral.getRight());
71 } 73 }
72 } 74 }
73 } 75 }
74 76
75 private static boolean isPositiveEquivalence(Literal literal) { 77 private static boolean isPositiveEquivalence(Literal literal) {
76 return literal instanceof EquivalenceLiteral equivalenceLiteral && equivalenceLiteral.positive(); 78 return literal instanceof EquivalenceLiteral equivalenceLiteral && equivalenceLiteral.isPositive();
77 } 79 }
78 80
79 private void mergeVariables(NodeVariable left, NodeVariable right) { 81 private void mergeVariables(Variable left, Variable right) {
80 var leftRepresentative = getRepresentative(left); 82 var leftRepresentative = getRepresentative(left);
81 var rightRepresentative = getRepresentative(right); 83 var rightRepresentative = getRepresentative(right);
82 var leftInfo = parameters.get(leftRepresentative); 84 var leftInfo = parameters.get(leftRepresentative);
@@ -89,7 +91,7 @@ class ClausePostProcessor {
89 } 91 }
90 } 92 }
91 93
92 private void doMergeVariables(NodeVariable parentRepresentative, NodeVariable newChildRepresentative) { 94 private void doMergeVariables(Variable parentRepresentative, Variable newChildRepresentative) {
93 var parentSet = getEquivalentVariables(parentRepresentative); 95 var parentSet = getEquivalentVariables(parentRepresentative);
94 var childSet = getEquivalentVariables(newChildRepresentative); 96 var childSet = getEquivalentVariables(newChildRepresentative);
95 parentSet.addAll(childSet); 97 parentSet.addAll(childSet);
@@ -99,18 +101,18 @@ class ClausePostProcessor {
99 } 101 }
100 } 102 }
101 103
102 private NodeVariable getRepresentative(NodeVariable variable) { 104 private Variable getRepresentative(Variable variable) {
103 return representatives.computeIfAbsent(variable, Function.identity()); 105 return representatives.computeIfAbsent(variable, Function.identity());
104 } 106 }
105 107
106 private Set<NodeVariable> getEquivalentVariables(NodeVariable variable) { 108 private Set<Variable> getEquivalentVariables(Variable variable) {
107 var representative = getRepresentative(variable); 109 var representative = getRepresentative(variable);
108 if (!representative.equals(variable)) { 110 if (!representative.equals(variable)) {
109 throw new AssertionError("NodeVariable %s already has a representative %s" 111 throw new AssertionError("NodeVariable %s already has a representative %s"
110 .formatted(variable, representative)); 112 .formatted(variable, representative));
111 } 113 }
112 return equivalencePartition.computeIfAbsent(variable, key -> { 114 return equivalencePartition.computeIfAbsent(variable, key -> {
113 var set = new HashSet<NodeVariable>(1); 115 var set = new HashSet<Variable>(1);
114 set.add(key); 116 set.add(key);
115 return set; 117 return set;
116 }); 118 });
@@ -121,7 +123,7 @@ class ClausePostProcessor {
121 var left = pair.getKey(); 123 var left = pair.getKey();
122 var right = pair.getValue(); 124 var right = pair.getValue();
123 if (!left.equals(right) && parameters.containsKey(left) && parameters.containsKey(right)) { 125 if (!left.equals(right) && parameters.containsKey(left) && parameters.containsKey(right)) {
124 substitutedLiterals.add(left.isEquivalent(right)); 126 substitutedLiterals.add(new EquivalenceLiteral(true, left, right));
125 } 127 }
126 } 128 }
127 } 129 }
@@ -147,20 +149,7 @@ class ClausePostProcessor {
147 149
148 private void computeExistentiallyQuantifiedVariables() { 150 private void computeExistentiallyQuantifiedVariables() {
149 for (var literal : substitutedLiterals) { 151 for (var literal : substitutedLiterals) {
150 for (var variable : literal.getOutputVariables()) { 152 existentiallyQuantifiedVariables.addAll(literal.getOutputVariables());
151 boolean added = existentiallyQuantifiedVariables.add(variable);
152 if (!variable.isUnifiable()) {
153 var parameterInfo = parameters.get(variable);
154 if (parameterInfo != null && parameterInfo.direction() == ParameterDirection.IN) {
155 throw new IllegalArgumentException("Trying to bind %s parameter %s"
156 .formatted(ParameterDirection.IN, variable));
157 }
158 if (!added) {
159 throw new IllegalArgumentException("Variable %s has multiple assigned values"
160 .formatted(variable));
161 }
162 }
163 }
164 } 153 }
165 } 154 }
166 155
@@ -172,7 +161,7 @@ class ClausePostProcessor {
172 // Inputs count as positive, because they are already bound when we evaluate literals. 161 // Inputs count as positive, because they are already bound when we evaluate literals.
173 positiveVariables.add(variable); 162 positiveVariables.add(variable);
174 } else if (!existentiallyQuantifiedVariables.contains(variable)) { 163 } else if (!existentiallyQuantifiedVariables.contains(variable)) {
175 throw new IllegalArgumentException("Unbound %s parameter %s" 164 throw new InvalidQueryException("Unbound %s parameter %s"
176 .formatted(ParameterDirection.OUT, variable)); 165 .formatted(ParameterDirection.OUT, variable));
177 } 166 }
178 } 167 }
@@ -184,7 +173,7 @@ class ClausePostProcessor {
184 var representative = pair.getKey(); 173 var representative = pair.getKey();
185 if (!positiveVariables.contains(representative)) { 174 if (!positiveVariables.contains(representative)) {
186 var variableSet = pair.getValue(); 175 var variableSet = pair.getValue();
187 throw new IllegalArgumentException("Variables %s were merged by equivalence but are not bound" 176 throw new InvalidQueryException("Variables %s were merged by equivalence but are not bound"
188 .formatted(variableSet)); 177 .formatted(variableSet));
189 } 178 }
190 } 179 }
@@ -196,7 +185,7 @@ class ClausePostProcessor {
196 for (var variable : literal.getPrivateVariables(positiveVariables)) { 185 for (var variable : literal.getPrivateVariables(positiveVariables)) {
197 var oldLiteral = negativeVariablesMap.put(variable, literal); 186 var oldLiteral = negativeVariablesMap.put(variable, literal);
198 if (oldLiteral != null) { 187 if (oldLiteral != null) {
199 throw new IllegalArgumentException("Unbound variable %s appears in multiple literals %s and %s" 188 throw new InvalidQueryException("Unbound variable %s appears in multiple literals %s and %s"
200 .formatted(variable, oldLiteral, literal)); 189 .formatted(variable, oldLiteral, literal));
201 } 190 }
202 } 191 }
@@ -218,11 +207,60 @@ class ClausePostProcessor {
218 variable.addToSortedLiterals(); 207 variable.addToSortedLiterals();
219 } 208 }
220 if (!variableToLiteralInputMap.isEmpty()) { 209 if (!variableToLiteralInputMap.isEmpty()) {
221 throw new IllegalArgumentException("Unbound input variables %s" 210 throw new InvalidQueryException("Unbound input variables %s"
222 .formatted(variableToLiteralInputMap.keySet())); 211 .formatted(variableToLiteralInputMap.keySet()));
223 } 212 }
224 } 213 }
225 214
215 private boolean hasContradictoryCall(Collection<Literal> filteredLiterals) {
216 var positiveCalls = new HashMap<Constraint, Set<CallLiteral>>();
217 for (var literal : filteredLiterals) {
218 if (literal instanceof CallLiteral callLiteral && callLiteral.getPolarity() == CallPolarity.POSITIVE) {
219 var callsOfTarget = positiveCalls.computeIfAbsent(callLiteral.getTarget(), key -> new HashSet<>());
220 callsOfTarget.add(callLiteral);
221 }
222 }
223 for (var literal : filteredLiterals) {
224 if (literal instanceof CallLiteral callLiteral && callLiteral.getPolarity() == CallPolarity.NEGATIVE) {
225 var callsOfTarget = positiveCalls.get(callLiteral.getTarget());
226 if (contradicts(callLiteral, callsOfTarget)) {
227 return true;
228 }
229 }
230 }
231 return false;
232 }
233
234 private boolean contradicts(CallLiteral negativeCall, Collection<CallLiteral> positiveCalls) {
235 if (positiveCalls == null) {
236 return false;
237 }
238 for (var positiveCall : positiveCalls) {
239 if (contradicts(negativeCall, positiveCall)) {
240 return true;
241 }
242 }
243 return false;
244 }
245
246 private boolean contradicts(CallLiteral negativeCall, CallLiteral positiveCall) {
247 var privateVariables = negativeCall.getPrivateVariables(positiveVariables);
248 var negativeArguments = negativeCall.getArguments();
249 var positiveArguments = positiveCall.getArguments();
250 int arity = negativeArguments.size();
251 for (int i = 0; i < arity; i++) {
252 var negativeArgument = negativeArguments.get(i);
253 if (privateVariables.contains(negativeArgument)) {
254 continue;
255 }
256 var positiveArgument = positiveArguments.get(i);
257 if (!negativeArgument.equals(positiveArgument)) {
258 return false;
259 }
260 }
261 return true;
262 }
263
226 private class SortableLiteral implements Comparable<SortableLiteral> { 264 private class SortableLiteral implements Comparable<SortableLiteral> {
227 private final int index; 265 private final int index;
228 private final Literal literal; 266 private final Literal literal;
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java
index 50b245f7..86a1b6b2 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java
@@ -6,9 +6,12 @@
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.Constraint; 8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.literal.Reduction; 9import tools.refinery.store.query.InvalidQueryException;
10import tools.refinery.store.query.equality.DnfEqualityChecker; 10import tools.refinery.store.query.equality.DnfEqualityChecker;
11import tools.refinery.store.query.equality.LiteralEqualityHelper; 11import tools.refinery.store.query.equality.LiteralEqualityHelper;
12import tools.refinery.store.query.equality.SubstitutingLiteralEqualityHelper;
13import tools.refinery.store.query.equality.SubstitutingLiteralHashCodeHelper;
14import tools.refinery.store.query.literal.Reduction;
12import tools.refinery.store.query.term.Parameter; 15import tools.refinery.store.query.term.Parameter;
13import tools.refinery.store.query.term.Variable; 16import tools.refinery.store.query.term.Variable;
14 17
@@ -53,7 +56,7 @@ public final class Dnf implements Constraint {
53 FunctionalDependency<Variable> functionalDependency) { 56 FunctionalDependency<Variable> functionalDependency) {
54 for (var variable : toValidate) { 57 for (var variable : toValidate) {
55 if (!parameterSet.contains(variable)) { 58 if (!parameterSet.contains(variable)) {
56 throw new IllegalArgumentException( 59 throw new InvalidQueryException(
57 "Variable %s of functional dependency %s does not appear in the parameter list %s" 60 "Variable %s of functional dependency %s does not appear in the parameter list %s"
58 .formatted(variable, functionalDependency, symbolicParameters)); 61 .formatted(variable, functionalDependency, symbolicParameters));
59 } 62 }
@@ -66,7 +69,7 @@ public final class Dnf implements Constraint {
66 } 69 }
67 70
68 public boolean isExplicitlyNamed() { 71 public boolean isExplicitlyNamed() {
69 return name == null; 72 return name != null;
70 } 73 }
71 74
72 public String getUniqueName() { 75 public String getUniqueName() {
@@ -129,7 +132,7 @@ public final class Dnf implements Constraint {
129 return false; 132 return false;
130 } 133 }
131 for (int i = 0; i < numClauses; i++) { 134 for (int i = 0; i < numClauses; i++) {
132 var literalEqualityHelper = new LiteralEqualityHelper(callEqualityChecker, symbolicParameters, 135 var literalEqualityHelper = new SubstitutingLiteralEqualityHelper(callEqualityChecker, symbolicParameters,
133 other.symbolicParameters); 136 other.symbolicParameters);
134 if (!clauses.get(i).equalsWithSubstitution(literalEqualityHelper, other.clauses.get(i))) { 137 if (!clauses.get(i).equalsWithSubstitution(literalEqualityHelper, other.clauses.get(i))) {
135 return false; 138 return false;
@@ -146,6 +149,18 @@ public final class Dnf implements Constraint {
146 return false; 149 return false;
147 } 150 }
148 151
152 public int hashCodeWithSubstitution() {
153 var helper = new SubstitutingLiteralHashCodeHelper();
154 int result = 0;
155 for (var symbolicParameter : symbolicParameters) {
156 result = result * 31 + symbolicParameter.hashCodeWithSubstitution(helper);
157 }
158 for (var clause : clauses) {
159 result = result * 31 + clause.hashCodeWithSubstitution(helper);
160 }
161 return result;
162 }
163
149 @Override 164 @Override
150 public String toString() { 165 public String toString() {
151 return "%s/%d".formatted(name(), arity()); 166 return "%s/%d".formatted(name(), arity());
@@ -201,6 +216,13 @@ public final class Dnf implements Constraint {
201 return new DnfBuilder(name); 216 return new DnfBuilder(name);
202 } 217 }
203 218
219 public static DnfBuilder builderFrom(Dnf original) {
220 var builder = builder(original.name());
221 builder.symbolicParameters(original.getSymbolicParameters());
222 builder.functionalDependencies(original.getFunctionalDependencies());
223 return builder;
224 }
225
204 public static Dnf of(Consumer<DnfBuilder> callback) { 226 public static Dnf of(Consumer<DnfBuilder> callback) {
205 return of(null, callback); 227 return of(null, callback);
206 } 228 }
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java
index 8e38ca6b..0f9fd366 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfBuilder.java
@@ -5,12 +5,10 @@
5 */ 5 */
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.dnf.callback.*; 9import tools.refinery.store.query.dnf.callback.*;
9import tools.refinery.store.query.literal.Literal; 10import tools.refinery.store.query.literal.Literal;
10import tools.refinery.store.query.term.DataVariable; 11import tools.refinery.store.query.term.*;
11import tools.refinery.store.query.term.NodeVariable;
12import tools.refinery.store.query.term.ParameterDirection;
13import tools.refinery.store.query.term.Variable;
14 12
15import java.util.*; 13import java.util.*;
16 14
@@ -62,6 +60,18 @@ public final class DnfBuilder {
62 return variable; 60 return variable;
63 } 61 }
64 62
63 public Variable parameter(Parameter parameter) {
64 return parameter(null, parameter);
65 }
66
67 public Variable parameter(String name, Parameter parameter) {
68 var type = parameter.tryGetType();
69 if (type.isPresent()) {
70 return parameter(name, type.get(), parameter.getDirection());
71 }
72 return parameter(name, parameter.getDirection());
73 }
74
65 public DnfBuilder parameter(Variable variable) { 75 public DnfBuilder parameter(Variable variable) {
66 return parameter(variable, ParameterDirection.OUT); 76 return parameter(variable, ParameterDirection.OUT);
67 } 77 }
@@ -88,7 +98,7 @@ public final class DnfBuilder {
88 public DnfBuilder symbolicParameter(SymbolicParameter symbolicParameter) { 98 public DnfBuilder symbolicParameter(SymbolicParameter symbolicParameter) {
89 var variable = symbolicParameter.getVariable(); 99 var variable = symbolicParameter.getVariable();
90 if (!parameterVariables.add(variable)) { 100 if (!parameterVariables.add(variable)) {
91 throw new IllegalArgumentException("Variable %s is already on the parameter list %s" 101 throw new InvalidQueryException("Variable %s is already on the parameter list %s"
92 .formatted(variable, parameters)); 102 .formatted(variable, parameters));
93 } 103 }
94 parameters.add(symbolicParameter); 104 parameters.add(symbolicParameter);
@@ -129,7 +139,7 @@ public final class DnfBuilder {
129 } 139 }
130 140
131 public <T> DnfBuilder clause(Class<T> type1, ClauseCallback1Data1<T> callback) { 141 public <T> DnfBuilder clause(Class<T> type1, ClauseCallback1Data1<T> callback) {
132 return clause(callback.toLiterals(Variable.of("v1", type1))); 142 return clause(callback.toLiterals(Variable.of("d1", type1)));
133 } 143 }
134 144
135 public DnfBuilder clause(ClauseCallback2Data0 callback) { 145 public DnfBuilder clause(ClauseCallback2Data0 callback) {
@@ -206,57 +216,10 @@ public final class DnfBuilder {
206 } 216 }
207 217
208 public Dnf build() { 218 public Dnf build() {
209 var postProcessedClauses = postProcessClauses(); 219 var postProcessor = new DnfPostProcessor(parameters, clauses);
220 var postProcessedClauses = postProcessor.postProcessClauses();
210 return new Dnf(name, Collections.unmodifiableList(parameters), 221 return new Dnf(name, Collections.unmodifiableList(parameters),
211 Collections.unmodifiableList(functionalDependencies), 222 Collections.unmodifiableList(functionalDependencies),
212 Collections.unmodifiableList(postProcessedClauses)); 223 Collections.unmodifiableList(postProcessedClauses));
213 } 224 }
214
215 private List<DnfClause> postProcessClauses() {
216 var parameterInfoMap = getParameterInfoMap();
217 var postProcessedClauses = new ArrayList<DnfClause>(clauses.size());
218 for (var literals : clauses) {
219 var postProcessor = new ClausePostProcessor(parameterInfoMap, literals);
220 var result = postProcessor.postProcessClause();
221 if (result instanceof ClausePostProcessor.ClauseResult clauseResult) {
222 postProcessedClauses.add(clauseResult.clause());
223 } else if (result instanceof ClausePostProcessor.ConstantResult constantResult) {
224 switch (constantResult) {
225 case ALWAYS_TRUE -> {
226 var inputVariables = getInputVariables();
227 return List.of(new DnfClause(inputVariables, List.of()));
228 }
229 case ALWAYS_FALSE -> {
230 // Skip this clause because it can never match.
231 }
232 default -> throw new IllegalStateException("Unexpected ClausePostProcessor.ConstantResult: " +
233 constantResult);
234 }
235 } else {
236 throw new IllegalStateException("Unexpected ClausePostProcessor.Result: " + result);
237 }
238 }
239 return postProcessedClauses;
240 }
241
242 private Map<Variable, ClausePostProcessor.ParameterInfo> getParameterInfoMap() {
243 var mutableParameterInfoMap = new LinkedHashMap<Variable, ClausePostProcessor.ParameterInfo>();
244 int arity = parameters.size();
245 for (int i = 0; i < arity; i++) {
246 var parameter = parameters.get(i);
247 mutableParameterInfoMap.put(parameter.getVariable(),
248 new ClausePostProcessor.ParameterInfo(parameter.getDirection(), i));
249 }
250 return Collections.unmodifiableMap(mutableParameterInfoMap);
251 }
252
253 private Set<Variable> getInputVariables() {
254 var inputParameters = new LinkedHashSet<Variable>();
255 for (var parameter : parameters) {
256 if (parameter.getDirection() == ParameterDirection.IN) {
257 inputParameters.add(parameter.getVariable());
258 }
259 }
260 return Collections.unmodifiableSet(inputParameters);
261 }
262} 225}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfClause.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfClause.java
index fdd0d47c..94327bad 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfClause.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfClause.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.literal.Literal; 10import tools.refinery.store.query.literal.Literal;
10import tools.refinery.store.query.term.Variable; 11import tools.refinery.store.query.term.Variable;
11 12
@@ -25,4 +26,12 @@ public record DnfClause(Set<Variable> positiveVariables, List<Literal> literals)
25 } 26 }
26 return true; 27 return true;
27 } 28 }
29
30 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
31 int result = 0;
32 for (var literal : literals) {
33 result = result * 31 + literal.hashCodeWithSubstitution(helper);
34 }
35 return result;
36 }
28} 37}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java
new file mode 100644
index 00000000..50236642
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/DnfPostProcessor.java
@@ -0,0 +1,112 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.dnf;
7
8import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.equality.DnfEqualityChecker;
10import tools.refinery.store.query.equality.SubstitutingLiteralEqualityHelper;
11import tools.refinery.store.query.equality.SubstitutingLiteralHashCodeHelper;
12import tools.refinery.store.query.literal.Literal;
13import tools.refinery.store.query.term.ParameterDirection;
14import tools.refinery.store.query.term.Variable;
15
16import java.util.*;
17
18class DnfPostProcessor {
19 private final List<SymbolicParameter> parameters;
20 private final List<List<Literal>> clauses;
21
22 public DnfPostProcessor(List<SymbolicParameter> parameters, List<List<Literal>> clauses) {
23 this.parameters = parameters;
24 this.clauses = clauses;
25 }
26
27 public List<DnfClause> postProcessClauses() {
28 var parameterInfoMap = getParameterInfoMap();
29 var postProcessedClauses = new LinkedHashSet<CanonicalClause>(clauses.size());
30 int index = 0;
31 for (var literals : clauses) {
32 var postProcessor = new ClausePostProcessor(parameterInfoMap, literals);
33 ClausePostProcessor.Result result;
34 try {
35 result = postProcessor.postProcessClause();
36 } catch (InvalidQueryException e) {
37 throw new InvalidClauseException(index, e);
38 }
39 if (result instanceof ClausePostProcessor.ClauseResult clauseResult) {
40 postProcessedClauses.add(new CanonicalClause(clauseResult.clause()));
41 } else if (result instanceof ClausePostProcessor.ConstantResult constantResult) {
42 switch (constantResult) {
43 case ALWAYS_TRUE -> {
44 var inputVariables = getInputVariables();
45 return List.of(new DnfClause(inputVariables, List.of()));
46 }
47 case ALWAYS_FALSE -> {
48 // Skip this clause because it can never match.
49 }
50 default -> throw new IllegalStateException("Unexpected ClausePostProcessor.ConstantResult: " +
51 constantResult);
52 }
53 } else {
54 throw new IllegalStateException("Unexpected ClausePostProcessor.Result: " + result);
55 }
56 index++;
57 }
58 return postProcessedClauses.stream().map(CanonicalClause::getDnfClause).toList();
59 }
60
61 private Map<Variable, ClausePostProcessor.ParameterInfo> getParameterInfoMap() {
62 var mutableParameterInfoMap = new LinkedHashMap<Variable, ClausePostProcessor.ParameterInfo>();
63 int arity = parameters.size();
64 for (int i = 0; i < arity; i++) {
65 var parameter = parameters.get(i);
66 mutableParameterInfoMap.put(parameter.getVariable(),
67 new ClausePostProcessor.ParameterInfo(parameter.getDirection(), i));
68 }
69 return Collections.unmodifiableMap(mutableParameterInfoMap);
70 }
71
72 private Set<Variable> getInputVariables() {
73 var inputParameters = new LinkedHashSet<Variable>();
74 for (var parameter : parameters) {
75 if (parameter.getDirection() == ParameterDirection.IN) {
76 inputParameters.add(parameter.getVariable());
77 }
78 }
79 return Collections.unmodifiableSet(inputParameters);
80 }
81
82 private class CanonicalClause {
83 private final DnfClause dnfClause;
84
85 public CanonicalClause(DnfClause dnfClause) {
86 this.dnfClause = dnfClause;
87 }
88
89 public DnfClause getDnfClause() {
90 return dnfClause;
91 }
92
93 @Override
94 public boolean equals(Object obj) {
95 if (this == obj) {
96 return true;
97 }
98 if (obj == null || getClass() != obj.getClass()) {
99 return false;
100 }
101 var otherCanonicalClause = (CanonicalClause) obj;
102 var helper = new SubstitutingLiteralEqualityHelper(DnfEqualityChecker.DEFAULT, parameters, parameters);
103 return dnfClause.equalsWithSubstitution(helper, otherCanonicalClause.dnfClause);
104 }
105
106 @Override
107 public int hashCode() {
108 var helper = new SubstitutingLiteralHashCodeHelper(parameters);
109 return dnfClause.hashCodeWithSubstitution(helper);
110 }
111 }
112}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java
index b00b2cb7..aef07ee3 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalDependency.java
@@ -5,6 +5,8 @@
5 */ 5 */
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.InvalidQueryException;
9
8import java.util.HashSet; 10import java.util.HashSet;
9import java.util.Set; 11import java.util.Set;
10 12
@@ -13,7 +15,7 @@ public record FunctionalDependency<T>(Set<T> forEach, Set<T> unique) {
13 var uniqueForEach = new HashSet<>(unique); 15 var uniqueForEach = new HashSet<>(unique);
14 uniqueForEach.retainAll(forEach); 16 uniqueForEach.retainAll(forEach);
15 if (!uniqueForEach.isEmpty()) { 17 if (!uniqueForEach.isEmpty()) {
16 throw new IllegalArgumentException("Variables %s appear on both sides of the functional dependency" 18 throw new InvalidQueryException("Variables %s appear on both sides of the functional dependency"
17 .formatted(uniqueForEach)); 19 .formatted(uniqueForEach));
18 } 20 }
19 } 21 }
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java
index 5a32b1ba..225f6844 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/FunctionalQuery.java
@@ -5,6 +5,7 @@
5 */ 5 */
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.literal.CallPolarity; 9import tools.refinery.store.query.literal.CallPolarity;
9import tools.refinery.store.query.term.Aggregator; 10import tools.refinery.store.query.term.Aggregator;
10import tools.refinery.store.query.term.AssignedValue; 11import tools.refinery.store.query.term.AssignedValue;
@@ -26,14 +27,14 @@ public final class FunctionalQuery<T> extends Query<T> {
26 var parameter = parameters.get(i); 27 var parameter = parameters.get(i);
27 var parameterType = parameter.tryGetType(); 28 var parameterType = parameter.tryGetType();
28 if (parameterType.isPresent()) { 29 if (parameterType.isPresent()) {
29 throw new IllegalArgumentException("Expected parameter %s of %s to be a node variable, got %s instead" 30 throw new InvalidQueryException("Expected parameter %s of %s to be a node variable, got %s instead"
30 .formatted(parameter, dnf, parameterType.get().getName())); 31 .formatted(parameter, dnf, parameterType.get().getName()));
31 } 32 }
32 } 33 }
33 var outputParameter = parameters.get(outputIndex); 34 var outputParameter = parameters.get(outputIndex);
34 var outputParameterType = outputParameter.tryGetType(); 35 var outputParameterType = outputParameter.tryGetType();
35 if (outputParameterType.isEmpty() || !outputParameterType.get().equals(type)) { 36 if (outputParameterType.isEmpty() || !outputParameterType.get().equals(type)) {
36 throw new IllegalArgumentException("Expected parameter %s of %s to be %s, but got %s instead".formatted( 37 throw new InvalidQueryException("Expected parameter %s of %s to be %s, but got %s instead".formatted(
37 outputParameter, dnf, type, outputParameterType.map(Class::getName).orElse("node"))); 38 outputParameter, dnf, type, outputParameterType.map(Class::getName).orElse("node")));
38 } 39 }
39 this.type = type; 40 this.type = type;
@@ -54,6 +55,16 @@ public final class FunctionalQuery<T> extends Query<T> {
54 return null; 55 return null;
55 } 56 }
56 57
58 @Override
59 protected FunctionalQuery<T> withDnfInternal(Dnf newDnf) {
60 return newDnf.asFunction(type);
61 }
62
63 @Override
64 public FunctionalQuery<T> withDnf(Dnf newDnf) {
65 return (FunctionalQuery<T>) super.withDnf(newDnf);
66 }
67
57 public AssignedValue<T> call(List<NodeVariable> arguments) { 68 public AssignedValue<T> call(List<NodeVariable> arguments) {
58 return targetVariable -> { 69 return targetVariable -> {
59 var argumentsWithTarget = new ArrayList<Variable>(arguments.size() + 1); 70 var argumentsWithTarget = new ArrayList<Variable>(arguments.size() + 1);
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java
new file mode 100644
index 00000000..747574b9
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/InvalidClauseException.java
@@ -0,0 +1,35 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.dnf;
7
8import tools.refinery.store.query.InvalidQueryException;
9
10public class InvalidClauseException extends InvalidQueryException {
11 private final int clauseIndex;
12
13 public InvalidClauseException(int clauseIndex) {
14 this.clauseIndex = clauseIndex;
15 }
16
17 public InvalidClauseException(int clauseIndex, String message) {
18 super(message);
19 this.clauseIndex = clauseIndex;
20 }
21
22 public InvalidClauseException(int clauseIndex, String message, Throwable cause) {
23 super(message, cause);
24 this.clauseIndex = clauseIndex;
25 }
26
27 public InvalidClauseException(int clauseIndex, Throwable cause) {
28 super(cause);
29 this.clauseIndex = clauseIndex;
30 }
31
32 public int getClauseIndex() {
33 return clauseIndex;
34 }
35}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Query.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Query.java
index aaa52ce6..83fe6ccd 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Query.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Query.java
@@ -43,6 +43,29 @@ public abstract sealed class Query<T> implements AnyQuery permits FunctionalQuer
43 43
44 public abstract T defaultValue(); 44 public abstract T defaultValue();
45 45
46 public Query<T> withDnf(Dnf newDnf) {
47 if (dnf.equals(newDnf)) {
48 return this;
49 }
50 int arity = dnf.arity();
51 if (newDnf.arity() != arity) {
52 throw new IllegalArgumentException("Arity of %s and %s do not match".formatted(dnf, newDnf));
53 }
54 var parameters = dnf.getParameters();
55 var newParameters = newDnf.getParameters();
56 for (int i = 0; i < arity; i++) {
57 var parameter = parameters.get(i);
58 var newParameter = newParameters.get(i);
59 if (!parameter.matches(newParameter)) {
60 throw new IllegalArgumentException("Parameter #%d mismatch: %s does not match %s"
61 .formatted(i, parameter, newParameter));
62 }
63 }
64 return withDnfInternal(newDnf);
65 }
66
67 protected abstract Query<T> withDnfInternal(Dnf newDnf);
68
46 @Override 69 @Override
47 public boolean equals(Object o) { 70 public boolean equals(Object o) {
48 if (this == o) return true; 71 if (this == o) return true;
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java
index d34a7ace..98f71e11 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/RelationalQuery.java
@@ -5,6 +5,7 @@
5 */ 5 */
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.literal.CallLiteral; 9import tools.refinery.store.query.literal.CallLiteral;
9import tools.refinery.store.query.literal.CallPolarity; 10import tools.refinery.store.query.literal.CallPolarity;
10import tools.refinery.store.query.term.AssignedValue; 11import tools.refinery.store.query.term.AssignedValue;
@@ -19,7 +20,7 @@ public final class RelationalQuery extends Query<Boolean> {
19 for (var parameter : dnf.getSymbolicParameters()) { 20 for (var parameter : dnf.getSymbolicParameters()) {
20 var parameterType = parameter.tryGetType(); 21 var parameterType = parameter.tryGetType();
21 if (parameterType.isPresent()) { 22 if (parameterType.isPresent()) {
22 throw new IllegalArgumentException("Expected parameter %s of %s to be a node variable, got %s instead" 23 throw new InvalidQueryException("Expected parameter %s of %s to be a node variable, got %s instead"
23 .formatted(parameter, dnf, parameterType.get().getName())); 24 .formatted(parameter, dnf, parameterType.get().getName()));
24 } 25 }
25 } 26 }
@@ -40,6 +41,16 @@ public final class RelationalQuery extends Query<Boolean> {
40 return false; 41 return false;
41 } 42 }
42 43
44 @Override
45 protected RelationalQuery withDnfInternal(Dnf newDnf) {
46 return newDnf.asRelation();
47 }
48
49 @Override
50 public RelationalQuery withDnf(Dnf newDnf) {
51 return (RelationalQuery) super.withDnf(newDnf);
52 }
53
43 public CallLiteral call(CallPolarity polarity, List<NodeVariable> arguments) { 54 public CallLiteral call(CallPolarity polarity, List<NodeVariable> arguments) {
44 return getDnf().call(polarity, Collections.unmodifiableList(arguments)); 55 return getDnf().call(polarity, Collections.unmodifiableList(arguments));
45 } 56 }
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/SymbolicParameter.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/SymbolicParameter.java
index e0d3ba1f..fe9cefcc 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/SymbolicParameter.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/SymbolicParameter.java
@@ -5,6 +5,7 @@
5 */ 5 */
6package tools.refinery.store.query.dnf; 6package tools.refinery.store.query.dnf;
7 7
8import tools.refinery.store.query.equality.LiteralHashCodeHelper;
8import tools.refinery.store.query.term.Parameter; 9import tools.refinery.store.query.term.Parameter;
9import tools.refinery.store.query.term.ParameterDirection; 10import tools.refinery.store.query.term.ParameterDirection;
10import tools.refinery.store.query.term.Variable; 11import tools.refinery.store.query.term.Variable;
@@ -23,8 +24,8 @@ public final class SymbolicParameter extends Parameter {
23 return variable; 24 return variable;
24 } 25 }
25 26
26 public boolean isUnifiable() { 27 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
27 return variable.isUnifiable(); 28 return Objects.hash(super.hashCode(), helper.getVariableHashCode(variable));
28 } 29 }
29 30
30 @Override 31 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java
index 1eeb5723..d6171314 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java
@@ -14,8 +14,7 @@ import tools.refinery.store.util.CycleDetectingMapper;
14import java.util.List; 14import java.util.List;
15 15
16public class DeepDnfEqualityChecker implements DnfEqualityChecker { 16public class DeepDnfEqualityChecker implements DnfEqualityChecker {
17 private final CycleDetectingMapper<Pair, Boolean> mapper = new CycleDetectingMapper<>(Pair::toString, 17 private final CycleDetectingMapper<Pair, Boolean> mapper = new CycleDetectingMapper<>(this::doCheckEqual);
18 this::doCheckEqual);
19 18
20 @Override 19 @Override
21 public boolean dnfEqual(Dnf left, Dnf right) { 20 public boolean dnfEqual(Dnf left, Dnf right) {
@@ -38,7 +37,7 @@ public class DeepDnfEqualityChecker implements DnfEqualityChecker {
38 return false; 37 return false;
39 } 38 }
40 for (int i = 0; i < numClauses; i++) { 39 for (int i = 0; i < numClauses; i++) {
41 var literalEqualityHelper = new LiteralEqualityHelper(this, symbolicParameters, 40 var literalEqualityHelper = new SubstitutingLiteralEqualityHelper(this, symbolicParameters,
42 other.getSymbolicParameters()); 41 other.getSymbolicParameters());
43 if (!equalsWithSubstitutionRaw(literalEqualityHelper, clauses.get(i), other.getClauses().get(i))) { 42 if (!equalsWithSubstitutionRaw(literalEqualityHelper, clauses.get(i), other.getClauses().get(i))) {
44 return false; 43 return false;
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DnfEqualityChecker.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DnfEqualityChecker.java
index 4a8bee3b..e2cfd79b 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DnfEqualityChecker.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DnfEqualityChecker.java
@@ -7,7 +7,11 @@ package tools.refinery.store.query.equality;
7 7
8import tools.refinery.store.query.dnf.Dnf; 8import tools.refinery.store.query.dnf.Dnf;
9 9
10import java.util.Objects;
11
10@FunctionalInterface 12@FunctionalInterface
11public interface DnfEqualityChecker { 13public interface DnfEqualityChecker {
14 DnfEqualityChecker DEFAULT = Objects::equals;
15
12 boolean dnfEqual(Dnf left, Dnf right); 16 boolean dnfEqual(Dnf left, Dnf right);
13} 17}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralEqualityHelper.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralEqualityHelper.java
index 9315fb30..5abc76ce 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralEqualityHelper.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralEqualityHelper.java
@@ -6,49 +6,22 @@
6package tools.refinery.store.query.equality; 6package tools.refinery.store.query.equality;
7 7
8import tools.refinery.store.query.dnf.Dnf; 8import tools.refinery.store.query.dnf.Dnf;
9import tools.refinery.store.query.dnf.SymbolicParameter;
10import tools.refinery.store.query.term.Variable; 9import tools.refinery.store.query.term.Variable;
11 10
12import java.util.HashMap; 11import java.util.Objects;
13import java.util.List;
14import java.util.Map;
15 12
16public class LiteralEqualityHelper { 13public interface LiteralEqualityHelper extends DnfEqualityChecker {
17 private final DnfEqualityChecker dnfEqualityChecker; 14 LiteralEqualityHelper DEFAULT = new LiteralEqualityHelper() {
18 private final Map<Variable, Variable> leftToRight; 15 @Override
19 private final Map<Variable, Variable> rightToLeft; 16 public boolean variableEqual(Variable left, Variable right) {
20 17 return Objects.equals(left, right);
21 public LiteralEqualityHelper(DnfEqualityChecker dnfEqualityChecker, List<SymbolicParameter> leftParameters,
22 List<SymbolicParameter> rightParameters) {
23 this.dnfEqualityChecker = dnfEqualityChecker;
24 var arity = leftParameters.size();
25 if (arity != rightParameters.size()) {
26 throw new IllegalArgumentException("Parameter lists have unequal length");
27 }
28 leftToRight = new HashMap<>(arity);
29 rightToLeft = new HashMap<>(arity);
30 for (int i = 0; i < arity; i++) {
31 if (!variableEqual(leftParameters.get(i).getVariable(), rightParameters.get(i).getVariable())) {
32 throw new IllegalArgumentException("Parameter lists cannot be unified: duplicate parameter " + i);
33 }
34 } 18 }
35 }
36
37 public boolean dnfEqual(Dnf left, Dnf right) {
38 return dnfEqualityChecker.dnfEqual(left, right);
39 }
40 19
41 public boolean variableEqual(Variable left, Variable right) { 20 @Override
42 if (checkMapping(leftToRight, left, right) && checkMapping(rightToLeft, right, left)) { 21 public boolean dnfEqual(Dnf left, Dnf right) {
43 leftToRight.put(left, right); 22 return DnfEqualityChecker.DEFAULT.dnfEqual(left, right);
44 rightToLeft.put(right, left);
45 return true;
46 } 23 }
47 return false; 24 };
48 }
49 25
50 private static boolean checkMapping(Map<Variable, Variable> map, Variable key, Variable expectedValue) { 26 boolean variableEqual(Variable left, Variable right);
51 var currentValue = map.get(key);
52 return currentValue == null || currentValue.equals(expectedValue);
53 }
54} 27}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralHashCodeHelper.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralHashCodeHelper.java
new file mode 100644
index 00000000..5495160a
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/LiteralHashCodeHelper.java
@@ -0,0 +1,17 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.equality;
7
8import tools.refinery.store.query.term.Variable;
9
10import java.util.Objects;
11
12@FunctionalInterface
13public interface LiteralHashCodeHelper {
14 LiteralHashCodeHelper DEFAULT = Objects::hashCode;
15
16 int getVariableHashCode(Variable variable);
17}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralEqualityHelper.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralEqualityHelper.java
new file mode 100644
index 00000000..50a79e07
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralEqualityHelper.java
@@ -0,0 +1,59 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.equality;
7
8import tools.refinery.store.query.dnf.Dnf;
9import tools.refinery.store.query.dnf.SymbolicParameter;
10import tools.refinery.store.query.term.Variable;
11
12import java.util.HashMap;
13import java.util.List;
14import java.util.Map;
15
16public class SubstitutingLiteralEqualityHelper implements LiteralEqualityHelper {
17 private final DnfEqualityChecker dnfEqualityChecker;
18 private final Map<Variable, Variable> leftToRight;
19 private final Map<Variable, Variable> rightToLeft;
20
21 public SubstitutingLiteralEqualityHelper(DnfEqualityChecker dnfEqualityChecker,
22 List<SymbolicParameter> leftParameters,
23 List<SymbolicParameter> rightParameters) {
24 this.dnfEqualityChecker = dnfEqualityChecker;
25 var arity = leftParameters.size();
26 if (arity != rightParameters.size()) {
27 throw new IllegalArgumentException("Parameter lists have unequal length");
28 }
29 leftToRight = new HashMap<>(arity);
30 rightToLeft = new HashMap<>(arity);
31 for (int i = 0; i < arity; i++) {
32 if (!variableEqual(leftParameters.get(i).getVariable(), rightParameters.get(i).getVariable())) {
33 throw new IllegalArgumentException("Parameter lists cannot be unified: duplicate parameter " + i);
34 }
35 }
36 }
37
38 @Override
39 public boolean dnfEqual(Dnf left, Dnf right) {
40 return dnfEqualityChecker.dnfEqual(left, right);
41 }
42
43 @Override
44 public boolean variableEqual(Variable left, Variable right) {
45 if (left.tryGetType().equals(right.tryGetType()) &&
46 checkMapping(leftToRight, left, right) &&
47 checkMapping(rightToLeft, right, left)) {
48 leftToRight.put(left, right);
49 rightToLeft.put(right, left);
50 return true;
51 }
52 return false;
53 }
54
55 private static boolean checkMapping(Map<Variable, Variable> map, Variable key, Variable expectedValue) {
56 var currentValue = map.get(key);
57 return currentValue == null || currentValue.equals(expectedValue);
58 }
59}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralHashCodeHelper.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralHashCodeHelper.java
new file mode 100644
index 00000000..754f6976
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/SubstitutingLiteralHashCodeHelper.java
@@ -0,0 +1,42 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.equality;
7
8import tools.refinery.store.query.dnf.SymbolicParameter;
9import tools.refinery.store.query.term.Variable;
10
11import java.util.LinkedHashMap;
12import java.util.List;
13import java.util.Map;
14
15public class SubstitutingLiteralHashCodeHelper implements LiteralHashCodeHelper {
16 private final Map<Variable, Integer> assignedHashCodes = new LinkedHashMap<>();
17
18 // 0 is for {@code null}, so we start with 1.
19 private int next = 1;
20
21 public SubstitutingLiteralHashCodeHelper() {
22 this(List.of());
23 }
24
25 public SubstitutingLiteralHashCodeHelper(List<SymbolicParameter> parameters) {
26 for (var parameter : parameters) {
27 getVariableHashCode(parameter.getVariable());
28 }
29 }
30
31 @Override
32 public int getVariableHashCode(Variable variable) {
33 if (variable == null) {
34 return 0;
35 }
36 return assignedHashCodes.computeIfAbsent(variable, key -> {
37 int sequenceNumber = next;
38 next++;
39 return variable.hashCodeWithSubstitution(sequenceNumber);
40 });
41 }
42}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java
index 8ef8e8b4..0e99d441 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCallLiteral.java
@@ -6,14 +6,18 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.Constraint; 8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.equality.LiteralEqualityHelper; 10import tools.refinery.store.query.equality.LiteralEqualityHelper;
11import tools.refinery.store.query.equality.LiteralHashCodeHelper;
10import tools.refinery.store.query.substitution.Substitution; 12import tools.refinery.store.query.substitution.Substitution;
11import tools.refinery.store.query.term.ParameterDirection; 13import tools.refinery.store.query.term.ParameterDirection;
12import tools.refinery.store.query.term.Variable; 14import tools.refinery.store.query.term.Variable;
13 15
14import java.util.*; 16import java.util.*;
15 17
16public abstract class AbstractCallLiteral implements Literal { 18// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
19@SuppressWarnings("squid:S2160")
20public abstract class AbstractCallLiteral extends AbstractLiteral {
17 private final Constraint target; 21 private final Constraint target;
18 private final List<Variable> arguments; 22 private final List<Variable> arguments;
19 private final Set<Variable> inArguments; 23 private final Set<Variable> inArguments;
@@ -24,7 +28,7 @@ public abstract class AbstractCallLiteral implements Literal {
24 protected AbstractCallLiteral(Constraint target, List<Variable> arguments) { 28 protected AbstractCallLiteral(Constraint target, List<Variable> arguments) {
25 int arity = target.arity(); 29 int arity = target.arity();
26 if (arguments.size() != arity) { 30 if (arguments.size() != arity) {
27 throw new IllegalArgumentException("%s needs %d arguments, but got %s".formatted(target.name(), 31 throw new InvalidQueryException("%s needs %d arguments, but got %s".formatted(target.name(),
28 target.arity(), arguments.size())); 32 target.arity(), arguments.size()));
29 } 33 }
30 this.target = target; 34 this.target = target;
@@ -36,21 +40,17 @@ public abstract class AbstractCallLiteral implements Literal {
36 var argument = arguments.get(i); 40 var argument = arguments.get(i);
37 var parameter = parameters.get(i); 41 var parameter = parameters.get(i);
38 if (!parameter.isAssignable(argument)) { 42 if (!parameter.isAssignable(argument)) {
39 throw new IllegalArgumentException("Argument %d of %s is not assignable to parameter %s" 43 throw new InvalidQueryException("Argument %d of %s is not assignable to parameter %s"
40 .formatted(i, target, parameter)); 44 .formatted(i, target, parameter));
41 } 45 }
42 switch (parameter.getDirection()) { 46 switch (parameter.getDirection()) {
43 case IN -> { 47 case IN -> {
44 if (mutableOutArguments.remove(argument)) { 48 mutableOutArguments.remove(argument);
45 checkInOutUnifiable(argument);
46 }
47 mutableInArguments.add(argument); 49 mutableInArguments.add(argument);
48 } 50 }
49 case OUT -> { 51 case OUT -> {
50 if (mutableInArguments.contains(argument)) { 52 if (!mutableInArguments.contains(argument)) {
51 checkInOutUnifiable(argument); 53 mutableOutArguments.add(argument);
52 } else if (!mutableOutArguments.add(argument)) {
53 checkDuplicateOutUnifiable(argument);
54 } 54 }
55 } 55 }
56 } 56 }
@@ -59,19 +59,6 @@ public abstract class AbstractCallLiteral implements Literal {
59 outArguments = Collections.unmodifiableSet(mutableOutArguments); 59 outArguments = Collections.unmodifiableSet(mutableOutArguments);
60 } 60 }
61 61
62 private static void checkInOutUnifiable(Variable argument) {
63 if (!argument.isUnifiable()) {
64 throw new IllegalArgumentException("Argument %s cannot appear with both %s and %s direction"
65 .formatted(argument, ParameterDirection.IN, ParameterDirection.OUT));
66 }
67 }
68
69 private static void checkDuplicateOutUnifiable(Variable argument) {
70 if (!argument.isUnifiable()) {
71 throw new IllegalArgumentException("Argument %s cannot be bound multiple times".formatted(argument));
72 }
73 }
74
75 public Constraint getTarget() { 62 public Constraint getTarget() {
76 return target; 63 return target;
77 } 64 }
@@ -110,9 +97,18 @@ public abstract class AbstractCallLiteral implements Literal {
110 97
111 protected abstract Literal doSubstitute(Substitution substitution, List<Variable> substitutedArguments); 98 protected abstract Literal doSubstitute(Substitution substitution, List<Variable> substitutedArguments);
112 99
100 public AbstractCallLiteral withTarget(Constraint newTarget) {
101 if (Objects.equals(target, newTarget)) {
102 return this;
103 }
104 return withArguments(newTarget, arguments);
105 }
106
107 public abstract AbstractCallLiteral withArguments(Constraint newTarget, List<Variable> newArguments);
108
113 @Override 109 @Override
114 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { 110 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) {
115 if (other == null || getClass() != other.getClass()) { 111 if (!super.equalsWithSubstitution(helper, other)) {
116 return false; 112 return false;
117 } 113 }
118 var otherCallLiteral = (AbstractCallLiteral) other; 114 var otherCallLiteral = (AbstractCallLiteral) other;
@@ -129,15 +125,11 @@ public abstract class AbstractCallLiteral implements Literal {
129 } 125 }
130 126
131 @Override 127 @Override
132 public boolean equals(Object o) { 128 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
133 if (this == o) return true; 129 int result = super.hashCodeWithSubstitution(helper) * 31 + target.hashCode();
134 if (o == null || getClass() != o.getClass()) return false; 130 for (var argument : arguments) {
135 AbstractCallLiteral that = (AbstractCallLiteral) o; 131 result = result * 31 + helper.getVariableHashCode(argument);
136 return target.equals(that.target) && arguments.equals(that.arguments); 132 }
137 } 133 return result;
138
139 @Override
140 public int hashCode() {
141 return Objects.hash(getClass(), target, arguments);
142 } 134 }
143} 135}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java
new file mode 100644
index 00000000..9bb572c0
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractCountLiteral.java
@@ -0,0 +1,107 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.literal;
7
8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.InvalidQueryException;
10import tools.refinery.store.query.equality.LiteralEqualityHelper;
11import tools.refinery.store.query.equality.LiteralHashCodeHelper;
12import tools.refinery.store.query.term.ConstantTerm;
13import tools.refinery.store.query.term.DataVariable;
14import tools.refinery.store.query.term.Variable;
15
16import java.util.List;
17import java.util.Objects;
18import java.util.Set;
19
20// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
21@SuppressWarnings("squid:S2160")
22public abstract class AbstractCountLiteral<T> extends AbstractCallLiteral {
23 private final Class<T> resultType;
24 private final DataVariable<T> resultVariable;
25
26 protected AbstractCountLiteral(Class<T> resultType, DataVariable<T> resultVariable, Constraint target,
27 List<Variable> arguments) {
28 super(target, arguments);
29 if (!resultVariable.getType().equals(resultType)) {
30 throw new InvalidQueryException("Count result variable %s must be of type %s, got %s instead".formatted(
31 resultVariable, resultType, resultVariable.getType().getName()));
32 }
33 if (arguments.contains(resultVariable)) {
34 throw new InvalidQueryException("Count result variable %s must not appear in the argument list"
35 .formatted(resultVariable));
36 }
37 this.resultType = resultType;
38 this.resultVariable = resultVariable;
39 }
40
41 public Class<T> getResultType() {
42 return resultType;
43 }
44
45 public DataVariable<T> getResultVariable() {
46 return resultVariable;
47 }
48
49 @Override
50 public Set<Variable> getOutputVariables() {
51 return Set.of(resultVariable);
52 }
53
54 protected abstract T zero();
55
56 protected abstract T one();
57
58 @Override
59 public Literal reduce() {
60 var reduction = getTarget().getReduction();
61 return switch (reduction) {
62 case ALWAYS_FALSE -> getResultVariable().assign(new ConstantTerm<>(resultType, zero()));
63 // The only way a constant {@code true} predicate can be called in a negative position is to have all of
64 // its arguments bound as input variables. Thus, there will only be a single match.
65 case ALWAYS_TRUE -> getResultVariable().assign(new ConstantTerm<>(resultType, one()));
66 case NOT_REDUCIBLE -> this;
67 };
68 }
69
70 @Override
71 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) {
72 if (!super.equalsWithSubstitution(helper, other)) {
73 return false;
74 }
75 var otherCountLiteral = (AbstractCountLiteral<?>) other;
76 return Objects.equals(resultType, otherCountLiteral.resultType) &&
77 helper.variableEqual(resultVariable, otherCountLiteral.resultVariable);
78 }
79
80 @Override
81 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
82 return Objects.hash(super.hashCodeWithSubstitution(helper), resultType,
83 helper.getVariableHashCode(resultVariable));
84 }
85
86 protected abstract String operatorName();
87
88 @Override
89 public String toString() {
90 var builder = new StringBuilder();
91 builder.append(resultVariable);
92 builder.append(" is ");
93 builder.append(operatorName());
94 builder.append(' ');
95 builder.append(getTarget().toReferenceString());
96 builder.append('(');
97 var argumentIterator = getArguments().iterator();
98 if (argumentIterator.hasNext()) {
99 builder.append(argumentIterator.next());
100 while (argumentIterator.hasNext()) {
101 builder.append(", ").append(argumentIterator.next());
102 }
103 }
104 builder.append(')');
105 return builder.toString();
106 }
107}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractLiteral.java
new file mode 100644
index 00000000..7d3cabd7
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AbstractLiteral.java
@@ -0,0 +1,34 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.literal;
7
8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
10
11public abstract class AbstractLiteral implements Literal {
12 @Override
13 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) {
14 return other != null && getClass() == other.getClass();
15 }
16
17 @Override
18 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
19 return getClass().hashCode();
20 }
21
22 @Override
23 public boolean equals(Object o) {
24 if (this == o) return true;
25 if (o == null || getClass() != o.getClass()) return false;
26 AbstractLiteral that = (AbstractLiteral) o;
27 return equalsWithSubstitution(LiteralEqualityHelper.DEFAULT, that);
28 }
29
30 @Override
31 public int hashCode() {
32 return hashCodeWithSubstitution(LiteralHashCodeHelper.DEFAULT);
33 }
34}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java
index 3a5eb5c7..e3acfacc 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AggregationLiteral.java
@@ -6,7 +6,9 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.Constraint; 8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.equality.LiteralEqualityHelper; 10import tools.refinery.store.query.equality.LiteralEqualityHelper;
11import tools.refinery.store.query.equality.LiteralHashCodeHelper;
10import tools.refinery.store.query.substitution.Substitution; 12import tools.refinery.store.query.substitution.Substitution;
11import tools.refinery.store.query.term.*; 13import tools.refinery.store.query.term.*;
12 14
@@ -14,6 +16,8 @@ import java.util.List;
14import java.util.Objects; 16import java.util.Objects;
15import java.util.Set; 17import java.util.Set;
16 18
19// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
20@SuppressWarnings("squid:S2160")
17public class AggregationLiteral<R, T> extends AbstractCallLiteral { 21public class AggregationLiteral<R, T> extends AbstractCallLiteral {
18 private final DataVariable<R> resultVariable; 22 private final DataVariable<R> resultVariable;
19 private final DataVariable<T> inputVariable; 23 private final DataVariable<T> inputVariable;
@@ -23,19 +27,19 @@ public class AggregationLiteral<R, T> extends AbstractCallLiteral {
23 DataVariable<T> inputVariable, Constraint target, List<Variable> arguments) { 27 DataVariable<T> inputVariable, Constraint target, List<Variable> arguments) {
24 super(target, arguments); 28 super(target, arguments);
25 if (!inputVariable.getType().equals(aggregator.getInputType())) { 29 if (!inputVariable.getType().equals(aggregator.getInputType())) {
26 throw new IllegalArgumentException("Input variable %s must of type %s, got %s instead".formatted( 30 throw new InvalidQueryException("Input variable %s must of type %s, got %s instead".formatted(
27 inputVariable, aggregator.getInputType().getName(), inputVariable.getType().getName())); 31 inputVariable, aggregator.getInputType().getName(), inputVariable.getType().getName()));
28 } 32 }
29 if (!getArgumentsOfDirection(ParameterDirection.OUT).contains(inputVariable)) { 33 if (!getArgumentsOfDirection(ParameterDirection.OUT).contains(inputVariable)) {
30 throw new IllegalArgumentException("Input variable %s must be bound with direction %s in the argument list" 34 throw new InvalidQueryException("Input variable %s must be bound with direction %s in the argument list"
31 .formatted(inputVariable, ParameterDirection.OUT)); 35 .formatted(inputVariable, ParameterDirection.OUT));
32 } 36 }
33 if (!resultVariable.getType().equals(aggregator.getResultType())) { 37 if (!resultVariable.getType().equals(aggregator.getResultType())) {
34 throw new IllegalArgumentException("Result variable %s must of type %s, got %s instead".formatted( 38 throw new InvalidQueryException("Result variable %s must of type %s, got %s instead".formatted(
35 resultVariable, aggregator.getResultType().getName(), resultVariable.getType().getName())); 39 resultVariable, aggregator.getResultType().getName(), resultVariable.getType().getName()));
36 } 40 }
37 if (arguments.contains(resultVariable)) { 41 if (arguments.contains(resultVariable)) {
38 throw new IllegalArgumentException("Result variable %s must not appear in the argument list".formatted( 42 throw new InvalidQueryException("Result variable %s must not appear in the argument list".formatted(
39 resultVariable)); 43 resultVariable));
40 } 44 }
41 this.resultVariable = resultVariable; 45 this.resultVariable = resultVariable;
@@ -63,7 +67,7 @@ public class AggregationLiteral<R, T> extends AbstractCallLiteral {
63 @Override 67 @Override
64 public Set<Variable> getInputVariables(Set<? extends Variable> positiveVariablesInClause) { 68 public Set<Variable> getInputVariables(Set<? extends Variable> positiveVariablesInClause) {
65 if (positiveVariablesInClause.contains(inputVariable)) { 69 if (positiveVariablesInClause.contains(inputVariable)) {
66 throw new IllegalArgumentException("Aggregation variable %s must not be bound".formatted(inputVariable)); 70 throw new InvalidQueryException("Aggregation variable %s must not be bound".formatted(inputVariable));
67 } 71 }
68 return super.getInputVariables(positiveVariablesInClause); 72 return super.getInputVariables(positiveVariablesInClause);
69 } 73 }
@@ -77,7 +81,7 @@ public class AggregationLiteral<R, T> extends AbstractCallLiteral {
77 yield emptyValue == null ? BooleanLiteral.FALSE : 81 yield emptyValue == null ? BooleanLiteral.FALSE :
78 resultVariable.assign(new ConstantTerm<>(resultVariable.getType(), emptyValue)); 82 resultVariable.assign(new ConstantTerm<>(resultVariable.getType(), emptyValue));
79 } 83 }
80 case ALWAYS_TRUE -> throw new IllegalArgumentException("Trying to aggregate over an infinite set"); 84 case ALWAYS_TRUE -> throw new InvalidQueryException("Trying to aggregate over an infinite set");
81 case NOT_REDUCIBLE -> this; 85 case NOT_REDUCIBLE -> this;
82 }; 86 };
83 } 87 }
@@ -89,6 +93,11 @@ public class AggregationLiteral<R, T> extends AbstractCallLiteral {
89 } 93 }
90 94
91 @Override 95 @Override
96 public AbstractCallLiteral withArguments(Constraint newTarget, List<Variable> newArguments) {
97 return new AggregationLiteral<>(resultVariable, aggregator, inputVariable, newTarget, newArguments);
98 }
99
100 @Override
92 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { 101 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) {
93 if (!super.equalsWithSubstitution(helper, other)) { 102 if (!super.equalsWithSubstitution(helper, other)) {
94 return false; 103 return false;
@@ -100,18 +109,9 @@ public class AggregationLiteral<R, T> extends AbstractCallLiteral {
100 } 109 }
101 110
102 @Override 111 @Override
103 public boolean equals(Object o) { 112 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
104 if (this == o) return true; 113 return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(resultVariable),
105 if (o == null || getClass() != o.getClass()) return false; 114 helper.getVariableHashCode(inputVariable), aggregator);
106 if (!super.equals(o)) return false;
107 AggregationLiteral<?, ?> that = (AggregationLiteral<?, ?>) o;
108 return resultVariable.equals(that.resultVariable) && inputVariable.equals(that.inputVariable) &&
109 aggregator.equals(that.aggregator);
110 }
111
112 @Override
113 public int hashCode() {
114 return Objects.hash(super.hashCode(), resultVariable, inputVariable, aggregator);
115 } 115 }
116 116
117 @Override 117 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java
index dbf999a2..dadf487f 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssignLiteral.java
@@ -5,7 +5,9 @@
5 */ 5 */
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 11import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.term.DataVariable; 12import tools.refinery.store.query.term.DataVariable;
11import tools.refinery.store.query.term.Term; 13import tools.refinery.store.query.term.Term;
@@ -15,17 +17,32 @@ import java.util.Collections;
15import java.util.Objects; 17import java.util.Objects;
16import java.util.Set; 18import java.util.Set;
17 19
18public record AssignLiteral<T>(DataVariable<T> variable, Term<T> term) implements Literal { 20// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
19 public AssignLiteral { 21@SuppressWarnings("squid:S2160")
22public class AssignLiteral<T> extends AbstractLiteral {
23 private final DataVariable<T> variable;
24 private final Term<T> term;
25
26 public AssignLiteral(DataVariable<T> variable, Term<T> term) {
20 if (!term.getType().equals(variable.getType())) { 27 if (!term.getType().equals(variable.getType())) {
21 throw new IllegalArgumentException("Term %s must be of type %s, got %s instead".formatted( 28 throw new InvalidQueryException("Term %s must be of type %s, got %s instead".formatted(
22 term, variable.getType().getName(), term.getType().getName())); 29 term, variable.getType().getName(), term.getType().getName()));
23 } 30 }
24 var inputVariables = term.getInputVariables(); 31 var inputVariables = term.getInputVariables();
25 if (inputVariables.contains(variable)) { 32 if (inputVariables.contains(variable)) {
26 throw new IllegalArgumentException("Result variable %s must not appear in the term %s".formatted( 33 throw new InvalidQueryException("Result variable %s must not appear in the term %s".formatted(
27 variable, term)); 34 variable, term));
28 } 35 }
36 this.variable = variable;
37 this.term = term;
38 }
39
40 public DataVariable<T> getVariable() {
41 return variable;
42 }
43
44 public Term<T> getTerm() {
45 return term;
29 } 46 }
30 47
31 @Override 48 @Override
@@ -53,27 +70,19 @@ public record AssignLiteral<T>(DataVariable<T> variable, Term<T> term) implement
53 if (other == null || getClass() != other.getClass()) { 70 if (other == null || getClass() != other.getClass()) {
54 return false; 71 return false;
55 } 72 }
56 var otherLetLiteral = (AssignLiteral<?>) other; 73 var otherAssignLiteral = (AssignLiteral<?>) other;
57 return helper.variableEqual(variable, otherLetLiteral.variable) && term.equalsWithSubstitution(helper, 74 return helper.variableEqual(variable, otherAssignLiteral.variable) &&
58 otherLetLiteral.term); 75 term.equalsWithSubstitution(helper, otherAssignLiteral.term);
59 }
60
61 @Override
62 public String toString() {
63 return "%s is (%s)".formatted(variable, term);
64 } 76 }
65 77
66 @Override 78 @Override
67 public boolean equals(Object obj) { 79 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
68 if (obj == this) return true; 80 return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(variable),
69 if (obj == null || obj.getClass() != this.getClass()) return false; 81 term.hashCodeWithSubstitution(helper));
70 var that = (AssignLiteral<?>) obj;
71 return Objects.equals(this.variable, that.variable) &&
72 Objects.equals(this.term, that.term);
73 } 82 }
74 83
75 @Override 84 @Override
76 public int hashCode() { 85 public String toString() {
77 return Objects.hash(getClass(), variable, term); 86 return "%s is (%s)".formatted(variable, term);
78 } 87 }
79} 88}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/BooleanLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/BooleanLiteral.java
index f312d202..6cd320da 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/BooleanLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/BooleanLiteral.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 10import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.term.Variable; 11import tools.refinery.store.query.term.Variable;
11 12
@@ -53,6 +54,11 @@ public enum BooleanLiteral implements CanNegate<BooleanLiteral> {
53 } 54 }
54 55
55 @Override 56 @Override
57 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
58 return hashCode();
59 }
60
61 @Override
56 public String toString() { 62 public String toString() {
57 return Boolean.toString(value); 63 return Boolean.toString(value);
58 } 64 }
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java
index 29772aee..2d0e4e97 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallLiteral.java
@@ -6,13 +6,17 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.Constraint; 8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.equality.LiteralEqualityHelper; 10import tools.refinery.store.query.equality.LiteralEqualityHelper;
11import tools.refinery.store.query.equality.LiteralHashCodeHelper;
10import tools.refinery.store.query.substitution.Substitution; 12import tools.refinery.store.query.substitution.Substitution;
11import tools.refinery.store.query.term.ParameterDirection; 13import tools.refinery.store.query.term.ParameterDirection;
12import tools.refinery.store.query.term.Variable; 14import tools.refinery.store.query.term.Variable;
13 15
14import java.util.*; 16import java.util.*;
15 17
18// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
19@SuppressWarnings("squid:S2160")
16public final class CallLiteral extends AbstractCallLiteral implements CanNegate<CallLiteral> { 20public final class CallLiteral extends AbstractCallLiteral implements CanNegate<CallLiteral> {
17 private final CallPolarity polarity; 21 private final CallPolarity polarity;
18 22
@@ -22,10 +26,14 @@ public final class CallLiteral extends AbstractCallLiteral implements CanNegate<
22 int arity = target.arity(); 26 int arity = target.arity();
23 if (polarity.isTransitive()) { 27 if (polarity.isTransitive()) {
24 if (arity != 2) { 28 if (arity != 2) {
25 throw new IllegalArgumentException("Transitive closures can only take binary relations"); 29 throw new InvalidQueryException("Transitive closures can only take binary relations");
26 } 30 }
27 if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) { 31 if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) {
28 throw new IllegalArgumentException("Transitive closures can only be computed over nodes"); 32 throw new InvalidQueryException("Transitive closures can only be computed over nodes");
33 }
34 if (parameters.get(0).getDirection() != ParameterDirection.OUT ||
35 parameters.get(1).getDirection() != ParameterDirection.OUT) {
36 throw new InvalidQueryException("Transitive closures cannot take input parameters");
29 } 37 }
30 } 38 }
31 this.polarity = polarity; 39 this.polarity = polarity;
@@ -85,22 +93,18 @@ public final class CallLiteral extends AbstractCallLiteral implements CanNegate<
85 } 93 }
86 94
87 @Override 95 @Override
88 public CallLiteral negate() { 96 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
89 return new CallLiteral(polarity.negate(), getTarget(), getArguments()); 97 return Objects.hash(super.hashCodeWithSubstitution(helper), polarity);
90 } 98 }
91 99
92 @Override 100 @Override
93 public boolean equals(Object o) { 101 public CallLiteral negate() {
94 if (this == o) return true; 102 return new CallLiteral(polarity.negate(), getTarget(), getArguments());
95 if (o == null || getClass() != o.getClass()) return false;
96 if (!super.equals(o)) return false;
97 CallLiteral that = (CallLiteral) o;
98 return polarity == that.polarity;
99 } 103 }
100 104
101 @Override 105 @Override
102 public int hashCode() { 106 public AbstractCallLiteral withArguments(Constraint newTarget, List<Variable> newArguments) {
103 return Objects.hash(super.hashCode(), polarity); 107 return new CallLiteral(polarity, newTarget, newArguments);
104 } 108 }
105 109
106 @Override 110 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java
index ca70b0fd..716c7109 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CallPolarity.java
@@ -5,6 +5,8 @@
5 */ 5 */
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.InvalidQueryException;
9
8public enum CallPolarity { 10public enum CallPolarity {
9 POSITIVE(true, false), 11 POSITIVE(true, false),
10 NEGATIVE(false, false), 12 NEGATIVE(false, false),
@@ -31,7 +33,7 @@ public enum CallPolarity {
31 return switch (this) { 33 return switch (this) {
32 case POSITIVE -> NEGATIVE; 34 case POSITIVE -> NEGATIVE;
33 case NEGATIVE -> POSITIVE; 35 case NEGATIVE -> POSITIVE;
34 case TRANSITIVE -> throw new IllegalArgumentException("Transitive polarity cannot be negated"); 36 case TRANSITIVE -> throw new InvalidQueryException("Transitive polarity cannot be negated");
35 }; 37 };
36 } 38 }
37} 39}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssumeLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java
index 1ca04c77..dfedd2cb 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/AssumeLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CheckLiteral.java
@@ -5,22 +5,35 @@
5 */ 5 */
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 11import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.term.ConstantTerm; 12import tools.refinery.store.query.term.ConstantTerm;
11import tools.refinery.store.query.term.Term; 13import tools.refinery.store.query.term.Term;
12import tools.refinery.store.query.term.Variable; 14import tools.refinery.store.query.term.Variable;
15import tools.refinery.store.query.term.bool.BoolNotTerm;
16import tools.refinery.store.query.term.bool.BoolTerms;
13 17
14import java.util.Collections; 18import java.util.Collections;
15import java.util.Objects; 19import java.util.Objects;
16import java.util.Set; 20import java.util.Set;
17 21
18public record AssumeLiteral(Term<Boolean> term) implements Literal { 22// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
19 public AssumeLiteral { 23@SuppressWarnings("squid:S2160")
24public class CheckLiteral extends AbstractLiteral implements CanNegate<CheckLiteral> {
25 private final Term<Boolean> term;
26
27 public CheckLiteral(Term<Boolean> term) {
20 if (!term.getType().equals(Boolean.class)) { 28 if (!term.getType().equals(Boolean.class)) {
21 throw new IllegalArgumentException("Term %s must be of type %s, got %s instead".formatted( 29 throw new InvalidQueryException("Term %s must be of type %s, got %s instead".formatted(
22 term, Boolean.class.getName(), term.getType().getName())); 30 term, Boolean.class.getName(), term.getType().getName()));
23 } 31 }
32 this.term = term;
33 }
34
35 public Term<Boolean> getTerm() {
36 return term;
24 } 37 }
25 38
26 @Override 39 @Override
@@ -38,10 +51,17 @@ public record AssumeLiteral(Term<Boolean> term) implements Literal {
38 return Set.of(); 51 return Set.of();
39 } 52 }
40 53
41
42 @Override 54 @Override
43 public Literal substitute(Substitution substitution) { 55 public Literal substitute(Substitution substitution) {
44 return new AssumeLiteral(term.substitute(substitution)); 56 return new CheckLiteral(term.substitute(substitution));
57 }
58
59 @Override
60 public CheckLiteral negate() {
61 if (term instanceof BoolNotTerm notTerm) {
62 return new CheckLiteral(notTerm.getBody());
63 }
64 return new CheckLiteral(BoolTerms.not(term));
45 } 65 }
46 66
47 @Override 67 @Override
@@ -49,11 +69,16 @@ public record AssumeLiteral(Term<Boolean> term) implements Literal {
49 if (other == null || getClass() != other.getClass()) { 69 if (other == null || getClass() != other.getClass()) {
50 return false; 70 return false;
51 } 71 }
52 var otherAssumeLiteral = (AssumeLiteral) other; 72 var otherAssumeLiteral = (CheckLiteral) other;
53 return term.equalsWithSubstitution(helper, otherAssumeLiteral.term); 73 return term.equalsWithSubstitution(helper, otherAssumeLiteral.term);
54 } 74 }
55 75
56 @Override 76 @Override
77 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
78 return Objects.hash(super.hashCodeWithSubstitution(helper), term.hashCodeWithSubstitution(helper));
79 }
80
81 @Override
57 public Literal reduce() { 82 public Literal reduce() {
58 if (term instanceof ConstantTerm<Boolean> constantTerm) { 83 if (term instanceof ConstantTerm<Boolean> constantTerm) {
59 // Return {@link BooleanLiteral#FALSE} for {@code false} or {@code null} literals. 84 // Return {@link BooleanLiteral#FALSE} for {@code false} or {@code null} literals.
@@ -67,17 +92,4 @@ public record AssumeLiteral(Term<Boolean> term) implements Literal {
67 public String toString() { 92 public String toString() {
68 return "(%s)".formatted(term); 93 return "(%s)".formatted(term);
69 } 94 }
70
71 @Override
72 public boolean equals(Object obj) {
73 if (obj == this) return true;
74 if (obj == null || obj.getClass() != this.getClass()) return false;
75 var that = (AssumeLiteral) obj;
76 return Objects.equals(this.term, that.term);
77 }
78
79 @Override
80 public int hashCode() {
81 return Objects.hash(getClass(), term);
82 }
83} 95}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Connectivity.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Connectivity.java
new file mode 100644
index 00000000..a058094d
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Connectivity.java
@@ -0,0 +1,18 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.literal;
7
8import java.util.Locale;
9
10public enum Connectivity {
11 WEAK,
12 STRONG;
13
14 @Override
15 public String toString() {
16 return name().toLowerCase(Locale.ROOT);
17 }
18}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/ConstantLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/ConstantLiteral.java
index 73545620..d83bd584 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/ConstantLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/ConstantLiteral.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 10import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.term.NodeVariable; 11import tools.refinery.store.query.term.NodeVariable;
11import tools.refinery.store.query.term.Variable; 12import tools.refinery.store.query.term.Variable;
@@ -13,7 +14,24 @@ import tools.refinery.store.query.term.Variable;
13import java.util.Objects; 14import java.util.Objects;
14import java.util.Set; 15import java.util.Set;
15 16
16public record ConstantLiteral(NodeVariable variable, int nodeId) implements Literal { 17public class ConstantLiteral extends AbstractLiteral {
18 private final NodeVariable variable;
19 private final int nodeId;
20
21 public ConstantLiteral(NodeVariable variable, int nodeId) {
22 this.variable = variable;
23 this.nodeId = nodeId;
24 }
25
26 public NodeVariable getVariable() {
27 return variable;
28 }
29
30 public int getNodeId() {
31 return nodeId;
32 }
33
34
17 @Override 35 @Override
18 public Set<Variable> getOutputVariables() { 36 public Set<Variable> getOutputVariables() {
19 return Set.of(variable); 37 return Set.of(variable);
@@ -43,23 +61,13 @@ public record ConstantLiteral(NodeVariable variable, int nodeId) implements Lite
43 return helper.variableEqual(variable, otherConstantLiteral.variable) && nodeId == otherConstantLiteral.nodeId; 61 return helper.variableEqual(variable, otherConstantLiteral.variable) && nodeId == otherConstantLiteral.nodeId;
44 } 62 }
45 63
46
47 @Override
48 public String toString() {
49 return "%s === @Constant %d".formatted(variable, nodeId);
50 }
51
52 @Override 64 @Override
53 public boolean equals(Object obj) { 65 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
54 if (obj == this) return true; 66 return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(variable), nodeId);
55 if (obj == null || obj.getClass() != this.getClass()) return false;
56 var that = (ConstantLiteral) obj;
57 return Objects.equals(this.variable, that.variable) &&
58 this.nodeId == that.nodeId;
59 } 67 }
60 68
61 @Override 69 @Override
62 public int hashCode() { 70 public String toString() {
63 return Objects.hash(getClass(), variable, nodeId); 71 return "%s === @Constant %d".formatted(variable, nodeId);
64 } 72 }
65} 73}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CountLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CountLiteral.java
index 4d4749c8..3d078d89 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CountLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/CountLiteral.java
@@ -6,96 +6,40 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.Constraint; 8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.substitution.Substitution; 9import tools.refinery.store.query.substitution.Substitution;
11import tools.refinery.store.query.term.DataVariable; 10import tools.refinery.store.query.term.DataVariable;
12import tools.refinery.store.query.term.Variable; 11import tools.refinery.store.query.term.Variable;
13import tools.refinery.store.query.term.int_.IntTerms;
14 12
15import java.util.List; 13import java.util.List;
16import java.util.Objects;
17import java.util.Set;
18
19public class CountLiteral extends AbstractCallLiteral {
20 private final DataVariable<Integer> resultVariable;
21 14
15public class CountLiteral extends AbstractCountLiteral<Integer> {
22 public CountLiteral(DataVariable<Integer> resultVariable, Constraint target, List<Variable> arguments) { 16 public CountLiteral(DataVariable<Integer> resultVariable, Constraint target, List<Variable> arguments) {
23 super(target, arguments); 17 super(Integer.class, resultVariable, target, arguments);
24 if (!resultVariable.getType().equals(Integer.class)) {
25 throw new IllegalArgumentException("Count result variable %s must be of type %s, got %s instead".formatted(
26 resultVariable, Integer.class.getName(), resultVariable.getType().getName()));
27 }
28 if (arguments.contains(resultVariable)) {
29 throw new IllegalArgumentException("Count result variable %s must not appear in the argument list"
30 .formatted(resultVariable));
31 }
32 this.resultVariable = resultVariable;
33 }
34
35 public DataVariable<Integer> getResultVariable() {
36 return resultVariable;
37 } 18 }
38 19
39 @Override 20 @Override
40 public Set<Variable> getOutputVariables() { 21 protected Integer zero() {
41 return Set.of(resultVariable); 22 return 0;
42 } 23 }
43 24
44 @Override 25 @Override
45 public Literal reduce() { 26 protected Integer one() {
46 var reduction = getTarget().getReduction(); 27 return 1;
47 return switch (reduction) {
48 case ALWAYS_FALSE -> getResultVariable().assign(IntTerms.constant(0));
49 // The only way a constant {@code true} predicate can be called in a negative position is to have all of
50 // its arguments bound as input variables. Thus, there will only be a single match.
51 case ALWAYS_TRUE -> getResultVariable().assign(IntTerms.constant(1));
52 case NOT_REDUCIBLE -> this;
53 };
54 } 28 }
55 29
56 @Override 30 @Override
57 protected Literal doSubstitute(Substitution substitution, List<Variable> substitutedArguments) { 31 protected Literal doSubstitute(Substitution substitution, List<Variable> substitutedArguments) {
58 return new CountLiteral(substitution.getTypeSafeSubstitute(resultVariable), getTarget(), substitutedArguments); 32 return new CountLiteral(substitution.getTypeSafeSubstitute(getResultVariable()), getTarget(),
59 } 33 substitutedArguments);
60
61 @Override
62 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) {
63 if (!super.equalsWithSubstitution(helper, other)) {
64 return false;
65 }
66 var otherCountLiteral = (CountLiteral) other;
67 return helper.variableEqual(resultVariable, otherCountLiteral.resultVariable);
68 }
69
70 @Override
71 public boolean equals(Object o) {
72 if (this == o) return true;
73 if (o == null || getClass() != o.getClass()) return false;
74 if (!super.equals(o)) return false;
75 CountLiteral that = (CountLiteral) o;
76 return resultVariable.equals(that.resultVariable);
77 } 34 }
78 35
79 @Override 36 @Override
80 public int hashCode() { 37 public AbstractCallLiteral withArguments(Constraint newTarget, List<Variable> newArguments) {
81 return Objects.hash(super.hashCode(), resultVariable); 38 return new CountLiteral(getResultVariable(), newTarget, newArguments);
82 } 39 }
83 40
84 @Override 41 @Override
85 public String toString() { 42 protected String operatorName() {
86 var builder = new StringBuilder(); 43 return "count";
87 builder.append(resultVariable);
88 builder.append(" is count ");
89 builder.append(getTarget().toReferenceString());
90 builder.append("(");
91 var argumentIterator = getArguments().iterator();
92 if (argumentIterator.hasNext()) {
93 builder.append(argumentIterator.next());
94 while (argumentIterator.hasNext()) {
95 builder.append(", ").append(argumentIterator.next());
96 }
97 }
98 builder.append(")");
99 return builder.toString();
100 } 44 }
101} 45}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java
index 28ba7625..7343f709 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/EquivalenceLiteral.java
@@ -5,16 +5,44 @@
5 */ 5 */
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 11import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.term.NodeVariable;
11import tools.refinery.store.query.term.Variable; 12import tools.refinery.store.query.term.Variable;
12 13
13import java.util.Objects; 14import java.util.Objects;
14import java.util.Set; 15import java.util.Set;
15 16
16public record EquivalenceLiteral(boolean positive, NodeVariable left, NodeVariable right) 17// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
17 implements CanNegate<EquivalenceLiteral> { 18@SuppressWarnings("squid:S2160")
19public final class EquivalenceLiteral extends AbstractLiteral implements CanNegate<EquivalenceLiteral> {
20 private final boolean positive;
21 private final Variable left;
22 private final Variable right;
23
24 public EquivalenceLiteral(boolean positive, Variable left, Variable right) {
25 if (!left.tryGetType().equals(right.tryGetType())) {
26 throw new InvalidQueryException("Variables %s and %s of different type cannot be equivalent"
27 .formatted(left, right));
28 }
29 this.positive = positive;
30 this.left = left;
31 this.right = right;
32 }
33
34 public boolean isPositive() {
35 return positive;
36 }
37
38 public Variable getLeft() {
39 return left;
40 }
41
42 public Variable getRight() {
43 return right;
44 }
45
18 @Override 46 @Override
19 public Set<Variable> getOutputVariables() { 47 public Set<Variable> getOutputVariables() {
20 return Set.of(left); 48 return Set.of(left);
@@ -37,8 +65,8 @@ public record EquivalenceLiteral(boolean positive, NodeVariable left, NodeVariab
37 65
38 @Override 66 @Override
39 public EquivalenceLiteral substitute(Substitution substitution) { 67 public EquivalenceLiteral substitute(Substitution substitution) {
40 return new EquivalenceLiteral(positive, substitution.getTypeSafeSubstitute(left), 68 return new EquivalenceLiteral(positive, substitution.getSubstitute(left),
41 substitution.getTypeSafeSubstitute(right)); 69 substitution.getSubstitute(right));
42 } 70 }
43 71
44 @Override 72 @Override
@@ -55,27 +83,18 @@ public record EquivalenceLiteral(boolean positive, NodeVariable left, NodeVariab
55 return false; 83 return false;
56 } 84 }
57 var otherEquivalenceLiteral = (EquivalenceLiteral) other; 85 var otherEquivalenceLiteral = (EquivalenceLiteral) other;
58 return helper.variableEqual(left, otherEquivalenceLiteral.left) && helper.variableEqual(right, 86 return helper.variableEqual(left, otherEquivalenceLiteral.left) &&
59 otherEquivalenceLiteral.right); 87 helper.variableEqual(right, otherEquivalenceLiteral.right);
60 }
61
62 @Override
63 public String toString() {
64 return "%s %s %s".formatted(left, positive ? "===" : "!==", right);
65 } 88 }
66 89
67 @Override 90 @Override
68 public boolean equals(Object obj) { 91 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
69 if (obj == this) return true; 92 return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(left),
70 if (obj == null || obj.getClass() != this.getClass()) return false; 93 helper.getVariableHashCode(right));
71 var that = (EquivalenceLiteral) obj;
72 return this.positive == that.positive &&
73 Objects.equals(this.left, that.left) &&
74 Objects.equals(this.right, that.right);
75 } 94 }
76 95
77 @Override 96 @Override
78 public int hashCode() { 97 public String toString() {
79 return Objects.hash(getClass(), positive, left, right); 98 return "%s %s %s".formatted(left, positive ? "===" : "!==", right);
80 } 99 }
81} 100}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literal.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literal.java
index ce6c11fe..cb16ab00 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literal.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literal.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.literal; 6package tools.refinery.store.query.literal;
7 7
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 10import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.term.Variable; 11import tools.refinery.store.query.term.Variable;
11 12
@@ -26,4 +27,6 @@ public interface Literal {
26 27
27 @SuppressWarnings("BooleanMethodIsAlwaysInverted") 28 @SuppressWarnings("BooleanMethodIsAlwaysInverted")
28 boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other); 29 boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other);
30
31 int hashCodeWithSubstitution(LiteralHashCodeHelper helper);
29} 32}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literals.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literals.java
index b3a87811..6056da45 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literals.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/Literals.java
@@ -16,7 +16,7 @@ public final class Literals {
16 return literal.negate(); 16 return literal.negate();
17 } 17 }
18 18
19 public static AssumeLiteral assume(Term<Boolean> term) { 19 public static CheckLiteral check(Term<Boolean> term) {
20 return new AssumeLiteral(term); 20 return new CheckLiteral(term);
21 } 21 }
22} 22}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java
new file mode 100644
index 00000000..f7323947
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/literal/RepresentativeElectionLiteral.java
@@ -0,0 +1,119 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.literal;
7
8import tools.refinery.store.query.Constraint;
9import tools.refinery.store.query.InvalidQueryException;
10import tools.refinery.store.query.equality.LiteralEqualityHelper;
11import tools.refinery.store.query.equality.LiteralHashCodeHelper;
12import tools.refinery.store.query.substitution.Substitution;
13import tools.refinery.store.query.term.NodeVariable;
14import tools.refinery.store.query.term.ParameterDirection;
15import tools.refinery.store.query.term.Variable;
16
17import java.util.List;
18import java.util.Set;
19
20// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}.
21@SuppressWarnings("squid:S2160")
22public class RepresentativeElectionLiteral extends AbstractCallLiteral {
23 private final Connectivity connectivity;
24
25 public RepresentativeElectionLiteral(Connectivity connectivity, Constraint target, NodeVariable specific,
26 NodeVariable representative) {
27 this(connectivity, target, List.of(specific, representative));
28 }
29
30 private RepresentativeElectionLiteral(Connectivity connectivity, Constraint target, List<Variable> arguments) {
31 super(target, arguments);
32 this.connectivity = connectivity;
33 var parameters = target.getParameters();
34 int arity = target.arity();
35 if (arity != 2) {
36 throw new InvalidQueryException("SCCs can only take binary relations");
37 }
38 if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) {
39 throw new InvalidQueryException("SCCs can only be computed over nodes");
40 }
41 if (parameters.get(0).getDirection() != ParameterDirection.OUT ||
42 parameters.get(1).getDirection() != ParameterDirection.OUT) {
43 throw new InvalidQueryException("SCCs cannot take input parameters");
44 }
45 }
46
47 public Connectivity getConnectivity() {
48 return connectivity;
49 }
50
51 @Override
52 protected Literal doSubstitute(Substitution substitution, List<Variable> substitutedArguments) {
53 return new RepresentativeElectionLiteral(connectivity, getTarget(), substitutedArguments);
54 }
55
56 @Override
57 public Set<Variable> getOutputVariables() {
58 return getArgumentsOfDirection(ParameterDirection.OUT);
59 }
60
61 @Override
62 public Set<Variable> getInputVariables(Set<? extends Variable> positiveVariablesInClause) {
63 return Set.of();
64 }
65
66 @Override
67 public Set<Variable> getPrivateVariables(Set<? extends Variable> positiveVariablesInClause) {
68 return Set.of();
69 }
70
71 @Override
72 public Literal reduce() {
73 var reduction = getTarget().getReduction();
74 return switch (reduction) {
75 case ALWAYS_FALSE -> BooleanLiteral.FALSE;
76 case ALWAYS_TRUE -> throw new InvalidQueryException(
77 "Trying to elect representatives over an infinite set");
78 case NOT_REDUCIBLE -> this;
79 };
80 }
81
82 @Override
83 public AbstractCallLiteral withArguments(Constraint newTarget, List<Variable> newArguments) {
84 return new RepresentativeElectionLiteral(connectivity, newTarget, newArguments);
85 }
86
87 @Override
88 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) {
89 if (!super.equalsWithSubstitution(helper, other)) {
90 return false;
91 }
92 var otherRepresentativeElectionLiteral = (RepresentativeElectionLiteral) other;
93 return connectivity.equals(otherRepresentativeElectionLiteral.connectivity);
94 }
95
96 @Override
97 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
98 return super.hashCodeWithSubstitution(helper) * 31 + connectivity.hashCode();
99 }
100
101 @Override
102 public String toString() {
103 var builder = new StringBuilder();
104 builder.append("@Representative(\"");
105 builder.append(connectivity);
106 builder.append("\") ");
107 builder.append(getTarget().toReferenceString());
108 builder.append("(");
109 var argumentIterator = getArguments().iterator();
110 if (argumentIterator.hasNext()) {
111 builder.append(argumentIterator.next());
112 while (argumentIterator.hasNext()) {
113 builder.append(", ").append(argumentIterator.next());
114 }
115 }
116 builder.append(")");
117 return builder.toString();
118 }
119}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AbstractResultSet.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AbstractResultSet.java
index a710c64d..dcfe6cc5 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AbstractResultSet.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AbstractResultSet.java
@@ -28,7 +28,7 @@ public abstract class AbstractResultSet<T> implements ResultSet<T> {
28 } 28 }
29 29
30 @Override 30 @Override
31 public Query<T> getQuery() { 31 public Query<T> getCanonicalQuery() {
32 return query; 32 return query;
33 } 33 }
34 34
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AnyResultSet.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AnyResultSet.java
index 02809477..5b75b103 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AnyResultSet.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/AnyResultSet.java
@@ -11,7 +11,7 @@ import tools.refinery.store.query.dnf.AnyQuery;
11public sealed interface AnyResultSet permits ResultSet { 11public sealed interface AnyResultSet permits ResultSet {
12 ModelQueryAdapter getAdapter(); 12 ModelQueryAdapter getAdapter();
13 13
14 AnyQuery getQuery(); 14 AnyQuery getCanonicalQuery();
15 15
16 int size(); 16 int size();
17} 17}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/EmptyResultSet.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/EmptyResultSet.java
index 2795a44b..991b1e32 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/EmptyResultSet.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/EmptyResultSet.java
@@ -18,7 +18,7 @@ public record EmptyResultSet<T>(ModelQueryAdapter adapter, Query<T> query) imple
18 } 18 }
19 19
20 @Override 20 @Override
21 public Query<T> getQuery() { 21 public Query<T> getCanonicalQuery() {
22 return query; 22 return query;
23 } 23 }
24 24
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/OrderedResultSet.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/OrderedResultSet.java
index 39006d65..df12b967 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/OrderedResultSet.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/OrderedResultSet.java
@@ -17,7 +17,7 @@ public class OrderedResultSet<T> implements AutoCloseable, ResultSet<T> {
17 private final ResultSet<T> resultSet; 17 private final ResultSet<T> resultSet;
18 private final OrderStatisticTree<Tuple> tree = new OrderStatisticTree<>(); 18 private final OrderStatisticTree<Tuple> tree = new OrderStatisticTree<>();
19 private final ResultSetListener<T> listener = (key, fromValue, toValue) -> { 19 private final ResultSetListener<T> listener = (key, fromValue, toValue) -> {
20 var defaultValue = getQuery().defaultValue(); 20 var defaultValue = getCanonicalQuery().defaultValue();
21 if (Objects.equals(defaultValue, toValue)) { 21 if (Objects.equals(defaultValue, toValue)) {
22 tree.remove(key); 22 tree.remove(key);
23 } else { 23 } else {
@@ -45,8 +45,8 @@ public class OrderedResultSet<T> implements AutoCloseable, ResultSet<T> {
45 } 45 }
46 46
47 @Override 47 @Override
48 public Query<T> getQuery() { 48 public Query<T> getCanonicalQuery() {
49 return resultSet.getQuery(); 49 return resultSet.getCanonicalQuery();
50 } 50 }
51 51
52 @Override 52 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/ResultSet.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/ResultSet.java
index 33d1ea95..a6e99784 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/ResultSet.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/resultset/ResultSet.java
@@ -10,7 +10,7 @@ import tools.refinery.store.query.dnf.Query;
10import tools.refinery.store.tuple.Tuple; 10import tools.refinery.store.tuple.Tuple;
11 11
12public non-sealed interface ResultSet<T> extends AnyResultSet { 12public non-sealed interface ResultSet<T> extends AnyResultSet {
13 Query<T> getQuery(); 13 Query<T> getCanonicalQuery();
14 14
15 T get(Tuple parameters); 15 T get(Tuple parameters);
16 16
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/AbstractRecursiveRewriter.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/AbstractRecursiveRewriter.java
new file mode 100644
index 00000000..fb4c14a7
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/AbstractRecursiveRewriter.java
@@ -0,0 +1,26 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.rewriter;
7
8import tools.refinery.store.query.dnf.Dnf;
9import tools.refinery.store.query.equality.DnfEqualityChecker;
10import tools.refinery.store.util.CycleDetectingMapper;
11
12public abstract class AbstractRecursiveRewriter implements DnfRewriter {
13 private final CycleDetectingMapper<Dnf, Dnf> mapper = new CycleDetectingMapper<>(Dnf::name, this::map);
14
15 @Override
16 public Dnf rewrite(Dnf dnf) {
17 return mapper.map(dnf);
18 }
19
20 protected Dnf map(Dnf dnf) {
21 var result = doRewrite(dnf);
22 return dnf.equalsWithSubstitution(DnfEqualityChecker.DEFAULT, result) ? dnf : result;
23 }
24
25 protected abstract Dnf doRewrite(Dnf dnf);
26}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/ClauseInputParameterResolver.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/ClauseInputParameterResolver.java
new file mode 100644
index 00000000..aa06a05a
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/ClauseInputParameterResolver.java
@@ -0,0 +1,160 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.rewriter;
7
8import tools.refinery.store.query.dnf.Dnf;
9import tools.refinery.store.query.dnf.DnfClause;
10import tools.refinery.store.query.literal.*;
11import tools.refinery.store.query.substitution.Substitution;
12import tools.refinery.store.query.term.ParameterDirection;
13import tools.refinery.store.query.term.Variable;
14
15import java.util.*;
16
17class ClauseInputParameterResolver {
18 private final InputParameterResolver rewriter;
19 private final String dnfName;
20 private final int clauseIndex;
21 private final Set<Variable> positiveVariables = new LinkedHashSet<>();
22 private final List<Literal> inlinedLiterals = new ArrayList<>();
23 private final Deque<Literal> workList;
24 private int helperIndex = 0;
25
26 public ClauseInputParameterResolver(InputParameterResolver rewriter, List<Literal> context, DnfClause clause,
27 String dnfName, int clauseIndex) {
28 this.rewriter = rewriter;
29 this.dnfName = dnfName;
30 this.clauseIndex = clauseIndex;
31 workList = new ArrayDeque<>(clause.literals().size() + context.size());
32 for (var literal : context) {
33 workList.addLast(literal);
34 }
35 for (var literal : clause.literals()) {
36 workList.addLast(literal);
37 }
38 }
39
40 public List<Literal> rewriteClause() {
41 while (!workList.isEmpty()) {
42 var literal = workList.removeFirst();
43 processLiteral(literal);
44 }
45 return inlinedLiterals;
46 }
47
48 private void processLiteral(Literal literal) {
49 if (!(literal instanceof AbstractCallLiteral abstractCallLiteral) ||
50 !(abstractCallLiteral.getTarget() instanceof Dnf targetDnf)) {
51 markAsDone(literal);
52 return;
53 }
54 boolean hasInputParameter = hasInputParameter(targetDnf);
55 if (!hasInputParameter) {
56 targetDnf = rewriter.rewrite(targetDnf);
57 }
58 if (inlinePositiveClause(abstractCallLiteral, targetDnf)) {
59 return;
60 }
61 if (eliminateDoubleNegation(abstractCallLiteral, targetDnf)) {
62 return;
63 }
64 if (hasInputParameter) {
65 rewriteWithCurrentContext(abstractCallLiteral, targetDnf);
66 return;
67 }
68 markAsDone(abstractCallLiteral.withTarget(targetDnf));
69 }
70
71 private void markAsDone(Literal literal) {
72 positiveVariables.addAll(literal.getOutputVariables());
73 inlinedLiterals.add(literal);
74 }
75
76 private boolean inlinePositiveClause(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf) {
77 var targetLiteral = getSingleLiteral(abstractCallLiteral, targetDnf, CallPolarity.POSITIVE);
78 if (targetLiteral == null) {
79 return false;
80 }
81 var substitution = asSubstitution(abstractCallLiteral, targetDnf);
82 var substitutedLiteral = targetLiteral.substitute(substitution);
83 workList.addFirst(substitutedLiteral);
84 return true;
85 }
86
87 private boolean eliminateDoubleNegation(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf) {
88 var targetLiteral = getSingleLiteral(abstractCallLiteral, targetDnf, CallPolarity.NEGATIVE);
89 if (!(targetLiteral instanceof CallLiteral targetCallLiteral) ||
90 targetCallLiteral.getPolarity() != CallPolarity.NEGATIVE) {
91 return false;
92 }
93 var substitution = asSubstitution(abstractCallLiteral, targetDnf);
94 var substitutedLiteral = (CallLiteral) targetCallLiteral.substitute(substitution);
95 workList.addFirst(substitutedLiteral.negate());
96 return true;
97 }
98
99 private void rewriteWithCurrentContext(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf) {
100 var contextBuilder = Dnf.builder("%s#clause%d#helper%d".formatted(dnfName, clauseIndex, helperIndex));
101 helperIndex++;
102 contextBuilder.parameters(positiveVariables, ParameterDirection.OUT);
103 contextBuilder.clause(inlinedLiterals);
104 var contextDnf = contextBuilder.build();
105 var contextCall = new CallLiteral(CallPolarity.POSITIVE, contextDnf, List.copyOf(positiveVariables));
106 inlinedLiterals.clear();
107 var substitution = Substitution.builder().renewing().build();
108 var context = new ArrayList<Literal>();
109 context.add(contextCall.substitute(substitution));
110 int arity = targetDnf.arity();
111 for (int i = 0; i < arity; i++) {
112 var parameter = targetDnf.getSymbolicParameters().get(i).getVariable();
113 var argument = abstractCallLiteral.getArguments().get(i);
114 context.add(new EquivalenceLiteral(true, parameter, substitution.getSubstitute(argument)));
115 }
116 var rewrittenDnf = rewriter.rewriteWithContext(context, targetDnf);
117 workList.addFirst(abstractCallLiteral.withTarget(rewrittenDnf));
118 workList.addFirst(contextCall);
119 }
120
121 private static boolean hasInputParameter(Dnf targetDnf) {
122 for (var parameter : targetDnf.getParameters()) {
123 if (parameter.getDirection() != ParameterDirection.OUT) {
124 return true;
125 }
126 }
127 return false;
128 }
129
130 private static Literal getSingleLiteral(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf,
131 CallPolarity polarity) {
132 if (!(abstractCallLiteral instanceof CallLiteral callLiteral) ||
133 callLiteral.getPolarity() != polarity) {
134 return null;
135 }
136 var clauses = targetDnf.getClauses();
137 if (clauses.size() != 1) {
138 return null;
139 }
140 var targetLiterals = clauses.get(0).literals();
141 if (targetLiterals.size() != 1) {
142 return null;
143 }
144 return targetLiterals.get(0);
145 }
146
147 private static Substitution asSubstitution(AbstractCallLiteral callLiteral, Dnf targetDnf) {
148 var builder = Substitution.builder().renewing();
149 var arguments = callLiteral.getArguments();
150 var parameters = targetDnf.getSymbolicParameters();
151 int arity = arguments.size();
152 if (parameters.size() != arity) {
153 throw new IllegalArgumentException("Call %s of %s arity mismatch".formatted(callLiteral, targetDnf));
154 }
155 for (int i = 0; i < arity; i++) {
156 builder.putChecked(parameters.get(i).getVariable(), arguments.get(i));
157 }
158 return builder.build();
159 }
160}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/CompositeRewriter.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/CompositeRewriter.java
new file mode 100644
index 00000000..5b4f65e5
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/CompositeRewriter.java
@@ -0,0 +1,29 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.rewriter;
7
8import tools.refinery.store.query.dnf.Dnf;
9
10import java.util.ArrayList;
11import java.util.List;
12
13public class CompositeRewriter implements DnfRewriter {
14 private final List<DnfRewriter> rewriterList = new ArrayList<>();
15
16 public void addFirst(DnfRewriter rewriter) {
17 rewriterList.add(rewriter);
18 }
19
20 @Override
21 public Dnf rewrite(Dnf dnf) {
22 Dnf rewrittenDnf = dnf;
23 for (int i = rewriterList.size() - 1; i >= 0; i--) {
24 var rewriter = rewriterList.get(i);
25 rewrittenDnf = rewriter.rewrite(rewrittenDnf);
26 }
27 return rewrittenDnf;
28 }
29}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DnfRewriter.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DnfRewriter.java
new file mode 100644
index 00000000..5d8359d1
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DnfRewriter.java
@@ -0,0 +1,24 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.rewriter;
7
8import tools.refinery.store.query.dnf.AnyQuery;
9import tools.refinery.store.query.dnf.Dnf;
10import tools.refinery.store.query.dnf.Query;
11
12@FunctionalInterface
13public interface DnfRewriter {
14 Dnf rewrite(Dnf dnf);
15
16 default AnyQuery rewrite(AnyQuery query) {
17 return rewrite((Query<?>) query);
18 }
19
20 default <T> Query<T> rewrite(Query<T> query) {
21 var rewrittenDnf = rewrite(query.getDnf());
22 return query.withDnf(rewrittenDnf);
23 }
24}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DuplicateDnfRemover.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DuplicateDnfRemover.java
new file mode 100644
index 00000000..0c786470
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/DuplicateDnfRemover.java
@@ -0,0 +1,98 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.rewriter;
7
8import tools.refinery.store.query.dnf.Dnf;
9import tools.refinery.store.query.dnf.DnfClause;
10import tools.refinery.store.query.dnf.Query;
11import tools.refinery.store.query.equality.DnfEqualityChecker;
12import tools.refinery.store.query.literal.AbstractCallLiteral;
13import tools.refinery.store.query.literal.Literal;
14
15import java.util.ArrayList;
16import java.util.HashMap;
17import java.util.List;
18import java.util.Map;
19
20public class DuplicateDnfRemover extends AbstractRecursiveRewriter {
21 private final Map<CanonicalDnf, Dnf> dnfCache = new HashMap<>();
22 private final Map<Dnf, Query<?>> queryCache = new HashMap<>();
23
24 @Override
25 protected Dnf map(Dnf dnf) {
26 var result = super.map(dnf);
27 return dnfCache.computeIfAbsent(new CanonicalDnf(result), CanonicalDnf::getDnf);
28 }
29
30 @Override
31 protected Dnf doRewrite(Dnf dnf) {
32 var builder = Dnf.builderFrom(dnf);
33 for (var clause : dnf.getClauses()) {
34 builder.clause(rewriteClause(clause));
35 }
36 return builder.build();
37 }
38
39 private List<Literal> rewriteClause(DnfClause clause) {
40 var originalLiterals = clause.literals();
41 var literals = new ArrayList<Literal>(originalLiterals.size());
42 for (var literal : originalLiterals) {
43 var rewrittenLiteral = literal;
44 if (literal instanceof AbstractCallLiteral abstractCallLiteral &&
45 abstractCallLiteral.getTarget() instanceof Dnf targetDnf) {
46 var rewrittenTarget = rewrite(targetDnf);
47 rewrittenLiteral = abstractCallLiteral.withTarget(rewrittenTarget);
48 }
49 literals.add(rewrittenLiteral);
50 }
51 return literals;
52 }
53
54 @Override
55 public <T> Query<T> rewrite(Query<T> query) {
56 var rewrittenDnf = rewrite(query.getDnf());
57 // {@code withDnf} will always return the appropriate type.
58 @SuppressWarnings("unchecked")
59 var rewrittenQuery = (Query<T>) queryCache.computeIfAbsent(rewrittenDnf, query::withDnf);
60 return rewrittenQuery;
61 }
62
63 private static class CanonicalDnf {
64 private final Dnf dnf;
65 private final int hash;
66
67 public CanonicalDnf(Dnf dnf) {
68 this.dnf = dnf;
69 hash = dnf.hashCodeWithSubstitution();
70 }
71
72 public Dnf getDnf() {
73 return dnf;
74 }
75
76 @Override
77 public boolean equals(Object obj) {
78 if (this == obj) {
79 return true;
80 }
81 if (obj == null || getClass() != obj.getClass()) {
82 return false;
83 }
84 var otherCanonicalDnf = (CanonicalDnf) obj;
85 return dnf.equalsWithSubstitution(DnfEqualityChecker.DEFAULT, otherCanonicalDnf.dnf);
86 }
87
88 @Override
89 public int hashCode() {
90 return hash;
91 }
92
93 @Override
94 public String toString() {
95 return dnf.name();
96 }
97 }
98}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/InputParameterResolver.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/InputParameterResolver.java
new file mode 100644
index 00000000..cd8a2e7d
--- /dev/null
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/rewriter/InputParameterResolver.java
@@ -0,0 +1,51 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.query.rewriter;
7
8import tools.refinery.store.query.dnf.Dnf;
9import tools.refinery.store.query.dnf.DnfBuilder;
10import tools.refinery.store.query.literal.Literal;
11import tools.refinery.store.query.term.ParameterDirection;
12import tools.refinery.store.query.term.Variable;
13
14import java.util.HashSet;
15import java.util.List;
16
17public class InputParameterResolver extends AbstractRecursiveRewriter {
18 @Override
19 protected Dnf doRewrite(Dnf dnf) {
20 return rewriteWithContext(List.of(), dnf);
21 }
22
23 Dnf rewriteWithContext(List<Literal> context, Dnf dnf) {
24 var dnfName = dnf.name();
25 var builder = Dnf.builder(dnfName);
26 createSymbolicParameters(context, dnf, builder);
27 builder.functionalDependencies(dnf.getFunctionalDependencies());
28 var clauses = dnf.getClauses();
29 int clauseCount = clauses.size();
30 for (int i = 0; i < clauseCount; i++) {
31 var clause = clauses.get(i);
32 var clauseRewriter = new ClauseInputParameterResolver(this, context, clause, dnfName, i);
33 builder.clause(clauseRewriter.rewriteClause());
34 }
35 return builder.build();
36 }
37
38 private static void createSymbolicParameters(List<Literal> context, Dnf dnf, DnfBuilder builder) {
39 var positiveInContext = new HashSet<Variable>();
40 for (var literal : context) {
41 positiveInContext.addAll(literal.getOutputVariables());
42 }
43 for (var symbolicParameter : dnf.getSymbolicParameters()) {
44 var variable = symbolicParameter.getVariable();
45 var isOutput = symbolicParameter.getDirection() == ParameterDirection.OUT ||
46 positiveInContext.contains(variable);
47 var direction = isOutput ? ParameterDirection.OUT : ParameterDirection.IN;
48 builder.parameter(variable, direction);
49 }
50 }
51}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AbstractTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AbstractTerm.java
index d0ae3c12..5cecc35b 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AbstractTerm.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AbstractTerm.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9 10
10import java.util.Objects; 11import java.util.Objects;
11 12
@@ -17,13 +18,18 @@ public abstract class AbstractTerm<T> implements Term<T> {
17 } 18 }
18 19
19 @Override 20 @Override
21 public Class<T> getType() {
22 return type;
23 }
24
25 @Override
20 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { 26 public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) {
21 return getClass().equals(other.getClass()) && type.equals(other.getType()); 27 return other != null && getClass() == other.getClass() && type.equals(other.getType());
22 } 28 }
23 29
24 @Override 30 @Override
25 public Class<T> getType() { 31 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
26 return type; 32 return Objects.hash(getClass(), type);
27 } 33 }
28 34
29 @Override 35 @Override
@@ -31,11 +37,11 @@ public abstract class AbstractTerm<T> implements Term<T> {
31 if (this == o) return true; 37 if (this == o) return true;
32 if (o == null || getClass() != o.getClass()) return false; 38 if (o == null || getClass() != o.getClass()) return false;
33 AbstractTerm<?> that = (AbstractTerm<?>) o; 39 AbstractTerm<?> that = (AbstractTerm<?>) o;
34 return type.equals(that.type); 40 return equalsWithSubstitution(LiteralEqualityHelper.DEFAULT, that);
35 } 41 }
36 42
37 @Override 43 @Override
38 public int hashCode() { 44 public int hashCode() {
39 return Objects.hash(getClass(), type); 45 return hashCodeWithSubstitution(LiteralHashCodeHelper.DEFAULT);
40 } 46 }
41} 47}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java
index 192c39c5..3801bc11 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyDataVariable.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import org.jetbrains.annotations.Nullable; 8import org.jetbrains.annotations.Nullable;
9import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.equality.LiteralEqualityHelper; 10import tools.refinery.store.query.equality.LiteralEqualityHelper;
10 11
11import java.util.Optional; 12import java.util.Optional;
@@ -22,8 +23,18 @@ public abstract sealed class AnyDataVariable extends Variable implements AnyTerm
22 } 23 }
23 24
24 @Override 25 @Override
26 public boolean isNodeVariable() {
27 return false;
28 }
29
30 @Override
31 public boolean isDataVariable() {
32 return true;
33 }
34
35 @Override
25 public NodeVariable asNodeVariable() { 36 public NodeVariable asNodeVariable() {
26 throw new IllegalStateException("%s is a data variable".formatted(this)); 37 throw new InvalidQueryException("%s is a data variable".formatted(this));
27 } 38 }
28 39
29 @Override 40 @Override
@@ -37,11 +48,6 @@ public abstract sealed class AnyDataVariable extends Variable implements AnyTerm
37 } 48 }
38 49
39 @Override 50 @Override
40 public boolean isUnifiable() {
41 return false;
42 }
43
44 @Override
45 public abstract AnyDataVariable renew(@Nullable String name); 51 public abstract AnyDataVariable renew(@Nullable String name);
46 52
47 @Override 53 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyTerm.java
index c12c0166..f136b68d 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyTerm.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/AnyTerm.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 8import tools.refinery.store.query.equality.LiteralEqualityHelper;
9import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 10import tools.refinery.store.query.substitution.Substitution;
10 11
11import java.util.Set; 12import java.util.Set;
@@ -17,5 +18,7 @@ public sealed interface AnyTerm permits AnyDataVariable, Term {
17 18
18 boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other); 19 boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other);
19 20
21 int hashCodeWithSubstitution(LiteralHashCodeHelper helper);
22
20 Set<AnyDataVariable> getInputVariables(); 23 Set<AnyDataVariable> getInputVariables();
21} 24}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java
index 8ad17839..cdbf592a 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/BinaryTerm.java
@@ -5,7 +5,9 @@
5 */ 5 */
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 11import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.valuation.Valuation; 12import tools.refinery.store.query.valuation.Valuation;
11 13
@@ -14,6 +16,8 @@ import java.util.HashSet;
14import java.util.Objects; 16import java.util.Objects;
15import java.util.Set; 17import java.util.Set;
16 18
19// {@link Object#equals(Object)} is implemented by {@link AbstractTerm}.
20@SuppressWarnings("squid:S2160")
17public abstract class BinaryTerm<R, T1, T2> extends AbstractTerm<R> { 21public abstract class BinaryTerm<R, T1, T2> extends AbstractTerm<R> {
18 private final Class<T1> leftType; 22 private final Class<T1> leftType;
19 private final Class<T2> rightType; 23 private final Class<T2> rightType;
@@ -23,11 +27,11 @@ public abstract class BinaryTerm<R, T1, T2> extends AbstractTerm<R> {
23 protected BinaryTerm(Class<R> type, Class<T1> leftType, Class<T2> rightType, Term<T1> left, Term<T2> right) { 27 protected BinaryTerm(Class<R> type, Class<T1> leftType, Class<T2> rightType, Term<T1> left, Term<T2> right) {
24 super(type); 28 super(type);
25 if (!left.getType().equals(leftType)) { 29 if (!left.getType().equals(leftType)) {
26 throw new IllegalArgumentException("Expected left %s to be of type %s, got %s instead".formatted( 30 throw new InvalidQueryException("Expected left %s to be of type %s, got %s instead".formatted(
27 left, leftType.getName(), left.getType().getName())); 31 left, leftType.getName(), left.getType().getName()));
28 } 32 }
29 if (!right.getType().equals(rightType)) { 33 if (!right.getType().equals(rightType)) {
30 throw new IllegalArgumentException("Expected right %s to be of type %s, got %s instead".formatted( 34 throw new InvalidQueryException("Expected right %s to be of type %s, got %s instead".formatted(
31 right, rightType.getName(), right.getType().getName())); 35 right, rightType.getName(), right.getType().getName()));
32 } 36 }
33 this.leftType = leftType; 37 this.leftType = leftType;
@@ -80,6 +84,12 @@ public abstract class BinaryTerm<R, T1, T2> extends AbstractTerm<R> {
80 } 84 }
81 85
82 @Override 86 @Override
87 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
88 return Objects.hash(super.hashCodeWithSubstitution(helper), leftType.hashCode(), rightType.hashCode(),
89 left.hashCodeWithSubstitution(helper), right.hashCodeWithSubstitution(helper));
90 }
91
92 @Override
83 public Term<R> substitute(Substitution substitution) { 93 public Term<R> substitute(Substitution substitution) {
84 return doSubstitute(substitution, left.substitute(substitution), right.substitute(substitution)); 94 return doSubstitute(substitution, left.substitute(substitution), right.substitute(substitution));
85 } 95 }
@@ -93,21 +103,4 @@ public abstract class BinaryTerm<R, T1, T2> extends AbstractTerm<R> {
93 inputVariables.addAll(right.getInputVariables()); 103 inputVariables.addAll(right.getInputVariables());
94 return Collections.unmodifiableSet(inputVariables); 104 return Collections.unmodifiableSet(inputVariables);
95 } 105 }
96
97 @Override
98 public boolean equals(Object o) {
99 if (this == o) return true;
100 if (o == null || getClass() != o.getClass()) return false;
101 if (!super.equals(o)) return false;
102 BinaryTerm<?, ?, ?> that = (BinaryTerm<?, ?, ?>) o;
103 return Objects.equals(leftType, that.leftType) &&
104 Objects.equals(rightType, that.rightType) &&
105 Objects.equals(left, that.left) &&
106 Objects.equals(right, that.right);
107 }
108
109 @Override
110 public int hashCode() {
111 return Objects.hash(super.hashCode(), leftType, rightType, left, right);
112 }
113} 106}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java
index 2f6c56d1..415ae286 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ConstantTerm.java
@@ -5,20 +5,24 @@
5 */ 5 */
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 11import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.valuation.Valuation; 12import tools.refinery.store.query.valuation.Valuation;
11 13
12import java.util.Objects; 14import java.util.Objects;
13import java.util.Set; 15import java.util.Set;
14 16
17// {@link Object#equals(Object)} is implemented by {@link AbstractTerm}.
18@SuppressWarnings("squid:S2160")
15public final class ConstantTerm<T> extends AbstractTerm<T> { 19public final class ConstantTerm<T> extends AbstractTerm<T> {
16 private final T value; 20 private final T value;
17 21
18 public ConstantTerm(Class<T> type, T value) { 22 public ConstantTerm(Class<T> type, T value) {
19 super(type); 23 super(type);
20 if (value != null && !type.isInstance(value)) { 24 if (value != null && !type.isInstance(value)) {
21 throw new IllegalArgumentException("Value %s is not an instance of %s".formatted(value, type.getName())); 25 throw new InvalidQueryException("Value %s is not an instance of %s".formatted(value, type.getName()));
22 } 26 }
23 this.value = value; 27 this.value = value;
24 } 28 }
@@ -47,6 +51,11 @@ public final class ConstantTerm<T> extends AbstractTerm<T> {
47 } 51 }
48 52
49 @Override 53 @Override
54 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
55 return Objects.hash(super.hashCodeWithSubstitution(helper), Objects.hash(value));
56 }
57
58 @Override
50 public Set<AnyDataVariable> getInputVariables() { 59 public Set<AnyDataVariable> getInputVariables() {
51 return Set.of(); 60 return Set.of();
52 } 61 }
@@ -55,17 +64,4 @@ public final class ConstantTerm<T> extends AbstractTerm<T> {
55 public String toString() { 64 public String toString() {
56 return value.toString(); 65 return value.toString();
57 } 66 }
58
59 @Override
60 public boolean equals(Object o) {
61 if (this == o) return true;
62 if (o == null || getClass() != o.getClass()) return false;
63 ConstantTerm<?> that = (ConstantTerm<?>) o;
64 return Objects.equals(value, that.value);
65 }
66
67 @Override
68 public int hashCode() {
69 return Objects.hash(value);
70 }
71} 67}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java
index 00950360..2206b522 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/DataVariable.java
@@ -6,7 +6,10 @@
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import org.jetbrains.annotations.Nullable; 8import org.jetbrains.annotations.Nullable;
9import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.equality.LiteralEqualityHelper; 10import tools.refinery.store.query.equality.LiteralEqualityHelper;
11import tools.refinery.store.query.equality.LiteralHashCodeHelper;
12import tools.refinery.store.query.literal.EquivalenceLiteral;
10import tools.refinery.store.query.literal.Literal; 13import tools.refinery.store.query.literal.Literal;
11import tools.refinery.store.query.substitution.Substitution; 14import tools.refinery.store.query.substitution.Substitution;
12import tools.refinery.store.query.valuation.Valuation; 15import tools.refinery.store.query.valuation.Valuation;
@@ -39,8 +42,8 @@ public final class DataVariable<T> extends AnyDataVariable implements Term<T> {
39 @Override 42 @Override
40 public <U> DataVariable<U> asDataVariable(Class<U> newType) { 43 public <U> DataVariable<U> asDataVariable(Class<U> newType) {
41 if (!getType().equals(newType)) { 44 if (!getType().equals(newType)) {
42 throw new IllegalStateException("%s is not of type %s but of type %s".formatted(this, newType.getName(), 45 throw new InvalidQueryException("%s is not of type %s but of type %s"
43 getType().getName())); 46 .formatted(this, newType.getName(), getType().getName()));
44 } 47 }
45 @SuppressWarnings("unchecked") 48 @SuppressWarnings("unchecked")
46 var result = (DataVariable<U>) this; 49 var result = (DataVariable<U>) this;
@@ -62,6 +65,16 @@ public final class DataVariable<T> extends AnyDataVariable implements Term<T> {
62 return other instanceof DataVariable<?> dataVariable && helper.variableEqual(this, dataVariable); 65 return other instanceof DataVariable<?> dataVariable && helper.variableEqual(this, dataVariable);
63 } 66 }
64 67
68 @Override
69 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
70 return helper.getVariableHashCode(this);
71 }
72
73 @Override
74 public int hashCodeWithSubstitution(int sequenceNumber) {
75 return Objects.hash(type, sequenceNumber);
76 }
77
65 public Literal assign(AssignedValue<T> value) { 78 public Literal assign(AssignedValue<T> value) {
66 return value.toLiteral(this); 79 return value.toLiteral(this);
67 } 80 }
@@ -79,4 +92,12 @@ public final class DataVariable<T> extends AnyDataVariable implements Term<T> {
79 public int hashCode() { 92 public int hashCode() {
80 return Objects.hash(super.hashCode(), type); 93 return Objects.hash(super.hashCode(), type);
81 } 94 }
95
96 public EquivalenceLiteral isEquivalent(DataVariable<T> other) {
97 return new EquivalenceLiteral(true, this, other);
98 }
99
100 public EquivalenceLiteral notEquivalent(DataVariable<T> other) {
101 return new EquivalenceLiteral(false, this, other);
102 }
82} 103}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java
index a2f3261f..53c32e20 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/NodeVariable.java
@@ -6,6 +6,7 @@
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import org.jetbrains.annotations.Nullable; 8import org.jetbrains.annotations.Nullable;
9import tools.refinery.store.query.InvalidQueryException;
9import tools.refinery.store.query.literal.ConstantLiteral; 10import tools.refinery.store.query.literal.ConstantLiteral;
10import tools.refinery.store.query.literal.EquivalenceLiteral; 11import tools.refinery.store.query.literal.EquivalenceLiteral;
11 12
@@ -22,11 +23,6 @@ public final class NodeVariable extends Variable {
22 } 23 }
23 24
24 @Override 25 @Override
25 public boolean isUnifiable() {
26 return true;
27 }
28
29 @Override
30 public NodeVariable renew(@Nullable String name) { 26 public NodeVariable renew(@Nullable String name) {
31 return Variable.of(name); 27 return Variable.of(name);
32 } 28 }
@@ -37,13 +33,28 @@ public final class NodeVariable extends Variable {
37 } 33 }
38 34
39 @Override 35 @Override
36 public boolean isNodeVariable() {
37 return true;
38 }
39
40 @Override
41 public boolean isDataVariable() {
42 return false;
43 }
44
45 @Override
40 public NodeVariable asNodeVariable() { 46 public NodeVariable asNodeVariable() {
41 return this; 47 return this;
42 } 48 }
43 49
44 @Override 50 @Override
45 public <T> DataVariable<T> asDataVariable(Class<T> type) { 51 public <T> DataVariable<T> asDataVariable(Class<T> type) {
46 throw new IllegalStateException("%s is a node variable".formatted(this)); 52 throw new InvalidQueryException("%s is a node variable".formatted(this));
53 }
54
55 @Override
56 public int hashCodeWithSubstitution(int sequenceNumber) {
57 return sequenceNumber;
47 } 58 }
48 59
49 public ConstantLiteral isConstant(int value) { 60 public ConstantLiteral isConstant(int value) {
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Parameter.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Parameter.java
index e5a0cdf1..577ac6e0 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Parameter.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Parameter.java
@@ -9,11 +9,15 @@ import java.util.Objects;
9import java.util.Optional; 9import java.util.Optional;
10 10
11public class Parameter { 11public class Parameter {
12 public static final Parameter NODE_OUT = new Parameter(null, ParameterDirection.OUT); 12 public static final Parameter NODE_OUT = new Parameter(null);
13 13
14 private final Class<?> dataType; 14 private final Class<?> dataType;
15 private final ParameterDirection direction; 15 private final ParameterDirection direction;
16 16
17 public Parameter(Class<?> dataType) {
18 this(dataType, ParameterDirection.OUT);
19 }
20
17 public Parameter(Class<?> dataType, ParameterDirection direction) { 21 public Parameter(Class<?> dataType, ParameterDirection direction) {
18 this.dataType = dataType; 22 this.dataType = dataType;
19 this.direction = direction; 23 this.direction = direction;
@@ -35,6 +39,10 @@ public class Parameter {
35 return direction; 39 return direction;
36 } 40 }
37 41
42 public boolean matches(Parameter other) {
43 return Objects.equals(dataType, other.dataType) && direction == other.direction;
44 }
45
38 public boolean isAssignable(Variable variable) { 46 public boolean isAssignable(Variable variable) {
39 if (variable instanceof AnyDataVariable dataVariable) { 47 if (variable instanceof AnyDataVariable dataVariable) {
40 return dataVariable.getType().equals(dataType); 48 return dataVariable.getType().equals(dataType);
@@ -50,7 +58,7 @@ public class Parameter {
50 if (this == o) return true; 58 if (this == o) return true;
51 if (o == null || getClass() != o.getClass()) return false; 59 if (o == null || getClass() != o.getClass()) return false;
52 Parameter parameter = (Parameter) o; 60 Parameter parameter = (Parameter) o;
53 return Objects.equals(dataType, parameter.dataType) && direction == parameter.direction; 61 return matches(parameter);
54 } 62 }
55 63
56 @Override 64 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java
index cd0739be..da83f3c3 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/ParameterDirection.java
@@ -6,8 +6,8 @@
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8public enum ParameterDirection { 8public enum ParameterDirection {
9 OUT("@Out"), 9 OUT("out"),
10 IN("@In"); 10 IN("in");
11 11
12 private final String name; 12 private final String name;
13 13
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java
index a46ebe31..a464ece5 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/UnaryTerm.java
@@ -5,13 +5,17 @@
5 */ 5 */
6package tools.refinery.store.query.term; 6package tools.refinery.store.query.term;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.query.equality.LiteralEqualityHelper; 9import tools.refinery.store.query.equality.LiteralEqualityHelper;
10import tools.refinery.store.query.equality.LiteralHashCodeHelper;
9import tools.refinery.store.query.substitution.Substitution; 11import tools.refinery.store.query.substitution.Substitution;
10import tools.refinery.store.query.valuation.Valuation; 12import tools.refinery.store.query.valuation.Valuation;
11 13
12import java.util.Objects; 14import java.util.Objects;
13import java.util.Set; 15import java.util.Set;
14 16
17// {@link Object#equals(Object)} is implemented by {@link AbstractTerm}.
18@SuppressWarnings("squid:S2160")
15public abstract class UnaryTerm<R, T> extends AbstractTerm<R> { 19public abstract class UnaryTerm<R, T> extends AbstractTerm<R> {
16 private final Class<T> bodyType; 20 private final Class<T> bodyType;
17 private final Term<T> body; 21 private final Term<T> body;
@@ -19,7 +23,7 @@ public abstract class UnaryTerm<R, T> extends AbstractTerm<R> {
19 protected UnaryTerm(Class<R> type, Class<T> bodyType, Term<T> body) { 23 protected UnaryTerm(Class<R> type, Class<T> bodyType, Term<T> body) {
20 super(type); 24 super(type);
21 if (!body.getType().equals(bodyType)) { 25 if (!body.getType().equals(bodyType)) {
22 throw new IllegalArgumentException("Expected body %s to be of type %s, got %s instead".formatted(body, 26 throw new InvalidQueryException("Expected body %s to be of type %s, got %s instead".formatted(body,
23 bodyType.getName(), body.getType().getName())); 27 bodyType.getName(), body.getType().getName()));
24 } 28 }
25 this.bodyType = bodyType; 29 this.bodyType = bodyType;
@@ -52,6 +56,11 @@ public abstract class UnaryTerm<R, T> extends AbstractTerm<R> {
52 } 56 }
53 57
54 @Override 58 @Override
59 public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) {
60 return Objects.hash(super.hashCodeWithSubstitution(helper), bodyType, body.hashCodeWithSubstitution(helper));
61 }
62
63 @Override
55 public Term<R> substitute(Substitution substitution) { 64 public Term<R> substitute(Substitution substitution) {
56 return doSubstitute(substitution, body.substitute(substitution)); 65 return doSubstitute(substitution, body.substitute(substitution));
57 } 66 }
@@ -62,18 +71,4 @@ public abstract class UnaryTerm<R, T> extends AbstractTerm<R> {
62 public Set<AnyDataVariable> getInputVariables() { 71 public Set<AnyDataVariable> getInputVariables() {
63 return body.getInputVariables(); 72 return body.getInputVariables();
64 } 73 }
65
66 @Override
67 public boolean equals(Object o) {
68 if (this == o) return true;
69 if (o == null || getClass() != o.getClass()) return false;
70 if (!super.equals(o)) return false;
71 UnaryTerm<?, ?> unaryTerm = (UnaryTerm<?, ?>) o;
72 return Objects.equals(bodyType, unaryTerm.bodyType) && Objects.equals(body, unaryTerm.body);
73 }
74
75 @Override
76 public int hashCode() {
77 return Objects.hash(super.hashCode(), bodyType, body);
78 }
79} 74}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Variable.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Variable.java
index a0268c8e..1b553704 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Variable.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/Variable.java
@@ -38,16 +38,20 @@ public abstract sealed class Variable permits AnyDataVariable, NodeVariable {
38 return uniqueName; 38 return uniqueName;
39 } 39 }
40 40
41 public abstract boolean isUnifiable();
42
43 public abstract Variable renew(@Nullable String name); 41 public abstract Variable renew(@Nullable String name);
44 42
45 public abstract Variable renew(); 43 public abstract Variable renew();
46 44
45 public abstract boolean isNodeVariable();
46
47 public abstract boolean isDataVariable();
48
47 public abstract NodeVariable asNodeVariable(); 49 public abstract NodeVariable asNodeVariable();
48 50
49 public abstract <T> DataVariable<T> asDataVariable(Class<T> type); 51 public abstract <T> DataVariable<T> asDataVariable(Class<T> type);
50 52
53 public abstract int hashCodeWithSubstitution(int sequenceNumber);
54
51 @Override 55 @Override
52 public String toString() { 56 public String toString() {
53 return getName(); 57 return getName();
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/uppercardinality/UpperCardinalitySumAggregator.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/uppercardinality/UpperCardinalitySumAggregator.java
index 5bbd3081..d31f00a2 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/term/uppercardinality/UpperCardinalitySumAggregator.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/term/uppercardinality/UpperCardinalitySumAggregator.java
@@ -70,7 +70,7 @@ public class UpperCardinalitySumAggregator implements StatefulAggregator<UpperCa
70 70
71 @Override 71 @Override
72 public UpperCardinality getResult() { 72 public UpperCardinality getResult() {
73 return countUnbounded > 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.valueOf(sumFiniteUpperBounds); 73 return countUnbounded > 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.atMost(sumFiniteUpperBounds);
74 } 74 }
75 75
76 @Override 76 @Override
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/utils/OrderStatisticTree.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/utils/OrderStatisticTree.java
index b568b99d..2d8a10d1 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/utils/OrderStatisticTree.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/utils/OrderStatisticTree.java
@@ -2,7 +2,7 @@
2 * Copyright (c) 2021 Rodion Efremov 2 * Copyright (c) 2021 Rodion Efremov
3 * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> 3 * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/>
4 * 4 *
5 * SPDX-License-Identifier: MIT OR EPL-2.0 5 * SPDX-License-Identifier: MIT
6 */ 6 */
7package tools.refinery.store.query.utils; 7package tools.refinery.store.query.utils;
8 8
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AbstractFunctionView.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AbstractFunctionView.java
index fd37604e..f130fa59 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AbstractFunctionView.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AbstractFunctionView.java
@@ -5,6 +5,7 @@
5 */ 5 */
6package tools.refinery.store.query.view; 6package tools.refinery.store.query.view;
7 7
8import tools.refinery.store.map.CursorAsIterator;
8import tools.refinery.store.model.Model; 9import tools.refinery.store.model.Model;
9import tools.refinery.store.query.dnf.FunctionalDependency; 10import tools.refinery.store.query.dnf.FunctionalDependency;
10import tools.refinery.store.query.term.Parameter; 11import tools.refinery.store.query.term.Parameter;
@@ -83,6 +84,20 @@ public abstract class AbstractFunctionView<T> extends SymbolView<T> {
83 } 84 }
84 85
85 @Override 86 @Override
87 public boolean canIndexSlot(int slot) {
88 return slot >= 0 && slot < getSymbol().arity();
89 }
90
91 @Override
92 public Iterable<Object[]> getAdjacent(Model model, int slot, Object value) {
93 if (!(value instanceof Tuple1 tuple1)) {
94 return Set.of();
95 }
96 return (() -> new CursorAsIterator<>(model.getInterpretation(getSymbol()).getAdjacent(slot, tuple1.get(0)),
97 this::forwardMap, this::filter));
98 }
99
100 @Override
86 public List<Parameter> getParameters() { 101 public List<Parameter> getParameters() {
87 return parameters; 102 return parameters;
88 } 103 }
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AnySymbolView.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AnySymbolView.java
index 90b27ebb..7e9bf6df 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AnySymbolView.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/AnySymbolView.java
@@ -28,4 +28,12 @@ public sealed interface AnySymbolView extends Constraint permits SymbolView {
28 boolean get(Model model, Object[] tuple); 28 boolean get(Model model, Object[] tuple);
29 29
30 Iterable<Object[]> getAll(Model model); 30 Iterable<Object[]> getAll(Model model);
31
32 default Iterable<Object[]> getAdjacent(Model model, int slot, Object value) {
33 throw new IllegalArgumentException("Cannot index slot " + slot);
34 }
35
36 default boolean canIndexSlot(int slot) {
37 return false;
38 }
31} 39}
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java
index abae6e5c..924277ed 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/FilteredView.java
@@ -5,6 +5,7 @@
5 */ 5 */
6package tools.refinery.store.query.view; 6package tools.refinery.store.query.view;
7 7
8import tools.refinery.store.query.InvalidQueryException;
8import tools.refinery.store.tuple.Tuple; 9import tools.refinery.store.tuple.Tuple;
9import tools.refinery.store.representation.Symbol; 10import tools.refinery.store.representation.Symbol;
10 11
@@ -66,7 +67,7 @@ public class FilteredView<T> extends TuplePreservingView<T> {
66 // The predicate doesn't need to handle the default value if it is null. 67 // The predicate doesn't need to handle the default value if it is null.
67 } 68 }
68 if (matchesDefaultValue) { 69 if (matchesDefaultValue) {
69 throw new IllegalArgumentException("Tuples with default value %s cannot be enumerated in %s" 70 throw new InvalidQueryException("Tuples with default value %s cannot be enumerated in %s"
70 .formatted(defaultValue, getSymbol())); 71 .formatted(defaultValue, getSymbol()));
71 } 72 }
72 } 73 }
diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/TuplePreservingView.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/TuplePreservingView.java
index 6bc5a708..ed12cd9d 100644
--- a/subprojects/store-query/src/main/java/tools/refinery/store/query/view/TuplePreservingView.java
+++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/view/TuplePreservingView.java
@@ -5,6 +5,7 @@
5 */ 5 */
6package tools.refinery.store.query.view; 6package tools.refinery.store.query.view;
7 7
8import tools.refinery.store.map.CursorAsIterator;
8import tools.refinery.store.model.Model; 9import tools.refinery.store.model.Model;
9import tools.refinery.store.query.term.Parameter; 10import tools.refinery.store.query.term.Parameter;
10import tools.refinery.store.representation.Symbol; 11import tools.refinery.store.representation.Symbol;
@@ -14,6 +15,7 @@ import tools.refinery.store.tuple.Tuple1;
14import java.util.Arrays; 15import java.util.Arrays;
15import java.util.List; 16import java.util.List;
16import java.util.Objects; 17import java.util.Objects;
18import java.util.Set;
17 19
18public abstract class TuplePreservingView<T> extends SymbolView<T> { 20public abstract class TuplePreservingView<T> extends SymbolView<T> {
19 private final List<Parameter> parameters; 21 private final List<Parameter> parameters;
@@ -56,6 +58,20 @@ public abstract class TuplePreservingView<T> extends SymbolView<T> {
56 } 58 }
57 59
58 @Override 60 @Override
61 public boolean canIndexSlot(int slot) {
62 return slot >= 0 && slot < getSymbol().arity();
63 }
64
65 @Override
66 public Iterable<Object[]> getAdjacent(Model model, int slot, Object value) {
67 if (!(value instanceof Tuple1 tuple1)) {
68 return Set.of();
69 }
70 return (() -> new CursorAsIterator<>(model.getInterpretation(getSymbol()).getAdjacent(slot, tuple1.get(0)),
71 this::forwardMap, this::filter));
72 }
73
74 @Override
59 public List<Parameter> getParameters() { 75 public List<Parameter> getParameters() {
60 return parameters; 76 return parameters;
61 } 77 }