aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2024-06-17 16:47:33 +0200
committerLibravatar GitHub <noreply@github.com>2024-06-17 16:47:33 +0200
commitac5b71917023cec36c3c5bacf779692542ff8e1a (patch)
treee14fc5d1473c4951ee32c4a82ea6d8b72e4857fd /subprojects/language
parentchore(deps): bump dependencies (diff)
parentchore(deps): bump dependencies (diff)
downloadrefinery-ac5b71917023cec36c3c5bacf779692542ff8e1a.tar.gz
refinery-ac5b71917023cec36c3c5bacf779692542ff8e1a.tar.zst
refinery-ac5b71917023cec36c3c5bacf779692542ff8e1a.zip
Merge pull request #60 from kris7t/rules
Refinement rule support
Diffstat (limited to 'subprojects/language')
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/Problem.xtext74
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java3
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java15
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/serializer/PreferShortAssertionsProblemSemanticSequencer.java12
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/typesystem/ExprType.java1
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/typesystem/FixedType.java3
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/typesystem/ModalLiteralType.java16
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/typesystem/TypedModule.java83
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java14
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ActionTargetCollector.java73
-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.java150
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java17
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java54
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java36
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAction.java16
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAssertionArgument.java2
18 files changed, 488 insertions, 158 deletions
diff --git a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
index 08f7a585..10e994a0 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
+++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
@@ -18,7 +18,7 @@ enum ModuleKind:
18Statement: 18Statement:
19 ImportStatement | Assertion | ClassDeclaration | EnumDeclaration | 19 ImportStatement | Assertion | ClassDeclaration | EnumDeclaration |
20 DatatypeDeclaration | AggregatorDeclaration | PredicateDefinition | 20 DatatypeDeclaration | AggregatorDeclaration | PredicateDefinition |
21 /* FunctionDefinition | RuleDefinition | */ 21 /* FunctionDefinition | */ RuleDefinition |
22 ScopeDeclaration | NodeDeclaration; 22 ScopeDeclaration | NodeDeclaration;
23 23
24ImportStatement: 24ImportStatement:
@@ -45,7 +45,7 @@ AggregatorDeclaration:
45 "extern" "aggregator" name=Identifier "."; 45 "extern" "aggregator" name=Identifier ".";
46 46
47enum ReferenceKind: 47enum ReferenceKind:
48 REFERENCE="refers" | CONTAINMENT="contains" | CONTAINER="container"; 48 REFERENCE="refers" | CONTAINMENT="contains" | CONTAINER="container" | PARTIAL="partial";
49 49
50ReferenceDeclaration: 50ReferenceDeclaration:
51 (referenceType=[Relation|NonContainmentQualifiedName] | 51 (referenceType=[Relation|NonContainmentQualifiedName] |
@@ -75,35 +75,40 @@ Conjunction:
75// 75//
76//Case: 76//Case:
77// Conjunction ({Match.condition=current} "->" value=Expr)?; 77// Conjunction ({Match.condition=current} "->" value=Expr)?;
78//RuleDefinition: 78
79// "rule" 79enum RuleKind:
80// name=Identifier 80 DECISION="decision" | PROPAGATION="propagation";
81// "(" (parameters+=Parameter ("," parameters+=Parameter)*)? ")" 81
82// (":" preconditions+=Conjunction (";" preconditions+=Conjunction)*)? 82RuleDefinition:
83// "==>" consequents+=Consequent (";" consequents+=Consequent)*)? 83 kind=RuleKind? "rule"
84// "."; 84 name=Identifier
85 "(" (parameters+=Parameter ("," parameters+=Parameter)*)? ")"
86 ("<->" preconditions+=Conjunction (";" preconditions+=Conjunction)*)?
87 ("==>" consequents+=Consequent (";" consequents+=Consequent)*)?
88 ".";
89
90enum ParameterBinding:
91 FOCUS="&" | MULTI="*";
85 92
86Parameter: 93Parameter:
87 parameterType=[Relation|QualifiedName]? name=Identifier; 94 (
95 (concreteness=Concreteness? modality=Modality)?
96 parameterType=[Relation|QualifiedName]
97 )? binding=ParameterBinding? name=Identifier;
88 98
89//Consequent: 99Consequent:
90// actions+=Action ("," actions+=Action)*; 100 actions+=Action ("," actions+=Action)*;
91// 101
92//Action: 102Action:
93// AssertionAction | DeleteAction | NewAction; 103 AssertionAction;
94// 104
95//AssertionAction: 105AssertionAction:
96// value=ShortLogicValue? atom=Atom | 106 relation=[Relation|QualifiedName]
97// atom=Atom (overwrite?=":=" | "<:") value=LogicValue; 107 "(" (arguments+=AssertionArgument ("," arguments+=AssertionArgument)*)? ")"
98// 108 ":" value=Expr |
99//DeleteAction: 109 value=ShortLogicConstant
100// "delete" variableOrNode=[VariableOrNode|QualifiedName]; 110 relation=[Relation|QualifiedName]
101// 111 "(" (arguments+=AssertionArgument ("," arguments+=AssertionArgument)*)? ")";
102//NewAction:
103// "new" variable=NewVariable ("<:" parent=[VariableOrNode|QualifiedName])?;
104//
105//NewVariable:
106// name=Identifier;
107 112
108Expr: 113Expr:
109 AssignmentExpr; 114 AssignmentExpr;
@@ -159,7 +164,7 @@ RangeExpr returns Expr:
159 164
160UnaryExpr returns Expr: 165UnaryExpr returns Expr:
161 ArithmeticUnaryExpr | NegationExpr | 166 ArithmeticUnaryExpr | NegationExpr |
162 CountExpr | AggregationExpr | CastExpr; 167 CountExpr | AggregationExpr | ModalExpr | CastExpr;
163 168
164enum UnaryOp: 169enum UnaryOp:
165 PLUS="+" | MINUS="-"; 170 PLUS="+" | MINUS="-";
@@ -177,6 +182,15 @@ AggregationExpr:
177 aggregator=[AggregatorDeclaration|QualifiedName] 182 aggregator=[AggregatorDeclaration|QualifiedName]
178 "{" value=Expr "|" condition=ComparisonExpr "}"; 183 "{" value=Expr "|" condition=ComparisonExpr "}";
179 184
185enum Concreteness:
186 CANDIDATE="candidate";
187
188enum Modality:
189 MUST="must" | MAY="may";
190
191ModalExpr:
192 concreteness=Concreteness? modality=Modality body=UnaryExpr;
193
180CastExpr returns Expr: 194CastExpr returns Expr:
181 CastExprBody ({CastExpr.body=current} "as" targetType=[Relation|QualifiedName])?; 195 CastExprBody ({CastExpr.body=current} "as" targetType=[Relation|QualifiedName])?;
182 196
@@ -281,7 +295,7 @@ Identifier:
281 295
282NonContainmentIdentifier: 296NonContainmentIdentifier:
283 ID | "atom" | "multi" | "contained" | "problem" | "module" | 297 ID | "atom" | "multi" | "contained" | "problem" | "module" |
284 "datatype" | "aggregator"; 298 "datatype" | "aggregator" | "decision" | "propagation";
285 299
286Real returns ecore::EDouble: 300Real returns ecore::EDouble:
287 EXPONENTIAL | INT "." (INT | EXPONENTIAL); 301 EXPONENTIAL | INT "." (INT | EXPONENTIAL);
diff --git a/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java b/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
index d6ece1ea..c6fca89a 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
@@ -35,7 +35,7 @@ public class ProblemFormatter extends AbstractJavaFormatter {
35 surroundNewLines(doc, assertion, this::singleNewLine); 35 surroundNewLines(doc, assertion, this::singleNewLine);
36 var region = regionFor(assertion); 36 var region = regionFor(assertion);
37 doc.append(region.feature(ProblemPackage.Literals.ASSERTION__DEFAULT), this::oneSpace); 37 doc.append(region.feature(ProblemPackage.Literals.ASSERTION__DEFAULT), this::oneSpace);
38 doc.append(region.feature(ProblemPackage.Literals.ASSERTION__RELATION), this::noSpace); 38 doc.append(region.feature(ProblemPackage.Literals.ABSTRACT_ASSERTION__RELATION), this::noSpace);
39 formatParenthesizedList(region, doc); 39 formatParenthesizedList(region, doc);
40 for (var argument : assertion.getArguments()) { 40 for (var argument : assertion.getArguments()) {
41 doc.format(argument); 41 doc.format(argument);
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java
index 8fd60364..505c7787 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java
@@ -154,6 +154,9 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti
154 if (ProblemUtil.isTypeLike(relation)) { 154 if (ProblemUtil.isTypeLike(relation)) {
155 builder.put(TYPE_LIKE, TYPE_LIKE_TRUE); 155 builder.put(TYPE_LIKE, TYPE_LIKE_TRUE);
156 } 156 }
157 } else if (eObject instanceof RuleDefinition) {
158 // Rule definitions and predicates live in the same namespace.
159 builder.put(SHADOWING_KEY, SHADOWING_KEY_RELATION);
157 } else if (eObject instanceof AggregatorDeclaration) { 160 } else if (eObject instanceof AggregatorDeclaration) {
158 builder.put(SHADOWING_KEY, SHADOWING_KEY_AGGREGATOR); 161 builder.put(SHADOWING_KEY, SHADOWING_KEY_AGGREGATOR);
159 } 162 }
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java
index d94c9a13..21f64236 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java
@@ -31,11 +31,11 @@ public class ProblemScopeProvider extends AbstractProblemScopeProvider {
31 public IScope getScope(EObject context, EReference reference) { 31 public IScope getScope(EObject context, EReference reference) {
32 var scope = super.getScope(context, reference); 32 var scope = super.getScope(context, reference);
33 if (reference == ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE) { 33 if (reference == ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE) {
34 return getNodesScope(context, scope); 34 // On the right side of a rule, assertion arguments may refer to variables.
35 var rule = EcoreUtil2.getContainerOfType(context, RuleDefinition.class);
36 return rule == null ? getNodesScope(context, scope) : getVariableScope(context, scope);
35 } 37 }
36 if (reference == ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE 38 if (reference == ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE) {
37 || reference == ProblemPackage.Literals.NEW_ACTION__PARENT
38 || reference == ProblemPackage.Literals.DELETE_ACTION__VARIABLE_OR_NODE) {
39 return getVariableScope(context, scope); 39 return getVariableScope(context, scope);
40 } 40 }
41 if (reference == ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) { 41 if (reference == ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) {
@@ -81,13 +81,6 @@ public class ProblemScopeProvider extends AbstractProblemScopeProvider {
81 switch (currentContext) { 81 switch (currentContext) {
82 case ExistentialQuantifier quantifier -> variables.addAll(quantifier.getImplicitVariables()); 82 case ExistentialQuantifier quantifier -> variables.addAll(quantifier.getImplicitVariables());
83 case Match match -> variables.addAll(match.getCondition().getImplicitVariables()); 83 case Match match -> variables.addAll(match.getCondition().getImplicitVariables());
84 case Consequent consequent -> {
85 for (var literal : consequent.getActions()) {
86 if (literal instanceof NewAction newAction && newAction.getVariable() != null) {
87 variables.add(newAction.getVariable());
88 }
89 }
90 }
91 default -> { 84 default -> {
92 // Nothing to add. 85 // Nothing to add.
93 } 86 }
diff --git a/subprojects/language/src/main/java/tools/refinery/language/serializer/PreferShortAssertionsProblemSemanticSequencer.java b/subprojects/language/src/main/java/tools/refinery/language/serializer/PreferShortAssertionsProblemSemanticSequencer.java
index 3432b2d8..225bf49d 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/serializer/PreferShortAssertionsProblemSemanticSequencer.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/serializer/PreferShortAssertionsProblemSemanticSequencer.java
@@ -12,9 +12,11 @@ import org.eclipse.xtext.serializer.sequencer.ITransientValueService.ValueTransi
12import tools.refinery.language.model.problem.Assertion; 12import tools.refinery.language.model.problem.Assertion;
13import tools.refinery.language.model.problem.LogicConstant; 13import tools.refinery.language.model.problem.LogicConstant;
14import tools.refinery.language.model.problem.LogicValue; 14import tools.refinery.language.model.problem.LogicValue;
15import tools.refinery.language.model.problem.ProblemPackage;
16import tools.refinery.language.services.ProblemGrammarAccess; 15import tools.refinery.language.services.ProblemGrammarAccess;
17 16
17import static tools.refinery.language.model.problem.ProblemPackage.Literals.ABSTRACT_ASSERTION__ARGUMENTS;
18import static tools.refinery.language.model.problem.ProblemPackage.Literals.ABSTRACT_ASSERTION__RELATION;
19
18public class PreferShortAssertionsProblemSemanticSequencer extends ProblemSemanticSequencer { 20public class PreferShortAssertionsProblemSemanticSequencer extends ProblemSemanticSequencer {
19 @Inject 21 @Inject
20 private ProblemGrammarAccess grammarAccess; 22 private ProblemGrammarAccess grammarAccess;
@@ -27,13 +29,13 @@ public class PreferShortAssertionsProblemSemanticSequencer extends ProblemSemant
27 return; 29 return;
28 } 30 }
29 if (errorAcceptor != null) { 31 if (errorAcceptor != null) {
30 if (transientValues.isValueTransient(semanticObject, ProblemPackage.Literals.ASSERTION__RELATION) == ValueTransient.YES) { 32 if (transientValues.isValueTransient(semanticObject, ABSTRACT_ASSERTION__RELATION) == ValueTransient.YES) {
31 errorAcceptor.accept(diagnosticProvider.createFeatureValueMissing(semanticObject, 33 errorAcceptor.accept(diagnosticProvider.createFeatureValueMissing(semanticObject,
32 ProblemPackage.Literals.ASSERTION__RELATION)); 34 ABSTRACT_ASSERTION__RELATION));
33 } 35 }
34 if (transientValues.isListTransient(semanticObject, ProblemPackage.Literals.ASSERTION__ARGUMENTS) == ListTransient.YES) { 36 if (transientValues.isListTransient(semanticObject, ABSTRACT_ASSERTION__ARGUMENTS) == ListTransient.YES) {
35 errorAcceptor.accept(diagnosticProvider.createFeatureValueMissing(semanticObject, 37 errorAcceptor.accept(diagnosticProvider.createFeatureValueMissing(semanticObject,
36 ProblemPackage.Literals.ASSERTION__ARGUMENTS)); 38 ABSTRACT_ASSERTION__ARGUMENTS));
37 } 39 }
38 } 40 }
39 var feeder = createSequencerFeeder(context, semanticObject); 41 var feeder = createSequencerFeeder(context, semanticObject);
diff --git a/subprojects/language/src/main/java/tools/refinery/language/typesystem/ExprType.java b/subprojects/language/src/main/java/tools/refinery/language/typesystem/ExprType.java
index 9e44063d..65721924 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/typesystem/ExprType.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/typesystem/ExprType.java
@@ -8,6 +8,7 @@ package tools.refinery.language.typesystem;
8public sealed interface ExprType permits FixedType, MutableType { 8public sealed interface ExprType permits FixedType, MutableType {
9 NodeType NODE = new NodeType(); 9 NodeType NODE = new NodeType();
10 LiteralType LITERAL = new LiteralType(); 10 LiteralType LITERAL = new LiteralType();
11 ModalLiteralType MODAL_LITERAL = new ModalLiteralType();
11 InvalidType INVALID = new InvalidType(); 12 InvalidType INVALID = new InvalidType();
12 13
13 FixedType getActualType(); 14 FixedType getActualType();
diff --git a/subprojects/language/src/main/java/tools/refinery/language/typesystem/FixedType.java b/subprojects/language/src/main/java/tools/refinery/language/typesystem/FixedType.java
index 1b2ded48..f9e75f26 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/typesystem/FixedType.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/typesystem/FixedType.java
@@ -5,7 +5,8 @@
5 */ 5 */
6package tools.refinery.language.typesystem; 6package tools.refinery.language.typesystem;
7 7
8public sealed interface FixedType extends ExprType permits NodeType, LiteralType, InvalidType, DataExprType { 8public sealed interface FixedType extends ExprType permits NodeType, LiteralType, ModalLiteralType, InvalidType,
9 DataExprType {
9 @Override 10 @Override
10 default FixedType getActualType() { 11 default FixedType getActualType() {
11 return this; 12 return this;
diff --git a/subprojects/language/src/main/java/tools/refinery/language/typesystem/ModalLiteralType.java b/subprojects/language/src/main/java/tools/refinery/language/typesystem/ModalLiteralType.java
new file mode 100644
index 00000000..6b44977a
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/typesystem/ModalLiteralType.java
@@ -0,0 +1,16 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.typesystem;
7
8public final class ModalLiteralType implements FixedType {
9 ModalLiteralType() {
10 }
11
12 @Override
13 public String toString() {
14 return "modal constraint";
15 }
16}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/typesystem/TypedModule.java b/subprojects/language/src/main/java/tools/refinery/language/typesystem/TypedModule.java
index 6691f0ae..63815506 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/typesystem/TypedModule.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/typesystem/TypedModule.java
@@ -63,6 +63,7 @@ public class TypedModule {
63 for (var statement : problem.getStatements()) { 63 for (var statement : problem.getStatements()) {
64 switch (statement) { 64 switch (statement) {
65 case PredicateDefinition predicateDefinition -> checkTypes(predicateDefinition); 65 case PredicateDefinition predicateDefinition -> checkTypes(predicateDefinition);
66 case RuleDefinition ruleDefinition -> checkTypes(ruleDefinition);
66 case Assertion assertion -> checkTypes(assertion); 67 case Assertion assertion -> checkTypes(assertion);
67 default -> { 68 default -> {
68 // Nothing to type check. 69 // Nothing to type check.
@@ -79,13 +80,33 @@ public class TypedModule {
79 } 80 }
80 } 81 }
81 82
83 private void checkTypes(RuleDefinition ruleDefinition) {
84 for (var conjunction : ruleDefinition.getPreconditions()) {
85 for (var literal : conjunction.getLiterals()) {
86 expectType(literal, ExprType.MODAL_LITERAL);
87 }
88 }
89 for (var consequent : ruleDefinition.getConsequents()) {
90 for (var action : consequent.getActions()) {
91 if (action instanceof AssertionAction assertionAction) {
92 checkTypes(assertionAction);
93 }
94 }
95 }
96 }
97
82 private void checkTypes(Assertion assertion) { 98 private void checkTypes(Assertion assertion) {
99 checkAssertionValueType(assertion);
100 checkNodeAssertionArgumentTypes(assertion, false);
101 }
102
103 private void checkAssertionValueType(AbstractAssertion assertion) {
83 var relation = assertion.getRelation(); 104 var relation = assertion.getRelation();
84 var value = assertion.getValue();
85 if (relation == null) { 105 if (relation == null) {
86 return; 106 return;
87 } 107 }
88 var type = signatureProvider.getSignature(relation).resultType(); 108 var type = signatureProvider.getSignature(relation).resultType();
109 var value = assertion.getValue();
89 if (type == ExprType.LITERAL) { 110 if (type == ExprType.LITERAL) {
90 if (value == null) { 111 if (value == null) {
91 return; 112 return;
@@ -95,11 +116,43 @@ public class TypedModule {
95 } 116 }
96 if (value == null) { 117 if (value == null) {
97 var message = "Assertion value of type %s is required.".formatted(type); 118 var message = "Assertion value of type %s is required.".formatted(type);
98 error(message, assertion, ProblemPackage.Literals.ASSERTION__RELATION, 0, ProblemValidator.TYPE_ERROR); 119 error(message, assertion, ProblemPackage.Literals.ABSTRACT_ASSERTION__VALUE, 0,
120 ProblemValidator.TYPE_ERROR);
99 } 121 }
100 expectType(value, type); 122 expectType(value, type);
101 } 123 }
102 124
125 private void checkNodeAssertionArgumentTypes(AbstractAssertion assertion, boolean allowVariables) {
126 for (var argument : assertion.getArguments()) {
127 if (argument instanceof NodeAssertionArgument nodeAssertionArgument) {
128 checkNodeAssertionArgumentType(nodeAssertionArgument, allowVariables);
129 }
130 }
131 }
132
133 private void checkNodeAssertionArgumentType(NodeAssertionArgument nodeAssertionArgument, boolean allowVariables) {
134 var variableOrNode = nodeAssertionArgument.getNode();
135 if (variableOrNode == null || variableOrNode.eIsProxy()) {
136 return;
137 }
138 if (allowVariables && variableOrNode instanceof Variable variable) {
139 var variableType = getVariableType(variable);
140 if (variableType == ExprType.INVALID || variableType == ExprType.NODE) {
141 return;
142 }
143 }
144 if (variableOrNode instanceof Node) {
145 return;
146 }
147 error("Assertion argument must be a node.", nodeAssertionArgument,
148 ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE, 0, ProblemValidator.TYPE_ERROR);
149 }
150
151 private void checkTypes(AssertionAction assertionAction) {
152 checkAssertionValueType(assertionAction);
153 checkNodeAssertionArgumentTypes(assertionAction, true);
154 }
155
103 public List<FeatureBasedDiagnostic> getDiagnostics() { 156 public List<FeatureBasedDiagnostic> getDiagnostics() {
104 return diagnostics; 157 return diagnostics;
105 } 158 }
@@ -202,6 +255,7 @@ public class TypedModule {
202 case RangeExpr rangeExpr -> computeExpressionType(rangeExpr); 255 case RangeExpr rangeExpr -> computeExpressionType(rangeExpr);
203 case ArithmeticBinaryExpr arithmeticBinaryExpr -> computeExpressionType(arithmeticBinaryExpr); 256 case ArithmeticBinaryExpr arithmeticBinaryExpr -> computeExpressionType(arithmeticBinaryExpr);
204 case CastExpr castExpr -> computeExpressionType(castExpr); 257 case CastExpr castExpr -> computeExpressionType(castExpr);
258 case ModalExpr modalExpr -> computeExpressionType(modalExpr);
205 default -> { 259 default -> {
206 error("Unknown expression: " + expr.getClass().getSimpleName(), expr, null, 0, 260 error("Unknown expression: " + expr.getClass().getSimpleName(), expr, null, 0,
207 ProblemValidator.UNKNOWN_EXPRESSION_ISSUE); 261 ProblemValidator.UNKNOWN_EXPRESSION_ISSUE);
@@ -290,6 +344,9 @@ public class TypedModule {
290 // Negation of literals yields another (non-enumerable) literal. 344 // Negation of literals yields another (non-enumerable) literal.
291 return ExprType.LITERAL; 345 return ExprType.LITERAL;
292 } 346 }
347 if (actualType == ExprType.MODAL_LITERAL) {
348 return ExprType.MODAL_LITERAL;
349 }
293 if (actualType == ExprType.INVALID) { 350 if (actualType == ExprType.INVALID) {
294 return ExprType.INVALID; 351 return ExprType.INVALID;
295 } 352 }
@@ -507,6 +564,28 @@ public class TypedModule {
507 return ExprType.INVALID; 564 return ExprType.INVALID;
508 } 565 }
509 566
567 @NotNull
568 private ExprType computeExpressionType(ModalExpr expr) {
569 var body = expr.getBody();
570 if (body == null) {
571 return ExprType.INVALID;
572 }
573 var actualType = getExpressionType(body);
574 if (actualType == ExprType.LITERAL || BuiltinTermInterpreter.BOOLEAN_TYPE.equals(actualType)) {
575 return ExprType.MODAL_LITERAL;
576 }
577 if (actualType == ExprType.INVALID) {
578 return ExprType.INVALID;
579 }
580 if (actualType instanceof MutableType) {
581 error(OPERAND_TYPE_ERROR_MESSAGE, body, null, 0, ProblemValidator.TYPE_ERROR);
582 return ExprType.INVALID;
583 }
584 var message = "Data type %s does not support modal operators.".formatted(actualType);
585 error(message, expr, null, 0, ProblemValidator.TYPE_ERROR);
586 return ExprType.INVALID;
587 }
588
510 private FixedType getCommonDataType(BinaryExpr expr) { 589 private FixedType getCommonDataType(BinaryExpr expr) {
511 var commonType = getCommonType(expr); 590 var commonType = getCommonType(expr);
512 if (!(commonType instanceof DataExprType) && commonType != ExprType.INVALID) { 591 if (!(commonType instanceof DataExprType) && commonType != ExprType.INVALID) {
diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
index 9daa8f61..e1820261 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
@@ -42,13 +42,11 @@ public final class ProblemUtil {
42 } 42 }
43 43
44 public static boolean isImplicit(EObject eObject) { 44 public static boolean isImplicit(EObject eObject) {
45 if (eObject instanceof Node node) { 45 return switch (eObject) {
46 return isImplicitNode(node); 46 case Node node -> isImplicitNode(node);
47 } else if (eObject instanceof Variable variable) { 47 case Variable variable -> isImplicitVariable(variable);
48 return isImplicitVariable(variable); 48 default -> false;
49 } else { 49 };
50 return false;
51 }
52 } 50 }
53 51
54 public static boolean isError(EObject eObject) { 52 public static boolean isError(EObject eObject) {
@@ -119,7 +117,7 @@ public final class ProblemUtil {
119 return false; 117 return false;
120 } 118 }
121 return switch (kind) { 119 return switch (kind) {
122 case CONTAINMENT -> false; 120 case CONTAINMENT, PARTIAL -> false;
123 case CONTAINER -> true; 121 case CONTAINER -> true;
124 case DEFAULT, REFERENCE -> { 122 case DEFAULT, REFERENCE -> {
125 var opposite = referenceDeclaration.getOpposite(); 123 var opposite = referenceDeclaration.getOpposite();
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ActionTargetCollector.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ActionTargetCollector.java
new file mode 100644
index 00000000..67cd5777
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ActionTargetCollector.java
@@ -0,0 +1,73 @@
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.*;
14
15import java.util.HashSet;
16import java.util.Set;
17
18@Singleton
19public class ActionTargetCollector {
20 private static final String ACTION_TARGETS =
21 "tools.refinery.language.validation.ActionTargetCollector.ACTION_TARGETS";
22
23 @Inject
24 private IResourceScopeCache cache = IResourceScopeCache.NullImpl.INSTANCE;
25
26 public boolean isActionTarget(Relation relation) {
27 var problem = EcoreUtil2.getContainerOfType(relation, Problem.class);
28 return problem != null && isActionTarget(problem, relation);
29 }
30
31 public boolean isActionTarget(Problem problem, Relation relation) {
32 var targets = getActionTargets(problem);
33 if (targets.contains(relation)) {
34 return true;
35 }
36 if (relation instanceof ReferenceDeclaration referenceDeclaration) {
37 var opposite = referenceDeclaration.getOpposite();
38 return opposite != null && targets.contains(opposite);
39 }
40 return false;
41 }
42
43 protected Set<Relation> getActionTargets(Problem problem) {
44 var resource = problem.eResource();
45 if (resource == null) {
46 return doGetActionTargets(problem);
47 }
48 return cache.get(Tuples.create(problem, ACTION_TARGETS), resource, () -> doGetActionTargets(problem));
49 }
50
51 protected Set<Relation> doGetActionTargets(Problem problem) {
52 var targets = new HashSet<Relation>();
53 for (var statement : problem.getStatements()) {
54 if (statement instanceof RuleDefinition ruleDefinition) {
55 collectTargets(ruleDefinition, targets);
56 }
57 }
58 return targets;
59 }
60
61 private static void collectTargets(RuleDefinition ruleDefinition, HashSet<Relation> targets) {
62 for (var consequent : ruleDefinition.getConsequents()) {
63 for (var action : consequent.getActions()) {
64 if (action instanceof AssertionAction assertionAction) {
65 var target = assertionAction.getRelation();
66 if (target != null) {
67 targets.add(target);
68 }
69 }
70 }
71 }
72 }
73}
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 745e2d2b..84218f17 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.
@@ -49,17 +46,26 @@ public class ProblemValidator extends AbstractProblemValidator {
49 public static final String INVALID_SUPERTYPE_ISSUE = ISSUE_PREFIX + "INVALID_SUPERTYPE"; 46 public static final String INVALID_SUPERTYPE_ISSUE = ISSUE_PREFIX + "INVALID_SUPERTYPE";
50 public static final String INVALID_REFERENCE_TYPE_ISSUE = ISSUE_PREFIX + "INVALID_REFERENCE_TYPE"; 47 public static final String INVALID_REFERENCE_TYPE_ISSUE = ISSUE_PREFIX + "INVALID_REFERENCE_TYPE";
51 public static final String INVALID_ARITY_ISSUE = ISSUE_PREFIX + "INVALID_ARITY"; 48 public static final String INVALID_ARITY_ISSUE = ISSUE_PREFIX + "INVALID_ARITY";
49 public static final String INVALID_MODALITY_ISSUE = ISSUE_PREFIX + "INVALID_MODALITY";
50 public static final String INVALID_RULE_ISSUE = ISSUE_PREFIX + "INVALID_RULE";
52 public static final String INVALID_TRANSITIVE_CLOSURE_ISSUE = ISSUE_PREFIX + "INVALID_TRANSITIVE_CLOSURE"; 51 public static final String INVALID_TRANSITIVE_CLOSURE_ISSUE = ISSUE_PREFIX + "INVALID_TRANSITIVE_CLOSURE";
53 public static final String INVALID_VALUE_ISSUE = ISSUE_PREFIX + "INVALID_VALUE";
54 public static final String UNSUPPORTED_ASSERTION_ISSUE = ISSUE_PREFIX + "UNSUPPORTED_ASSERTION"; 52 public static final String UNSUPPORTED_ASSERTION_ISSUE = ISSUE_PREFIX + "UNSUPPORTED_ASSERTION";
55 public static final String UNKNOWN_EXPRESSION_ISSUE = ISSUE_PREFIX + "UNKNOWN_EXPRESSION"; 53 public static final String UNKNOWN_EXPRESSION_ISSUE = ISSUE_PREFIX + "UNKNOWN_EXPRESSION";
56 public static final String INVALID_ASSIGNMENT_ISSUE = ISSUE_PREFIX + "INVALID_ASSIGNMENT"; 54 public static final String INVALID_ASSIGNMENT_ISSUE = ISSUE_PREFIX + "INVALID_ASSIGNMENT";
57 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";
57 public static final String UNUSED_PARTIAL_RELATION = ISSUE_PREFIX + "UNUSED_PARTIAL_RELATION";
58 58
59 @Inject 59 @Inject
60 private ReferenceCounter referenceCounter; 60 private ReferenceCounter referenceCounter;
61 61
62 @Inject 62 @Inject
63 private ExistsVariableCollector existsVariableCollector;
64
65 @Inject
66 private ActionTargetCollector actionTargetCollector;
67
68 @Inject
63 private IQualifiedNameConverter qualifiedNameConverter; 69 private IQualifiedNameConverter qualifiedNameConverter;
64 70
65 @Inject 71 @Inject
@@ -128,6 +134,14 @@ public class ProblemValidator extends AbstractProblemValidator {
128 var variableOrNode = expr.getVariableOrNode(); 134 var variableOrNode = expr.getVariableOrNode();
129 if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) 135 if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable)
130 && !ProblemUtil.isSingletonVariable(variable)) { 136 && !ProblemUtil.isSingletonVariable(variable)) {
137 if (EcoreUtil2.getContainerOfType(variable, ParametricDefinition.class) instanceof RuleDefinition &&
138 EcoreUtil2.getContainerOfType(variable, NegationExpr.class) == null &&
139 // If there is an exists constraint, it is the only constraint.
140 existsVariableCollector.missingExistsConstraint(variable)) {
141 // Existentially quantified variables in rules should not be singletons,
142 // because we have to add an {@code exists} constraint as well.
143 return;
144 }
131 var problem = EcoreUtil2.getContainerOfType(variable, Problem.class); 145 var problem = EcoreUtil2.getContainerOfType(variable, Problem.class);
132 if (problem != null && referenceCounter.countReferences(problem, variable) <= 1) { 146 if (problem != null && referenceCounter.countReferences(problem, variable) <= 1) {
133 var name = variable.getName(); 147 var name = variable.getName();
@@ -152,13 +166,32 @@ public class ProblemValidator extends AbstractProblemValidator {
152 } 166 }
153 167
154 @Check 168 @Check
169 public void checkNodeAssertionArgumentConstants(NodeAssertionArgument argument) {
170 var rule = EcoreUtil2.getContainerOfType(argument, RuleDefinition.class);
171 if (rule == null) {
172 return;
173 }
174 var variableOrNode = argument.getNode();
175 if (variableOrNode instanceof Node node && !ProblemUtil.isAtomNode(node)) {
176 var name = node.getName();
177 var message = ("Only atoms can be referenced in rule actions. " +
178 "Mark '%s' as an atom with the declaration 'atom %s.'").formatted(name, name);
179 error(message, argument, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE,
180 INSIGNIFICANT_INDEX, NODE_CONSTANT_ISSUE);
181 }
182 }
183
184 @Check
155 public void checkUniqueDeclarations(Problem problem) { 185 public void checkUniqueDeclarations(Problem problem) {
156 var relations = new ArrayList<Relation>(); 186 var relations = new ArrayList<NamedElement>();
157 var nodes = new ArrayList<Node>(); 187 var nodes = new ArrayList<Node>();
158 var aggregators = new ArrayList<AggregatorDeclaration>(); 188 var aggregators = new ArrayList<AggregatorDeclaration>();
159 for (var statement : problem.getStatements()) { 189 for (var statement : problem.getStatements()) {
160 if (statement instanceof Relation relation) { 190 if (statement instanceof Relation relation) {
161 relations.add(relation); 191 relations.add(relation);
192 } else if (statement instanceof RuleDefinition ruleDefinition) {
193 // Rule definitions and predicates live in the same namespace.
194 relations.add(ruleDefinition);
162 } else if (statement instanceof NodeDeclaration nodeDeclaration) { 195 } else if (statement instanceof NodeDeclaration nodeDeclaration) {
163 nodes.addAll(nodeDeclaration.getNodes()); 196 nodes.addAll(nodeDeclaration.getNodes());
164 } else if (statement instanceof AggregatorDeclaration aggregatorDeclaration) { 197 } else if (statement instanceof AggregatorDeclaration aggregatorDeclaration) {
@@ -298,6 +331,22 @@ public class ProblemValidator extends AbstractProblemValidator {
298 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, 331 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
299 INVALID_OPPOSITE_ISSUE); 332 INVALID_OPPOSITE_ISSUE);
300 } 333 }
334 } else if (kind == ReferenceKind.PARTIAL && opposite != null && opposite.getKind() != ReferenceKind.PARTIAL) {
335 acceptError("Opposite '%s' of partial reference '%s' is not a partial reference."
336 .formatted(opposite.getName(), referenceDeclaration.getName()),
337 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
338 INVALID_OPPOSITE_ISSUE);
339 }
340 }
341
342 @Check
343 public void checkPartialReference(ReferenceDeclaration referenceDeclaration) {
344 if (referenceDeclaration.getKind() == ReferenceKind.PARTIAL &&
345 !actionTargetCollector.isActionTarget(referenceDeclaration)) {
346 var message = "Add decision or propagation rules to refine partial relation '%s'."
347 .formatted(referenceDeclaration.getName());
348 acceptWarning(message, referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__KIND, 0,
349 UNUSED_PARTIAL_RELATION);
301 } 350 }
302 } 351 }
303 352
@@ -336,8 +385,33 @@ public class ProblemValidator extends AbstractProblemValidator {
336 } 385 }
337 386
338 @Check 387 @Check
339 public void checkParameterType(Parameter parameter) { 388 public void checkParameter(Parameter parameter) {
340 checkArity(parameter, ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE, 1); 389 checkArity(parameter, ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE, 1);
390 var parametricDefinition = EcoreUtil2.getContainerOfType(parameter, ParametricDefinition.class);
391 if (parametricDefinition instanceof RuleDefinition rule) {
392 if (parameter.getParameterType() != null && parameter.getModality() == Modality.NONE) {
393 acceptError("Parameter type modality must be specified.", parameter,
394 ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE, 0, INVALID_MODALITY_ISSUE);
395 }
396 var kind = rule.getKind();
397 var binding = parameter.getBinding();
398 if (kind == RuleKind.PROPAGATION && binding != ParameterBinding.SINGLE) {
399 acceptError("Parameter binding annotations are not supported in propagation rules.", parameter,
400 ProblemPackage.Literals.PARAMETER__BINDING, 0, INVALID_MODALITY_ISSUE);
401 } else if (kind != RuleKind.DECISION && binding == ParameterBinding.MULTI) {
402 acceptError("Explicit multi-object bindings are only supported in decision rules.", parameter,
403 ProblemPackage.Literals.PARAMETER__BINDING, 0, INVALID_MODALITY_ISSUE);
404 }
405 } else {
406 if (parameter.getConcreteness() != Concreteness.PARTIAL || parameter.getModality() != Modality.NONE) {
407 acceptError("Modal parameter types are only supported in rule definitions.", parameter, null, 0,
408 INVALID_MODALITY_ISSUE);
409 }
410 if (parameter.getBinding() != ParameterBinding.SINGLE) {
411 acceptError("Parameter binding annotations are only supported in decision rules.", parameter,
412 ProblemPackage.PARAMETER__BINDING, 0, INVALID_MODALITY_ISSUE);
413 }
414 }
341 } 415 }
342 416
343 @Check 417 @Check
@@ -353,16 +427,46 @@ public class ProblemValidator extends AbstractProblemValidator {
353 } 427 }
354 428
355 @Check 429 @Check
356 public void checkAssertion(Assertion assertion) { 430 public void checkRuleDefinition(RuleDefinition ruleDefinition) {
431 if (ruleDefinition.getConsequents().size() != 1) {
432 acceptError("Rules must have exactly one consequent.", ruleDefinition,
433 ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, INVALID_RULE_ISSUE);
434 }
435 var unquantifiedVariables = new HashSet<Variable>();
436 for (var variable : EcoreUtil2.getAllContentsOfType(ruleDefinition, Variable.class)) {
437 if (existsVariableCollector.missingExistsConstraint(variable)) {
438 unquantifiedVariables.add(variable);
439 }
440 }
441 for (var expr : EcoreUtil2.getAllContentsOfType(ruleDefinition, VariableOrNodeExpr.class)) {
442 if (expr.getVariableOrNode() instanceof Variable variable && unquantifiedVariables.contains(variable)) {
443 unquantifiedVariables.remove(variable);
444 var name = variable.getName();
445 String message;
446 if (ProblemUtil.isSingletonVariable(variable)) {
447 message = ("Remove the singleton variable marker '_' and clarify the quantification of variable " +
448 "'%s'.").formatted(name);
449 } else {
450 message = ("Add a 'must exists(%s)', 'may exists(%s)', or 'may !exists(%s)' constraint to " +
451 "clarify the quantification of variable '%s'.").formatted(name, name, name, name);
452 }
453 acceptWarning(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, 0,
454 VARIABLE_WITHOUT_EXISTS);
455 }
456 }
457 }
458
459 @Check
460 public void checkAssertion(AbstractAssertion assertion) {
357 var relation = assertion.getRelation(); 461 var relation = assertion.getRelation();
358 if (relation instanceof DatatypeDeclaration) { 462 if (relation instanceof DatatypeDeclaration) {
359 var message = "Assertions for data types are not supported."; 463 var message = "Assertions for data types are not supported.";
360 acceptError(message, assertion, ProblemPackage.Literals.ASSERTION__RELATION, 0, 464 acceptError(message, assertion, ProblemPackage.Literals.ABSTRACT_ASSERTION__RELATION, 0,
361 UNSUPPORTED_ASSERTION_ISSUE); 465 UNSUPPORTED_ASSERTION_ISSUE);
362 return; 466 return;
363 } 467 }
364 int argumentCount = assertion.getArguments().size(); 468 int argumentCount = assertion.getArguments().size();
365 checkArity(assertion, ProblemPackage.Literals.ASSERTION__RELATION, argumentCount); 469 checkArity(assertion, ProblemPackage.Literals.ABSTRACT_ASSERTION__RELATION, argumentCount);
366 } 470 }
367 471
368 @Check 472 @Check
@@ -466,15 +570,17 @@ public class ProblemValidator extends AbstractProblemValidator {
466 570
467 @Nullable 571 @Nullable
468 private Node getNodeArgumentForMultiObjectAssertion(AssertionArgument argument) { 572 private Node getNodeArgumentForMultiObjectAssertion(AssertionArgument argument) {
469 if (argument instanceof WildcardAssertionArgument) { 573 return switch (argument) {
470 acceptError("Wildcard arguments for 'exists' are not supported.", argument, null, 0, 574 case null -> null;
471 UNSUPPORTED_ASSERTION_ISSUE); 575 case WildcardAssertionArgument ignoredWildcardAssertionArgument -> {
472 return null; 576 acceptError("Wildcard arguments for 'exists' are not supported.", argument, null, 0,
473 } 577 UNSUPPORTED_ASSERTION_ISSUE);
474 if (argument instanceof NodeAssertionArgument nodeAssertionArgument) { 578 yield null;
475 return nodeAssertionArgument.getNode(); 579 }
476 } 580 case NodeAssertionArgument nodeAssertionArgument ->
477 throw new IllegalArgumentException("Unknown assertion argument: " + argument); 581 nodeAssertionArgument.getNode() instanceof Node node ? node : null;
582 default -> throw new IllegalArgumentException("Unknown assertion argument: " + argument);
583 };
478 } 584 }
479 585
480 @Check 586 @Check
@@ -484,8 +590,10 @@ public class ProblemValidator extends AbstractProblemValidator {
484 } 590 }
485 for (var argument : assertion.getArguments()) { 591 for (var argument : assertion.getArguments()) {
486 if (argument instanceof NodeAssertionArgument nodeAssertionArgument) { 592 if (argument instanceof NodeAssertionArgument nodeAssertionArgument) {
487 var node = nodeAssertionArgument.getNode(); 593 var variableOrNode = nodeAssertionArgument.getNode();
488 if (node != null && !node.eIsProxy() && ProblemUtil.isImplicitNode(node)) { 594 if (variableOrNode instanceof Node node &&
595 !variableOrNode.eIsProxy() &&
596 ProblemUtil.isImplicitNode(node)) {
489 var name = node.getName(); 597 var name = node.getName();
490 var message = ("Implicit nodes are not allowed in modules. Explicitly declare '%s' as a node " + 598 var message = ("Implicit nodes are not allowed in modules. Explicitly declare '%s' as a node " +
491 "with the declaration 'declare %s.'").formatted(name, name); 599 "with the declaration 'declare %s.'").formatted(name, name);
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) {
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java
index 56e65550..15636483 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java
@@ -9,7 +9,6 @@ import com.google.inject.Inject;
9import org.eclipse.xtext.testing.InjectWith; 9import org.eclipse.xtext.testing.InjectWith;
10import org.eclipse.xtext.testing.extensions.InjectionExtension; 10import org.eclipse.xtext.testing.extensions.InjectionExtension;
11import org.junit.jupiter.api.Disabled; 11import org.junit.jupiter.api.Disabled;
12import org.junit.jupiter.api.Test;
13import org.junit.jupiter.api.extension.ExtendWith; 12import org.junit.jupiter.api.extension.ExtendWith;
14import org.junit.jupiter.params.ParameterizedTest; 13import org.junit.jupiter.params.ParameterizedTest;
15import org.junit.jupiter.params.provider.ValueSource; 14import org.junit.jupiter.params.provider.ValueSource;
@@ -17,7 +16,7 @@ import tools.refinery.language.model.tests.utils.ProblemParseHelper;
17import tools.refinery.language.tests.ProblemInjectorProvider; 16import tools.refinery.language.tests.ProblemInjectorProvider;
18 17
19import static org.hamcrest.MatcherAssert.assertThat; 18import static org.hamcrest.MatcherAssert.assertThat;
20import static org.hamcrest.Matchers.*; 19import static org.hamcrest.Matchers.empty;
21 20
22@Disabled("TODO: Rework transformation rules") 21@Disabled("TODO: Rework transformation rules")
23@ExtendWith(InjectionExtension.class) 22@ExtendWith(InjectionExtension.class)
@@ -29,62 +28,13 @@ class RuleParsingTest {
29 @ParameterizedTest 28 @ParameterizedTest
30 @ValueSource(strings = { """ 29 @ValueSource(strings = { """
31 pred Person(p). 30 pred Person(p).
32 rule r(p1): must Person(p1) ==> Person(p1) := false. 31 rule r(p1): must Person(p1) ==> Person(p1): false.
33 """, """
34 pred Person(p).
35 rule r(p1): must Person(p1) ==> Person(p1) <: false.
36 """, """ 32 """, """
37 pred Person(p). 33 pred Person(p).
38 rule r(p1): must Person(p1) ==> !Person(p1). 34 rule r(p1): must Person(p1) ==> !Person(p1).
39 """, """
40 pred Person(p).
41 rule r(p1): must Person(p1) ==> delete p1.
42 """ }) 35 """ })
43 void simpleTest(String text) { 36 void simpleTest(String text) {
44 var problem = parseHelper.parse(text); 37 var problem = parseHelper.parse(text);
45 assertThat(problem.getResourceErrors(), empty()); 38 assertThat(problem.getResourceErrors(), empty());
46 } 39 }
47
48 @Test
49 void newNodeTest() {
50 var problem = parseHelper.parse("""
51 pred Person(p).
52 rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown.
53 """);
54 assertThat(problem.getResourceErrors(), empty());
55 assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable()));
56 assertThat(problem.rule("r").consequent(0).action(0).newVar(),
57 equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable()));
58 }
59
60 @Test
61 void differentScopeTest() {
62 var problem = parseHelper.parse("""
63 pred Friend(a, b).
64 rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true.
65 """);
66 assertThat(problem.getResourceErrors(), empty());
67 assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(),
68 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable())));
69 }
70
71 @Test
72 void parameterShadowingTest() {
73 var problem = parseHelper.parse("""
74 pred Friend(a, b).
75 rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true.
76 """);
77 assertThat(problem.getResourceErrors(), empty());
78 assertThat(problem.rule("r").param(1),
79 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable())));
80 }
81
82 @Test
83 void deleteDifferentScopeNodeTest() {
84 var problem = parseHelper.parse("""
85 pred Person(p).
86 rule r(p1): must Friend(p1, p2) ==> delete p2.
87 """);
88 assertThat(problem.getResourceErrors(), not(empty()));
89 }
90} 40}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java
index 57602377..d73ef8e7 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java
@@ -57,6 +57,18 @@ class OppositeValidationTest {
57 class Foo { 57 class Foo {
58 Foo foo[] opposite foo 58 Foo foo[] opposite foo
59 } 59 }
60 """, """
61 class Foo {
62 partial Bar bar opposite foo
63 }
64
65 class Bar {
66 partial Foo foo opposite bar
67 }
68 """, """
69 class Foo {
70 partial Foo foo[] opposite foo
71 }
60 """}) 72 """})
61 void validOppositeTest(String text) { 73 void validOppositeTest(String text) {
62 var problem = parseHelper.parse(text); 74 var problem = parseHelper.parse(text);
@@ -188,7 +200,7 @@ class OppositeValidationTest {
188 } 200 }
189 201
190 @ParameterizedTest 202 @ParameterizedTest
191 @ValueSource(strings = {"Foo foo", "container Foo foo"}) 203 @ValueSource(strings = {"Foo foo", "container Foo foo", "partial Foo foo"})
192 void containerInvalidOppositeTest(String reference) { 204 void containerInvalidOppositeTest(String reference) {
193 var problem = parseHelper.parse(""" 205 var problem = parseHelper.parse("""
194 class Foo { 206 class Foo {
@@ -203,7 +215,27 @@ class OppositeValidationTest {
203 assertThat(issues, hasItem(allOf( 215 assertThat(issues, hasItem(allOf(
204 hasProperty("severity", is(Diagnostic.ERROR)), 216 hasProperty("severity", is(Diagnostic.ERROR)),
205 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), 217 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
206 hasProperty("message", stringContainsInOrder("foo", "bar")) 218 hasProperty("message", stringContainsInOrder("foo", "container", "bar"))
219 )));
220 }
221
222 @ParameterizedTest
223 @ValueSource(strings = {"Foo foo", "contains Foo foo", "container Foo foo"})
224 void partialWithConcreteOppositeTest(String reference) {
225 var problem = parseHelper.parse("""
226 class Foo {
227 partial Bar bar opposite foo
228 }
229
230 class Bar {
231 %s opposite bar
232 }
233 """.formatted(reference));
234 var issues = problem.validate();
235 assertThat(issues, hasItem(allOf(
236 hasProperty("severity", is(Diagnostic.ERROR)),
237 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
238 hasProperty("message", stringContainsInOrder("foo", "partial", "bar"))
207 ))); 239 )));
208 } 240 }
209} 241}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAction.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAction.java
index 3a49a0b9..4987cb89 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAction.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAction.java
@@ -6,25 +6,9 @@
6package tools.refinery.language.model.tests.utils; 6package tools.refinery.language.model.tests.utils;
7 7
8import tools.refinery.language.model.problem.Action; 8import tools.refinery.language.model.problem.Action;
9import tools.refinery.language.model.problem.AssertionAction;
10import tools.refinery.language.model.problem.DeleteAction;
11import tools.refinery.language.model.problem.NewAction;
12import tools.refinery.language.model.problem.VariableOrNode;
13 9
14public record WrappedAction(Action action) { 10public record WrappedAction(Action action) {
15 public Action get() { 11 public Action get() {
16 return action; 12 return action;
17 } 13 }
18
19 public VariableOrNode newVar() {
20 return ((NewAction) action).getVariable();
21 }
22
23 public VariableOrNode deleteVar() {
24 return ((DeleteAction) action).getVariableOrNode();
25 }
26
27 public WrappedAtom assertedAtom() {
28 return new WrappedAtom(((AssertionAction) action).getAtom());
29 }
30} 14}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAssertionArgument.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAssertionArgument.java
index b36f2506..50cb958d 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAssertionArgument.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedAssertionArgument.java
@@ -15,6 +15,6 @@ public record WrappedAssertionArgument(AssertionArgument assertionArgument) {
15 } 15 }
16 16
17 public Node node() { 17 public Node node() {
18 return ((NodeAssertionArgument) assertionArgument).getNode(); 18 return (Node) ((NodeAssertionArgument) assertionArgument).getNode();
19 } 19 }
20} 20}