aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2024-06-16 23:24:09 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2024-06-16 23:24:09 +0200
commitc2907e921954749a8c22899ef4542b4b8db44c42 (patch)
treea83585db1dcdfc320f957657a8cf99f85420182b
parentbuild: publish CLI as Docker container (diff)
downloadrefinery-c2907e921954749a8c22899ef4542b4b8db44c42.tar.gz
refinery-c2907e921954749a8c22899ef4542b4b8db44c42.tar.zst
refinery-c2907e921954749a8c22899ef4542b4b8db44c42.zip
feat(language): validate quantification in rules
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ExistsVariableCollector.java75
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java39
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java17
3 files changed, 119 insertions, 12 deletions
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ExistsVariableCollector.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ExistsVariableCollector.java
new file mode 100644
index 00000000..5812bdc4
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ExistsVariableCollector.java
@@ -0,0 +1,75 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.validation;
7
8import com.google.inject.Inject;
9import com.google.inject.Singleton;
10import org.eclipse.xtext.EcoreUtil2;
11import org.eclipse.xtext.util.IResourceScopeCache;
12import org.eclipse.xtext.util.Tuples;
13import tools.refinery.language.model.problem.*;
14import tools.refinery.language.scoping.imports.ImportAdapterProvider;
15import tools.refinery.language.utils.ProblemUtil;
16
17import java.util.HashSet;
18import java.util.Set;
19
20@Singleton
21public class ExistsVariableCollector {
22 private static final String EXISTS_VARIABLES =
23 "tools.refinery.language.validation.ExistsVariableCollector.EXISTS_VARIABLES";
24
25 @Inject
26 private IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE;
27
28 @Inject
29 private ImportAdapterProvider importAdapterProvider;
30
31 public boolean missingExistsConstraint(Variable variable) {
32 if (variable instanceof Parameter) {
33 return false;
34 }
35 if (ProblemUtil.isImplicitVariable(variable)) {
36 var negation = EcoreUtil2.getContainerOfType(variable, NegationExpr.class);
37 if (negation != null) {
38 // Negations are implicitly quantified.
39 return false;
40 }
41 }
42 var conjunction = EcoreUtil2.getContainerOfType(variable, Conjunction.class);
43 if (conjunction == null) {
44 return true;
45 }
46 var variables = getExistsVariables(conjunction);
47 return !variables.contains(variable);
48 }
49
50 protected Set<Variable> getExistsVariables(Conjunction conjunction) {
51 var resource = conjunction.eResource();
52 if (resource == null) {
53 return doGetExistsVariables(conjunction);
54 }
55 return cache.get(Tuples.create(conjunction, EXISTS_VARIABLES), resource,
56 () -> doGetExistsVariables(conjunction));
57 }
58
59 protected Set<Variable> doGetExistsVariables(Conjunction conjunction) {
60 var builtinSymbols = importAdapterProvider.getBuiltinSymbols(conjunction);
61 var existsRelation = builtinSymbols.exists();
62 var set = new HashSet<Variable>();
63 for (var atom : EcoreUtil2.getAllContentsOfType(conjunction, Atom.class)) {
64 if (existsRelation.equals(atom.getRelation())) {
65 for (var argument : atom.getArguments()) {
66 if (argument instanceof VariableOrNodeExpr variableOrNodeExpr &&
67 variableOrNodeExpr.getVariableOrNode() instanceof Variable variable) {
68 set.add(variable);
69 }
70 }
71 }
72 }
73 return set;
74 }
75}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java
index 754572d1..21dd7218 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java
@@ -24,10 +24,7 @@ import tools.refinery.language.typesystem.ProblemTypeAnalyzer;
24import tools.refinery.language.typesystem.SignatureProvider; 24import tools.refinery.language.typesystem.SignatureProvider;
25import tools.refinery.language.utils.ProblemUtil; 25import tools.refinery.language.utils.ProblemUtil;
26 26
27import java.util.ArrayList; 27import java.util.*;
28import java.util.LinkedHashMap;
29import java.util.LinkedHashSet;
30import java.util.Set;
31 28
32/** 29/**
33 * This class contains custom validation rules. 30 * This class contains custom validation rules.
@@ -56,11 +53,15 @@ public class ProblemValidator extends AbstractProblemValidator {
56 public static final String UNKNOWN_EXPRESSION_ISSUE = ISSUE_PREFIX + "UNKNOWN_EXPRESSION"; 53 public static final String UNKNOWN_EXPRESSION_ISSUE = ISSUE_PREFIX + "UNKNOWN_EXPRESSION";
57 public static final String INVALID_ASSIGNMENT_ISSUE = ISSUE_PREFIX + "INVALID_ASSIGNMENT"; 54 public static final String INVALID_ASSIGNMENT_ISSUE = ISSUE_PREFIX + "INVALID_ASSIGNMENT";
58 public static final String TYPE_ERROR = ISSUE_PREFIX + "TYPE_ERROR"; 55 public static final String TYPE_ERROR = ISSUE_PREFIX + "TYPE_ERROR";
56 public static final String VARIABLE_WITHOUT_EXISTS = ISSUE_PREFIX + "VARIABLE_WITHOUT_EXISTS";
59 57
60 @Inject 58 @Inject
61 private ReferenceCounter referenceCounter; 59 private ReferenceCounter referenceCounter;
62 60
63 @Inject 61 @Inject
62 private ExistsVariableCollector existsVariableCollector;
63
64 @Inject
64 private IQualifiedNameConverter qualifiedNameConverter; 65 private IQualifiedNameConverter qualifiedNameConverter;
65 66
66 @Inject 67 @Inject
@@ -129,6 +130,14 @@ public class ProblemValidator extends AbstractProblemValidator {
129 var variableOrNode = expr.getVariableOrNode(); 130 var variableOrNode = expr.getVariableOrNode();
130 if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) 131 if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable)
131 && !ProblemUtil.isSingletonVariable(variable)) { 132 && !ProblemUtil.isSingletonVariable(variable)) {
133 if (EcoreUtil2.getContainerOfType(variable, ParametricDefinition.class) instanceof RuleDefinition &&
134 EcoreUtil2.getContainerOfType(variable, NegationExpr.class) == null &&
135 // If there is an exists constraint, it is the only constraint.
136 existsVariableCollector.missingExistsConstraint(variable)) {
137 // Existentially quantified variables in rules should not be singletons,
138 // because we have to add an {@code exists} constraint as well.
139 return;
140 }
132 var problem = EcoreUtil2.getContainerOfType(variable, Problem.class); 141 var problem = EcoreUtil2.getContainerOfType(variable, Problem.class);
133 if (problem != null && referenceCounter.countReferences(problem, variable) <= 1) { 142 if (problem != null && referenceCounter.countReferences(problem, variable) <= 1) {
134 var name = variable.getName(); 143 var name = variable.getName();
@@ -412,6 +421,28 @@ public class ProblemValidator extends AbstractProblemValidator {
412 acceptError("Rules must have exactly one consequent.", ruleDefinition, 421 acceptError("Rules must have exactly one consequent.", ruleDefinition,
413 ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, INVALID_RULE_ISSUE); 422 ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, INVALID_RULE_ISSUE);
414 } 423 }
424 var unquantifiedVariables = new HashSet<Variable>();
425 for (var variable : EcoreUtil2.getAllContentsOfType(ruleDefinition, Variable.class)) {
426 if (existsVariableCollector.missingExistsConstraint(variable)) {
427 unquantifiedVariables.add(variable);
428 }
429 }
430 for (var expr : EcoreUtil2.getAllContentsOfType(ruleDefinition, VariableOrNodeExpr.class)) {
431 if (expr.getVariableOrNode() instanceof Variable variable && unquantifiedVariables.contains(variable)) {
432 unquantifiedVariables.remove(variable);
433 var name = variable.getName();
434 String message;
435 if (ProblemUtil.isSingletonVariable(variable)) {
436 message = ("Remove the singleton variable marker '_' and clarify the quantification of variable " +
437 "'%s'.").formatted(name);
438 } else {
439 message = ("Add a 'must exists(%s)', 'may exists(%s)', or 'may !exists(%s)' constraint to " +
440 "clarify the quantification of variable '%s'.").formatted(name, name, name, name);
441 }
442 acceptWarning(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, 0,
443 VARIABLE_WITHOUT_EXISTS);
444 }
445 }
415 } 446 }
416 447
417 @Check 448 @Check
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java
index 55cbd71d..40781168 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java
@@ -5,20 +5,21 @@
5 */ 5 */
6package tools.refinery.language.validation; 6package tools.refinery.language.validation;
7 7
8import java.util.HashMap; 8import com.google.inject.Inject;
9import java.util.Map; 9import com.google.inject.Singleton;
10
11import org.eclipse.emf.ecore.EObject; 10import org.eclipse.emf.ecore.EObject;
12import org.eclipse.xtext.util.IResourceScopeCache; 11import org.eclipse.xtext.util.IResourceScopeCache;
13import org.eclipse.xtext.util.Tuples; 12import org.eclipse.xtext.util.Tuples;
14
15import com.google.inject.Inject;
16import com.google.inject.Singleton;
17
18import tools.refinery.language.model.problem.Problem; 13import tools.refinery.language.model.problem.Problem;
19 14
15import java.util.HashMap;
16import java.util.Map;
17
20@Singleton 18@Singleton
21public class ReferenceCounter { 19public class ReferenceCounter {
20 private static final String REFERENCE_COUNTS =
21 "tools.refinery.language.validation.ReferenceCounter.REFERENCE_COUNTS";
22
22 @Inject 23 @Inject
23 private IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE; 24 private IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE;
24 25
@@ -35,7 +36,7 @@ public class ReferenceCounter {
35 if (resource == null) { 36 if (resource == null) {
36 return doGetReferenceCounts(problem); 37 return doGetReferenceCounts(problem);
37 } 38 }
38 return cache.get(Tuples.create(problem, "referenceCounts"), resource, () -> doGetReferenceCounts(problem)); 39 return cache.get(Tuples.create(problem, REFERENCE_COUNTS), resource, () -> doGetReferenceCounts(problem));
39 } 40 }
40 41
41 protected Map<EObject, Integer> doGetReferenceCounts(Problem problem) { 42 protected Map<EObject, Integer> doGetReferenceCounts(Problem problem) {