aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2023-11-19 14:58:55 +0100
committerLibravatar GitHub <noreply@github.com>2023-11-19 14:58:55 +0100
commitdf0f7cbf05558ce8691759256caa64ab01dd5d9c (patch)
tree0a842960e27ad25d458c18f7bd9f8b4bf5b89b6c /subprojects
parentbuild: prepare for Maven publication (diff)
parentfeat(langauge): validate exists and equals (diff)
downloadrefinery-df0f7cbf05558ce8691759256caa64ab01dd5d9c.tar.gz
refinery-df0f7cbf05558ce8691759256caa64ab01dd5d9c.tar.zst
refinery-df0f7cbf05558ce8691759256caa64ab01dd5d9c.zip
Merge pull request #48 from kris7t/validator
Improve content assist and validator
Diffstat (limited to 'subprojects')
-rw-r--r--subprojects/frontend/config/detectDevModeOptions.ts6
-rw-r--r--subprojects/frontend/package.json78
-rw-r--r--subprojects/frontend/src/DirectionalSplitPane.tsx5
-rw-r--r--subprojects/frontend/src/graph/VisibilityDialog.tsx9
-rw-r--r--subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java120
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java8
-rw-r--r--subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java6
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/Problem.xtext5
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java16
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java72
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java263
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java9
-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/utils/ProblemUtil.java45
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java31
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java360
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java (renamed from subprojects/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java)2
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java2
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java98
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java4
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java10
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java28
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java249
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java111
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java119
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java209
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java13
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java13
-rw-r--r--subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java17
-rw-r--r--subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java2
-rw-r--r--subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java37
-rw-r--r--subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java157
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java38
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java4
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java71
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java2
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java21
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java10
-rw-r--r--subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java30
39 files changed, 2148 insertions, 147 deletions
diff --git a/subprojects/frontend/config/detectDevModeOptions.ts b/subprojects/frontend/config/detectDevModeOptions.ts
index 6052e047..7a3de4cb 100644
--- a/subprojects/frontend/config/detectDevModeOptions.ts
+++ b/subprojects/frontend/config/detectDevModeOptions.ts
@@ -63,6 +63,11 @@ export default function detectDevModeOptions(): DevModeOptions {
63 const api = detectListenOptions('API', '127.0.0.1', 1312); 63 const api = detectListenOptions('API', '127.0.0.1', 1312);
64 const publicAddress = detectListenOptions('PUBLIC', listen.host, listen.port); 64 const publicAddress = detectListenOptions('PUBLIC', listen.host, listen.port);
65 65
66 if (listen.secure) {
67 // Since nodejs 20, we'd need to pass in HTTPS options manually.
68 throw new Error(`Preview on secure port ${listen.port} is not supported`);
69 }
70
66 const backendConfig: BackendConfig = { 71 const backendConfig: BackendConfig = {
67 webSocketURL: `${listenURL(publicAddress, 'ws')}/${API_ENDPOINT}`, 72 webSocketURL: `${listenURL(publicAddress, 'ws')}/${API_ENDPOINT}`,
68 }; 73 };
@@ -75,7 +80,6 @@ export default function detectDevModeOptions(): DevModeOptions {
75 host: listen.host, 80 host: listen.host,
76 port: listen.port, 81 port: listen.port,
77 strictPort: true, 82 strictPort: true,
78 https: listen.secure,
79 headers: { 83 headers: {
80 // Enable strict origin isolation, see e.g., 84 // Enable strict origin isolation, see e.g.,
81 // https://github.com/vitejs/vite/issues/3909#issuecomment-1065893956 85 // https://github.com/vitejs/vite/issues/3909#issuecomment-1065893956
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json
index 748cc78d..e710a506 100644
--- a/subprojects/frontend/package.json
+++ b/subprojects/frontend/package.json
@@ -28,74 +28,74 @@
28 }, 28 },
29 "homepage": "https://refinery.tools", 29 "homepage": "https://refinery.tools",
30 "dependencies": { 30 "dependencies": {
31 "@codemirror/autocomplete": "^6.10.2", 31 "@codemirror/autocomplete": "^6.11.0",
32 "@codemirror/commands": "^6.3.0", 32 "@codemirror/commands": "^6.3.0",
33 "@codemirror/language": "^6.9.1", 33 "@codemirror/language": "^6.9.2",
34 "@codemirror/lint": "^6.4.2", 34 "@codemirror/lint": "^6.4.2",
35 "@codemirror/search": "^6.5.4", 35 "@codemirror/search": "^6.5.4",
36 "@codemirror/state": "^6.3.1", 36 "@codemirror/state": "^6.3.1",
37 "@codemirror/view": "^6.21.3", 37 "@codemirror/view": "^6.22.0",
38 "@emotion/react": "^11.11.1", 38 "@emotion/react": "^11.11.1",
39 "@emotion/styled": "^11.11.0", 39 "@emotion/styled": "^11.11.0",
40 "@fontsource-variable/jetbrains-mono": "^5.0.16", 40 "@fontsource-variable/jetbrains-mono": "^5.0.18",
41 "@fontsource-variable/open-sans": "^5.0.16", 41 "@fontsource-variable/open-sans": "^5.0.18",
42 "@hpcc-js/wasm": "^2.14.1", 42 "@hpcc-js/wasm": "^2.14.1",
43 "@lezer/common": "^1.1.0", 43 "@lezer/common": "^1.1.1",
44 "@lezer/highlight": "^1.1.6", 44 "@lezer/highlight": "^1.2.0",
45 "@lezer/lr": "^1.3.13", 45 "@lezer/lr": "^1.3.14",
46 "@material-icons/svg": "^1.0.33", 46 "@material-icons/svg": "^1.0.33",
47 "@mui/icons-material": "5.14.14", 47 "@mui/icons-material": "5.14.18",
48 "@mui/material": "5.14.14", 48 "@mui/material": "5.14.18",
49 "@mui/system": "^5.14.14", 49 "@mui/system": "^5.14.18",
50 "@mui/x-data-grid": "^6.16.2", 50 "@mui/x-data-grid": "^6.18.1",
51 "ansi-styles": "^6.2.1", 51 "ansi-styles": "^6.2.1",
52 "csstype": "^3.1.2", 52 "csstype": "^3.1.2",
53 "d3": "^7.8.5", 53 "d3": "^7.8.5",
54 "d3-graphviz": "patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch", 54 "d3-graphviz": "patch:d3-graphviz@npm%3A5.2.0#~/.yarn/patches/d3-graphviz-npm-5.2.0-161b1fbad4.patch",
55 "d3-selection": "^3.0.0", 55 "d3-selection": "^3.0.0",
56 "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", 56 "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch",
57 "escape-string-regexp": "^5.0.0", 57 "escape-string-regexp": "^5.0.0",
58 "lodash-es": "^4.17.21", 58 "lodash-es": "^4.17.21",
59 "loglevel": "^1.8.1", 59 "loglevel": "^1.8.1",
60 "loglevel-plugin-prefix": "^0.8.4", 60 "loglevel-plugin-prefix": "^0.8.4",
61 "mobx": "^6.10.2", 61 "mobx": "^6.11.0",
62 "mobx-react-lite": "^4.0.5", 62 "mobx-react-lite": "^4.0.5",
63 "ms": "^2.1.3", 63 "ms": "^2.1.3",
64 "nanoid": "^5.0.2", 64 "nanoid": "^5.0.3",
65 "notistack": "^3.0.1", 65 "notistack": "^3.0.1",
66 "react": "^18.2.0", 66 "react": "^18.2.0",
67 "react-dom": "^18.2.0", 67 "react-dom": "^18.2.0",
68 "react-resize-detector": "^9.1.0", 68 "react-resize-detector": "^9.1.0",
69 "xstate": "^4.38.2", 69 "xstate": "^4.38.3",
70 "zod": "^3.22.4" 70 "zod": "^3.22.4"
71 }, 71 },
72 "devDependencies": { 72 "devDependencies": {
73 "@lezer/generator": "^1.5.1", 73 "@lezer/generator": "^1.5.1",
74 "@types/d3": "^7.4.2", 74 "@types/d3": "^7.4.3",
75 "@types/d3-graphviz": "^2.6.9", 75 "@types/d3-graphviz": "^2.6.10",
76 "@types/d3-selection": "^3.0.8", 76 "@types/d3-selection": "^3.0.10",
77 "@types/d3-zoom": "^3.0.6", 77 "@types/d3-zoom": "^3.0.8",
78 "@types/eslint": "^8.44.6", 78 "@types/eslint": "^8.44.7",
79 "@types/html-minifier-terser": "^7.0.1", 79 "@types/html-minifier-terser": "^7.0.2",
80 "@types/lodash-es": "^4.17.10", 80 "@types/lodash-es": "^4.17.11",
81 "@types/micromatch": "^4.0.4", 81 "@types/micromatch": "^4.0.5",
82 "@types/ms": "^0.7.33", 82 "@types/ms": "^0.7.34",
83 "@types/node": "^18.18.6", 83 "@types/node": "^20.9.2",
84 "@types/pnpapi": "^0.0.4", 84 "@types/pnpapi": "^0.0.5",
85 "@types/react": "^18.2.29", 85 "@types/react": "^18.2.37",
86 "@types/react-dom": "^18.2.14", 86 "@types/react-dom": "^18.2.15",
87 "@typescript-eslint/eslint-plugin": "^6.8.0", 87 "@typescript-eslint/eslint-plugin": "^6.11.0",
88 "@typescript-eslint/parser": "^6.8.0", 88 "@typescript-eslint/parser": "^6.11.0",
89 "@vitejs/plugin-react-swc": "^3.4.0", 89 "@vitejs/plugin-react-swc": "^3.5.0",
90 "@xstate/cli": "^0.5.7", 90 "@xstate/cli": "^0.5.11",
91 "cross-env": "^7.0.3", 91 "cross-env": "^7.0.3",
92 "eslint": "^8.51.0", 92 "eslint": "^8.54.0",
93 "eslint-config-airbnb": "^19.0.4", 93 "eslint-config-airbnb": "^19.0.4",
94 "eslint-config-airbnb-typescript": "^17.1.0", 94 "eslint-config-airbnb-typescript": "^17.1.0",
95 "eslint-config-prettier": "^9.0.0", 95 "eslint-config-prettier": "^9.0.0",
96 "eslint-import-resolver-typescript": "^3.6.1", 96 "eslint-import-resolver-typescript": "^3.6.1",
97 "eslint-plugin-import": "^2.28.1", 97 "eslint-plugin-import": "^2.29.0",
98 "eslint-plugin-jsx-a11y": "^6.7.1", 98 "eslint-plugin-jsx-a11y": "^6.8.0",
99 "eslint-plugin-mobx": "^0.0.9", 99 "eslint-plugin-mobx": "^0.0.9",
100 "eslint-plugin-prettier": "^5.0.1", 100 "eslint-plugin-prettier": "^5.0.1",
101 "eslint-plugin-react": "^7.33.2", 101 "eslint-plugin-react": "^7.33.2",
@@ -103,10 +103,10 @@
103 "html-minifier-terser": "^7.2.0", 103 "html-minifier-terser": "^7.2.0",
104 "micromatch": "^4.0.5", 104 "micromatch": "^4.0.5",
105 "pnpapi": "^0.0.0", 105 "pnpapi": "^0.0.0",
106 "prettier": "^3.0.3", 106 "prettier": "^3.1.0",
107 "typescript": "5.2.2", 107 "typescript": "5.2.2",
108 "vite": "^4.5.0", 108 "vite": "^5.0.0",
109 "vite-plugin-pwa": "^0.16.5", 109 "vite-plugin-pwa": "^0.17.0",
110 "workbox-window": "^7.0.0" 110 "workbox-window": "^7.0.0"
111 } 111 }
112} 112}
diff --git a/subprojects/frontend/src/DirectionalSplitPane.tsx b/subprojects/frontend/src/DirectionalSplitPane.tsx
index 59c8b739..110bb202 100644
--- a/subprojects/frontend/src/DirectionalSplitPane.tsx
+++ b/subprojects/frontend/src/DirectionalSplitPane.tsx
@@ -64,9 +64,8 @@ export default function DirectionalSplitPane({
64 [axis]: '0px', 64 [axis]: '0px',
65 display: showLeftOnly || showRightOnly ? 'none' : 'flex', 65 display: showLeftOnly || showRightOnly ? 'none' : 'flex',
66 flexDirection: direction, 66 flexDirection: direction,
67 [horizontalSplit 67 [horizontalSplit ? 'borderBottom' : 'borderRight']:
68 ? 'borderBottom' 68 `1px solid ${theme.palette.outer.border}`,
69 : 'borderRight']: `1px solid ${theme.palette.outer.border}`,
70 }} 69 }}
71 > 70 >
72 <Box 71 <Box
diff --git a/subprojects/frontend/src/graph/VisibilityDialog.tsx b/subprojects/frontend/src/graph/VisibilityDialog.tsx
index f1fef28b..bfdcd59f 100644
--- a/subprojects/frontend/src/graph/VisibilityDialog.tsx
+++ b/subprojects/frontend/src/graph/VisibilityDialog.tsx
@@ -210,7 +210,10 @@ function VisibilityDialog({
210 } 210 }
211 /> 211 />
212 </td> 212 </td>
213 <td onClick={() => graph.cycleVisibility(name)}> 213 <td
214 onClick={() => graph.cycleVisibility(name)}
215 aria-label="Toggle visiblity"
216 >
214 <div className="VisibilityDialog-nowrap"> 217 <div className="VisibilityDialog-nowrap">
215 <RelationName metadata={metadata} abbreviate={graph.abbreviate} /> 218 <RelationName metadata={metadata} abbreviate={graph.abbreviate} />
216 </div> 219 </div>
@@ -261,10 +264,10 @@ function VisibilityDialog({
261 <table cellSpacing={0}> 264 <table cellSpacing={0}>
262 <thead> 265 <thead>
263 <tr> 266 <tr>
264 <th> 267 <th aria-label="Show true and error values">
265 <LabelIcon /> 268 <LabelIcon />
266 </th> 269 </th>
267 <th> 270 <th aria-label="Show unknown values">
268 <LabelOutlinedIcon /> 271 <LabelOutlinedIcon />
269 </th> 272 </th>
270 <th>Symbol</th> 273 <th>Symbol</th>
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java
index ea90a82e..166b4400 100644
--- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java
+++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java
@@ -6,9 +6,12 @@
6package tools.refinery.language.ide.contentassist; 6package tools.refinery.language.ide.contentassist;
7 7
8import com.google.inject.Inject; 8import com.google.inject.Inject;
9import org.eclipse.emf.ecore.EClass;
9import org.eclipse.emf.ecore.EObject; 10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.emf.ecore.EReference;
10import org.eclipse.emf.ecore.util.EcoreUtil; 12import org.eclipse.emf.ecore.util.EcoreUtil;
11import org.eclipse.xtext.CrossReference; 13import org.eclipse.xtext.CrossReference;
14import org.eclipse.xtext.EcoreUtil2;
12import org.eclipse.xtext.GrammarUtil; 15import org.eclipse.xtext.GrammarUtil;
13import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext; 16import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext;
14import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; 17import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider;
@@ -16,10 +19,14 @@ import org.eclipse.xtext.naming.QualifiedName;
16import org.eclipse.xtext.nodemodel.util.NodeModelUtils; 19import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
17import org.eclipse.xtext.resource.IEObjectDescription; 20import org.eclipse.xtext.resource.IEObjectDescription;
18import org.eclipse.xtext.scoping.IScope; 21import org.eclipse.xtext.scoping.IScope;
19import tools.refinery.language.model.problem.Problem; 22import org.eclipse.xtext.xtext.CurrentTypeFinder;
23import org.jetbrains.annotations.Nullable;
24import tools.refinery.language.model.problem.*;
20import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; 25import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
21import tools.refinery.language.resource.ReferenceCounter; 26import tools.refinery.language.utils.BuiltinSymbols;
27import tools.refinery.language.utils.ProblemDesugarer;
22import tools.refinery.language.utils.ProblemUtil; 28import tools.refinery.language.utils.ProblemUtil;
29import tools.refinery.language.validation.ReferenceCounter;
23 30
24import java.util.ArrayList; 31import java.util.ArrayList;
25import java.util.HashMap; 32import java.util.HashMap;
@@ -28,8 +35,14 @@ import java.util.Objects;
28 35
29public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider { 36public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider {
30 @Inject 37 @Inject
38 private CurrentTypeFinder currentTypeFinder;
39
40 @Inject
31 private ReferenceCounter referenceCounter; 41 private ReferenceCounter referenceCounter;
32 42
43 @Inject
44 private ProblemDesugarer desugarer;
45
33 @Override 46 @Override
34 protected Iterable<IEObjectDescription> queryScope(IScope scope, CrossReference crossReference, 47 protected Iterable<IEObjectDescription> queryScope(IScope scope, CrossReference crossReference,
35 ContentAssistContext context) { 48 ContentAssistContext context) {
@@ -49,7 +62,7 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider
49 for (var candidates : eObjectDescriptionsByName.values()) { 62 for (var candidates : eObjectDescriptionsByName.values()) {
50 if (candidates.size() == 1) { 63 if (candidates.size() == 1) {
51 var candidate = candidates.get(0); 64 var candidate = candidates.get(0);
52 if (shouldBeVisible(candidate)) { 65 if (shouldBeVisible(candidate, crossReference, context)) {
53 eObjectDescriptions.add(candidate); 66 eObjectDescriptions.add(candidate);
54 } 67 }
55 } 68 }
@@ -81,9 +94,106 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider
81 return true; 94 return true;
82 } 95 }
83 96
84 protected boolean shouldBeVisible(IEObjectDescription candidate) { 97 protected boolean shouldBeVisible(IEObjectDescription candidate, CrossReference crossReference,
98 ContentAssistContext context) {
85 var errorPredicate = candidate.getUserData(ProblemResourceDescriptionStrategy.ERROR_PREDICATE); 99 var errorPredicate = candidate.getUserData(ProblemResourceDescriptionStrategy.ERROR_PREDICATE);
86 return !ProblemResourceDescriptionStrategy.ERROR_PREDICATE_TRUE.equals(errorPredicate); 100 if (ProblemResourceDescriptionStrategy.ERROR_PREDICATE_TRUE.equals(errorPredicate)) {
101 return false;
102 }
103
104 var eReference = getEReference(crossReference);
105 if (eReference == null) {
106 return true;
107 }
108
109 var candidateEObjectOrProxy = candidate.getEObjectOrProxy();
110
111 if (eReference.equals(ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) &&
112 candidateEObjectOrProxy instanceof ReferenceDeclaration candidateReferenceDeclaration) {
113 return oppositeShouldBeVisible(candidateReferenceDeclaration, context);
114 }
115
116 var builtinSymbolsOption = desugarer.getBuiltinSymbols(context.getRootModel());
117 if (builtinSymbolsOption.isEmpty()) {
118 return true;
119 }
120 var builtinSymbols = builtinSymbolsOption.get();
121
122 return builtinSymbolAwareShouldBeVisible(candidate, context, eReference, builtinSymbols,
123 candidateEObjectOrProxy);
124 }
125
126 private static boolean oppositeShouldBeVisible(ReferenceDeclaration candidateReferenceDeclaration,
127 ContentAssistContext context) {
128 var referenceDeclaration = EcoreUtil2.getContainerOfType(context.getCurrentModel(),
129 ReferenceDeclaration.class);
130 if (referenceDeclaration == null) {
131 return true;
132 }
133 var classDeclaration = EcoreUtil2.getContainerOfType(referenceDeclaration, ClassDeclaration.class);
134 if (classDeclaration == null) {
135 return true;
136 }
137 var oppositeType = candidateReferenceDeclaration.getReferenceType();
138 if (oppositeType == null) {
139 return true;
140 }
141 var resolvedOppositeType = EcoreUtil.resolve(oppositeType, candidateReferenceDeclaration);
142 return classDeclaration.equals(resolvedOppositeType);
143 }
144
145 private boolean builtinSymbolAwareShouldBeVisible(
146 IEObjectDescription candidate, ContentAssistContext context, EReference eReference,
147 BuiltinSymbols builtinSymbols, EObject candidateEObjectOrProxy) {
148 if (eReference.equals(ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE) &&
149 context.getCurrentModel() instanceof ReferenceDeclaration referenceDeclaration &&
150 (referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT ||
151 referenceDeclaration.getKind() == ReferenceKind.CONTAINER)) {
152 // Containment or container references must have a class type.
153 // We don't support {@code node} as a container or contained type.
154 return ProblemPackage.Literals.CLASS_DECLARATION.isSuperTypeOf(candidate.getEClass()) &&
155 !builtinSymbols.node().equals(candidateEObjectOrProxy);
156 }
157
158 if (eReference.equals(ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE) ||
159 eReference.equals(ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE) ||
160 eReference.equals(ProblemPackage.Literals.TYPE_SCOPE__TARGET_TYPE)) {
161 if (builtinSymbols.exists().equals(candidateEObjectOrProxy)) {
162 return false;
163 }
164 var arity = candidate.getUserData(ProblemResourceDescriptionStrategy.ARITY);
165 return arity == null || arity.equals("1");
166 }
167
168 if (eReference.equals(ProblemPackage.Literals.CLASS_DECLARATION__SUPER_TYPES)) {
169 return supertypeShouldBeVisible(candidate, context, builtinSymbols, candidateEObjectOrProxy);
170 }
171
172 return true;
173 }
174
175 private boolean supertypeShouldBeVisible(IEObjectDescription candidate, ContentAssistContext context,
176 BuiltinSymbols builtinSymbols, EObject candidateEObjectOrProxy) {
177 if (!ProblemPackage.Literals.CLASS_DECLARATION.isSuperTypeOf(candidate.getEClass()) ||
178 builtinSymbols.node().equals(candidateEObjectOrProxy) ||
179 builtinSymbols.contained().equals(candidateEObjectOrProxy)) {
180 return false;
181 }
182 if (context.getCurrentModel() instanceof ClassDeclaration classDeclaration &&
183 candidateEObjectOrProxy instanceof ClassDeclaration candidateClassDeclaration) {
184 return !classDeclaration.equals(candidateClassDeclaration) &&
185 !classDeclaration.getSuperTypes().contains(candidateClassDeclaration);
186 }
187 return true;
188 }
189
190 @Nullable
191 private EReference getEReference(CrossReference crossReference) {
192 var type = currentTypeFinder.findCurrentTypeAfter(crossReference);
193 if (!(type instanceof EClass eClass)) {
194 return null;
195 }
196 return GrammarUtil.getReference(crossReference, eClass);
87 } 197 }
88 198
89 protected EObject getCurrentValue(CrossReference crossRef, ContentAssistContext context) { 199 protected EObject getCurrentValue(CrossReference crossRef, ContentAssistContext context) {
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java
index b3c58366..ecaa7e0d 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java
@@ -63,6 +63,8 @@ public class ModelInitializer {
63 63
64 private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>(); 64 private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>();
65 65
66 private final Set<PartialRelation> targetTypes = new HashSet<>();
67
66 private final MetamodelBuilder metamodelBuilder = Metamodel.builder(); 68 private final MetamodelBuilder metamodelBuilder = Metamodel.builder();
67 69
68 private Metamodel metamodel; 70 private Metamodel metamodel;
@@ -285,6 +287,7 @@ public class ModelInitializer {
285 var relation = getPartialRelation(referenceDeclaration); 287 var relation = getPartialRelation(referenceDeclaration);
286 var source = getPartialRelation(classDeclaration); 288 var source = getPartialRelation(classDeclaration);
287 var target = getPartialRelation(referenceDeclaration.getReferenceType()); 289 var target = getPartialRelation(referenceDeclaration.getReferenceType());
290 targetTypes.add(target);
288 boolean containment = referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT; 291 boolean containment = referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT;
289 var opposite = referenceDeclaration.getOpposite(); 292 var opposite = referenceDeclaration.getOpposite();
290 PartialRelation oppositeRelation = null; 293 PartialRelation oppositeRelation = null;
@@ -473,17 +476,16 @@ public class ModelInitializer {
473 private void collectPredicateDefinition(PredicateDefinition predicateDefinition, ModelStoreBuilder storeBuilder) { 476 private void collectPredicateDefinition(PredicateDefinition predicateDefinition, ModelStoreBuilder storeBuilder) {
474 var partialRelation = getPartialRelation(predicateDefinition); 477 var partialRelation = getPartialRelation(predicateDefinition);
475 var query = toQuery(partialRelation.name(), predicateDefinition); 478 var query = toQuery(partialRelation.name(), predicateDefinition);
476 boolean mutable; 479 boolean mutable = targetTypes.contains(partialRelation);
477 TruthValue defaultValue; 480 TruthValue defaultValue;
478 if (predicateDefinition.isError()) { 481 if (predicateDefinition.isError()) {
479 mutable = false;
480 defaultValue = TruthValue.FALSE; 482 defaultValue = TruthValue.FALSE;
481 } else { 483 } else {
482 var seed = modelSeed.getSeed(partialRelation); 484 var seed = modelSeed.getSeed(partialRelation);
483 defaultValue = seed.reducedValue() == TruthValue.FALSE ? TruthValue.FALSE : TruthValue.UNKNOWN; 485 defaultValue = seed.reducedValue() == TruthValue.FALSE ? TruthValue.FALSE : TruthValue.UNKNOWN;
484 var cursor = seed.getCursor(defaultValue, problemTrace.getNodeTrace().size()); 486 var cursor = seed.getCursor(defaultValue, problemTrace.getNodeTrace().size());
485 // The symbol should be mutable if there is at least one non-default entry in the seed. 487 // The symbol should be mutable if there is at least one non-default entry in the seed.
486 mutable = cursor.move(); 488 mutable = mutable || cursor.move();
487 } 489 }
488 var translator = new PredicateTranslator(partialRelation, query, mutable, defaultValue); 490 var translator = new PredicateTranslator(partialRelation, query, mutable, defaultValue);
489 storeBuilder.with(translator); 491 storeBuilder.with(translator);
diff --git a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java
index 899e3cb3..b4abce81 100644
--- a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java
+++ b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java
@@ -77,7 +77,7 @@ class ModelGenerationTest {
77 % Scope 77 % Scope
78 scope Post = 5, Person = 5. 78 scope Post = 5, Person = 5.
79 """); 79 """);
80 assertThat(parsedProblem.errors(), empty()); 80 assertThat(parsedProblem.getResourceErrors(), empty());
81 var problem = parsedProblem.problem(); 81 var problem = parsedProblem.problem();
82 82
83 var storeBuilder = ModelStore.builder() 83 var storeBuilder = ModelStore.builder()
@@ -211,7 +211,7 @@ class ModelGenerationTest {
211 211
212 scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. 212 scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1.
213 """); 213 """);
214 assertThat(parsedProblem.errors(), empty()); 214 assertThat(parsedProblem.getResourceErrors(), empty());
215 var problem = parsedProblem.problem(); 215 var problem = parsedProblem.problem();
216 216
217 var storeBuilder = ModelStore.builder() 217 var storeBuilder = ModelStore.builder()
@@ -278,7 +278,7 @@ class ModelGenerationTest {
278 278
279 scope Filesystem += 0, Entry = 100. 279 scope Filesystem += 0, Entry = 100.
280 """); 280 """);
281 assertThat(parsedProblem.errors(), empty()); 281 assertThat(parsedProblem.getResourceErrors(), empty());
282 var problem = parsedProblem.problem(); 282 var problem = parsedProblem.problem();
283 283
284 var storeBuilder = ModelStore.builder() 284 var storeBuilder = ModelStore.builder()
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 0a91178b..0fb96954 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
+++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
@@ -40,10 +40,13 @@ enum ReferenceKind:
40ReferenceDeclaration: 40ReferenceDeclaration:
41 (referenceType=[Relation|NonContainmentQualifiedName] | 41 (referenceType=[Relation|NonContainmentQualifiedName] |
42 kind=ReferenceKind referenceType=[Relation|QualifiedName]) 42 kind=ReferenceKind referenceType=[Relation|QualifiedName])
43 ("[" multiplicity=Multiplicity "]")? 43 (multiplicity=ReferenceMultiplicity)?
44 name=Identifier 44 name=Identifier
45 ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; 45 ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?;
46 46
47ReferenceMultiplicity returns Multiplicity:
48 "[" Multiplicity "]";
49
47//enum PrimitiveType: 50//enum PrimitiveType:
48// INT="int" | REAL="real" | STRING="string"; 51// INT="int" | REAL="real" | STRING="string";
49// 52//
diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
index 2636a8ee..0a5cb3c2 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
@@ -12,6 +12,7 @@ package tools.refinery.language;
12import com.google.inject.Binder; 12import com.google.inject.Binder;
13import com.google.inject.name.Names; 13import com.google.inject.name.Names;
14import org.eclipse.xtext.conversion.IValueConverterService; 14import org.eclipse.xtext.conversion.IValueConverterService;
15import org.eclipse.xtext.linking.ILinkingService;
15import org.eclipse.xtext.naming.IQualifiedNameConverter; 16import org.eclipse.xtext.naming.IQualifiedNameConverter;
16import org.eclipse.xtext.parser.IParser; 17import org.eclipse.xtext.parser.IParser;
17import org.eclipse.xtext.resource.*; 18import org.eclipse.xtext.resource.*;
@@ -19,17 +20,21 @@ import org.eclipse.xtext.scoping.IGlobalScopeProvider;
19import org.eclipse.xtext.scoping.IScopeProvider; 20import org.eclipse.xtext.scoping.IScopeProvider;
20import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; 21import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider;
21import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; 22import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
23import org.eclipse.xtext.validation.IDiagnosticConverter;
22import org.eclipse.xtext.validation.IResourceValidator; 24import org.eclipse.xtext.validation.IResourceValidator;
23import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; 25import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator;
24import tools.refinery.language.conversion.ProblemValueConverterService; 26import tools.refinery.language.conversion.ProblemValueConverterService;
27import tools.refinery.language.linking.ProblemLinkingService;
25import tools.refinery.language.naming.ProblemQualifiedNameConverter; 28import tools.refinery.language.naming.ProblemQualifiedNameConverter;
26import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; 29import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser;
27import tools.refinery.language.resource.ProblemDerivedStateComputer; 30import tools.refinery.language.resource.ProblemDerivedStateComputer;
28import tools.refinery.language.resource.ProblemLocationInFileProvider; 31import tools.refinery.language.resource.ProblemLocationInFileProvider;
32import tools.refinery.language.resource.ProblemResource;
29import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; 33import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
30import tools.refinery.language.scoping.ProblemGlobalScopeProvider; 34import tools.refinery.language.scoping.ProblemGlobalScopeProvider;
31import tools.refinery.language.scoping.ProblemLocalScopeProvider; 35import tools.refinery.language.scoping.ProblemLocalScopeProvider;
32import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; 36import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer;
37import tools.refinery.language.validation.ProblemDiagnosticConverter;
33 38
34/** 39/**
35 * Use this class to register components to be used at runtime / without the 40 * Use this class to register components to be used at runtime / without the
@@ -55,6 +60,11 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
55 } 60 }
56 61
57 @Override 62 @Override
63 public Class<? extends ILinkingService> bindILinkingService() {
64 return ProblemLinkingService.class;
65 }
66
67 @Override
58 public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() { 68 public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() {
59 return ProblemGlobalScopeProvider.class; 69 return ProblemGlobalScopeProvider.class;
60 } 70 }
@@ -67,7 +77,7 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
67 77
68 @Override 78 @Override
69 public Class<? extends XtextResource> bindXtextResource() { 79 public Class<? extends XtextResource> bindXtextResource() {
70 return DerivedStateAwareResource.class; 80 return ProblemResource.class;
71 } 81 }
72 82
73 // Method name follows Xtext convention. 83 // Method name follows Xtext convention.
@@ -93,4 +103,8 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
93 public Class<? extends ISemanticSequencer> bindISemanticSequencer() { 103 public Class<? extends ISemanticSequencer> bindISemanticSequencer() {
94 return PreferShortAssertionsProblemSemanticSequencer.class; 104 return PreferShortAssertionsProblemSemanticSequencer.class;
95 } 105 }
106
107 public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() {
108 return ProblemDiagnosticConverter.class;
109 }
96} 110}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java b/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java
new file mode 100644
index 00000000..511ed420
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java
@@ -0,0 +1,72 @@
1/*******************************************************************************
2 * Copyright (c) 2008, 2018 itemis AG (http://www.itemis.eu) and others.
3 * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/>
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License 2.0 which is available at
6 * http://www.eclipse.org/legal/epl-2.0.
7 * SPDX-License-Identifier: EPL-2.0
8 *******************************************************************************/
9package tools.refinery.language.linking;
10
11import com.google.inject.Inject;
12import org.apache.log4j.Logger;
13import org.eclipse.emf.ecore.EClass;
14import org.eclipse.emf.ecore.EObject;
15import org.eclipse.emf.ecore.EReference;
16import org.eclipse.xtext.linking.impl.DefaultLinkingService;
17import org.eclipse.xtext.linking.impl.IllegalNodeException;
18import org.eclipse.xtext.naming.IQualifiedNameConverter;
19import org.eclipse.xtext.naming.QualifiedName;
20import org.eclipse.xtext.nodemodel.INode;
21import org.eclipse.xtext.resource.IEObjectDescription;
22import org.eclipse.xtext.scoping.IScope;
23
24import java.util.*;
25
26public class ProblemLinkingService extends DefaultLinkingService {
27 @Inject
28 private IQualifiedNameConverter qualifiedNameConverter;
29
30 private static final Logger logger = Logger.getLogger(ProblemLinkingService.class);
31
32 @Override
33 public List<EObject> getLinkedObjects(EObject context, EReference ref, INode node) throws IllegalNodeException {
34 final EClass requiredType = ref.getEReferenceType();
35 if (requiredType == null) {
36 return List.of();
37 }
38 final String crossRefString = getCrossRefNodeAsString(node);
39 if (crossRefString == null || crossRefString.isEmpty()) {
40 return List.of();
41 }
42 if (logger.isDebugEnabled()) {
43 logger.debug("before getLinkedObjects: node: '%s'".formatted(crossRefString));
44 }
45 final IScope scope = getScope(context, ref);
46 if (scope == null) {
47 throw new AssertionError(("Scope provider must not return null for context %s, reference %s! Consider to" +
48 " return IScope.NULLSCOPE instead.").formatted(context, ref));
49 }
50 final QualifiedName qualifiedLinkName = qualifiedNameConverter.toQualifiedName(crossRefString);
51 final Iterator<IEObjectDescription> iterator = scope.getElements(qualifiedLinkName).iterator();
52 StringBuilder debug = null;
53 final Set<EObject> result = new LinkedHashSet<>();
54 if (logger.isDebugEnabled()) {
55 debug = new StringBuilder()
56 .append("after getLinkedObjects: node: '")
57 .append(crossRefString)
58 .append("' result: ");
59 }
60 while (iterator.hasNext()) {
61 var eObjectDescription = iterator.next();
62 if (debug != null) {
63 debug.append(eObjectDescription).append(", ");
64 }
65 result.add(eObjectDescription.getEObjectOrProxy());
66 }
67 if (debug != null) {
68 logger.debug(debug);
69 }
70 return List.copyOf(result);
71 }
72}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java
new file mode 100644
index 00000000..43239ffe
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java
@@ -0,0 +1,263 @@
1/*******************************************************************************
2 * Copyright (c) 2008, 2023 itemis AG (http://www.itemis.eu) and others.
3 * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/>
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License 2.0 which is available at
6 * http://www.eclipse.org/legal/epl-2.0.
7 * SPDX-License-Identifier: EPL-2.0
8 *******************************************************************************/
9package tools.refinery.language.resource;
10
11import com.google.inject.Inject;
12import org.apache.log4j.Logger;
13import org.eclipse.emf.ecore.EObject;
14import org.eclipse.emf.ecore.EReference;
15import org.eclipse.xtext.EcoreUtil2;
16import org.eclipse.xtext.diagnostics.DiagnosticMessage;
17import org.eclipse.xtext.diagnostics.Severity;
18import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider;
19import org.eclipse.xtext.linking.impl.IllegalNodeException;
20import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic;
21import org.eclipse.xtext.linking.lazy.LazyLinkingResource;
22import org.eclipse.xtext.nodemodel.INode;
23import org.eclipse.xtext.resource.DerivedStateAwareResource;
24import org.eclipse.xtext.util.Triple;
25import org.jetbrains.annotations.Nullable;
26
27import java.util.Arrays;
28import java.util.List;
29import java.util.Objects;
30import java.util.Set;
31
32public class ProblemResource extends DerivedStateAwareResource {
33 private static final Logger log = Logger.getLogger(ProblemResource.class);
34
35 @Inject
36 private ILinkingDiagnosticMessageProvider.Extended linkingDiagnosticMessageProvider;
37
38 /**
39 * Our own version of this field, because the original is not accessible.
40 */
41 private int cyclicLinkingDetectionCounter = 0;
42
43 /**
44 * Tries to resolve a reference and emits a diagnostic if the reference is unresolvable or ambiguous.
45 * <p>
46 * This method was copied from {@link LazyLinkingResource#getEObject(String, Triple)}, but we modified it to also
47 * handle ambiguous references.
48 *
49 * @param uriFragment The URI fragment to resolve.
50 * @param triple The linking triple.
51 * @return The resolved {@link EObject}.
52 * @throws AssertionError If the URI fragment is unresolvable.
53 */
54 @Override
55 protected EObject getEObject(String uriFragment, Triple<EObject, EReference, INode> triple) throws AssertionError {
56 cyclicLinkingDetectionCounter++;
57 if (cyclicLinkingDetectionCounter > cyclicLinkingDectectionCounterLimit && !resolving.add(triple)) {
58 return handleCyclicResolution(triple);
59 }
60 try {
61 Set<String> unresolvableProxies = getUnresolvableURIFragments();
62 if (unresolvableProxies.contains(uriFragment)) {
63 return null;
64 }
65 var result = doGetEObject(triple);
66 if (result == null) {
67 if (isUnresolveableProxyCacheable(triple)) {
68 unresolvableProxies.add(uriFragment);
69 }
70 } else {
71 // remove previously added error markers, since everything should be fine now
72 unresolvableProxies.remove(uriFragment);
73 }
74 return result;
75 } catch (IllegalNodeException e) {
76 createAndAddDiagnostic(triple, e);
77 return null;
78 } finally {
79 if (cyclicLinkingDetectionCounter > cyclicLinkingDectectionCounterLimit) {
80 resolving.remove(triple);
81 }
82 cyclicLinkingDetectionCounter--;
83 }
84 }
85
86 @Nullable
87 private EObject doGetEObject(Triple<EObject, EReference, INode> triple) {
88 EReference reference = triple.getSecond();
89 try {
90 List<EObject> linkedObjects = getLinkingService().getLinkedObjects(triple.getFirst(), reference,
91 triple.getThird());
92 if (linkedObjects.isEmpty()) {
93 createAndAddDiagnostic(triple);
94 return null;
95 }
96 if (linkedObjects.size() > 1) {
97 createAndAddAmbiguousReferenceDiagnostic(triple);
98 return null;
99 }
100 EObject result = linkedObjects.get(0);
101 if (!EcoreUtil2.isAssignableFrom(reference.getEReferenceType(), result.eClass())) {
102 log.error("An element of type %s is not assignable to the reference %s.%s".formatted(
103 result.getClass().getName(), reference.getEContainingClass().getName(), reference.getName()));
104 createAndAddDiagnostic(triple);
105 return null;
106 }
107 removeDiagnostic(triple);
108 return result;
109 } catch (CyclicLinkingError e) {
110 if (e.triple.equals(triple)) {
111 log.error(e.getMessage(), e);
112 createAndAddDiagnostic(triple);
113 return null;
114 } else {
115 throw e;
116 }
117 }
118 }
119
120 @Override
121 protected EObject handleCyclicResolution(Triple<EObject, EReference, INode> triple) throws AssertionError {
122 // Throw our own version of {@link LazyLinkingResource.CyclicLinkingException}.
123 throw new CyclicLinkingError("Cyclic resolution of lazy links : %s in resource '%s'.".formatted(
124 getReferences(triple, resolving), getURI()), triple);
125 }
126
127 @Override
128 protected void createAndAddDiagnostic(Triple<EObject, EReference, INode> triple) {
129 if (isValidationDisabled()) {
130 return;
131 }
132 DiagnosticMessage message = createDiagnosticMessage(triple);
133 addOrReplaceDiagnostic(triple, message);
134 }
135
136 @Override
137 protected void createAndAddDiagnostic(Triple<EObject, EReference, INode> triple, IllegalNodeException ex) {
138 if (isValidationDisabled()) {
139 return;
140 }
141 ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext context = createDiagnosticMessageContext(triple);
142 DiagnosticMessage message = linkingDiagnosticMessageProvider.getIllegalNodeMessage(context, ex);
143 addOrReplaceDiagnostic(triple, message);
144 }
145
146 protected void createAndAddAmbiguousReferenceDiagnostic(Triple<EObject, EReference, INode> triple) {
147 if (isValidationDisabled()) {
148 return;
149 }
150 var context = createDiagnosticMessageContext(triple);
151 var typeName = context.getReference().getEReferenceType().getName();
152 String linkText = "";
153 try {
154 linkText = context.getLinkText();
155 } catch (IllegalNodeException e) {
156 linkText = e.getNode().getText();
157 }
158 var messageString = "Ambiguous reference to %s '%s'.".formatted(typeName, linkText);
159 var message = new DiagnosticMessage(messageString, Severity.ERROR,
160 org.eclipse.xtext.diagnostics.Diagnostic.LINKING_DIAGNOSTIC);
161 addOrReplaceDiagnostic(triple, message);
162 }
163
164 /**
165 * Adds a diagnostic message while maintaining the invariant that at most one
166 * {@link ProblemResourceLinkingDiagnostic} is added to the {@link #getErrors()} list.
167 *
168 * @param triple The triple to add the diagnostic for.
169 * @param message The diagnostic message. Must have {@link Severity#ERROR}.
170 */
171 protected void addOrReplaceDiagnostic(Triple<EObject, EReference, INode> triple, DiagnosticMessage message) {
172 if (message == null) {
173 return;
174 }
175 if (message.getSeverity() != Severity.ERROR) {
176 throw new IllegalArgumentException("Only linking diagnostics of ERROR severity are supported");
177 }
178 var list = getDiagnosticList(message);
179 var iterator = list.iterator();
180 while (iterator.hasNext()) {
181 var diagnostic = iterator.next();
182 if (diagnostic instanceof ProblemResourceLinkingDiagnostic linkingDiagnostic &&
183 linkingDiagnostic.matchesNode(triple.getThird())) {
184 if (linkingDiagnostic.matchesMessage(message)) {
185 return;
186 }
187 iterator.remove();
188 break;
189 }
190 }
191 var diagnostic = createDiagnostic(triple, message);
192 list.add(diagnostic);
193 }
194
195 /**
196 * Removes the {@link ProblemResourceLinkingDiagnostic} corresponding to the given node, if prevesent, from the
197 * {@link #getErrors()} list.
198 *
199 * @param triple The triple to add the diagnostic for.
200 */
201 @Override
202 protected void removeDiagnostic(Triple<EObject, EReference, INode> triple) {
203 if (getErrors().isEmpty()) {
204 return;
205 }
206 var list = getErrors();
207 if (list.isEmpty()) {
208 return;
209 }
210 var iterator = list.iterator();
211 while (iterator.hasNext()) {
212 var diagnostic = iterator.next();
213 if (diagnostic instanceof ProblemResourceLinkingDiagnostic linkingDiagnostic &&
214 linkingDiagnostic.matchesNode(triple.getThird())) {
215 iterator.remove();
216 return;
217 }
218 }
219 }
220
221 @Override
222 protected Diagnostic createDiagnostic(Triple<EObject, EReference, INode> triple, DiagnosticMessage message) {
223 return new ProblemResourceLinkingDiagnostic(triple.getThird(), message.getMessage(),
224 message.getIssueCode(), message.getIssueData());
225 }
226
227 /**
228 * Our own version of {@link LazyLinkingResource.CyclicLinkingException}, because the {@code tripe} field in the
229 * original one is not accessible.
230 * <p>
231 * Renamed from {@code CyclicLinkingException} to satisfy naming conventions enforced by Sonar.
232 */
233 public static final class CyclicLinkingError extends AssertionError {
234 private final transient Triple<EObject, EReference, INode> triple;
235
236 private CyclicLinkingError(Object detailMessage, Triple<EObject, EReference, INode> triple) {
237 super(detailMessage);
238 this.triple = triple;
239 }
240 }
241
242 /**
243 * Marks all diagnostics inserted by {@link ProblemResource} with a common superclass so that they can
244 * later be removed.
245 * <p>
246 * We have to inherit from {@link XtextLinkingDiagnostic} to access the protected function {@link #getNode()}.
247 */
248 protected static class ProblemResourceLinkingDiagnostic extends XtextLinkingDiagnostic {
249 public ProblemResourceLinkingDiagnostic(INode node, String message, String code, String... data) {
250 super(node, message, code, data);
251 }
252
253 public boolean matchesNode(INode node) {
254 return Objects.equals(getNode(), node);
255 }
256
257 public boolean matchesMessage(DiagnosticMessage message) {
258 return Objects.equals(getMessage(), message.getMessage()) &&
259 Objects.equals(getCode(), message.getIssueCode()) &&
260 Arrays.equals(getData(), message.getIssueData());
261 }
262 }
263}
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 a16f77eb..cac1f265 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
@@ -24,8 +24,9 @@ import java.util.Map;
24 24
25@Singleton 25@Singleton
26public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { 26public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy {
27 public static final String ERROR_PREDICATE = "tools.refinery.language.resource" + 27 private static final String DATA_PREFIX = "tools.refinery.language.resource.ProblemResourceDescriptionStrategy.";
28 ".ProblemResourceDescriptionStrategy.ERROR_PREDICATE"; 28 public static final String ARITY = DATA_PREFIX + "ARITY";
29 public static final String ERROR_PREDICATE = DATA_PREFIX + "ERROR_PREDICATE";
29 public static final String ERROR_PREDICATE_TRUE = "true"; 30 public static final String ERROR_PREDICATE_TRUE = "true";
30 31
31 @Inject 32 @Inject
@@ -97,6 +98,10 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti
97 98
98 protected Map<String, String> getUserData(EObject eObject) { 99 protected Map<String, String> getUserData(EObject eObject) {
99 var builder = ImmutableMap.<String, String>builder(); 100 var builder = ImmutableMap.<String, String>builder();
101 if (eObject instanceof Relation relation) {
102 int arity = ProblemUtil.getArity(relation);
103 builder.put(ARITY, Integer.toString(arity));
104 }
100 if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { 105 if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) {
101 builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); 106 builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE);
102 } 107 }
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 cf099aba..a4437ba6 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
@@ -44,7 +44,7 @@ public class ProblemScopeProvider extends AbstractProblemScopeProvider {
44 return getVariableScope(context, scope); 44 return getVariableScope(context, scope);
45 } 45 }
46 if (reference == ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) { 46 if (reference == ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) {
47 return getOppositeScope(context, scope); 47 return getOppositeScope(context);
48 } 48 }
49 return scope; 49 return scope;
50 } 50 }
@@ -96,16 +96,19 @@ public class ProblemScopeProvider extends AbstractProblemScopeProvider {
96 } 96 }
97 } 97 }
98 98
99 protected IScope getOppositeScope(EObject context, IScope delegateScope) { 99 protected IScope getOppositeScope(EObject context) {
100 var referenceDeclaration = EcoreUtil2.getContainerOfType(context, ReferenceDeclaration.class); 100 var referenceDeclaration = EcoreUtil2.getContainerOfType(context, ReferenceDeclaration.class);
101 if (referenceDeclaration == null) { 101 if (referenceDeclaration == null) {
102 return delegateScope; 102 return IScope.NULLSCOPE;
103 } 103 }
104 var relation = referenceDeclaration.getReferenceType(); 104 var relation = referenceDeclaration.getReferenceType();
105 if (!(relation instanceof ClassDeclaration classDeclaration)) { 105 if (!(relation instanceof ClassDeclaration classDeclaration)) {
106 return delegateScope; 106 return IScope.NULLSCOPE;
107 } 107 }
108 var referenceDeclarations = desugarer.getAllReferenceDeclarations(classDeclaration); 108 var referenceDeclarations = classDeclaration.getFeatureDeclarations()
109 return Scopes.scopeFor(referenceDeclarations, delegateScope); 109 .stream()
110 .filter(ReferenceDeclaration.class::isInstance)
111 .toList();
112 return Scopes.scopeFor(referenceDeclarations);
110 } 113 }
111} 114}
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 a9efc4bb..7b6407e1 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
@@ -7,11 +7,12 @@ package tools.refinery.language.utils;
7 7
8import org.eclipse.emf.common.util.URI; 8import org.eclipse.emf.common.util.URI;
9import org.eclipse.emf.ecore.EObject; 9import org.eclipse.emf.ecore.EObject;
10import org.eclipse.emf.ecore.util.EcoreUtil;
10import tools.refinery.language.model.problem.*; 11import tools.refinery.language.model.problem.*;
11 12
12public final class ProblemUtil { 13public final class ProblemUtil {
13 public static final String BUILTIN_LIBRARY_NAME = "builtin"; 14 public static final String BUILTIN_LIBRARY_NAME = "builtin";
14 public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME); 15 public static final URI BUILTIN_LIBRARY_URI = getLibraryUri();
15 16
16 private ProblemUtil() { 17 private ProblemUtil() {
17 throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); 18 throw new IllegalStateException("This is a static utility class and should not be instantiated directly");
@@ -83,8 +84,44 @@ public final class ProblemUtil {
83 return true; 84 return true;
84 } 85 }
85 86
86 private static URI getLibraryUri(String libraryName) { 87 public static int getArity(Relation relation) {
87 return URI.createURI(ProblemUtil.class.getClassLoader() 88 if (relation instanceof ClassDeclaration || relation instanceof EnumDeclaration) {
88 .getResource("tools/refinery/language/%s.problem".formatted(libraryName)).toString()); 89 return 1;
90 }
91 if (relation instanceof ReferenceDeclaration) {
92 return 2;
93 }
94 if (relation instanceof PredicateDefinition predicateDefinition) {
95 return predicateDefinition.getParameters().size();
96 }
97 throw new IllegalArgumentException("Unknown Relation: " + relation);
98 }
99
100 public static boolean isContainerReference(ReferenceDeclaration referenceDeclaration) {
101 var kind = referenceDeclaration.getKind();
102 if (kind == null) {
103 return false;
104 }
105 return switch (kind) {
106 case CONTAINMENT -> false;
107 case CONTAINER -> true;
108 case REFERENCE -> {
109 var opposite = referenceDeclaration.getOpposite();
110 if (opposite == null) {
111 yield false;
112 }
113 opposite = (ReferenceDeclaration) EcoreUtil.resolve(opposite, referenceDeclaration);
114 yield opposite.getKind() == ReferenceKind.CONTAINMENT;
115 }
116 };
117 }
118
119 private static URI getLibraryUri() {
120 var libraryResource = ProblemUtil.class.getClassLoader()
121 .getResource("tools/refinery/language/%s.problem".formatted(BUILTIN_LIBRARY_NAME));
122 if (libraryResource == null) {
123 throw new AssertionError("Library '%s' was not found".formatted(BUILTIN_LIBRARY_NAME));
124 }
125 return URI.createURI(libraryResource.toString());
89 } 126 }
90} 127}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java
new file mode 100644
index 00000000..0b7cc315
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java
@@ -0,0 +1,31 @@
1/*
2 * SPDX-FileCopyrightText: 2023 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 org.eclipse.emf.ecore.EObject;
10import org.eclipse.emf.ecore.EStructuralFeature;
11import org.eclipse.xtext.validation.DiagnosticConverterImpl;
12import tools.refinery.language.model.problem.Multiplicity;
13import tools.refinery.language.model.problem.ReferenceDeclaration;
14import tools.refinery.language.services.ProblemGrammarAccess;
15
16public class ProblemDiagnosticConverter extends DiagnosticConverterImpl {
17 @Inject
18 private ProblemGrammarAccess grammarAccess;
19
20 @Override
21 protected IssueLocation getLocationData(EObject obj, EStructuralFeature structuralFeature, int index) {
22 if (structuralFeature == null && obj instanceof Multiplicity &&
23 obj.eContainer() instanceof ReferenceDeclaration referenceDeclaration) {
24 // Include the enclosing {@code []} square braces in the error location.
25 // This lets use have a non-0 length error marker for invalid container references such as
26 // {@code container Foo[] foo opposite bar}, where unbounded multiplicities are disallowed.
27 return getLocationData(referenceDeclaration, obj.eContainingFeature());
28 }
29 return super.getLocationData(obj, structuralFeature, index);
30 }
31}
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 88d50c5b..8bda4b95 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
@@ -9,15 +9,21 @@
9 */ 9 */
10package tools.refinery.language.validation; 10package tools.refinery.language.validation;
11 11
12import com.google.inject.Inject;
13import org.eclipse.emf.ecore.EObject;
14import org.eclipse.emf.ecore.EReference;
12import org.eclipse.xtext.EcoreUtil2; 15import org.eclipse.xtext.EcoreUtil2;
13import org.eclipse.xtext.validation.Check; 16import org.eclipse.xtext.validation.Check;
14 17import org.jetbrains.annotations.Nullable;
15import com.google.inject.Inject;
16
17import tools.refinery.language.model.problem.*; 18import tools.refinery.language.model.problem.*;
18import tools.refinery.language.resource.ReferenceCounter; 19import tools.refinery.language.utils.ProblemDesugarer;
19import tools.refinery.language.utils.ProblemUtil; 20import tools.refinery.language.utils.ProblemUtil;
20 21
22import java.util.ArrayList;
23import java.util.LinkedHashMap;
24import java.util.LinkedHashSet;
25import java.util.Set;
26
21/** 27/**
22 * This class contains custom validation rules. 28 * This class contains custom validation rules.
23 * <p> 29 * <p>
@@ -29,13 +35,38 @@ public class ProblemValidator extends AbstractProblemValidator {
29 35
30 public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; 36 public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE";
31 37
32 public static final String NON_INDIVIDUAL_NODE_ISSUE = ISSUE_PREFIX + "NON_INDIVIDUAL_NODE"; 38 public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE";
39
40 public static final String DUPLICATE_NAME_ISSUE = ISSUE_PREFIX + "DUPLICATE_NAME";
41
42 public static final String INVALID_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "INVALID_MULTIPLICITY";
43
44 public static final String ZERO_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "ZERO_MULTIPLICITY";
45
46 public static final String MISSING_OPPOSITE_ISSUE = ISSUE_PREFIX + "MISSING_OPPOSITE";
47
48 public static final String INVALID_OPPOSITE_ISSUE = ISSUE_PREFIX + "INVALID_OPPOSITE";
49
50 public static final String INVALID_SUPERTYPE_ISSUE = ISSUE_PREFIX + "INVALID_SUPERTYPE";
51
52 public static final String INVALID_REFERENCE_TYPE_ISSUE = ISSUE_PREFIX + "INVALID_REFERENCE_TYPE";
53
54 public static final String INVALID_ARITY_ISSUE = ISSUE_PREFIX + "INVALID_ARITY";
55
56 public static final String INVALID_TRANSITIVE_CLOSURE_ISSUE = ISSUE_PREFIX + "INVALID_TRANSITIVE_CLOSURE";
57
58 public static final String INVALID_VALUE_ISSUE = ISSUE_PREFIX + "INVALID_VALUE";
59
60 public static final String UNSUPPORTED_ASSERTION_ISSUE = ISSUE_PREFIX + "UNSUPPORTED_ASSERTION";
33 61
34 @Inject 62 @Inject
35 private ReferenceCounter referenceCounter; 63 private ReferenceCounter referenceCounter;
36 64
65 @Inject
66 private ProblemDesugarer desugarer;
67
37 @Check 68 @Check
38 public void checkUniqueVariable(VariableOrNodeExpr expr) { 69 public void checkSingletonVariable(VariableOrNodeExpr expr) {
39 var variableOrNode = expr.getVariableOrNode(); 70 var variableOrNode = expr.getVariableOrNode();
40 if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) 71 if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable)
41 && !ProblemUtil.isSingletonVariable(variable)) { 72 && !ProblemUtil.isSingletonVariable(variable)) {
@@ -51,14 +82,325 @@ public class ProblemValidator extends AbstractProblemValidator {
51 } 82 }
52 83
53 @Check 84 @Check
54 public void checkNonUniqueNode(VariableOrNodeExpr expr) { 85 public void checkNodeConstants(VariableOrNodeExpr expr) {
55 var variableOrNode = expr.getVariableOrNode(); 86 var variableOrNode = expr.getVariableOrNode();
56 if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) { 87 if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) {
57 var name = node.getName(); 88 var name = node.getName();
58 var message = ("Only individual nodes can be referenced in predicates. " + 89 var message = ("Only individuals can be referenced in predicates. " +
59 "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name); 90 "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name);
60 error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, 91 error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE,
61 INSIGNIFICANT_INDEX, NON_INDIVIDUAL_NODE_ISSUE); 92 INSIGNIFICANT_INDEX, NODE_CONSTANT_ISSUE);
93 }
94 }
95
96 @Check
97 public void checkUniqueDeclarations(Problem problem) {
98 var relations = new ArrayList<Relation>();
99 var individuals = new ArrayList<Node>();
100 for (var statement : problem.getStatements()) {
101 if (statement instanceof Relation relation) {
102 relations.add(relation);
103 } else if (statement instanceof IndividualDeclaration individualDeclaration) {
104 individuals.addAll(individualDeclaration.getNodes());
105 }
106 }
107 checkUniqueSimpleNames(relations);
108 checkUniqueSimpleNames(individuals);
109 }
110
111 @Check
112 public void checkUniqueFeatures(ClassDeclaration classDeclaration) {
113 checkUniqueSimpleNames(classDeclaration.getFeatureDeclarations());
114 }
115
116 @Check
117 public void checkUniqueLiterals(EnumDeclaration enumDeclaration) {
118 checkUniqueSimpleNames(enumDeclaration.getLiterals());
119 }
120
121 protected void checkUniqueSimpleNames(Iterable<? extends NamedElement> namedElements) {
122 var names = new LinkedHashMap<String, Set<NamedElement>>();
123 for (var namedElement : namedElements) {
124 var name = namedElement.getName();
125 var objectsWithName = names.computeIfAbsent(name, ignored -> new LinkedHashSet<>());
126 objectsWithName.add(namedElement);
127 }
128 for (var entry : names.entrySet()) {
129 var objectsWithName = entry.getValue();
130 if (objectsWithName.size() <= 1) {
131 continue;
132 }
133 var name = entry.getKey();
134 var message = "Duplicate name '%s'.".formatted(name);
135 for (var namedElement : objectsWithName) {
136 acceptError(message, namedElement, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0,
137 DUPLICATE_NAME_ISSUE);
138 }
139 }
140 }
141
142 @Check
143 public void checkRangeMultiplicity(RangeMultiplicity rangeMultiplicity) {
144 int lower = rangeMultiplicity.getLowerBound();
145 int upper = rangeMultiplicity.getUpperBound();
146 if (upper >= 0 && lower > upper) {
147 var message = "Multiplicity range [%d..%d] is inconsistent.";
148 acceptError(message, rangeMultiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE);
149 }
150 }
151
152 @Check
153 public void checkReferenceMultiplicity(ReferenceDeclaration referenceDeclaration) {
154 var multiplicity = referenceDeclaration.getMultiplicity();
155 if (multiplicity == null) {
156 return;
157 }
158 if (ProblemUtil.isContainerReference(referenceDeclaration) && (
159 !(multiplicity instanceof RangeMultiplicity rangeMultiplicity) ||
160 rangeMultiplicity.getLowerBound() != 0 ||
161 rangeMultiplicity.getUpperBound() != 1)) {
162 var message = "The only allowed multiplicity for container references is [0..1]";
163 acceptError(message, multiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE);
164 }
165 if ((multiplicity instanceof ExactMultiplicity exactMultiplicity &&
166 exactMultiplicity.getExactValue() == 0) ||
167 (multiplicity instanceof RangeMultiplicity rangeMultiplicity &&
168 rangeMultiplicity.getLowerBound() == 0 &&
169 rangeMultiplicity.getUpperBound() == 0)) {
170 var message = "The multiplicity constraint does not allow any reference links";
171 acceptWarning(message, multiplicity, null, 0, ZERO_MULTIPLICITY_ISSUE);
172 }
173 }
174
175 @Check
176 public void checkOpposite(ReferenceDeclaration referenceDeclaration) {
177 var opposite = referenceDeclaration.getOpposite();
178 if (opposite == null || opposite.eIsProxy()) {
179 return;
180 }
181 var oppositeOfOpposite = opposite.getOpposite();
182 if (oppositeOfOpposite == null) {
183 acceptError("Reference '%s' does not declare '%s' as an opposite."
184 .formatted(opposite.getName(), referenceDeclaration.getName()),
185 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
186 INVALID_OPPOSITE_ISSUE);
187 var oppositeResource = opposite.eResource();
188 if (oppositeResource != null && oppositeResource.equals(referenceDeclaration.eResource())) {
189 acceptError("Missing opposite '%s' for reference '%s'."
190 .formatted(referenceDeclaration.getName(), opposite.getName()),
191 opposite, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE);
192 }
193 return;
194 }
195 if (!referenceDeclaration.equals(oppositeOfOpposite)) {
196 var messageBuilder = new StringBuilder()
197 .append("Expected reference '")
198 .append(opposite.getName())
199 .append("' to have opposite '")
200 .append(referenceDeclaration.getName())
201 .append("'");
202 var oppositeOfOppositeName = oppositeOfOpposite.getName();
203 if (oppositeOfOppositeName != null) {
204 messageBuilder.append(", got '")
205 .append(oppositeOfOppositeName)
206 .append("' instead");
207 }
208 messageBuilder.append(".");
209 acceptError(messageBuilder.toString(), referenceDeclaration,
210 ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, INVALID_OPPOSITE_ISSUE);
211 }
212 }
213
214 @Check
215 public void checkContainerOpposite(ReferenceDeclaration referenceDeclaration) {
216 var kind = referenceDeclaration.getKind();
217 var opposite = referenceDeclaration.getOpposite();
218 if (opposite != null && opposite.eIsProxy()) {
219 // If {@code opposite} is a proxy, we have already emitted a linker error.
220 return;
221 }
222 if (kind == ReferenceKind.CONTAINMENT) {
223 if (opposite != null && opposite.getKind() == ReferenceKind.CONTAINMENT) {
224 acceptError("Opposite '%s' of containment reference '%s' is not a container reference."
225 .formatted(opposite.getName(), referenceDeclaration.getName()),
226 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
227 INVALID_OPPOSITE_ISSUE);
228 }
229 } else if (kind == ReferenceKind.CONTAINER) {
230 if (opposite == null) {
231 acceptError("Container reference '%s' requires an opposite.".formatted(referenceDeclaration.getName()),
232 referenceDeclaration, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE);
233 } else if (opposite.getKind() != ReferenceKind.CONTAINMENT) {
234 acceptError("Opposite '%s' of container reference '%s' is not a containment reference."
235 .formatted(opposite.getName(), referenceDeclaration.getName()),
236 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
237 INVALID_OPPOSITE_ISSUE);
238 }
239 }
240 }
241
242 @Check
243 public void checkSupertypes(ClassDeclaration classDeclaration) {
244 var supertypes = classDeclaration.getSuperTypes();
245 int supertypeCount = supertypes.size();
246 for (int i = 0; i < supertypeCount; i++) {
247 var supertype = supertypes.get(i);
248 if (!supertype.eIsProxy() && !(supertype instanceof ClassDeclaration)) {
249 var message = "Supertype '%s' of '%s' is not a class."
250 .formatted(supertype.getName(), classDeclaration.getName());
251 acceptError(message, classDeclaration, ProblemPackage.Literals.CLASS_DECLARATION__SUPER_TYPES, i,
252 INVALID_SUPERTYPE_ISSUE);
253 }
254 }
255 }
256
257 @Check
258 public void checkReferenceType(ReferenceDeclaration referenceDeclaration) {
259 if (referenceDeclaration.getKind() == ReferenceKind.REFERENCE &&
260 !ProblemUtil.isContainerReference(referenceDeclaration)) {
261 checkArity(referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE, 1);
262 return;
263 }
264 var referenceType = referenceDeclaration.getReferenceType();
265 if (referenceType == null || referenceType.eIsProxy() || referenceType instanceof ClassDeclaration) {
266 // Either correct, or a missing reference type where we are probably already emitting another error.
267 return;
268 }
269 var message = "Reference type '%s' of the containment or container reference '%s' is not a class."
270 .formatted(referenceType.getName(), referenceDeclaration.getName());
271 acceptError(message, referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE, 0,
272 INVALID_REFERENCE_TYPE_ISSUE);
273 }
274
275 @Check
276 public void checkParameterType(Parameter parameter) {
277 checkArity(parameter, ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE, 1);
278 }
279
280 @Check
281 public void checkAtom(Atom atom) {
282 int argumentCount = atom.getArguments().size();
283 checkArity(atom, ProblemPackage.Literals.ATOM__RELATION, argumentCount);
284 if (atom.isTransitiveClosure() && argumentCount != 2) {
285 var message = "Transitive closure needs exactly 2 arguments, got %d arguments instead."
286 .formatted(argumentCount);
287 acceptError(message, atom, ProblemPackage.Literals.ATOM__TRANSITIVE_CLOSURE, 0,
288 INVALID_TRANSITIVE_CLOSURE_ISSUE);
289 }
290 }
291
292 @Check
293 public void checkAssertion(Assertion assertion) {
294 int argumentCount = assertion.getArguments().size();
295 if (!(assertion.getValue() instanceof LogicConstant)) {
296 var message = "Assertion value must be one of 'true', 'false', 'unknown', or 'error'.";
297 acceptError(message, assertion, ProblemPackage.Literals.ASSERTION__VALUE, 0, INVALID_VALUE_ISSUE);
298 }
299 checkArity(assertion, ProblemPackage.Literals.ASSERTION__RELATION, argumentCount);
300 }
301
302 @Check
303 public void checkTypeScope(TypeScope typeScope) {
304 checkArity(typeScope, ProblemPackage.Literals.TYPE_SCOPE__TARGET_TYPE, 1);
305 }
306
307 private void checkArity(EObject eObject, EReference reference, int expectedArity) {
308 var value = eObject.eGet(reference);
309 if (!(value instanceof Relation relation) || relation.eIsProxy()) {
310 // Feature does not point to a {@link Relation}, we are probably already emitting another error.
311 return;
312 }
313 int arity = ProblemUtil.getArity(relation);
314 if (arity == expectedArity) {
315 return;
316 }
317 var message = "Expected symbol '%s' to have arity %d, got arity %d instead."
318 .formatted(relation.getName(), expectedArity, arity);
319 acceptError(message, eObject, reference, 0, INVALID_ARITY_ISSUE);
320 }
321
322 @Check
323 public void checkMultiObjectAssertion(Assertion assertion) {
324 var builtinSymbolsOption = desugarer.getBuiltinSymbols(assertion);
325 if (builtinSymbolsOption.isEmpty()) {
326 return;
327 }
328 var builtinSymbols = builtinSymbolsOption.get();
329 var relation = assertion.getRelation();
330 boolean isExists = builtinSymbols.exists().equals(relation);
331 boolean isEquals = builtinSymbols.equals().equals(relation);
332 if ((!isExists && !isEquals) || !(assertion.getValue() instanceof LogicConstant logicConstant)) {
333 return;
334 }
335 var value = logicConstant.getLogicValue();
336 if (assertion.isDefault()) {
337 acceptError("Default assertions for 'exists' and 'equals' are not supported.", assertion,
338 ProblemPackage.Literals.ASSERTION__DEFAULT, 0, UNSUPPORTED_ASSERTION_ISSUE);
339 return;
340 }
341 if (value == LogicValue.ERROR) {
342 acceptError("Error assertions for 'exists' and 'equals' are not supported.", assertion,
343 ProblemPackage.Literals.ASSERTION__DEFAULT, 0, UNSUPPORTED_ASSERTION_ISSUE);
344 return;
345 }
346 if (isExists) {
347 checkExistsAssertion(assertion, value);
348 return;
349 }
350 checkEqualsAssertion(assertion, value);
351 }
352
353 private void checkExistsAssertion(Assertion assertion, LogicValue value) {
354 if (value == LogicValue.TRUE || value == LogicValue.UNKNOWN) {
355 // {@code true} is always a valid value for {@code exists}, while {@code unknown} values may always be
356 // refined to {@code true} if necessary (e.g., for individual nodes).
357 return;
358 }
359 var arguments = assertion.getArguments();
360 if (arguments.isEmpty()) {
361 // We already report an error on invalid arity.
362 return;
363 }
364 var node = getNodeArgumentForMultiObjectAssertion(arguments.get(0));
365 if (node != null && !node.eIsProxy() && ProblemUtil.isIndividualNode(node)) {
366 acceptError("Individual nodes must exist.", assertion, null, 0, UNSUPPORTED_ASSERTION_ISSUE);
367 }
368 }
369
370 private void checkEqualsAssertion(Assertion assertion, LogicValue value) {
371 var arguments = assertion.getArguments();
372 if (arguments.size() < 2) {
373 // We already report an error on invalid arity.
374 return;
375 }
376 var left = getNodeArgumentForMultiObjectAssertion(arguments.get(0));
377 var right = getNodeArgumentForMultiObjectAssertion(arguments.get(1));
378 if (left == null || left.eIsProxy() || right == null || right.eIsProxy()) {
379 return;
380 }
381 if (left.equals(right)) {
382 if (value == LogicValue.FALSE || value == LogicValue.ERROR) {
383 acceptError("A node cannot be necessarily unequal to itself.", assertion, null, 0,
384 UNSUPPORTED_ASSERTION_ISSUE);
385 }
386 } else {
387 if (value != LogicValue.FALSE) {
388 acceptError("Equalities between distinct nodes are not supported.", assertion, null, 0,
389 UNSUPPORTED_ASSERTION_ISSUE);
390 }
391 }
392 }
393
394 @Nullable
395 private Node getNodeArgumentForMultiObjectAssertion(AssertionArgument argument) {
396 if (argument instanceof WildcardAssertionArgument) {
397 acceptError("Wildcard arguments for 'exists' are not supported.", argument, null, 0,
398 UNSUPPORTED_ASSERTION_ISSUE);
399 return null;
400 }
401 if (argument instanceof NodeAssertionArgument nodeAssertionArgument) {
402 return nodeAssertionArgument.getNode();
62 } 403 }
404 throw new IllegalArgumentException("Unknown assertion argument: " + argument);
63 } 405 }
64} 406}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java
index f1be55ee..55cbd71d 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java
@@ -3,7 +3,7 @@
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6package tools.refinery.language.resource; 6package tools.refinery.language.validation;
7 7
8import java.util.HashMap; 8import java.util.HashMap;
9import java.util.Map; 9import java.util.Map;
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
index 96e7cf9c..72d57f54 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
@@ -51,6 +51,6 @@ class ProblemParsingTest {
51 ?children(bob, ciri). 51 ?children(bob, ciri).
52 taxStatus(anne, ADULT). 52 taxStatus(anne, ADULT).
53 """); 53 """);
54 assertThat(problem.errors(), empty()); 54 assertThat(problem.getResourceErrors(), empty());
55 } 55 }
56} 56}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java
new file mode 100644
index 00000000..464c207c
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.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.language.tests.linking;
7
8
9import com.google.inject.Inject;
10import org.eclipse.xtext.diagnostics.Diagnostic;
11import org.eclipse.xtext.testing.InjectWith;
12import org.eclipse.xtext.testing.extensions.InjectionExtension;
13import org.junit.jupiter.api.extension.ExtendWith;
14import org.junit.jupiter.params.ParameterizedTest;
15import org.junit.jupiter.params.provider.ValueSource;
16import tools.refinery.language.model.tests.utils.ProblemParseHelper;
17import tools.refinery.language.tests.ProblemInjectorProvider;
18
19import static org.hamcrest.MatcherAssert.assertThat;
20import static org.hamcrest.Matchers.*;
21
22@ExtendWith(InjectionExtension.class)
23@InjectWith(ProblemInjectorProvider.class)
24class AmbiguousReferenceTest {
25 @Inject
26 private ProblemParseHelper parseHelper;
27
28 @ParameterizedTest
29 @ValueSource(strings = {"""
30 class Foo {
31 contains Quux quux
32 }
33
34 class Quux.
35
36 quux(f, q).
37 """, """
38 class Foo {
39 contains Quux quux
40 }
41
42 class Quux.
43
44 pred example(Foo f, Quux q) <-> quux(f, q).
45 """, """
46 class Foo {
47 contains Quux quux opposite foo
48 }
49
50 class Bar {
51 contains Quux quux opposite bar
52 }
53
54 class Quux {
55 container Foo foo opposite quux
56 container Bar bar opposite quux
57 }
58 """})
59 void unambiguousReferenceTest(String text) {
60 var problem = parseHelper.parse(text);
61 assertThat(problem.getResourceErrors(), empty());
62 }
63
64 @ParameterizedTest
65 @ValueSource(strings = {"""
66 class Foo {
67 contains Quux quux
68 }
69
70 class Bar {
71 contains Quux quux
72 }
73
74 class Quux.
75
76 quux(f, q).
77 """, """
78 class Foo {
79 contains Quux quux
80 }
81
82 class Bar {
83 contains Quux quux
84 }
85
86 class Quux.
87
88 pred example(Foo f, Quuq q) <-> quux(f, q).
89 """})
90 void ambiguousReferenceTest(String text) {
91 var problem = parseHelper.parse(text);
92 var errors = problem.getResourceErrors();
93 assertThat(problem.getResourceErrors(), hasItem(allOf(
94 hasProperty("code", is(Diagnostic.LINKING_DIAGNOSTIC)),
95 hasProperty("message", containsString("'quux'"))
96 )));
97 }
98}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java
index ed193e90..a9c5f62a 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java
@@ -31,7 +31,7 @@ class TransitiveClosureParserTest {
31 var problem = parseHelper.parse(""" 31 var problem = parseHelper.parse("""
32 pred foo(a, b) <-> a + (b) > 10. 32 pred foo(a, b) <-> a + (b) > 10.
33 """); 33 """);
34 assertThat(problem.errors(), empty()); 34 assertThat(problem.getResourceErrors(), empty());
35 var literal = problem.pred("foo").conj(0).lit(0).get(); 35 var literal = problem.pred("foo").conj(0).lit(0).get();
36 assertThat(literal, instanceOf(ComparisonExpr.class)); 36 assertThat(literal, instanceOf(ComparisonExpr.class));
37 var left = ((ComparisonExpr) literal).getLeft(); 37 var left = ((ComparisonExpr) literal).getLeft();
@@ -45,7 +45,7 @@ class TransitiveClosureParserTest {
45 var problem = parseHelper.parse(""" 45 var problem = parseHelper.parse("""
46 pred foo(a, b) <-> equals+(a, b). 46 pred foo(a, b) <-> equals+(a, b).
47 """); 47 """);
48 assertThat(problem.errors(), empty()); 48 assertThat(problem.getResourceErrors(), empty());
49 var literal = problem.pred("foo").conj(0).lit(0).get(); 49 var literal = problem.pred("foo").conj(0).lit(0).get();
50 assertThat(literal, instanceOf(Atom.class)); 50 assertThat(literal, instanceOf(Atom.class));
51 var atom = (Atom) literal; 51 var atom = (Atom) literal;
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 68514bfa..56e65550 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
@@ -42,7 +42,7 @@ class RuleParsingTest {
42 """ }) 42 """ })
43 void simpleTest(String text) { 43 void simpleTest(String text) {
44 var problem = parseHelper.parse(text); 44 var problem = parseHelper.parse(text);
45 assertThat(problem.errors(), empty()); 45 assertThat(problem.getResourceErrors(), empty());
46 } 46 }
47 47
48 @Test 48 @Test
@@ -51,7 +51,7 @@ class RuleParsingTest {
51 pred Person(p). 51 pred Person(p).
52 rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown. 52 rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown.
53 """); 53 """);
54 assertThat(problem.errors(), empty()); 54 assertThat(problem.getResourceErrors(), empty());
55 assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable())); 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(), 56 assertThat(problem.rule("r").consequent(0).action(0).newVar(),
57 equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable())); 57 equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable()));
@@ -63,7 +63,7 @@ class RuleParsingTest {
63 pred Friend(a, b). 63 pred Friend(a, b).
64 rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. 64 rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true.
65 """); 65 """);
66 assertThat(problem.errors(), empty()); 66 assertThat(problem.getResourceErrors(), empty());
67 assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(), 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()))); 68 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable())));
69 } 69 }
@@ -74,7 +74,7 @@ class RuleParsingTest {
74 pred Friend(a, b). 74 pred Friend(a, b).
75 rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. 75 rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true.
76 """); 76 """);
77 assertThat(problem.errors(), empty()); 77 assertThat(problem.getResourceErrors(), empty());
78 assertThat(problem.rule("r").param(1), 78 assertThat(problem.rule("r").param(1),
79 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); 79 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable())));
80 } 80 }
@@ -85,6 +85,6 @@ class RuleParsingTest {
85 pred Person(p). 85 pred Person(p).
86 rule r(p1): must Friend(p1, p2) ==> delete p2. 86 rule r(p1): must Friend(p1, p2) ==> delete p2.
87 """); 87 """);
88 assertThat(problem.errors(), not(empty())); 88 assertThat(problem.getResourceErrors(), not(empty()));
89 } 89 }
90} 90}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
index 734bfcd1..e76d2993 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
@@ -36,7 +36,7 @@ class NodeScopingTest {
36 var problem = parse(""" 36 var problem = parse("""
37 pred predicate({PARAM}node a). 37 pred predicate({PARAM}node a).
38 """, qualifiedNamePrefix); 38 """, qualifiedNamePrefix);
39 assertThat(problem.errors(), empty()); 39 assertThat(problem.getResourceErrors(), empty());
40 assertThat(problem.pred("predicate").param(0).getParameterType(), 40 assertThat(problem.pred("predicate").param(0).getParameterType(),
41 equalTo(problem.builtin().findClass("node").get())); 41 equalTo(problem.builtin().findClass("node").get()));
42 } 42 }
@@ -48,7 +48,7 @@ class NodeScopingTest {
48 predicate(a, a). 48 predicate(a, a).
49 ?predicate(a, b). 49 ?predicate(a, b).
50 """); 50 """);
51 assertThat(problem.errors(), empty()); 51 assertThat(problem.getResourceErrors(), empty());
52 assertThat(problem.nodeNames(), hasItems("a", "b")); 52 assertThat(problem.nodeNames(), hasItems("a", "b"));
53 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a"))); 53 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a")));
54 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a"))); 54 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a")));
@@ -62,7 +62,7 @@ class NodeScopingTest {
62 pred predicate(node a) <-> node(b). 62 pred predicate(node a) <-> node(b).
63 predicate(b). 63 predicate(b).
64 """); 64 """);
65 assertThat(problem.errors(), empty()); 65 assertThat(problem.getResourceErrors(), empty());
66 assertThat(problem.nodeNames(), hasItems("b")); 66 assertThat(problem.nodeNames(), hasItems("b"));
67 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b"))); 67 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b")));
68 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); 68 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b")));
@@ -77,7 +77,7 @@ class NodeScopingTest {
77 predicate({PARAM}a, {PARAM}a). 77 predicate({PARAM}a, {PARAM}a).
78 ?predicate({PARAM}a, {PARAM}b). 78 ?predicate({PARAM}a, {PARAM}b).
79 """, qualifiedNamePrefix, namedProblem); 79 """, qualifiedNamePrefix, namedProblem);
80 assertThat(problem.errors(), empty()); 80 assertThat(problem.getResourceErrors(), empty());
81 assertThat(problem.nodeNames(), empty()); 81 assertThat(problem.nodeNames(), empty());
82 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); 82 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a")));
83 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); 83 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a")));
@@ -92,7 +92,7 @@ class NodeScopingTest {
92 indiv b. 92 indiv b.
93 pred predicate(node a) <-> node({PARAM}b). 93 pred predicate(node a) <-> node({PARAM}b).
94 """); 94 """);
95 assertThat(problem.errors(), empty()); 95 assertThat(problem.getResourceErrors(), empty());
96 assertThat(problem.nodeNames(), empty()); 96 assertThat(problem.nodeNames(), empty());
97 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); 97 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b")));
98 } 98 }
@@ -109,7 +109,7 @@ class NodeScopingTest {
109 pred predicate(node x) <-> node(x). 109 pred predicate(node x) <-> node(x).
110 predicate({PARAM}). 110 predicate({PARAM}).
111 """, qualifiedName); 111 """, qualifiedName);
112 assertThat(problem.errors(), empty()); 112 assertThat(problem.getResourceErrors(), empty());
113 assertThat(problem.nodeNames(), empty()); 113 assertThat(problem.nodeNames(), empty());
114 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode())); 114 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode()));
115 } 115 }
@@ -121,7 +121,7 @@ class NodeScopingTest {
121 var problem = parse(""" 121 var problem = parse("""
122 pred predicate(node x) <-> node({PARAM}). 122 pred predicate(node x) <-> node({PARAM}).
123 """, qualifiedName); 123 """, qualifiedName);
124 assertThat(problem.errors(), empty()); 124 assertThat(problem.getResourceErrors(), empty());
125 assertThat(problem.nodeNames(), empty()); 125 assertThat(problem.nodeNames(), empty());
126 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 126 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
127 equalTo(problem.builtin().findClass("int").get().getNewNode())); 127 equalTo(problem.builtin().findClass("int").get().getNewNode()));
@@ -139,7 +139,7 @@ class NodeScopingTest {
139 pred predicate(node x) <-> node(x). 139 pred predicate(node x) <-> node(x).
140 predicate({PARAM}). 140 predicate({PARAM}).
141 """, qualifiedName, namedProblem); 141 """, qualifiedName, namedProblem);
142 assertThat(problem.errors(), empty()); 142 assertThat(problem.getResourceErrors(), empty());
143 assertThat(problem.nodeNames(), empty()); 143 assertThat(problem.nodeNames(), empty());
144 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); 144 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode()));
145 } 145 }
@@ -151,7 +151,7 @@ class NodeScopingTest {
151 class Foo. 151 class Foo.
152 pred predicate(node x) <-> node({PARAM}). 152 pred predicate(node x) <-> node({PARAM}).
153 """, qualifiedName, namedProblem); 153 """, qualifiedName, namedProblem);
154 assertThat(problem.errors(), empty()); 154 assertThat(problem.getResourceErrors(), empty());
155 assertThat(problem.nodeNames(), empty()); 155 assertThat(problem.nodeNames(), empty());
156 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 156 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
157 equalTo(problem.findClass("Foo").get().getNewNode())); 157 equalTo(problem.findClass("Foo").get().getNewNode()));
@@ -169,7 +169,7 @@ class NodeScopingTest {
169 pred predicate(node x) <-> node(x). 169 pred predicate(node x) <-> node(x).
170 predicate(new). 170 predicate(new).
171 """); 171 """);
172 assertThat(problem.errors(), empty()); 172 assertThat(problem.getResourceErrors(), empty());
173 assertThat(problem.nodeNames(), hasItems("new")); 173 assertThat(problem.nodeNames(), hasItems("new"));
174 assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode()))); 174 assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode())));
175 } 175 }
@@ -182,7 +182,7 @@ class NodeScopingTest {
182 pred predicate(Foo a) <-> node(a). 182 pred predicate(Foo a) <-> node(a).
183 predicate({PARAM}). 183 predicate({PARAM}).
184 """, qualifiedName, namedProblem); 184 """, qualifiedName, namedProblem);
185 assertThat(problem.errors(), empty()); 185 assertThat(problem.getResourceErrors(), empty());
186 assertThat(problem.nodeNames(), empty()); 186 assertThat(problem.nodeNames(), empty());
187 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); 187 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha")));
188 } 188 }
@@ -194,7 +194,7 @@ class NodeScopingTest {
194 enum Foo { alpha, beta } 194 enum Foo { alpha, beta }
195 pred predicate(Foo a) <-> node({PARAM}). 195 pred predicate(Foo a) <-> node({PARAM}).
196 """, qualifiedName, namedProblem); 196 """, qualifiedName, namedProblem);
197 assertThat(problem.errors(), empty()); 197 assertThat(problem.getResourceErrors(), empty());
198 assertThat(problem.nodeNames(), empty()); 198 assertThat(problem.nodeNames(), empty());
199 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 199 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
200 equalTo(problem.findEnum("Foo").literal("alpha"))); 200 equalTo(problem.findEnum("Foo").literal("alpha")));
@@ -214,7 +214,7 @@ class NodeScopingTest {
214 pred predicate(node a) <-> node(a). 214 pred predicate(node a) <-> node(a).
215 predicate({PARAM}). 215 predicate({PARAM}).
216 """, qualifiedName); 216 """, qualifiedName);
217 assertThat(problem.errors(), empty()); 217 assertThat(problem.getResourceErrors(), empty());
218 assertThat(problem.nodeNames(), empty()); 218 assertThat(problem.nodeNames(), empty());
219 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true"))); 219 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true")));
220 } 220 }
@@ -226,7 +226,7 @@ class NodeScopingTest {
226 var problem = parse(""" 226 var problem = parse("""
227 pred predicate() <-> node({PARAM}). 227 pred predicate() <-> node({PARAM}).
228 """, qualifiedName); 228 """, qualifiedName);
229 assertThat(problem.errors(), empty()); 229 assertThat(problem.getResourceErrors(), empty());
230 assertThat(problem.nodeNames(), empty()); 230 assertThat(problem.nodeNames(), empty());
231 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 231 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
232 equalTo(problem.builtin().findEnum("bool").literal("true"))); 232 equalTo(problem.builtin().findEnum("bool").literal("true")));
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java
new file mode 100644
index 00000000..68e9fa8d
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java
@@ -0,0 +1,249 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.Diagnostic;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.Test;
13import org.junit.jupiter.api.extension.ExtendWith;
14import org.junit.jupiter.params.ParameterizedTest;
15import org.junit.jupiter.params.provider.Arguments;
16import org.junit.jupiter.params.provider.MethodSource;
17import org.junit.jupiter.params.provider.ValueSource;
18import tools.refinery.language.model.tests.utils.ProblemParseHelper;
19import tools.refinery.language.tests.ProblemInjectorProvider;
20import tools.refinery.language.validation.ProblemValidator;
21
22import java.util.stream.Stream;
23
24import static org.hamcrest.MatcherAssert.assertThat;
25import static org.hamcrest.Matchers.*;
26
27@ExtendWith(InjectionExtension.class)
28@InjectWith(ProblemInjectorProvider.class)
29class ArityValidationTest {
30 @Inject
31 private ProblemParseHelper parseHelper;
32
33 @ParameterizedTest
34 @ValueSource(strings = {"""
35 pred Foo(node n) <-> false.
36 """, """
37 pred Foo(node n, node m) <-> false.
38 """, """
39 enum Foo { FOO_A, FOO_B }
40 """})
41 void invalidSupertypeTest(String supertypeDefinition) {
42 var problem = parseHelper.parse("""
43 %s
44
45 class Bar extends Foo.
46 """.formatted(supertypeDefinition));
47 var issues = problem.validate();
48 assertThat(issues, hasItem(allOf(
49 hasProperty("severity", is(Diagnostic.ERROR)),
50 hasProperty("issueCode", is(ProblemValidator.INVALID_SUPERTYPE_ISSUE)),
51 hasProperty("message", stringContainsInOrder("Foo", "Bar"))
52 )));
53 }
54
55 @ParameterizedTest
56 @ValueSource(strings = {"""
57 class Foo.
58 """, """
59 abstract class Foo.
60 """})
61 void validSupertypeTest(String supertypeDefinition) {
62 var problem = parseHelper.parse("""
63 %s
64
65 class Bar extends Foo.
66 """.formatted(supertypeDefinition));
67 var issues = problem.validate();
68 assertThat(issues, empty());
69 }
70
71 @ParameterizedTest
72 @ValueSource(strings = {"""
73 foo().
74 """, """
75 foo(a1, a2, a3).
76 """, """
77 pred bar() <-> foo().
78 """, """
79 pred bar(node n) <-> foo(n, n, n).
80 """, """
81 pred bar(foo n) <-> false.
82 """, """
83 scope foo = 1..10.
84 """, """
85 class Bar {
86 foo[] f
87 }
88 """, """
89 class Bar {
90 refers foo[] f
91 }
92 """})
93 void invalidArityTest(String usage) {
94 var problem = parseHelper.parse("""
95 pred foo(node a, node b) <-> a != b.
96
97 %s
98 """.formatted(usage));
99 var issues = problem.validate();
100 assertThat(issues, hasItem(allOf(
101 hasProperty("severity", is(Diagnostic.ERROR)),
102 hasProperty("issueCode", is(ProblemValidator.INVALID_ARITY_ISSUE)),
103 hasProperty("message", containsString("foo"))
104 )));
105 }
106
107 @ParameterizedTest
108 @ValueSource(strings = {"""
109 foo(a).
110 """, """
111 pred bar(node m) <-> !foo(m).
112 """, """
113 pred bar(foo f) <-> true.
114 """, """
115 scope foo = 1..10.
116 """, """
117 class Bar {
118 foo[] quux
119 }
120 """, """
121 class Bar {
122 refers foo[] quux
123 }
124 """})
125 void validUnaryArityTest(String supertypeDefinition) {
126 var problem = parseHelper.parse("""
127 pred foo(node n) <-> false.
128
129 %s
130 """.formatted(supertypeDefinition));
131 var issues = problem.validate();
132 assertThat(issues, empty());
133 }
134
135 @ParameterizedTest
136 @ValueSource(strings = {"""
137 foo(a, b).
138 """, """
139 pred bar(node m) <-> !foo(m, m).
140 """, /* Also test for parameters without any type annotation. */ """
141 pred bar(m) <-> foo(m, m).
142 """})
143 void validBinaryArityTest(String supertypeDefinition) {
144 var problem = parseHelper.parse("""
145 pred foo(node n, node m) <-> false.
146
147 %s
148 """.formatted(supertypeDefinition));
149 var issues = problem.validate();
150 assertThat(issues, empty());
151 }
152
153 @Test
154 void notResolvedArityTest() {
155 var problem = parseHelper.parse("""
156 notResolved(a, b).
157 """);
158 var issues = problem.validate();
159 assertThat(issues, not(contains(hasProperty("issueCode", is(ProblemValidator.INVALID_ARITY_ISSUE)))));
160 }
161
162 @Test
163 void validTransitiveClosure() {
164 var problem = parseHelper.parse("""
165 pred foo(node a, node b) <-> false.
166
167 pred bar(a, b) <-> foo+(a, b).
168 """);
169 var issues = problem.validate();
170 assertThat(issues, not(contains(hasProperty("issueCode",
171 is(ProblemValidator.INVALID_TRANSITIVE_CLOSURE_ISSUE)))));
172 }
173
174 @Test
175 void invalidTransitiveClosure() {
176 // 0 and 1 argument transitive closures do not get parsed as transitive closure
177 // due to the ambiguity with the addition operator {@code a + (b)}.
178 var problem = parseHelper.parse("""
179 pred foo(node a, node b) <-> false.
180
181 pred bar(node a, node b) <-> foo+(a, b, a).
182 """);
183 var issues = problem.validate();
184 assertThat(issues, hasItem(allOf(
185 hasProperty("severity", is(Diagnostic.ERROR)),
186 hasProperty("issueCode", is(ProblemValidator.INVALID_TRANSITIVE_CLOSURE_ISSUE))
187 )));
188 }
189
190 @ParameterizedTest
191 @MethodSource
192 void invalidReferenceTypeTest(String definition, String referenceKind) {
193 var problem = parseHelper.parse("""
194 %s
195
196 class Bar {
197 %s Foo foo
198 }
199 """.formatted(definition, referenceKind));
200 var issues = problem.validate();
201 assertThat(issues, allOf(
202 hasItem(allOf(
203 hasProperty("severity", is(Diagnostic.ERROR)),
204 hasProperty("issueCode", is(ProblemValidator.INVALID_REFERENCE_TYPE_ISSUE)),
205 hasProperty("message", stringContainsInOrder("Foo", "foo"))
206 )),
207 not(hasItem(hasProperty("issueCode", is(ProblemValidator.INVALID_ARITY_ISSUE))))
208 ));
209 }
210
211 static Stream<Arguments> invalidReferenceTypeTest() {
212 return Stream.of(
213 "pred Foo(node n) <-> true.",
214 "pred Foo(node n, node m) <-> true.",
215 "enum Foo { FOO_A, FOO_B }"
216 ).flatMap(definition -> Stream.of(
217 Arguments.of(definition, "contains"),
218 Arguments.of(definition, "container")
219 ));
220 }
221
222
223 @ParameterizedTest
224 @MethodSource
225 void validReferenceTypeTest(String definition, String referenceKind) {
226 var problem = parseHelper.parse("""
227 %s
228
229 class Bar {
230 %s Foo foo
231 }
232 """.formatted(definition, referenceKind));
233 var issues = problem.validate();
234 assertThat(issues, not(hasItem(hasProperty("issueCode", anyOf(
235 is(ProblemValidator.INVALID_REFERENCE_TYPE_ISSUE),
236 is(ProblemValidator.INVALID_ARITY_ISSUE)
237 )))));
238 }
239
240 static Stream<Arguments> validReferenceTypeTest() {
241 return Stream.of(
242 "class Foo.",
243 "abstract class Foo."
244 ).flatMap(definition -> Stream.of(
245 Arguments.of(definition, "contains"),
246 Arguments.of(definition, "container")
247 ));
248 }
249}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java
new file mode 100644
index 00000000..82dea31b
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java
@@ -0,0 +1,111 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.xtext.testing.InjectWith;
10import org.eclipse.xtext.testing.extensions.InjectionExtension;
11import org.junit.jupiter.api.Test;
12import org.junit.jupiter.api.extension.ExtendWith;
13import org.junit.jupiter.params.ParameterizedTest;
14import org.junit.jupiter.params.provider.ValueSource;
15import tools.refinery.language.model.tests.utils.ProblemParseHelper;
16import tools.refinery.language.tests.ProblemInjectorProvider;
17import tools.refinery.language.validation.ProblemValidator;
18
19import static org.hamcrest.MatcherAssert.assertThat;
20import static org.hamcrest.Matchers.*;
21import static org.hamcrest.Matchers.is;
22
23@ExtendWith(InjectionExtension.class)
24@InjectWith(ProblemInjectorProvider.class)
25class AssertionValidationTest {
26 @Inject
27 private ProblemParseHelper parseHelper;
28
29 @Test
30 void invalidValueTest() {
31 var problem = parseHelper.parse("""
32 class Foo.
33
34 Foo(n): 5.
35 """);
36 var issues = problem.validate();
37 assertThat(issues, hasItem(hasProperty("issueCode",
38 is(ProblemValidator.INVALID_VALUE_ISSUE))));
39 }
40
41 @ParameterizedTest
42 @ValueSource(strings = {
43 "default exists(n).",
44 "!exists(A).",
45 "exists(A): error.",
46 "exists(n): error.",
47 "!exists(*).",
48 "exists(*): error.",
49 "default equals(n, n).",
50 "equals(n, m).",
51 "?equals(n, m).",
52 "equals(n, m): error.",
53 "equals(A, B).",
54 "?equals(A, B).",
55 "equals(A, B): error.",
56 "!equals(n, n).",
57 "equals(n, n): error.",
58 "!equals(A, A).",
59 "equals(A, A): error.",
60 "?equals(n, *).",
61 "?equals(*, m).",
62 "equals(*, *).",
63 "!equals(*, *).",
64 "?equals(*, *).",
65 "equals(*, *): error."
66 })
67 void invalidMultiObjectTest(String assertion) {
68 var problem = parseHelper.parse("""
69 enum Bar { A, B }
70
71 %s
72 """.formatted(assertion));
73 var issues = problem.validate();
74 assertThat(issues, hasItem(hasProperty("issueCode",
75 is(ProblemValidator.UNSUPPORTED_ASSERTION_ISSUE))));
76 }
77
78 @ParameterizedTest
79 @ValueSource(strings = {
80 "exists(A).",
81 "?exists(A).",
82 "exists(n).",
83 "?exists(n).",
84 "!exists(n).",
85 "exists(*).",
86 "?exists(*).",
87 "exists(Foo::new).",
88 "?exists(Foo::new).",
89 "!exists(Foo::new).",
90 "equals(A, A).",
91 "?equals(A, A).",
92 "!equals(A, B).",
93 "equals(n, n).",
94 "?equals(n, n).",
95 "!equals(n, m).",
96 "equals(Foo::new, Foo::new).",
97 "?equals(Foo::new, Foo::new)."
98 })
99 void validMultiObjectTest(String assertion) {
100 var problem = parseHelper.parse("""
101 class Foo.
102
103 enum Bar { A, B }
104
105 %s
106 """.formatted(assertion));
107 var issues = problem.validate();
108 assertThat(issues, not(hasItem(hasProperty("issueCode",
109 is(ProblemValidator.UNSUPPORTED_ASSERTION_ISSUE)))));
110 }
111}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java
new file mode 100644
index 00000000..a8bcb1a6
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.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.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.Diagnostic;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.extension.ExtendWith;
13import org.junit.jupiter.params.ParameterizedTest;
14import org.junit.jupiter.params.provider.ValueSource;
15import tools.refinery.language.model.tests.utils.ProblemParseHelper;
16import tools.refinery.language.tests.ProblemInjectorProvider;
17import tools.refinery.language.validation.ProblemValidator;
18
19import static org.hamcrest.MatcherAssert.assertThat;
20import static org.hamcrest.Matchers.*;
21
22@ExtendWith(InjectionExtension.class)
23@InjectWith(ProblemInjectorProvider.class)
24class MultiplicityValidationTest {
25 @Inject
26 private ProblemParseHelper parseHelper;
27
28 @ParameterizedTest
29 @ValueSource(strings = {"2..5", "2..2", "2..*", "2", ""})
30 void validReferenceMultiplicityTest(String range) {
31 var problem = parseHelper.parse("""
32 class Foo {
33 Bar[%s] bar
34 }
35
36 class Bar.
37 """.formatted(range));
38 assertThat(problem.validate(), empty());
39 }
40
41 @ParameterizedTest
42 @ValueSource(strings = {"2..5", "2..2", "2..*", "2", "0..0", "0"})
43 void validScopeMultiplicityTest(String range) {
44 var problem = parseHelper.parse("""
45 class Foo.
46
47 scope Foo = %s.
48 """.formatted(range));
49 assertThat(problem.validate(), empty());
50 }
51
52
53 @ParameterizedTest
54 @ValueSource(strings = {"0", "0..0"})
55 void zeroMReferenceMultiplicityTest(String range) {
56 var problem = parseHelper.parse("""
57 class Foo {
58 Bar[%s] bar
59 }
60
61 class Bar.
62 """.formatted(range));
63 assertThat(problem.validate(), hasItem(allOf(
64 hasProperty("severity", is(Diagnostic.WARNING)),
65 hasProperty("issueCode", is(ProblemValidator.ZERO_MULTIPLICITY_ISSUE))
66 )));
67 }
68
69 @ParameterizedTest
70 @ValueSource(strings = {
71 "container Bar bar opposite foo",
72 "container Bar[0..1] bar opposite foo",
73 "container Bar bar", // Invalid, but has valid multiplicity.
74 "container Bar[0..1] bar", // Invalid, but has valid multiplicity.
75 "Bar bar opposite foo",
76 "Bar[0..1] bar opposite foo"
77 })
78 void validContainerReference(String referenceText) {
79 var problem = parseHelper.parse("""
80 class Foo {
81 %s
82 }
83
84 class Bar {
85 contains Foo foo opposite bar
86 }
87 """.formatted(referenceText));
88 assertThat(problem.validate(), not(hasItem(hasProperty("issueCode",
89 is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE)))));
90 }
91
92 @ParameterizedTest
93 @ValueSource(strings = {
94 "container Bar[1] bar opposite foo",
95 "container Bar[1..2] bar opposite foo",
96 "container Bar[] bar opposite foo",
97 "container Bar[1] bar", // Also otherwise invalid, because the {@code opposite} is missing.
98 "container Bar[1..2] bar", // Also otherwise invalid, because the {@code opposite} is missing.
99 "container Bar[] bar", // Also otherwise invalid, because the {@code opposite} is missing.
100 "Bar[1] bar opposite foo",
101 "Bar[1..2] bar opposite foo",
102 "Bar[] bar opposite foo"
103 })
104 void invalidContainerReference(String referenceText) {
105 var problem = parseHelper.parse("""
106 class Foo {
107 %s
108 }
109
110 class Bar {
111 contains Foo foo opposite bar
112 }
113 """.formatted(referenceText));
114 assertThat(problem.validate(), hasItem(allOf(
115 hasProperty("severity", is(Diagnostic.ERROR)),
116 hasProperty("issueCode", is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE))
117 )));
118 }
119}
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
new file mode 100644
index 00000000..57602377
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java
@@ -0,0 +1,209 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.Diagnostic;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.Test;
13import org.junit.jupiter.api.extension.ExtendWith;
14import org.junit.jupiter.params.ParameterizedTest;
15import org.junit.jupiter.params.provider.ValueSource;
16import tools.refinery.language.model.tests.utils.ProblemParseHelper;
17import tools.refinery.language.tests.ProblemInjectorProvider;
18import tools.refinery.language.validation.ProblemValidator;
19
20import java.util.Set;
21
22import static org.hamcrest.MatcherAssert.assertThat;
23import static org.hamcrest.Matchers.*;
24
25@ExtendWith(InjectionExtension.class)
26@InjectWith(ProblemInjectorProvider.class)
27class OppositeValidationTest {
28 @Inject
29 private ProblemParseHelper parseHelper;
30
31 @ParameterizedTest
32 @ValueSource(strings = {"""
33 class Foo {
34 Bar bar opposite foo
35 }
36
37 class Bar {
38 Foo foo opposite bar
39 }
40 """, """
41 class Foo {
42 contains Bar bar opposite foo
43 }
44
45 class Bar {
46 Foo foo opposite bar
47 }
48 """, """
49 class Foo {
50 contains Bar bar opposite foo
51 }
52
53 class Bar {
54 container Foo foo opposite bar
55 }
56 """, """
57 class Foo {
58 Foo foo[] opposite foo
59 }
60 """})
61 void validOppositeTest(String text) {
62 var problem = parseHelper.parse(text);
63 var issues = problem.validate();
64 assertThat(issues, not(hasItems(hasProperty("issueCode", in(Set.of(
65 ProblemValidator.INVALID_OPPOSITE_ISSUE,
66 ProblemValidator.MISSING_OPPOSITE_ISSUE
67 ))))));
68 }
69
70 @Test
71 void missingOppositeTest() {
72 var problem = parseHelper.parse("""
73 class Foo {
74 Bar bar opposite foo
75 }
76
77 class Bar {
78 Foo foo
79 }
80 """);
81 var issues = problem.validate();
82 assertThat(issues, hasItems(allOf(
83 hasProperty("severity", is(Diagnostic.ERROR)),
84 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
85 hasProperty("message", stringContainsInOrder("foo", "bar"))
86 ), allOf(
87 hasProperty("severity", is(Diagnostic.ERROR)),
88 hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)),
89 hasProperty("message", stringContainsInOrder("bar", "foo"))
90 )));
91 }
92
93 @Test
94 void oppositeMismatchTest() {
95 var problem = parseHelper.parse("""
96 class Foo {
97 Bar bar opposite foo
98 Bar quux opposite foo
99 }
100
101 class Bar {
102 Foo foo opposite bar
103 }
104 """);
105 var issues = problem.validate();
106 assertThat(issues, hasItem(allOf(
107 hasProperty("severity", is(Diagnostic.ERROR)),
108 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
109 hasProperty("message", stringContainsInOrder("foo", "quux", "bar"))
110 )));
111 }
112
113 @Test
114 void oppositeMismatchProxyTest() {
115 var problem = parseHelper.parse("""
116 class Foo {
117 Bar bar opposite foo
118 }
119
120 class Bar {
121 Foo foo opposite quux
122 }
123 """);
124 var issues = problem.validate();
125 assertThat(issues, hasItem(allOf(
126 hasProperty("severity", is(Diagnostic.ERROR)),
127 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
128 hasProperty("message", allOf(
129 stringContainsInOrder("foo", "bar"),
130 not(containsString("null"))
131 ))
132 )));
133 }
134
135 @ParameterizedTest
136 @ValueSource(strings = {"contains", "container"})
137 void containmentWithProxyOppositeTest(String keyword) {
138 var problem = parseHelper.parse("""
139 class Foo {
140 %s Bar bar opposite foo
141 }
142
143 class Bar.
144 """.formatted(keyword));
145 var issues = problem.validate();
146 assertThat(issues, not(hasItem(hasProperty("issueCode",
147 is(ProblemValidator.INVALID_OPPOSITE_ISSUE)))));
148 }
149
150 @Test
151 void containmentWithContainmentOppositeTest() {
152 var problem = parseHelper.parse("""
153 class Foo {
154 contains Bar bar opposite foo
155 }
156
157 class Bar {
158 contains Foo foo opposite bar
159 }
160 """);
161 var issues = problem.validate();
162 assertThat(issues, hasItems(allOf(
163 hasProperty("severity", is(Diagnostic.ERROR)),
164 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
165 hasProperty("message", stringContainsInOrder("foo", "bar"))
166 ), allOf(
167 hasProperty("severity", is(Diagnostic.ERROR)),
168 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
169 hasProperty("message", stringContainsInOrder("foo", "bar"))
170 )));
171 }
172
173 @Test
174 void containerWithoutOppositeTest() {
175 var problem = parseHelper.parse("""
176 class Foo {
177 container Bar bar
178 }
179
180 class Bar.
181 """);
182 var issues = problem.validate();
183 assertThat(issues, hasItem(allOf(
184 hasProperty("severity", is(Diagnostic.ERROR)),
185 hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)),
186 hasProperty("message", containsString("bar"))
187 )));
188 }
189
190 @ParameterizedTest
191 @ValueSource(strings = {"Foo foo", "container Foo foo"})
192 void containerInvalidOppositeTest(String reference) {
193 var problem = parseHelper.parse("""
194 class Foo {
195 container Bar bar opposite foo
196 }
197
198 class Bar {
199 %s opposite bar
200 }
201 """.formatted(reference));
202 var issues = problem.validate();
203 assertThat(issues, hasItem(allOf(
204 hasProperty("severity", is(Diagnostic.ERROR)),
205 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
206 hasProperty("message", stringContainsInOrder("foo", "bar"))
207 )));
208 }
209}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java
index 6f6a87f7..f1535716 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java
@@ -5,23 +5,26 @@
5 */ 5 */
6package tools.refinery.language.model.tests.utils; 6package tools.refinery.language.model.tests.utils;
7 7
8import org.eclipse.xtext.testing.util.ParseHelper;
9
10import com.google.inject.Inject; 8import com.google.inject.Inject;
11 9import org.eclipse.emf.ecore.util.EcoreUtil;
10import org.eclipse.xtext.testing.util.ParseHelper;
11import org.eclipse.xtext.validation.IResourceValidator;
12import tools.refinery.language.model.problem.Problem; 12import tools.refinery.language.model.problem.Problem;
13 13
14public class ProblemParseHelper { 14public class ProblemParseHelper {
15 @Inject 15 @Inject
16 private IResourceValidator resourceValidator;
17 @Inject
16 private ParseHelper<Problem> parseHelper; 18 private ParseHelper<Problem> parseHelper;
17 19
18 public WrappedProblem parse(String text) { 20 public WrappedProblem parse(String text) {
19 Problem problem; 21 Problem problem;
20 try { 22 try {
21 problem = parseHelper.parse(text); 23 problem = parseHelper.parse(text);
22 } catch (Exception e) { 24 } catch (Exception e) {
23 throw new RuntimeException("Unexpected exception while parsing Problem", e); 25 throw new AssertionError("Unexpected exception while parsing Problem", e);
24 } 26 }
27 EcoreUtil.resolveAll(problem);
25 return new WrappedProblem(problem); 28 return new WrappedProblem(problem);
26 } 29 }
27} 30}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
index e5aa0043..fc51ff57 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
@@ -6,7 +6,7 @@
6package tools.refinery.language.model.tests.utils; 6package tools.refinery.language.model.tests.utils;
7 7
8import org.eclipse.emf.ecore.resource.Resource.Diagnostic; 8import org.eclipse.emf.ecore.resource.Resource.Diagnostic;
9import org.eclipse.emf.ecore.util.EcoreUtil; 9import org.eclipse.emf.ecore.util.Diagnostician;
10import tools.refinery.language.model.problem.*; 10import tools.refinery.language.model.problem.*;
11import tools.refinery.language.utils.BuiltinSymbols; 11import tools.refinery.language.utils.BuiltinSymbols;
12import tools.refinery.language.utils.ProblemDesugarer; 12import tools.refinery.language.utils.ProblemDesugarer;
@@ -19,11 +19,18 @@ public record WrappedProblem(Problem problem) {
19 return problem; 19 return problem;
20 } 20 }
21 21
22 public List<Diagnostic> errors() { 22 public List<Diagnostic> getResourceErrors() {
23 EcoreUtil.resolveAll(problem);
24 return problem.eResource().getErrors(); 23 return problem.eResource().getErrors();
25 } 24 }
26 25
26 public List<Diagnostic> getResourceWarnings() {
27 return problem.eResource().getWarnings();
28 }
29
30 public List<org.eclipse.emf.common.util.Diagnostic> validate() {
31 return Diagnostician.INSTANCE.validate(problem).getChildren();
32 }
33
27 public WrappedProblem builtin() { 34 public WrappedProblem builtin() {
28 return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow()); 35 return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow());
29 } 36 }
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java
index 2be92464..702e570f 100644
--- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java
+++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java
@@ -13,9 +13,12 @@ import tools.refinery.store.query.dnf.AnyQuery;
13import tools.refinery.store.query.dnf.Query; 13import tools.refinery.store.query.dnf.Query;
14import tools.refinery.store.query.dnf.RelationalQuery; 14import tools.refinery.store.query.dnf.RelationalQuery;
15import tools.refinery.store.query.term.Variable; 15import tools.refinery.store.query.term.Variable;
16import tools.refinery.store.query.term.uppercardinality.UpperCardinalityTerms;
16import tools.refinery.store.reasoning.ReasoningBuilder; 17import tools.refinery.store.reasoning.ReasoningBuilder;
17import tools.refinery.store.reasoning.literal.CountCandidateLowerBoundLiteral; 18import tools.refinery.store.reasoning.literal.CountCandidateLowerBoundLiteral;
19import tools.refinery.store.reasoning.literal.CountUpperBoundLiteral;
18import tools.refinery.store.reasoning.representation.PartialRelation; 20import tools.refinery.store.reasoning.representation.PartialRelation;
21import tools.refinery.store.representation.cardinality.UpperCardinality;
19 22
20import java.util.Collection; 23import java.util.Collection;
21import java.util.List; 24import java.util.List;
@@ -39,7 +42,7 @@ class LowerTypeScopePropagator extends TypeScopePropagator {
39 constraint.setLb((lowerBound - getSingleCount())); 42 constraint.setLb((lowerBound - getSingleCount()));
40 } 43 }
41 44
42 public static class Factory extends TypeScopePropagator.Factory { 45 static class Factory extends TypeScopePropagator.Factory {
43 private final PartialRelation type; 46 private final PartialRelation type;
44 private final int lowerBound; 47 private final int lowerBound;
45 private final RelationalQuery allMay; 48 private final RelationalQuery allMay;
@@ -77,10 +80,18 @@ class LowerTypeScopePropagator extends TypeScopePropagator {
77 output.assign(sub(constant(lowerBound), candidateLowerBound)), 80 output.assign(sub(constant(lowerBound), candidateLowerBound)),
78 check(greater(output, constant(0))) 81 check(greater(output, constant(0)))
79 ))); 82 )));
83 var tooFewObjects = Query.of(type.name() + "#tooFew", builder -> builder
84 .clause(UpperCardinality.class, upperBound -> List.of(
85 new CountUpperBoundLiteral(upperBound, type, List.of(Variable.of())),
86 check(UpperCardinalityTerms.less(upperBound,
87 UpperCardinalityTerms.constant(UpperCardinality.of(lowerBound))))
88 )));
80 89
81 storeBuilder.getAdapter(ReasoningBuilder.class).objective(Objectives.value(requiredObjects)); 90 storeBuilder.getAdapter(ReasoningBuilder.class).objective(Objectives.value(requiredObjects));
82 storeBuilder.tryGetAdapter(DesignSpaceExplorationBuilder.class).ifPresent(dseBuilder -> 91 storeBuilder.tryGetAdapter(DesignSpaceExplorationBuilder.class).ifPresent(dseBuilder -> {
83 dseBuilder.accept(Criteria.whenNoMatch(requiredObjects))); 92 dseBuilder.accept(Criteria.whenNoMatch(requiredObjects));
93 dseBuilder.exclude(Criteria.whenHasMatch(tooFewObjects));
94 });
84 } 95 }
85 } 96 }
86} 97}
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java
index bb50656b..193c132c 100644
--- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java
+++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java
@@ -59,7 +59,7 @@ abstract class TypeScopePropagator {
59 adapter.markAsChanged(); 59 adapter.markAsChanged();
60 } 60 }
61 61
62 public abstract static class Factory { 62 abstract static class Factory {
63 public abstract TypeScopePropagator createPropagator(BoundScopePropagator adapter); 63 public abstract TypeScopePropagator createPropagator(BoundScopePropagator adapter);
64 64
65 protected abstract Collection<AnyQuery> getQueries(); 65 protected abstract Collection<AnyQuery> getQueries();
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java
index 4aba5aac..b2f8d39b 100644
--- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java
+++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java
@@ -5,14 +5,24 @@
5 */ 5 */
6package tools.refinery.store.reasoning.scope; 6package tools.refinery.store.reasoning.scope;
7 7
8import tools.refinery.store.dse.transition.DesignSpaceExplorationBuilder;
9import tools.refinery.store.dse.transition.objectives.Criteria;
10import tools.refinery.store.dse.transition.objectives.Objectives;
11import tools.refinery.store.model.ModelStoreBuilder;
8import tools.refinery.store.query.dnf.AnyQuery; 12import tools.refinery.store.query.dnf.AnyQuery;
9import tools.refinery.store.query.dnf.Query; 13import tools.refinery.store.query.dnf.Query;
10import tools.refinery.store.query.dnf.RelationalQuery; 14import tools.refinery.store.query.dnf.RelationalQuery;
15import tools.refinery.store.query.term.Variable;
16import tools.refinery.store.reasoning.ReasoningBuilder;
17import tools.refinery.store.reasoning.literal.CountCandidateUpperBoundLiteral;
18import tools.refinery.store.reasoning.literal.CountLowerBoundLiteral;
11import tools.refinery.store.reasoning.representation.PartialRelation; 19import tools.refinery.store.reasoning.representation.PartialRelation;
12 20
13import java.util.Collection; 21import java.util.Collection;
14import java.util.List; 22import java.util.List;
15 23
24import static tools.refinery.store.query.literal.Literals.check;
25import static tools.refinery.store.query.term.int_.IntTerms.*;
16import static tools.refinery.store.reasoning.literal.PartialLiterals.must; 26import static tools.refinery.store.reasoning.literal.PartialLiterals.must;
17import static tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator.MULTI_VIEW; 27import static tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator.MULTI_VIEW;
18 28
@@ -30,12 +40,14 @@ class UpperTypeScopePropagator extends TypeScopePropagator {
30 constraint.setUb((upperBound - getSingleCount())); 40 constraint.setUb((upperBound - getSingleCount()));
31 } 41 }
32 42
33 public static class Factory extends TypeScopePropagator.Factory { 43 static class Factory extends TypeScopePropagator.Factory {
44 private final PartialRelation type;
34 private final int upperBound; 45 private final int upperBound;
35 private final RelationalQuery allMust; 46 private final RelationalQuery allMust;
36 private final RelationalQuery multiMust; 47 private final RelationalQuery multiMust;
37 48
38 public Factory(PartialRelation type, int upperBound) { 49 public Factory(PartialRelation type, int upperBound) {
50 this.type = type;
39 this.upperBound = upperBound; 51 this.upperBound = upperBound;
40 allMust = Query.of(type.name() + "#must", (builder, instance) -> builder.clause( 52 allMust = Query.of(type.name() + "#must", (builder, instance) -> builder.clause(
41 must(type.call(instance)) 53 must(type.call(instance))
@@ -55,5 +67,28 @@ class UpperTypeScopePropagator extends TypeScopePropagator {
55 protected Collection<AnyQuery> getQueries() { 67 protected Collection<AnyQuery> getQueries() {
56 return List.of(allMust, multiMust); 68 return List.of(allMust, multiMust);
57 } 69 }
70
71 @Override
72 public void configure(ModelStoreBuilder storeBuilder) {
73 super.configure(storeBuilder);
74
75 var excessObjects = Query.of(type.name() + "#excess", Integer.class, (builder, output) -> builder
76 .clause(Integer.class, candidateUpperBound -> List.of(
77 new CountCandidateUpperBoundLiteral(candidateUpperBound, type, List.of(Variable.of())),
78 output.assign(sub(candidateUpperBound, constant(upperBound))),
79 check(greater(output, constant(0)))
80 )));
81 var tooManyObjects = Query.of(type.name() + "#tooMany", builder -> builder
82 .clause(Integer.class, lowerBound -> List.of(
83 new CountLowerBoundLiteral(lowerBound, type, List.of(Variable.of())),
84 check(greater(lowerBound, constant(upperBound)))
85 )));
86
87 storeBuilder.getAdapter(ReasoningBuilder.class).objective(Objectives.value(excessObjects));
88 storeBuilder.tryGetAdapter(DesignSpaceExplorationBuilder.class).ifPresent(dseBuilder -> {
89 dseBuilder.accept(Criteria.whenNoMatch(excessObjects));
90 dseBuilder.exclude(Criteria.whenHasMatch(tooManyObjects));
91 });
92 }
58 } 93 }
59} 94}
diff --git a/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java
new file mode 100644
index 00000000..a2c56a6b
--- /dev/null
+++ b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java
@@ -0,0 +1,157 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.reasoning.scope;
7
8import org.junit.jupiter.api.Tag;
9import org.junit.jupiter.api.Test;
10import org.junit.jupiter.params.ParameterizedTest;
11import org.junit.jupiter.params.provider.ValueSource;
12import tools.refinery.store.dse.propagation.PropagationAdapter;
13import tools.refinery.store.dse.strategy.BestFirstStoreManager;
14import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter;
15import tools.refinery.store.model.ModelStore;
16import tools.refinery.store.query.dnf.Query;
17import tools.refinery.store.query.interpreter.QueryInterpreterAdapter;
18import tools.refinery.store.query.term.Variable;
19import tools.refinery.store.reasoning.ReasoningAdapter;
20import tools.refinery.store.reasoning.ReasoningStoreAdapter;
21import tools.refinery.store.reasoning.interpretation.PartialInterpretation;
22import tools.refinery.store.reasoning.literal.Concreteness;
23import tools.refinery.store.reasoning.representation.PartialRelation;
24import tools.refinery.store.reasoning.seed.ModelSeed;
25import tools.refinery.store.reasoning.translator.PartialRelationTranslator;
26import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator;
27import tools.refinery.store.reasoning.translator.metamodel.Metamodel;
28import tools.refinery.store.reasoning.translator.metamodel.MetamodelTranslator;
29import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator;
30import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity;
31import tools.refinery.store.representation.TruthValue;
32import tools.refinery.store.representation.cardinality.CardinalityIntervals;
33import tools.refinery.store.statecoding.StateCoderAdapter;
34import tools.refinery.store.tuple.Tuple;
35
36import static org.hamcrest.MatcherAssert.assertThat;
37import static org.hamcrest.Matchers.is;
38import static tools.refinery.store.query.literal.Literals.not;
39
40class PredicateScopeTest {
41 private static final PartialRelation index = new PartialRelation("Index", 1);
42 private static final PartialRelation next = new PartialRelation("next", 2);
43 private static final PartialRelation nextInvalidMultiplicity = new PartialRelation("next::invalidMultiplicity", 1);
44 private static final PartialRelation prev = new PartialRelation("prev", 2);
45 private static final PartialRelation prevInvalidMultiplicity = new PartialRelation("prev::invalidMultiplicity", 1);
46 private static final PartialRelation loop = new PartialRelation("loop", 1);
47 private static final PartialRelation first = new PartialRelation("first", 1);
48 private static final PartialRelation last = new PartialRelation("last", 1);
49
50 @Tag("slow")
51 @ParameterizedTest
52 @ValueSource(ints = {1, 2, 3, 4, 5})
53 void generateTest(int randomSeed) {
54 var store = createStore();
55 var newIndex = Tuple.of(0);
56 var modelSeed = ModelSeed.builder(1)
57 .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder
58 .reducedValue(CardinalityIntervals.ONE)
59 .put(newIndex, CardinalityIntervals.SET))
60 .seed(ContainmentHierarchyTranslator.CONTAINED_SYMBOL,
61 builder -> builder.reducedValue(TruthValue.UNKNOWN))
62 .seed(index, builder -> builder
63 .reducedValue(TruthValue.UNKNOWN)
64 .put(newIndex, TruthValue.TRUE))
65 .seed(next, builder -> builder.reducedValue(TruthValue.UNKNOWN))
66 .seed(prev, builder -> builder.reducedValue(TruthValue.UNKNOWN))
67 .build();
68 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
69 var initialVersion = model.commit();
70 var bestFistSearch = new BestFirstStoreManager(store, 1);
71 bestFistSearch.startExploration(initialVersion, randomSeed);
72 model.restore(bestFistSearch.getSolutionStore().getSolutions().get(0).version());
73 var reasoningAdapter = model.getAdapter(ReasoningAdapter.class);
74 var firstInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, first);
75 assertSize(firstInterpretation, 1);
76 var lastInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, last);
77 assertSize(lastInterpretation, 1);
78 }
79
80 @Test
81 void invalidResultTest() {
82 var store = createStore();
83 var modelSeed = ModelSeed.builder(8)
84 .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder.reducedValue(CardinalityIntervals.ONE))
85 .seed(ContainmentHierarchyTranslator.CONTAINED_SYMBOL,
86 builder -> builder.reducedValue(TruthValue.UNKNOWN))
87 .seed(ContainmentHierarchyTranslator.CONTAINS_SYMBOL,
88 builder -> builder.reducedValue(TruthValue.UNKNOWN))
89 .seed(index, builder -> builder.reducedValue(TruthValue.TRUE))
90 .seed(next, builder -> builder
91 .reducedValue(TruthValue.UNKNOWN)
92 .put(Tuple.of(1, 2), TruthValue.TRUE)
93 .put(Tuple.of(2, 3), TruthValue.TRUE)
94 .put(Tuple.of(3, 4), TruthValue.TRUE)
95 .put(Tuple.of(4, 5), TruthValue.TRUE)
96 .put(Tuple.of(6, 1), TruthValue.TRUE))
97 .seed(prev, builder -> builder.reducedValue(TruthValue.UNKNOWN))
98 .build();
99 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
100 var reasoningAdapter = model.getAdapter(ReasoningAdapter.class);
101 var firstInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, first);
102 assertSize(firstInterpretation, 3);
103 var lastInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, last);
104 assertSize(lastInterpretation, 3);
105 var designSpaceAdapter = model.getAdapter(DesignSpaceExplorationAdapter.class);
106 assertThat(designSpaceAdapter.checkAccept(), is(false));
107 }
108
109 private ModelStore createStore() {
110 var metamodel = Metamodel.builder()
111 .type(index)
112 .reference(next, index, false,
113 ConstrainedMultiplicity.of(CardinalityIntervals.LONE, nextInvalidMultiplicity), index, prev)
114 .reference(prev, index, false,
115 ConstrainedMultiplicity.of(CardinalityIntervals.LONE, prevInvalidMultiplicity), index, next)
116 .build();
117 return ModelStore.builder()
118 .with(QueryInterpreterAdapter.builder())
119 .with(StateCoderAdapter.builder())
120 .with(PropagationAdapter.builder())
121 .with(DesignSpaceExplorationAdapter.builder())
122 .with(ReasoningAdapter.builder())
123 .with(new MultiObjectTranslator())
124 .with(new MetamodelTranslator(metamodel))
125 .with(PartialRelationTranslator.of(loop)
126 .query(Query.of("loop", (builder, p1) -> builder.clause(
127 index.call(p1),
128 next.callTransitive(p1, p1)
129 )))
130 .mayNever())
131 .with(PartialRelationTranslator.of(first)
132 .query(Query.of("first", (builder, p1) -> builder.clause(
133 index.call(p1),
134 not(prev.call(p1, Variable.of()))
135 ))))
136 .with(PartialRelationTranslator.of(last)
137 .query(Query.of("last", (builder, p1) -> builder.clause(
138 index.call(p1),
139 not(next.call(p1, Variable.of()))
140 ))))
141 .with(new ScopePropagator()
142 .scope(index, CardinalityIntervals.exactly(8))
143 .scope(first, CardinalityIntervals.ONE)
144 .scope(last, CardinalityIntervals.ONE))
145 .build();
146 }
147
148 private void assertSize(PartialInterpretation<TruthValue, Boolean> partialInterpretation, int expected) {
149 int size = 0;
150 var cursor = partialInterpretation.getAll();
151 while (cursor.move()) {
152 assertThat(cursor.getValue(), is(TruthValue.TRUE));
153 size++;
154 }
155 assertThat(size, is(expected));
156 }
157}
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java
index 61037be3..c85bd8b7 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java
@@ -52,9 +52,10 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration {
52 52
53 private final Symbol<InferredContainment> containsStorage = Symbol.of("CONTAINS", 2, InferredContainment.class, 53 private final Symbol<InferredContainment> containsStorage = Symbol.of("CONTAINS", 2, InferredContainment.class,
54 InferredContainment.UNKNOWN); 54 InferredContainment.UNKNOWN);
55 private final AnySymbolView mustAnyContainmentLinkView = new MustAnyContainmentLinkView(containsStorage);
55 private final AnySymbolView forbiddenContainsView = new ForbiddenContainsView(containsStorage); 56 private final AnySymbolView forbiddenContainsView = new ForbiddenContainsView(containsStorage);
56 private final RelationalQuery containsMayNewTargetHelper; 57 private final RelationalQuery containsMayNewTargetHelper;
57 private final RelationalQuery containsMayExistingHelper; 58 private final RelationalQuery containsWithoutLink;
58 private final RelationalQuery weakComponents; 59 private final RelationalQuery weakComponents;
59 private final RelationalQuery strongComponents; 60 private final RelationalQuery strongComponents;
60 private final Map<PartialRelation, ContainmentInfo> containmentInfoMap; 61 private final Map<PartialRelation, ContainmentInfo> containmentInfoMap;
@@ -67,18 +68,15 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration {
67 containsMayNewTargetHelper = Query.of(name + "#mayNewTargetHelper", (builder, child) -> builder 68 containsMayNewTargetHelper = Query.of(name + "#mayNewTargetHelper", (builder, child) -> builder
68 .clause(Integer.class, existingContainers -> List.of( 69 .clause(Integer.class, existingContainers -> List.of(
69 may(CONTAINED_SYMBOL.call(child)), 70 may(CONTAINED_SYMBOL.call(child)),
70 new CountLowerBoundLiteral(existingContainers, CONTAINS_SYMBOL, List.of(Variable.of(), child)), 71 new CountLowerBoundLiteral(existingContainers, CONTAINS_SYMBOL,
72 List.of(Variable.of(), child)),
71 check(less(existingContainers, constant(1))) 73 check(less(existingContainers, constant(1)))
72 ))); 74 )));
73 75
74 containsMayExistingHelper = Query.of(name + "#mayExistingHelper", (builder, parent, child) -> builder 76 containsWithoutLink = Query.of(name + "#withoutLink", (builder, parent, child) -> builder.clause(
75 .clause(Integer.class, existingContainers -> List.of( 77 must(CONTAINS_SYMBOL.call(parent, child)),
76 must(CONTAINS_SYMBOL.call(parent, child)), 78 not(mustAnyContainmentLinkView.call(parent, child))
77 not(forbiddenContainsView.call(parent, child)) 79 ));
78 // Violation of monotonicity:
79 // Containment edges violating upper multiplicity will not be marked as {@code ERROR}, but the
80 // {@code invalidNumberOfContainers} error pattern will already mark the node as invalid.
81 )));
82 80
83 var mustExistBothContains = Query.of(name + "#mustExistBoth", (builder, parent, child) -> builder.clause( 81 var mustExistBothContains = Query.of(name + "#mustExistBoth", (builder, parent, child) -> builder.clause(
84 must(CONTAINS_SYMBOL.call(parent, child)), 82 must(CONTAINS_SYMBOL.call(parent, child)),
@@ -139,13 +137,21 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration {
139 var mayNewHelper = Query.of(name + "#mayNewHelper", (builder, parent, child) -> builder.clause( 137 var mayNewHelper = Query.of(name + "#mayNewHelper", (builder, parent, child) -> builder.clause(
140 mayNewSourceHelper.call(parent), 138 mayNewSourceHelper.call(parent),
141 mayNewTargetHelper.call(child), 139 mayNewTargetHelper.call(child),
142 not(must(CONTAINS_SYMBOL.call(parent, child))), 140 not(mustAnyContainmentLinkView.call(parent, child)),
143 not(forbiddenLinkView.call(parent, child)) 141 not(forbiddenLinkView.call(parent, child))
144 )); 142 ));
145 143
144 var existingContainsLink = Query.of(name + "#existingContaints", (builder, parent, child) -> builder
145 .clause(
146 must(linkType.call(parent, child))
147 )
148 .clause(
149 containsWithoutLink.call(parent, child)
150 ));
151
146 var mayExistingHelper = Query.of(name + "#mayExistingHelper", (builder, parent, child) -> builder.clause( 152 var mayExistingHelper = Query.of(name + "#mayExistingHelper", (builder, parent, child) -> builder.clause(
147 must(linkType.call(parent, child)), 153 existingContainsLink.call(parent, child),
148 containsMayExistingHelper.call(parent, child), 154 not(forbiddenContainsView.call(parent, child)),
149 may(sourceType.call(parent)), 155 may(sourceType.call(parent)),
150 may(targetType.call(child)), 156 may(targetType.call(child)),
151 not(forbiddenLinkView.call(parent, child)) 157 not(forbiddenLinkView.call(parent, child))
@@ -224,7 +230,9 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration {
224 })) 230 }))
225 .must(Query.of(mustName, (builder, parent, child) -> builder.clause( 231 .must(Query.of(mustName, (builder, parent, child) -> builder.clause(
226 new MustContainsView(containsStorage).call(parent, child) 232 new MustContainsView(containsStorage).call(parent, child)
227 )))); 233 )))
234 .refiner(ContainsRefiner.of(containsStorage))
235 .initializer(new RefinementBasedInitializer<>(CONTAINS_SYMBOL)));
228 } 236 }
229 237
230 private void translateInvalidContainer(ModelStoreBuilder storeBuilder) { 238 private void translateInvalidContainer(ModelStoreBuilder storeBuilder) {
@@ -245,7 +253,7 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration {
245 ) 253 )
246 .clause(container -> List.of( 254 .clause(container -> List.of(
247 MultiObjectTranslator.MULTI_VIEW.call(multi), 255 MultiObjectTranslator.MULTI_VIEW.call(multi),
248 must(CONTAINS_SYMBOL.call(container, multi)), 256 mustAnyContainmentLinkView.call(container, multi),
249 not(MultiObjectTranslator.MULTI_VIEW.call(container)) 257 not(MultiObjectTranslator.MULTI_VIEW.call(container))
250 )) 258 ))
251 .action( 259 .action(
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java
index 497ed98f..e44fcffd 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java
@@ -24,8 +24,8 @@ class ContainmentLinkRefiner extends AbstractPartialInterpretationRefiner<TruthV
24 private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner; 24 private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner;
25 private final PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner; 25 private final PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner;
26 26
27 public ContainmentLinkRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, 27 private ContainmentLinkRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol,
28 Factory factory) { 28 Factory factory) {
29 super(adapter, partialSymbol); 29 super(adapter, partialSymbol);
30 this.factory = factory; 30 this.factory = factory;
31 interpretation = adapter.getModel().getInterpretation(factory.symbol); 31 interpretation = adapter.getModel().getInterpretation(factory.symbol);
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java
new file mode 100644
index 00000000..a7196a1c
--- /dev/null
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java
@@ -0,0 +1,71 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.reasoning.translator.containment;
7
8import tools.refinery.store.model.Interpretation;
9import tools.refinery.store.reasoning.ReasoningAdapter;
10import tools.refinery.store.reasoning.refinement.AbstractPartialInterpretationRefiner;
11import tools.refinery.store.reasoning.refinement.PartialInterpretationRefiner;
12import tools.refinery.store.reasoning.representation.PartialSymbol;
13import tools.refinery.store.representation.Symbol;
14import tools.refinery.store.representation.TruthValue;
15import tools.refinery.store.tuple.Tuple;
16
17import java.util.LinkedHashMap;
18import java.util.Map;
19import java.util.Set;
20
21class ContainsRefiner extends AbstractPartialInterpretationRefiner<TruthValue, Boolean> {
22 private static final Map<TruthValue, InferredContainment> EMPTY_VALUES;
23
24 static {
25 var values = TruthValue.values();
26 EMPTY_VALUES = new LinkedHashMap<>(values.length);
27 for (var value : values) {
28 EMPTY_VALUES.put(value, new InferredContainment(value, Set.of(), Set.of()));
29 }
30 }
31
32 private final Interpretation<InferredContainment> interpretation;
33 private final PartialInterpretationRefiner<TruthValue, Boolean> containedRefiner;
34
35 private ContainsRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol,
36 Symbol<InferredContainment> containsStorage) {
37 super(adapter, partialSymbol);
38 interpretation = adapter.getModel().getInterpretation(containsStorage);
39 containedRefiner = adapter.getRefiner(ContainmentHierarchyTranslator.CONTAINED_SYMBOL);
40 }
41
42 @Override
43 public boolean merge(Tuple key, TruthValue value) {
44 var oldValue = interpretation.get(key);
45 var newValue = mergeLink(oldValue, value);
46 if (oldValue != newValue) {
47 interpretation.put(key, newValue);
48 }
49 if (value.must()) {
50 return containedRefiner.merge(Tuple.of(key.get(1)), TruthValue.TRUE);
51 }
52 return true;
53 }
54
55 public InferredContainment mergeLink(InferredContainment oldValue, TruthValue toMerge) {
56 var newContains = oldValue.contains().merge(toMerge);
57 if (newContains.equals(oldValue.contains())) {
58 return oldValue;
59 }
60 var mustLinks = oldValue.mustLinks();
61 var forbiddenLinks = oldValue.forbiddenLinks();
62 if (mustLinks.isEmpty() && forbiddenLinks.isEmpty()) {
63 return EMPTY_VALUES.get(newContains);
64 }
65 return new InferredContainment(newContains, mustLinks, forbiddenLinks);
66 }
67
68 public static PartialInterpretationRefiner.Factory<TruthValue, Boolean> of(Symbol<InferredContainment> symbol) {
69 return (adapter, partialSymbol) -> new ContainsRefiner(adapter, partialSymbol, symbol);
70 }
71}
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java
index 8df23d9a..8a757ed2 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java
@@ -24,7 +24,7 @@ final class InferredContainment {
24 this.contains = adjustContains(contains, mustLinks, forbiddenLinks); 24 this.contains = adjustContains(contains, mustLinks, forbiddenLinks);
25 this.mustLinks = mustLinks; 25 this.mustLinks = mustLinks;
26 this.forbiddenLinks = forbiddenLinks; 26 this.forbiddenLinks = forbiddenLinks;
27 hashCode = Objects.hash(contains, mustLinks, forbiddenLinks); 27 hashCode = Objects.hash(this.contains, mustLinks, forbiddenLinks);
28 } 28 }
29 29
30 private static TruthValue adjustContains(TruthValue contains, Set<PartialRelation> mustLinks, 30 private static TruthValue adjustContains(TruthValue contains, Set<PartialRelation> mustLinks,
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java
new file mode 100644
index 00000000..1cc537c6
--- /dev/null
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java
@@ -0,0 +1,21 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.store.reasoning.translator.containment;
7
8import tools.refinery.store.query.view.TuplePreservingView;
9import tools.refinery.store.representation.Symbol;
10import tools.refinery.store.tuple.Tuple;
11
12class MustAnyContainmentLinkView extends TuplePreservingView<InferredContainment> {
13 public MustAnyContainmentLinkView(Symbol<InferredContainment> symbol) {
14 super(symbol, "contains#mustAnyLink");
15 }
16
17 @Override
18 protected boolean doFilter(Tuple key, InferredContainment value) {
19 return !value.mustLinks().isEmpty();
20 }
21}
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java
index 0700f9f7..2e804b44 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java
@@ -15,15 +15,17 @@ import tools.refinery.store.representation.TruthValue;
15import tools.refinery.store.tuple.Tuple; 15import tools.refinery.store.tuple.Tuple;
16 16
17class DirectedCrossReferenceRefiner extends ConcreteSymbolRefiner<TruthValue, Boolean> { 17class DirectedCrossReferenceRefiner extends ConcreteSymbolRefiner<TruthValue, Boolean> {
18 private final PartialRelation targetType;
18 private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner; 19 private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner;
19 private final PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner; 20 private PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner;
20 21
21 public DirectedCrossReferenceRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, 22 public DirectedCrossReferenceRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol,
22 Symbol<TruthValue> concreteSymbol, PartialRelation sourceType, 23 Symbol<TruthValue> concreteSymbol, PartialRelation sourceType,
23 PartialRelation targetType) { 24 PartialRelation targetType) {
24 super(adapter, partialSymbol, concreteSymbol); 25 super(adapter, partialSymbol, concreteSymbol);
26 this.targetType = targetType;
27 // Source is always a class, so we can rely on the fact that it is always constructed before this refiner.
25 sourceRefiner = adapter.getRefiner(sourceType); 28 sourceRefiner = adapter.getRefiner(sourceType);
26 targetRefiner = adapter.getRefiner(targetType);
27 } 29 }
28 30
29 @Override 31 @Override
@@ -32,6 +34,10 @@ class DirectedCrossReferenceRefiner extends ConcreteSymbolRefiner<TruthValue, Bo
32 return false; 34 return false;
33 } 35 }
34 if (value.must()) { 36 if (value.must()) {
37 if (targetRefiner == null) {
38 // Access the target refinery lazily, since it may be constructed after this refiner.
39 targetRefiner = getAdapter().getRefiner(targetType);
40 }
35 return sourceRefiner.merge(Tuple.of(key.get(0)), TruthValue.TRUE) && 41 return sourceRefiner.merge(Tuple.of(key.get(0)), TruthValue.TRUE) &&
36 targetRefiner.merge(Tuple.of(key.get(1)), TruthValue.TRUE); 42 targetRefiner.merge(Tuple.of(key.get(1)), TruthValue.TRUE);
37 } 43 }
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java
index ad0288ed..74022fc6 100644
--- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java
+++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java
@@ -153,15 +153,11 @@ public class MetamodelBuilder {
153 return; 153 return;
154 } 154 }
155 var sourceType = info.sourceType(); 155 var sourceType = info.sourceType();
156 var targetType = info.targetType();
157 if (typeHierarchyBuilder.isInvalidType(sourceType)) { 156 if (typeHierarchyBuilder.isInvalidType(sourceType)) {
158 throw new TranslationException(linkType, "Source type %s of %s is not in type hierarchy" 157 throw new TranslationException(linkType, "Source type %s of %s is not in type hierarchy"
159 .formatted(sourceType, linkType)); 158 .formatted(sourceType, linkType));
160 } 159 }
161 if (typeHierarchyBuilder.isInvalidType(targetType)) { 160 var targetType = info.targetType();
162 throw new TranslationException(linkType, "Target type %s of %s is not in type hierarchy"
163 .formatted(targetType, linkType));
164 }
165 var opposite = info.opposite(); 161 var opposite = info.opposite();
166 Multiplicity targetMultiplicity = UnconstrainedMultiplicity.INSTANCE; 162 Multiplicity targetMultiplicity = UnconstrainedMultiplicity.INSTANCE;
167 if (opposite != null) { 163 if (opposite != null) {
@@ -185,18 +181,30 @@ public class MetamodelBuilder {
185 oppositeReferences.put(opposite, linkType); 181 oppositeReferences.put(opposite, linkType);
186 } 182 }
187 if (info.containment()) { 183 if (info.containment()) {
188 if (!UnconstrainedMultiplicity.INSTANCE.equals(targetMultiplicity)) { 184 processContainmentInfo(linkType, info, targetMultiplicity);
189 throw new TranslationException(opposite, "Invalid opposite %s with multiplicity %s of containment %s"
190 .formatted(opposite, targetMultiplicity, linkType));
191 }
192 containedTypes.add(targetType);
193 containmentHierarchy.put(linkType, new ContainmentInfo(sourceType, info.multiplicity(), targetType));
194 return; 185 return;
195 } 186 }
196 directedCrossReferences.put(linkType, new DirectedCrossReferenceInfo(sourceType, info.multiplicity(), 187 directedCrossReferences.put(linkType, new DirectedCrossReferenceInfo(sourceType, info.multiplicity(),
197 targetType, targetMultiplicity)); 188 targetType, targetMultiplicity));
198 } 189 }
199 190
191 private void processContainmentInfo(PartialRelation linkType, ReferenceInfo info,
192 Multiplicity targetMultiplicity) {
193 var sourceType = info.sourceType();
194 var targetType = info.targetType();
195 var opposite = info.opposite();
196 if (typeHierarchyBuilder.isInvalidType(targetType)) {
197 throw new TranslationException(linkType, "Target type %s of %s is not in type hierarchy"
198 .formatted(targetType, linkType));
199 }
200 if (!UnconstrainedMultiplicity.INSTANCE.equals(targetMultiplicity)) {
201 throw new TranslationException(opposite, "Invalid opposite %s with multiplicity %s of containment %s"
202 .formatted(opposite, targetMultiplicity, linkType));
203 }
204 containedTypes.add(targetType);
205 containmentHierarchy.put(linkType, new ContainmentInfo(sourceType, info.multiplicity(), targetType));
206 }
207
200 private static void validateOpposite(PartialRelation linkType, ReferenceInfo info, PartialRelation opposite, 208 private static void validateOpposite(PartialRelation linkType, ReferenceInfo info, PartialRelation opposite,
201 ReferenceInfo oppositeInfo) { 209 ReferenceInfo oppositeInfo) {
202 var sourceType = info.sourceType(); 210 var sourceType = info.sourceType();